You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by fi...@apache.org on 2013/01/22 02:57:58 UTC
[39/52] [partial] support for 2.4.0rc1. "vendored" the platform libs
in. added Gord and Braden as contributors. removed dependency on unzip and
axed the old download-cordova code.
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/FileTransfer.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/FileTransfer.java b/lib/cordova-android/framework/src/org/apache/cordova/FileTransfer.java
new file mode 100644
index 0000000..c0bd1bd
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/FileTransfer.java
@@ -0,0 +1,827 @@
+/*
+ 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.cordova.api.CallbackContext;
+import org.apache.cordova.api.CordovaPlugin;
+import org.apache.cordova.api.PluginResult;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+import android.webkit.CookieManager;
+
+public class FileTransfer extends CordovaPlugin {
+
+ private static final String LOG_TAG = "FileTransfer";
+ private static final String LINE_START = "--";
+ private static final String LINE_END = "\r\n";
+ private static final String BOUNDARY = "+++++";
+
+ public static int FILE_NOT_FOUND_ERR = 1;
+ public static int INVALID_URL_ERR = 2;
+ public static int CONNECTION_ERR = 3;
+ public static int ABORTED_ERR = 4;
+
+ private static HashMap<String, RequestContext> activeRequests = new HashMap<String, RequestContext>();
+ private static final int MAX_BUFFER_SIZE = 16 * 1024;
+
+ private static final class RequestContext {
+ String source;
+ String target;
+ CallbackContext callbackContext;
+ InputStream currentInputStream;
+ OutputStream currentOutputStream;
+ boolean aborted;
+ RequestContext(String source, String target, CallbackContext callbackContext) {
+ this.source = source;
+ this.target = target;
+ this.callbackContext = callbackContext;
+ }
+ void sendPluginResult(PluginResult pluginResult) {
+ synchronized (this) {
+ if (!aborted) {
+ callbackContext.sendPluginResult(pluginResult);
+ }
+ }
+ }
+ }
+
+ /**
+ * Works around a bug on Android 2.3.
+ * http://code.google.com/p/android/issues/detail?id=14562
+ */
+ private static final class DoneHandlerInputStream extends FilterInputStream {
+ private boolean done;
+
+ public DoneHandlerInputStream(InputStream stream) {
+ super(stream);
+ }
+
+ @Override
+ public int read() throws IOException {
+ int result = done ? -1 : super.read();
+ done = (result == -1);
+ return result;
+ }
+
+ @Override
+ public int read(byte[] buffer) throws IOException {
+ int result = done ? -1 : super.read(buffer);
+ done = (result == -1);
+ return result;
+ }
+
+ @Override
+ public int read(byte[] bytes, int offset, int count) throws IOException {
+ int result = done ? -1 : super.read(bytes, offset, count);
+ done = (result == -1);
+ return result;
+ }
+ }
+
+ @Override
+ public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
+ if (action.equals("upload") || action.equals("download")) {
+ String source = args.getString(0);
+ String target = args.getString(1);
+
+ if (action.equals("upload")) {
+ try {
+ source = URLDecoder.decode(source, "UTF-8");
+ upload(source, target, args, callbackContext);
+ } catch (UnsupportedEncodingException e) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.MALFORMED_URL_EXCEPTION, "UTF-8 error."));
+ }
+ } else {
+ download(source, target, args, callbackContext);
+ }
+ return true;
+ } else if (action.equals("abort")) {
+ String objectId = args.getString(0);
+ abort(objectId);
+ callbackContext.success();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Uploads the specified file to the server URL provided using an HTTP multipart request.
+ * @param source Full path of the file on the file system
+ * @param target URL of the server to receive the file
+ * @param args JSON Array of args
+ * @param callbackContext callback id for optional progress reports
+ *
+ * args[2] fileKey Name of file request parameter
+ * args[3] fileName File name to be used on server
+ * args[4] mimeType Describes file content type
+ * args[5] params key:value pairs of user-defined parameters
+ * @return FileUploadResult containing result of upload request
+ */
+ private void upload(final String source, final String target, JSONArray args, CallbackContext callbackContext) throws JSONException {
+ Log.d(LOG_TAG, "upload " + source + " to " + target);
+
+ // Setup the options
+ final String fileKey = getArgument(args, 2, "file");
+ final String fileName = getArgument(args, 3, "image.jpg");
+ final String mimeType = getArgument(args, 4, "image/jpeg");
+ final JSONObject params = args.optJSONObject(5) == null ? new JSONObject() : args.optJSONObject(5);
+ final boolean trustEveryone = args.optBoolean(6);
+ // Always use chunked mode unless set to false as per API
+ final boolean chunkedMode = args.optBoolean(7) || args.isNull(7);
+ // Look for headers on the params map for backwards compatibility with older Cordova versions.
+ final JSONObject headers = args.optJSONObject(8) == null ? params.optJSONObject("headers") : args.optJSONObject(8);
+ final String objectId = args.getString(9);
+
+ Log.d(LOG_TAG, "fileKey: " + fileKey);
+ Log.d(LOG_TAG, "fileName: " + fileName);
+ Log.d(LOG_TAG, "mimeType: " + mimeType);
+ Log.d(LOG_TAG, "params: " + params);
+ Log.d(LOG_TAG, "trustEveryone: " + trustEveryone);
+ Log.d(LOG_TAG, "chunkedMode: " + chunkedMode);
+ Log.d(LOG_TAG, "headers: " + headers);
+ Log.d(LOG_TAG, "objectId: " + objectId);
+
+ final URL url;
+ try {
+ url = new URL(target);
+ } catch (MalformedURLException e) {
+ JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, 0);
+ Log.e(LOG_TAG, error.toString(), e);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+ return;
+ }
+ final boolean useHttps = url.getProtocol().toLowerCase().equals("https");
+
+ final RequestContext context = new RequestContext(source, target, callbackContext);
+ synchronized (activeRequests) {
+ activeRequests.put(objectId, context);
+ }
+
+ cordova.getThreadPool().execute(new Runnable() {
+ public void run() {
+ if (context.aborted) {
+ return;
+ }
+ HttpURLConnection conn = null;
+ HostnameVerifier oldHostnameVerifier = null;
+ SSLSocketFactory oldSocketFactory = null;
+ try {
+ // Create return object
+ FileUploadResult result = new FileUploadResult();
+ FileProgressResult progress = new FileProgressResult();
+
+ //------------------ CLIENT REQUEST
+ // Open a HTTP connection to the URL based on protocol
+ if (useHttps) {
+ // Using standard HTTPS connection. Will not allow self signed certificate
+ if (!trustEveryone) {
+ conn = (HttpsURLConnection) url.openConnection();
+ }
+ // Use our HTTPS connection that blindly trusts everyone.
+ // This should only be used in debug environments
+ else {
+ // Setup the HTTPS connection class to trust everyone
+ HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
+ oldSocketFactory = trustAllHosts(https);
+ // Save the current hostnameVerifier
+ oldHostnameVerifier = https.getHostnameVerifier();
+ // Setup the connection not to verify hostnames
+ https.setHostnameVerifier(DO_NOT_VERIFY);
+ conn = https;
+ }
+ }
+ // Return a standard HTTP connection
+ else {
+ conn = (HttpURLConnection) url.openConnection();
+ }
+
+ // Allow Inputs
+ conn.setDoInput(true);
+
+ // Allow Outputs
+ conn.setDoOutput(true);
+
+ // Don't use a cached copy.
+ conn.setUseCaches(false);
+
+ // Use a post method.
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Connection", "Keep-Alive");
+ conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + BOUNDARY);
+
+ // Set the cookies on the response
+ String cookie = CookieManager.getInstance().getCookie(target);
+ if (cookie != null) {
+ conn.setRequestProperty("Cookie", cookie);
+ }
+
+ // Handle the other headers
+ if (headers != null) {
+ try {
+ for (Iterator<?> iter = headers.keys(); iter.hasNext(); ) {
+ String headerKey = iter.next().toString();
+ JSONArray headerValues = headers.optJSONArray(headerKey);
+ if (headerValues == null) {
+ headerValues = new JSONArray();
+ headerValues.put(headers.getString(headerKey));
+ }
+ conn.setRequestProperty(headerKey, headerValues.getString(0));
+ for (int i = 1; i < headerValues.length(); ++i) {
+ conn.addRequestProperty(headerKey, headerValues.getString(i));
+ }
+ }
+ } catch (JSONException e1) {
+ // No headers to be manipulated!
+ }
+ }
+
+ /*
+ * Store the non-file portions of the multipart data as a string, so that we can add it
+ * to the contentSize, since it is part of the body of the HTTP request.
+ */
+ String extraParams = "";
+ try {
+ for (Iterator<?> iter = params.keys(); iter.hasNext();) {
+ Object key = iter.next();
+ if(!String.valueOf(key).equals("headers"))
+ {
+ extraParams += LINE_START + BOUNDARY + LINE_END;
+ extraParams += "Content-Disposition: form-data; name=\"" + key.toString() + "\";";
+ extraParams += LINE_END + LINE_END;
+ extraParams += params.getString(key.toString());
+ extraParams += LINE_END;
+ }
+ }
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+
+ extraParams += LINE_START + BOUNDARY + LINE_END;
+ extraParams += "Content-Disposition: form-data; name=\"" + fileKey + "\";" + " filename=\"";
+ byte[] extraBytes = extraParams.getBytes("UTF-8");
+
+ String midParams = "\"" + LINE_END + "Content-Type: " + mimeType + LINE_END + LINE_END;
+ String tailParams = LINE_END + LINE_START + BOUNDARY + LINE_START + LINE_END;
+ byte[] fileNameBytes = fileName.getBytes("UTF-8");
+
+
+ // Get a input stream of the file on the phone
+ InputStream sourceInputStream = getPathFromUri(source);
+
+ int stringLength = extraBytes.length + midParams.length() + tailParams.length() + fileNameBytes.length;
+ Log.d(LOG_TAG, "String Length: " + stringLength);
+ int fixedLength = -1;
+ if (sourceInputStream instanceof FileInputStream) {
+ fixedLength = (int) ((FileInputStream)sourceInputStream).getChannel().size() + stringLength;
+ progress.setLengthComputable(true);
+ progress.setTotal(fixedLength);
+ }
+ Log.d(LOG_TAG, "Content Length: " + fixedLength);
+ // setFixedLengthStreamingMode causes and OutOfMemoryException on pre-Froyo devices.
+ // http://code.google.com/p/android/issues/detail?id=3164
+ // It also causes OOM if HTTPS is used, even on newer devices.
+ boolean useChunkedMode = chunkedMode && (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO || useHttps);
+ useChunkedMode = useChunkedMode || (fixedLength == -1);
+
+ if (useChunkedMode) {
+ conn.setChunkedStreamingMode(MAX_BUFFER_SIZE);
+ // Although setChunkedStreamingMode sets this header, setting it explicitly here works
+ // around an OutOfMemoryException when using https.
+ conn.setRequestProperty("Transfer-Encoding", "chunked");
+ } else {
+ conn.setFixedLengthStreamingMode(fixedLength);
+ }
+
+ DataOutputStream dos = null;
+ try {
+ dos = new DataOutputStream( conn.getOutputStream() );
+ synchronized (context) {
+ if (context.aborted) {
+ return;
+ }
+ context.currentOutputStream = dos;
+ }
+ //We don't want to change encoding, we just want this to write for all Unicode.
+ dos.write(extraBytes);
+ dos.write(fileNameBytes);
+ dos.writeBytes(midParams);
+
+ // create a buffer of maximum size
+ int bytesAvailable = sourceInputStream.available();
+ int bufferSize = Math.min(bytesAvailable, MAX_BUFFER_SIZE);
+ byte[] buffer = new byte[bufferSize];
+
+ // read file and write it into form...
+ int bytesRead = sourceInputStream.read(buffer, 0, bufferSize);
+ long totalBytes = 0;
+
+ long prevBytesRead = 0;
+ while (bytesRead > 0) {
+ totalBytes += bytesRead;
+ result.setBytesSent(totalBytes);
+ dos.write(buffer, 0, bufferSize);
+ if (totalBytes > prevBytesRead + 102400) {
+ prevBytesRead = totalBytes;
+ Log.d(LOG_TAG, "Uploaded " + totalBytes + " of " + fixedLength + " bytes");
+ }
+ bytesAvailable = sourceInputStream.available();
+ bufferSize = Math.min(bytesAvailable, MAX_BUFFER_SIZE);
+ bytesRead = sourceInputStream.read(buffer, 0, bufferSize);
+
+ // Send a progress event.
+ progress.setLoaded(totalBytes);
+ PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());
+ progressResult.setKeepCallback(true);
+ context.sendPluginResult(progressResult);
+ }
+
+ // send multipart form data necessary after file data...
+ dos.writeBytes(tailParams);
+ dos.flush();
+ } finally {
+ safeClose(sourceInputStream);
+ safeClose(dos);
+ }
+ context.currentOutputStream = null;
+
+ //------------------ read the SERVER RESPONSE
+ String responseString;
+ int responseCode = conn.getResponseCode();
+ InputStream inStream = null;
+ try {
+ inStream = getInputStream(conn);
+ synchronized (context) {
+ if (context.aborted) {
+ return;
+ }
+ context.currentInputStream = inStream;
+ }
+
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int bytesRead = 0;
+ // write bytes to file
+ while ((bytesRead = inStream.read(buffer)) > 0) {
+ out.write(buffer, 0, bytesRead);
+ }
+ responseString = out.toString("UTF-8");
+ } finally {
+ context.currentInputStream = null;
+ safeClose(inStream);
+ }
+
+ Log.d(LOG_TAG, "got response from server");
+ Log.d(LOG_TAG, responseString.substring(0, Math.min(256, responseString.length())));
+
+ // send request and retrieve response
+ result.setResponseCode(responseCode);
+ result.setResponse(responseString);
+
+ context.sendPluginResult(new PluginResult(PluginResult.Status.OK, result.toJSONObject()));
+ } catch (FileNotFoundException e) {
+ JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, conn);
+ Log.e(LOG_TAG, error.toString(), e);
+ context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+ } catch (IOException e) {
+ JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn);
+ Log.e(LOG_TAG, error.toString(), e);
+ context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ context.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
+ } catch (Throwable t) {
+ // Shouldn't happen, but will
+ JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, conn);
+ Log.e(LOG_TAG, error.toString(), t);
+ context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+ } finally {
+ synchronized (activeRequests) {
+ activeRequests.remove(objectId);
+ }
+
+ if (conn != null) {
+ // Revert back to the proper verifier and socket factories
+ // Revert back to the proper verifier and socket factories
+ if (trustEveryone && useHttps) {
+ HttpsURLConnection https = (HttpsURLConnection) conn;
+ https.setHostnameVerifier(oldHostnameVerifier);
+ https.setSSLSocketFactory(oldSocketFactory);
+ }
+
+ conn.disconnect();
+ }
+ }
+ }
+ });
+ }
+
+ private static void safeClose(Closeable stream) {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ private static InputStream getInputStream(HttpURLConnection conn) throws IOException {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+ return new DoneHandlerInputStream(conn.getInputStream());
+ }
+ return conn.getInputStream();
+ }
+
+ // always verify the host - don't check for certificate
+ private static final HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ };
+ // Create a trust manager that does not validate certificate chains
+ private static final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
+ public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+ return new java.security.cert.X509Certificate[] {};
+ }
+
+ public void checkClientTrusted(X509Certificate[] chain,
+ String authType) throws CertificateException {
+ }
+
+ public void checkServerTrusted(X509Certificate[] chain,
+ String authType) throws CertificateException {
+ }
+ } };
+
+ /**
+ * This function will install a trust manager that will blindly trust all SSL
+ * certificates. The reason this code is being added is to enable developers
+ * to do development using self signed SSL certificates on their web server.
+ *
+ * The standard HttpsURLConnection class will throw an exception on self
+ * signed certificates if this code is not run.
+ */
+ private static SSLSocketFactory trustAllHosts(HttpsURLConnection connection) {
+ // Install the all-trusting trust manager
+ SSLSocketFactory oldFactory = connection.getSSLSocketFactory();
+ try {
+ // Install our all trusting manager
+ SSLContext sc = SSLContext.getInstance("TLS");
+ sc.init(null, trustAllCerts, new java.security.SecureRandom());
+ SSLSocketFactory newFactory = sc.getSocketFactory();
+ connection.setSSLSocketFactory(newFactory);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return oldFactory;
+ }
+
+ private static JSONObject createFileTransferError(int errorCode, String source, String target, HttpURLConnection connection) {
+
+ Integer httpStatus = null;
+
+ if (connection != null) {
+ try {
+ httpStatus = connection.getResponseCode();
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Error getting HTTP status code from connection.", e);
+ }
+ }
+
+ return createFileTransferError(errorCode, source, target, httpStatus);
+ }
+
+ /**
+ * Create an error object based on the passed in errorCode
+ * @param errorCode the error
+ * @return JSONObject containing the error
+ */
+ private static JSONObject createFileTransferError(int errorCode, String source, String target, Integer httpStatus) {
+ JSONObject error = null;
+ try {
+ error = new JSONObject();
+ error.put("code", errorCode);
+ error.put("source", source);
+ error.put("target", target);
+ if (httpStatus != null) {
+ error.put("http_status", httpStatus);
+ }
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return error;
+ }
+
+ /**
+ * Convenience method to read a parameter from the list of JSON args.
+ * @param args the args passed to the Plugin
+ * @param position the position to retrieve the arg from
+ * @param defaultString the default to be used if the arg does not exist
+ * @return String with the retrieved value
+ */
+ private static String getArgument(JSONArray args, int position, String defaultString) {
+ String arg = defaultString;
+ if (args.length() >= position) {
+ arg = args.optString(position);
+ if (arg == null || "null".equals(arg)) {
+ arg = defaultString;
+ }
+ }
+ return arg;
+ }
+
+ /**
+ * Downloads a file form a given URL and saves it to the specified directory.
+ *
+ * @param source URL of the server to receive the file
+ * @param target Full path of the file on the file system
+ */
+ private void download(final String source, final String target, JSONArray args, CallbackContext callbackContext) throws JSONException {
+ Log.d(LOG_TAG, "download " + source + " to " + target);
+
+ final boolean trustEveryone = args.optBoolean(2);
+ final String objectId = args.getString(3);
+
+ final URL url;
+ try {
+ url = new URL(source);
+ } catch (MalformedURLException e) {
+ JSONObject error = createFileTransferError(INVALID_URL_ERR, source, target, 0);
+ Log.e(LOG_TAG, error.toString(), e);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+ return;
+ }
+ final boolean useHttps = url.getProtocol().toLowerCase().equals("https");
+
+ if (!Config.isUrlWhiteListed(source)) {
+ Log.w(LOG_TAG, "Source URL is not in white list: '" + source + "'");
+ JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, 401);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+ return;
+ }
+
+
+ final RequestContext context = new RequestContext(source, target, callbackContext);
+ synchronized (activeRequests) {
+ activeRequests.put(objectId, context);
+ }
+
+ cordova.getThreadPool().execute(new Runnable() {
+ public void run() {
+ if (context.aborted) {
+ return;
+ }
+ HttpURLConnection connection = null;
+ HostnameVerifier oldHostnameVerifier = null;
+ SSLSocketFactory oldSocketFactory = null;
+
+ try {
+
+ // create needed directories
+ File file = getFileFromPath(target);
+ file.getParentFile().mkdirs();
+
+ // connect to server
+ // Open a HTTP connection to the URL based on protocol
+ if (useHttps) {
+ // Using standard HTTPS connection. Will not allow self signed certificate
+ if (!trustEveryone) {
+ connection = (HttpsURLConnection) url.openConnection();
+ }
+ // Use our HTTPS connection that blindly trusts everyone.
+ // This should only be used in debug environments
+ else {
+ // Setup the HTTPS connection class to trust everyone
+ HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
+ oldSocketFactory = trustAllHosts(https);
+ // Save the current hostnameVerifier
+ oldHostnameVerifier = https.getHostnameVerifier();
+ // Setup the connection not to verify hostnames
+ https.setHostnameVerifier(DO_NOT_VERIFY);
+ connection = https;
+ }
+ }
+ // Return a standard HTTP connection
+ else {
+ connection = (HttpURLConnection) url.openConnection();
+ }
+
+ connection.setRequestMethod("GET");
+
+ //Add cookie support
+ String cookie = CookieManager.getInstance().getCookie(source);
+ if(cookie != null)
+ {
+ connection.setRequestProperty("cookie", cookie);
+ }
+
+ connection.connect();
+
+ Log.d(LOG_TAG, "Download file:" + url);
+
+ FileProgressResult progress = new FileProgressResult();
+ if (connection.getContentEncoding() == null) {
+ // Only trust content-length header if no gzip etc
+ progress.setLengthComputable(true);
+ progress.setTotal(connection.getContentLength());
+ }
+
+ FileOutputStream outputStream = null;
+ InputStream inputStream = null;
+
+ try {
+ inputStream = getInputStream(connection);
+ outputStream = new FileOutputStream(file);
+ synchronized (context) {
+ if (context.aborted) {
+ return;
+ }
+ context.currentInputStream = inputStream;
+ }
+
+ // write bytes to file
+ byte[] buffer = new byte[MAX_BUFFER_SIZE];
+ int bytesRead = 0;
+ long totalBytes = 0;
+ while ((bytesRead = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, bytesRead);
+ totalBytes += bytesRead;
+ // Send a progress event.
+ progress.setLoaded(totalBytes);
+ PluginResult progressResult = new PluginResult(PluginResult.Status.OK, progress.toJSONObject());
+ progressResult.setKeepCallback(true);
+ context.sendPluginResult(progressResult);
+ }
+ } finally {
+ context.currentInputStream = null;
+ safeClose(inputStream);
+ safeClose(outputStream);
+ }
+
+ Log.d(LOG_TAG, "Saved file: " + target);
+
+ // create FileEntry object
+ FileUtils fileUtil = new FileUtils();
+ JSONObject fileEntry = fileUtil.getEntry(file);
+
+ context.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileEntry));
+ } catch (FileNotFoundException e) {
+ JSONObject error = createFileTransferError(FILE_NOT_FOUND_ERR, source, target, connection);
+ Log.e(LOG_TAG, error.toString(), e);
+ context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+ } catch (IOException e) {
+ JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection);
+ Log.e(LOG_TAG, error.toString(), e);
+ context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ context.sendPluginResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION));
+ } catch (Throwable e) {
+ JSONObject error = createFileTransferError(CONNECTION_ERR, source, target, connection);
+ Log.e(LOG_TAG, error.toString(), e);
+ context.sendPluginResult(new PluginResult(PluginResult.Status.IO_EXCEPTION, error));
+ } finally {
+ synchronized (activeRequests) {
+ activeRequests.remove(objectId);
+ }
+
+ if (connection != null) {
+ // Revert back to the proper verifier and socket factories
+ if (trustEveryone && useHttps) {
+ HttpsURLConnection https = (HttpsURLConnection) connection;
+ https.setHostnameVerifier(oldHostnameVerifier);
+ https.setSSLSocketFactory(oldSocketFactory);
+ }
+
+ connection.disconnect();
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Get an input stream based on file path or content:// uri
+ *
+ * @param path foo
+ * @return an input stream
+ * @throws FileNotFoundException
+ */
+ private InputStream getPathFromUri(String path) throws FileNotFoundException {
+ if (path.startsWith("content:")) {
+ Uri uri = Uri.parse(path);
+ return cordova.getActivity().getContentResolver().openInputStream(uri);
+ }
+ else if (path.startsWith("file://")) {
+ int question = path.indexOf("?");
+ if (question == -1) {
+ return new FileInputStream(path.substring(7));
+ } else {
+ return new FileInputStream(path.substring(7, question));
+ }
+ }
+ else {
+ return new FileInputStream(path);
+ }
+ }
+
+ /**
+ * Get a File object from the passed in path
+ *
+ * @param path file path
+ * @return file object
+ */
+ private File getFileFromPath(String path) throws FileNotFoundException {
+ File file;
+ String prefix = "file://";
+
+ if (path.startsWith(prefix)) {
+ file = new File(path.substring(prefix.length()));
+ } else {
+ file = new File(path);
+ }
+
+ if (file.getParent() == null) {
+ throw new FileNotFoundException();
+ }
+
+ return file;
+ }
+
+ /**
+ * Abort an ongoing upload or download.
+ */
+ private void abort(String objectId) {
+ final RequestContext context;
+ synchronized (activeRequests) {
+ context = activeRequests.remove(objectId);
+ }
+ if (context != null) {
+ // Trigger the abort callback immediately to minimize latency between it and abort() being called.
+ JSONObject error = createFileTransferError(ABORTED_ERR, context.source, context.target, -1);
+ synchronized (context) {
+ context.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, error));
+ context.aborted = true;
+ }
+ // Closing the streams can block, so execute on a background thread.
+ cordova.getThreadPool().execute(new Runnable() {
+ public void run() {
+ synchronized (context) {
+ safeClose(context.currentInputStream);
+ safeClose(context.currentOutputStream);
+ }
+ }
+ });
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/FileUploadResult.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/FileUploadResult.java b/lib/cordova-android/framework/src/org/apache/cordova/FileUploadResult.java
new file mode 100644
index 0000000..b556869
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/FileUploadResult.java
@@ -0,0 +1,73 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+package org.apache.cordova;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Encapsulates the result and/or status of uploading a file to a remote server.
+ */
+public class FileUploadResult {
+
+ private long bytesSent = 0; // bytes sent
+ private int responseCode = -1; // HTTP response code
+ private String response = null; // HTTP response
+ private String objectId = null; // FileTransfer object id
+
+ public long getBytesSent() {
+ return bytesSent;
+ }
+
+ public void setBytesSent(long bytes) {
+ this.bytesSent = bytes;
+ }
+
+ public int getResponseCode() {
+ return responseCode;
+ }
+
+ public void setResponseCode(int responseCode) {
+ this.responseCode = responseCode;
+ }
+
+ public String getResponse() {
+ return response;
+ }
+
+ public void setResponse(String response) {
+ this.response = response;
+ }
+
+ public String getObjectId() {
+ return objectId;
+ }
+
+ public void setObjectId(String objectId) {
+ this.objectId = objectId;
+ }
+
+ public JSONObject toJSONObject() throws JSONException {
+ return new JSONObject(
+ "{bytesSent:" + bytesSent +
+ ",responseCode:" + responseCode +
+ ",response:" + JSONObject.quote(response) +
+ ",objectId:" + JSONObject.quote(objectId) + "}");
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/FileUtils.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/FileUtils.java b/lib/cordova-android/framework/src/org/apache/cordova/FileUtils.java
new file mode 100755
index 0000000..554458b
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/FileUtils.java
@@ -0,0 +1,1141 @@
+/*
+ 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;
+
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.channels.FileChannel;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.cordova.api.CallbackContext;
+import org.apache.cordova.api.CordovaInterface;
+import org.apache.cordova.api.CordovaPlugin;
+import org.apache.cordova.api.PluginResult;
+import org.apache.cordova.file.EncodingException;
+import org.apache.cordova.file.FileExistsException;
+import org.apache.cordova.file.InvalidModificationException;
+import org.apache.cordova.file.NoModificationAllowedException;
+import org.apache.cordova.file.TypeMismatchException;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+//import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+//import android.app.Activity;
+
+/**
+ * This class provides SD card file and directory services to JavaScript.
+ * Only files on the SD card can be accessed.
+ */
+public class FileUtils extends CordovaPlugin {
+ @SuppressWarnings("unused")
+ private static final String LOG_TAG = "FileUtils";
+ private static final String _DATA = "_data"; // The column name where the file path is stored
+
+ public static int NOT_FOUND_ERR = 1;
+ public static int SECURITY_ERR = 2;
+ public static int ABORT_ERR = 3;
+
+ public static int NOT_READABLE_ERR = 4;
+ public static int ENCODING_ERR = 5;
+ public static int NO_MODIFICATION_ALLOWED_ERR = 6;
+ public static int INVALID_STATE_ERR = 7;
+ public static int SYNTAX_ERR = 8;
+ public static int INVALID_MODIFICATION_ERR = 9;
+ public static int QUOTA_EXCEEDED_ERR = 10;
+ public static int TYPE_MISMATCH_ERR = 11;
+ public static int PATH_EXISTS_ERR = 12;
+
+ public static int TEMPORARY = 0;
+ public static int PERSISTENT = 1;
+ public static int RESOURCE = 2;
+ public static int APPLICATION = 3;
+
+ FileReader f_in;
+ FileWriter f_out;
+
+ /**
+ * Constructor.
+ */
+ public FileUtils() {
+ }
+
+ /**
+ * Executes the request and returns whether the action was valid.
+ *
+ * @param action The action to execute.
+ * @param args JSONArry of arguments for the plugin.
+ * @param callbackContext The callback context used when calling back into JavaScript.
+ * @return True if the action was valid, false otherwise.
+ */
+ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
+ try {
+ if (action.equals("testSaveLocationExists")) {
+ boolean b = DirectoryManager.testSaveLocationExists();
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+ }
+ else if (action.equals("getFreeDiskSpace")) {
+ long l = DirectoryManager.getFreeDiskSpace(false);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, l));
+ }
+ else if (action.equals("testFileExists")) {
+ boolean b = DirectoryManager.testFileExists(args.getString(0));
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+ }
+ else if (action.equals("testDirectoryExists")) {
+ boolean b = DirectoryManager.testFileExists(args.getString(0));
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, b));
+ }
+ else if (action.equals("readAsText")) {
+ int start = 0;
+ int end = Integer.MAX_VALUE;
+ if (args.length() >= 3) {
+ start = args.getInt(2);
+ }
+ if (args.length() >= 4) {
+ end = args.getInt(3);
+ }
+
+ String s = this.readAsText(args.getString(0), args.getString(1), start, end);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, s));
+ }
+ else if (action.equals("readAsDataURL")) {
+ int start = 0;
+ int end = Integer.MAX_VALUE;
+ if (args.length() >= 2) {
+ start = args.getInt(1);
+ }
+ if (args.length() >= 3) {
+ end = args.getInt(2);
+ }
+
+ String s = this.readAsDataURL(args.getString(0), start, end);
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, s));
+ }
+ else if (action.equals("write")) {
+ long fileSize = this.write(args.getString(0), args.getString(1), args.getInt(2));
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+ }
+ else if (action.equals("truncate")) {
+ long fileSize = this.truncateFile(args.getString(0), args.getLong(1));
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, fileSize));
+ }
+ else if (action.equals("requestFileSystem")) {
+ long size = args.optLong(1);
+ if (size != 0 && size > (DirectoryManager.getFreeDiskSpace(true) * 1024)) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, FileUtils.QUOTA_EXCEEDED_ERR));
+ } else {
+ JSONObject obj = requestFileSystem(args.getInt(0));
+ callbackContext.success(obj);
+ }
+ }
+ else if (action.equals("resolveLocalFileSystemURI")) {
+ JSONObject obj = resolveLocalFileSystemURI(args.getString(0));
+ callbackContext.success(obj);
+ }
+ else if (action.equals("getMetadata")) {
+ callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, getMetadata(args.getString(0))));
+ }
+ else if (action.equals("getFileMetadata")) {
+ JSONObject obj = getFileMetadata(args.getString(0));
+ callbackContext.success(obj);
+ }
+ else if (action.equals("getParent")) {
+ JSONObject obj = getParent(args.getString(0));
+ callbackContext.success(obj);
+ }
+ else if (action.equals("getDirectory")) {
+ JSONObject obj = getFile(args.getString(0), args.getString(1), args.optJSONObject(2), true);
+ callbackContext.success(obj);
+ }
+ else if (action.equals("getFile")) {
+ JSONObject obj = getFile(args.getString(0), args.getString(1), args.optJSONObject(2), false);
+ callbackContext.success(obj);
+ }
+ else if (action.equals("remove")) {
+ boolean success;
+
+ success = remove(args.getString(0));
+
+ if (success) {
+ notifyDelete(args.getString(0));
+ callbackContext.success();
+ } else {
+ callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ }
+ }
+ else if (action.equals("removeRecursively")) {
+ boolean success = removeRecursively(args.getString(0));
+ if (success) {
+ callbackContext.success();
+ } else {
+ callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ }
+ }
+ else if (action.equals("moveTo")) {
+ JSONObject entry = transferTo(args.getString(0), args.getString(1), args.getString(2), true);
+ callbackContext.success(entry);
+ }
+ else if (action.equals("copyTo")) {
+ JSONObject entry = transferTo(args.getString(0), args.getString(1), args.getString(2), false);
+ callbackContext.success(entry);
+ }
+ else if (action.equals("readEntries")) {
+ JSONArray entries = readEntries(args.getString(0));
+ callbackContext.success(entries);
+ }
+ else {
+ return false;
+ }
+ } catch (FileNotFoundException e) {
+ callbackContext.error(FileUtils.NOT_FOUND_ERR);
+ } catch (FileExistsException e) {
+ callbackContext.error(FileUtils.PATH_EXISTS_ERR);
+ } catch (NoModificationAllowedException e) {
+ callbackContext.error(FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ } catch (InvalidModificationException e) {
+ callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
+ } catch (MalformedURLException e) {
+ callbackContext.error(FileUtils.ENCODING_ERR);
+ } catch (IOException e) {
+ callbackContext.error(FileUtils.INVALID_MODIFICATION_ERR);
+ } catch (EncodingException e) {
+ callbackContext.error(FileUtils.ENCODING_ERR);
+ } catch (TypeMismatchException e) {
+ callbackContext.error(FileUtils.TYPE_MISMATCH_ERR);
+ }
+ return true;
+ }
+
+ /**
+ * Need to check to see if we need to clean up the content store
+ *
+ * @param filePath the path to check
+ */
+ private void notifyDelete(String filePath) {
+ String newFilePath = stripFileProtocol(filePath);
+ try {
+ this.cordova.getActivity().getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Images.Media.DATA + " = ?",
+ new String[] { newFilePath });
+ } 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.
+ }
+ }
+
+ /**
+ * Allows the user to look up the Entry for a file or directory referred to by a local URI.
+ *
+ * @param url of the file/directory to look up
+ * @return a JSONObject representing a Entry from the filesystem
+ * @throws MalformedURLException if the url is not valid
+ * @throws FileNotFoundException if the file does not exist
+ * @throws IOException if the user can't read the file
+ * @throws JSONException
+ */
+ @SuppressWarnings("deprecation")
+ private JSONObject resolveLocalFileSystemURI(String url) throws IOException, JSONException {
+ String decoded = URLDecoder.decode(url, "UTF-8");
+
+ File fp = null;
+
+ // Handle the special case where you get an Android content:// uri.
+ if (decoded.startsWith("content:")) {
+ Cursor cursor = this.cordova.getActivity().managedQuery(Uri.parse(decoded), new String[] { MediaStore.Images.Media.DATA }, null, null, null);
+ // Note: MediaStore.Images/Audio/Video.Media.DATA is always "_data"
+ int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
+ cursor.moveToFirst();
+ fp = new File(cursor.getString(column_index));
+ } else {
+ // Test to see if this is a valid URL first
+ @SuppressWarnings("unused")
+ URL testUrl = new URL(decoded);
+
+ if (decoded.startsWith("file://")) {
+ int questionMark = decoded.indexOf("?");
+ if (questionMark < 0) {
+ fp = new File(decoded.substring(7, decoded.length()));
+ } else {
+ fp = new File(decoded.substring(7, questionMark));
+ }
+ } else {
+ fp = new File(decoded);
+ }
+ }
+
+ if (!fp.exists()) {
+ throw new FileNotFoundException();
+ }
+ if (!fp.canRead()) {
+ throw new IOException();
+ }
+ return getEntry(fp);
+ }
+
+ /**
+ * Read the list of files from this directory.
+ *
+ * @param fileName the directory to read from
+ * @return a JSONArray containing JSONObjects that represent Entry objects.
+ * @throws FileNotFoundException if the directory is not found.
+ * @throws JSONException
+ */
+ private JSONArray readEntries(String fileName) throws FileNotFoundException, JSONException {
+ File fp = createFileObject(fileName);
+
+ if (!fp.exists()) {
+ // The directory we are listing doesn't exist so we should fail.
+ throw new FileNotFoundException();
+ }
+
+ JSONArray entries = new JSONArray();
+
+ if (fp.isDirectory()) {
+ File[] files = fp.listFiles();
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].canRead()) {
+ entries.put(getEntry(files[i]));
+ }
+ }
+ }
+
+ return entries;
+ }
+
+ /**
+ * A setup method that handles the move/copy of files/directories
+ *
+ * @param fileName to be copied/moved
+ * @param newParent is the location where the file will be copied/moved to
+ * @param newName for the file directory to be called, if null use existing file name
+ * @param move if false do a copy, if true do a move
+ * @return a Entry object
+ * @throws NoModificationAllowedException
+ * @throws IOException
+ * @throws InvalidModificationException
+ * @throws EncodingException
+ * @throws JSONException
+ * @throws FileExistsException
+ */
+ private JSONObject transferTo(String fileName, String newParent, String newName, boolean move) throws JSONException, NoModificationAllowedException, IOException, InvalidModificationException, EncodingException, FileExistsException {
+ fileName = stripFileProtocol(fileName);
+ newParent = stripFileProtocol(newParent);
+
+ // Check for invalid file name
+ if (newName != null && newName.contains(":")) {
+ throw new EncodingException("Bad file name");
+ }
+
+ File source = new File(fileName);
+
+ if (!source.exists()) {
+ // The file/directory we are copying doesn't exist so we should fail.
+ throw new FileNotFoundException("The source does not exist");
+ }
+
+ 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
+ File destination = createDestination(newName, source, destinationDir);
+
+ //Log.d(LOG_TAG, "Source: " + source.getAbsolutePath());
+ //Log.d(LOG_TAG, "Destin: " + destination.getAbsolutePath());
+
+ // Check to see if source and destination are the same file
+ if (source.getAbsolutePath().equals(destination.getAbsolutePath())) {
+ throw new InvalidModificationException("Can't copy a file onto itself");
+ }
+
+ if (source.isDirectory()) {
+ if (move) {
+ return moveDirectory(source, destination);
+ } else {
+ return copyDirectory(source, destination);
+ }
+ } else {
+ if (move) {
+ return moveFile(source, destination);
+ } else {
+ return copyFile(source, destination);
+ }
+ }
+ }
+
+ /**
+ * Creates the destination File object based on name passed in
+ *
+ * @param newName for the file directory to be called, if null use existing file name
+ * @param fp represents the source file
+ * @param destination represents the destination file
+ * @return a File object that represents the destination
+ */
+ private File createDestination(String newName, File fp, File destination) {
+ File destFile = null;
+
+ // I know this looks weird but it is to work around a JSON bug.
+ if ("null".equals(newName) || "".equals(newName)) {
+ newName = null;
+ }
+
+ if (newName != null) {
+ destFile = new File(destination.getAbsolutePath() + File.separator + newName);
+ } else {
+ destFile = new File(destination.getAbsolutePath() + File.separator + fp.getName());
+ }
+ return destFile;
+ }
+
+ /**
+ * Copy a file
+ *
+ * @param srcFile file to be copied
+ * @param destFile destination to be copied to
+ * @return a FileEntry object
+ * @throws IOException
+ * @throws InvalidModificationException
+ * @throws JSONException
+ */
+ private JSONObject copyFile(File srcFile, File destFile) throws IOException, InvalidModificationException, JSONException {
+ // Renaming a file to an existing directory should fail
+ if (destFile.exists() && destFile.isDirectory()) {
+ throw new InvalidModificationException("Can't rename a file to a directory");
+ }
+
+ copyAction(srcFile, destFile);
+
+ return getEntry(destFile);
+ }
+
+ /**
+ * Moved this code into it's own method so moveTo could use it when the move is across file systems
+ */
+ private void copyAction(File srcFile, File destFile)
+ throws FileNotFoundException, IOException {
+ FileInputStream istream = new FileInputStream(srcFile);
+ FileOutputStream ostream = new FileOutputStream(destFile);
+ FileChannel input = istream.getChannel();
+ FileChannel output = ostream.getChannel();
+
+ try {
+ input.transferTo(0, input.size(), output);
+ } finally {
+ istream.close();
+ ostream.close();
+ input.close();
+ output.close();
+ }
+ }
+
+ /**
+ * Copy a directory
+ *
+ * @param srcDir directory to be copied
+ * @param destinationDir destination to be copied to
+ * @return a DirectoryEntry object
+ * @throws JSONException
+ * @throws IOException
+ * @throws NoModificationAllowedException
+ * @throws InvalidModificationException
+ */
+ private JSONObject copyDirectory(File srcDir, File destinationDir) throws JSONException, IOException, NoModificationAllowedException, InvalidModificationException {
+ // Renaming a file to an existing directory should fail
+ if (destinationDir.exists() && destinationDir.isFile()) {
+ throw new InvalidModificationException("Can't rename a file to a directory");
+ }
+
+ // Check to make sure we are not copying the directory into itself
+ if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) {
+ throw new InvalidModificationException("Can't copy itself into itself");
+ }
+
+ // See if the destination directory exists. If not create it.
+ if (!destinationDir.exists()) {
+ if (!destinationDir.mkdir()) {
+ // If we can't create the directory then fail
+ throw new NoModificationAllowedException("Couldn't create the destination direcotry");
+ }
+ }
+
+ for (File file : srcDir.listFiles()) {
+ if (file.isDirectory()) {
+ copyDirectory(file, destinationDir);
+ } else {
+ File destination = new File(destinationDir.getAbsoluteFile() + File.separator + file.getName());
+ copyFile(file, destination);
+ }
+ }
+
+ return getEntry(destinationDir);
+ }
+
+ /**
+ * Check to see if the user attempted to copy an entry into its parent without changing its name,
+ * or attempted to copy a directory into a directory that it contains directly or indirectly.
+ *
+ * @param srcDir
+ * @param destinationDir
+ * @return
+ */
+ private boolean isCopyOnItself(String src, String dest) {
+
+ // This weird test is to determine if we are copying or moving a directory into itself.
+ // Copy /sdcard/myDir to /sdcard/myDir-backup is okay but
+ // Copy /sdcard/myDir to /sdcard/myDir/backup should throw an INVALID_MODIFICATION_ERR
+ if (dest.startsWith(src) && dest.indexOf(File.separator, src.length() - 1) != -1) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Move a file
+ *
+ * @param srcFile file to be copied
+ * @param destFile destination to be copied to
+ * @return a FileEntry object
+ * @throws IOException
+ * @throws InvalidModificationException
+ * @throws JSONException
+ */
+ private JSONObject moveFile(File srcFile, File destFile) throws IOException, JSONException, InvalidModificationException {
+ // Renaming a file to an existing directory should fail
+ if (destFile.exists() && destFile.isDirectory()) {
+ throw new InvalidModificationException("Can't rename a file to a directory");
+ }
+
+ // Try to rename the file
+ if (!srcFile.renameTo(destFile)) {
+ // Trying to rename the file failed. Possibly because we moved across file system on the device.
+ // Now we have to do things the hard way
+ // 1) Copy all the old file
+ // 2) delete the src file
+ copyAction(srcFile, destFile);
+ if (destFile.exists()) {
+ srcFile.delete();
+ } else {
+ throw new IOException("moved failed");
+ }
+ }
+
+ return getEntry(destFile);
+ }
+
+ /**
+ * Move a directory
+ *
+ * @param srcDir directory to be copied
+ * @param destinationDir destination to be copied to
+ * @return a DirectoryEntry object
+ * @throws JSONException
+ * @throws IOException
+ * @throws InvalidModificationException
+ * @throws NoModificationAllowedException
+ * @throws FileExistsException
+ */
+ private JSONObject moveDirectory(File srcDir, File destinationDir) throws IOException, JSONException, InvalidModificationException, NoModificationAllowedException, FileExistsException {
+ // Renaming a file to an existing directory should fail
+ if (destinationDir.exists() && destinationDir.isFile()) {
+ throw new InvalidModificationException("Can't rename a file to a directory");
+ }
+
+ // Check to make sure we are not copying the directory into itself
+ if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) {
+ throw new InvalidModificationException("Can't move itself into itself");
+ }
+
+ // If the destination directory already exists and is empty then delete it. This is according to spec.
+ if (destinationDir.exists()) {
+ if (destinationDir.list().length > 0) {
+ throw new InvalidModificationException("directory is not empty");
+ }
+ }
+
+ // Try to rename the directory
+ if (!srcDir.renameTo(destinationDir)) {
+ // Trying to rename the directory failed. Possibly because we moved across file system on the device.
+ // Now we have to do things the hard way
+ // 1) Copy all the old files
+ // 2) delete the src directory
+ copyDirectory(srcDir, destinationDir);
+ if (destinationDir.exists()) {
+ removeDirRecursively(srcDir);
+ } else {
+ throw new IOException("moved failed");
+ }
+ }
+
+ return getEntry(destinationDir);
+ }
+
+ /**
+ * Deletes a directory and all of its contents, if any. In the event of an error
+ * [e.g. trying to delete a directory that contains a file that cannot be removed],
+ * some of the contents of the directory may be deleted.
+ * It is an error to attempt to delete the root directory of a filesystem.
+ *
+ * @param filePath the directory to be removed
+ * @return a boolean representing success of failure
+ * @throws FileExistsException
+ */
+ private boolean removeRecursively(String filePath) throws FileExistsException {
+ File fp = createFileObject(filePath);
+
+ // You can't delete the root directory.
+ if (atRootDirectory(filePath)) {
+ return false;
+ }
+
+ return removeDirRecursively(fp);
+ }
+
+ /**
+ * Loops through a directory deleting all the files.
+ *
+ * @param directory to be removed
+ * @return a boolean representing success of failure
+ * @throws FileExistsException
+ */
+ private 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;
+ }
+ }
+
+ /**
+ * Deletes a file or directory. It is an error to attempt to delete a directory that is not empty.
+ * It is an error to attempt to delete the root directory of a filesystem.
+ *
+ * @param filePath file or directory to be removed
+ * @return a boolean representing success of failure
+ * @throws NoModificationAllowedException
+ * @throws InvalidModificationException
+ */
+ private boolean remove(String filePath) throws NoModificationAllowedException, InvalidModificationException {
+ File fp = createFileObject(filePath);
+
+ // You can't delete the root directory.
+ if (atRootDirectory(filePath)) {
+ throw new NoModificationAllowedException("You can't delete the root directory");
+ }
+
+ // 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();
+ }
+
+ /**
+ * Creates or looks up a file.
+ *
+ * @param dirPath base directory
+ * @param fileName file/directory to lookup or create
+ * @param options specify whether to create or not
+ * @param directory if true look up directory, if false look up file
+ * @return a Entry object
+ * @throws FileExistsException
+ * @throws IOException
+ * @throws TypeMismatchException
+ * @throws EncodingException
+ * @throws JSONException
+ */
+ private JSONObject getFile(String dirPath, String fileName, 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 (fileName.contains(":")) {
+ throw new EncodingException("This file has a : in it's name");
+ }
+
+ File fp = createFileObject(dirPath, fileName);
+
+ 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 getEntry(fp);
+ }
+
+ /**
+ * If the path starts with a '/' just return that file object. If not construct the file
+ * object from the path passed in and the file name.
+ *
+ * @param dirPath root directory
+ * @param fileName new file name
+ * @return
+ */
+ private File createFileObject(String dirPath, String fileName) {
+ File fp = null;
+ if (fileName.startsWith("/")) {
+ fp = new File(fileName);
+ } else {
+ dirPath = stripFileProtocol(dirPath);
+ fp = new File(dirPath + File.separator + fileName);
+ }
+ return fp;
+ }
+
+ /**
+ * Look up the parent DirectoryEntry containing this Entry.
+ * If this Entry is the root of its filesystem, its parent is itself.
+ *
+ * @param filePath
+ * @return
+ * @throws JSONException
+ */
+ private JSONObject getParent(String filePath) throws JSONException {
+ filePath = stripFileProtocol(filePath);
+
+ if (atRootDirectory(filePath)) {
+ return getEntry(filePath);
+ }
+ return getEntry(new File(filePath).getParent());
+ }
+
+ /**
+ * Checks to see if we are at the root directory. Useful since we are
+ * not allow to delete this directory.
+ *
+ * @param filePath to directory
+ * @return true if we are at the root, false otherwise.
+ */
+ private boolean atRootDirectory(String filePath) {
+ filePath = stripFileProtocol(filePath);
+
+ if (filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + cordova.getActivity().getPackageName() + "/cache") ||
+ filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath()) ||
+ filePath.equals("/data/data/" + cordova.getActivity().getPackageName())) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This method removes the "file://" from the passed in filePath
+ *
+ * @param filePath to be checked.
+ * @return
+ */
+ public static String stripFileProtocol(String filePath) {
+ if (filePath.startsWith("file://")) {
+ filePath = filePath.substring(7);
+ }
+ return filePath;
+ }
+
+ /**
+ * Create a File object from the passed in path
+ *
+ * @param filePath
+ * @return
+ */
+ private File createFileObject(String filePath) {
+ filePath = stripFileProtocol(filePath);
+
+ File file = new File(filePath);
+ return file;
+ }
+
+ /**
+ * Look up metadata about this entry.
+ *
+ * @param filePath to entry
+ * @return a long
+ * @throws FileNotFoundException
+ */
+ private long getMetadata(String filePath) throws FileNotFoundException {
+ File file = createFileObject(filePath);
+
+ if (!file.exists()) {
+ throw new FileNotFoundException("Failed to find file in getMetadata");
+ }
+
+ return file.lastModified();
+ }
+
+ /**
+ * Returns a File that represents the current state of the file that this FileEntry represents.
+ *
+ * @param filePath to entry
+ * @return returns a JSONObject represent a W3C File object
+ * @throws FileNotFoundException
+ * @throws JSONException
+ */
+ private JSONObject getFileMetadata(String filePath) throws FileNotFoundException, JSONException {
+ File file = createFileObject(filePath);
+
+ if (!file.exists()) {
+ throw new FileNotFoundException("File: " + filePath + " does not exist.");
+ }
+
+ JSONObject metadata = new JSONObject();
+ metadata.put("size", file.length());
+ metadata.put("type", getMimeType(filePath));
+ metadata.put("name", file.getName());
+ metadata.put("fullPath", file.getAbsolutePath());
+ metadata.put("lastModifiedDate", file.lastModified());
+
+ return metadata;
+ }
+
+ /**
+ * Requests a filesystem in which to store application data.
+ *
+ * @param type of file system requested
+ * @return a JSONObject representing the file system
+ * @throws IOException
+ * @throws JSONException
+ */
+ private JSONObject requestFileSystem(int type) throws IOException, JSONException {
+ JSONObject fs = new JSONObject();
+ if (type == TEMPORARY) {
+ File fp;
+ fs.put("name", "temporary");
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ fp = new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
+ "/Android/data/" + cordova.getActivity().getPackageName() + "/cache/");
+ // Create the cache dir if it doesn't exist.
+ fp.mkdirs();
+ fs.put("root", getEntry(Environment.getExternalStorageDirectory().getAbsolutePath() +
+ "/Android/data/" + cordova.getActivity().getPackageName() + "/cache/"));
+ } else {
+ fp = new File("/data/data/" + cordova.getActivity().getPackageName() + "/cache/");
+ // Create the cache dir if it doesn't exist.
+ fp.mkdirs();
+ fs.put("root", getEntry("/data/data/" + cordova.getActivity().getPackageName() + "/cache/"));
+ }
+ }
+ else if (type == PERSISTENT) {
+ fs.put("name", "persistent");
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ fs.put("root", getEntry(Environment.getExternalStorageDirectory()));
+ } else {
+ fs.put("root", getEntry("/data/data/" + cordova.getActivity().getPackageName()));
+ }
+ }
+ else {
+ throw new IOException("No filesystem of type requested");
+ }
+
+ return fs;
+ }
+
+ /**
+ * Returns a JSON Object representing a directory on the device's file system
+ *
+ * @param path to the directory
+ * @return
+ * @throws JSONException
+ */
+ public JSONObject getEntry(File file) throws JSONException {
+ JSONObject entry = new JSONObject();
+
+ entry.put("isFile", file.isFile());
+ entry.put("isDirectory", file.isDirectory());
+ entry.put("name", file.getName());
+ entry.put("fullPath", "file://" + file.getAbsolutePath());
+ // I can't add the next thing it as it would be an infinite loop
+ //entry.put("filesystem", null);
+
+ return entry;
+ }
+
+ /**
+ * Returns a JSON Object representing a directory on the device's file system
+ *
+ * @param path to the directory
+ * @return
+ * @throws JSONException
+ */
+ private JSONObject getEntry(String path) throws JSONException {
+ return getEntry(new File(path));
+ }
+
+ /**
+ * Identifies if action to be executed returns a value and should be run synchronously.
+ *
+ * @param action The action to execute
+ * @return T=returns value
+ */
+ public boolean isSynch(String action) {
+ if (action.equals("testSaveLocationExists")) {
+ return true;
+ }
+ else if (action.equals("getFreeDiskSpace")) {
+ return true;
+ }
+ else if (action.equals("testFileExists")) {
+ return true;
+ }
+ else if (action.equals("testDirectoryExists")) {
+ return true;
+ }
+ return false;
+ }
+
+ //--------------------------------------------------------------------------
+ // LOCAL METHODS
+ //--------------------------------------------------------------------------
+
+ /**
+ * Read content of text file.
+ *
+ * @param filename The name of the file.
+ * @param encoding The encoding to return contents as. Typical value is UTF-8.
+ * (see http://www.iana.org/assignments/character-sets)
+ * @param start Start position in the file.
+ * @param end End position to stop at (exclusive).
+ * @return Contents of file.
+ * @throws FileNotFoundException, IOException
+ */
+ public String readAsText(String filename, String encoding, int start, int end) throws FileNotFoundException, IOException {
+ int diff = end - start;
+ byte[] bytes = new byte[1000];
+ BufferedInputStream bis = new BufferedInputStream(getPathFromUri(filename), 1024);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ int numRead = 0;
+
+ if (start > 0) {
+ bis.skip(start);
+ }
+
+ while ( diff > 0 && (numRead = bis.read(bytes, 0, Math.min(1000, diff))) >= 0) {
+ diff -= numRead;
+ bos.write(bytes, 0, numRead);
+ }
+
+ return new String(bos.toByteArray(), encoding);
+ }
+
+ /**
+ * Read content of text file and return as base64 encoded data url.
+ *
+ * @param filename The name of the file.
+ * @return Contents of file = data:<media type>;base64,<data>
+ * @throws FileNotFoundException, IOException
+ */
+ public String readAsDataURL(String filename, int start, int end) throws FileNotFoundException, IOException {
+ int diff = end - start;
+ byte[] bytes = new byte[1000];
+ BufferedInputStream bis = new BufferedInputStream(getPathFromUri(filename), 1024);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ int numRead = 0;
+
+ if (start > 0) {
+ bis.skip(start);
+ }
+
+ while (diff > 0 && (numRead = bis.read(bytes, 0, Math.min(1000, diff))) >= 0) {
+ diff -= numRead;
+ bos.write(bytes, 0, numRead);
+ }
+
+ // Determine content type from file name
+ String contentType = null;
+ if (filename.startsWith("content:")) {
+ Uri fileUri = Uri.parse(filename);
+ contentType = this.cordova.getActivity().getContentResolver().getType(fileUri);
+ }
+ else {
+ contentType = getMimeType(filename);
+ }
+
+ byte[] base64 = Base64.encodeBase64(bos.toByteArray());
+ String data = "data:" + contentType + ";base64," + new String(base64);
+ return data;
+ }
+
+ /**
+ * Looks up the mime type of a given file name.
+ *
+ * @param filename
+ * @return a mime type
+ */
+ public static String getMimeType(String filename) {
+ if (filename != null) {
+ // Stupid bug in getFileExtensionFromUrl when the file name has a space
+ // So we need to replace the space with a url encoded %20
+
+ // CB-2185: Stupid bug not putting JPG extension in the mime-type map
+ String url = filename.replace(" ", "%20").toLowerCase();
+ MimeTypeMap map = MimeTypeMap.getSingleton();
+ String extension = MimeTypeMap.getFileExtensionFromUrl(url);
+ if (extension.toLowerCase().equals("3ga")) {
+ return "audio/3gpp";
+ } else {
+ return map.getMimeTypeFromExtension(extension);
+ }
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Write contents of file.
+ *
+ * @param filename The name of the file.
+ * @param data The contents of the file.
+ * @param offset The position to begin writing the file.
+ * @throws FileNotFoundException, IOException
+ */
+ /**/
+ public long write(String filename, String data, int offset) throws FileNotFoundException, IOException {
+ filename = stripFileProtocol(filename);
+
+ boolean append = false;
+ if (offset > 0) {
+ this.truncateFile(filename, offset);
+ append = true;
+ }
+
+ byte[] rawData = data.getBytes();
+ ByteArrayInputStream in = new ByteArrayInputStream(rawData);
+ FileOutputStream out = new FileOutputStream(filename, append);
+ byte buff[] = new byte[rawData.length];
+ in.read(buff, 0, buff.length);
+ out.write(buff, 0, rawData.length);
+ out.flush();
+ out.close();
+
+ return rawData.length;
+ }
+
+ /**
+ * Truncate the file to size
+ *
+ * @param filename
+ * @param size
+ * @throws FileNotFoundException, IOException
+ */
+ private long truncateFile(String filename, long size) throws FileNotFoundException, IOException {
+ filename = stripFileProtocol(filename);
+
+ RandomAccessFile raf = new RandomAccessFile(filename, "rw");
+ try {
+ if (raf.length() >= size) {
+ FileChannel channel = raf.getChannel();
+ channel.truncate(size);
+ return size;
+ }
+
+ return raf.length();
+ } finally {
+ raf.close();
+ }
+ }
+
+ /**
+ * Get an input stream based on file path or content:// uri
+ *
+ * @param path
+ * @return an input stream
+ * @throws FileNotFoundException
+ */
+ private InputStream getPathFromUri(String path) throws FileNotFoundException {
+ if (path.startsWith("content")) {
+ Uri uri = Uri.parse(path);
+ return cordova.getActivity().getContentResolver().openInputStream(uri);
+ }
+ else {
+ path = stripFileProtocol(path);
+ return new FileInputStream(path);
+ }
+ }
+
+ /**
+ * Queries the media store to find out what the file path is for the Uri we supply
+ *
+ * @param contentUri the Uri of the audio/image/video
+ * @param cordova the current application context
+ * @return the full path to the file
+ */
+ @SuppressWarnings("deprecation")
+ protected static String getRealPathFromURI(Uri contentUri, CordovaInterface cordova) {
+ final String scheme = contentUri.getScheme();
+
+ if (scheme.compareTo("content") == 0) {
+ String[] proj = { _DATA };
+ Cursor cursor = cordova.getActivity().managedQuery(contentUri, proj, null, null, null);
+ int column_index = cursor.getColumnIndexOrThrow(_DATA);
+ cursor.moveToFirst();
+ return cursor.getString(column_index);
+ } else if (scheme.compareTo("file") == 0) {
+ return contentUri.getPath();
+ } else {
+ return contentUri.toString();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/cordova-cli/blob/d61deccd/lib/cordova-android/framework/src/org/apache/cordova/GPSListener.java
----------------------------------------------------------------------
diff --git a/lib/cordova-android/framework/src/org/apache/cordova/GPSListener.java b/lib/cordova-android/framework/src/org/apache/cordova/GPSListener.java
new file mode 100755
index 0000000..daaf7ee
--- /dev/null
+++ b/lib/cordova-android/framework/src/org/apache/cordova/GPSListener.java
@@ -0,0 +1,50 @@
+/*
+ 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;
+
+import android.location.LocationManager;
+
+/**
+ * This class handles requests for GPS location services.
+ *
+ */
+public class GPSListener extends CordovaLocationListener {
+ public GPSListener(LocationManager locationManager, GeoBroker m) {
+ super(locationManager, m, "[Cordova GPSListener]");
+ }
+
+
+ /**
+ * Start requesting location updates.
+ *
+ * @param interval
+ */
+ @Override
+ protected void start() {
+ if (!this.running) {
+ if (this.locationManager.getProvider(LocationManager.GPS_PROVIDER) != null) {
+ this.running = true;
+ this.locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 60000, 0, this);
+ } else {
+ this.fail(CordovaLocationListener.POSITION_UNAVAILABLE, "GPS provider is not available.");
+ }
+ }
+ }
+}