/* 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.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; import org.apache.cordova.CordovaInterface; import org.apache.cordova.CordovaResourceApi; import org.apache.cordova.CordovaWebView; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; import android.provider.OpenableColumns; public class ContentFilesystem extends Filesystem { private CordovaInterface cordova; private CordovaResourceApi resourceApi; public ContentFilesystem(String name, CordovaInterface cordova, CordovaWebView webView) { this.name = name; this.cordova = cordova; this.resourceApi = new CordovaResourceApi(webView.getContext(), webView.pluginManager); } @Override public JSONObject getEntryForLocalURL(LocalFilesystemURL inputURL) throws IOException { // Get the cursor to validate that the file exists Cursor cursor = openCursorForURL(inputURL); String filePath = null; try { if (cursor == null || !cursor.moveToFirst()) { throw new FileNotFoundException(); } filePath = filesystemPathForCursor(cursor); } finally { if (cursor != null) cursor.close(); } if (filePath == null) { filePath = inputURL.URL.toString(); } else { filePath = "file://" + filePath; } try { return makeEntryForPath(inputURL.fullPath, inputURL.filesystemName, false /*fp.isDirectory()*/, filePath); } catch (JSONException e) { throw new IOException(); } } @Override public JSONObject getFileForLocalURL(LocalFilesystemURL inputURL, String fileName, JSONObject options, boolean directory) throws IOException, TypeMismatchException, JSONException { if (options != null) { if (options.optBoolean("create")) { throw new IOException("Cannot create content url"); } } LocalFilesystemURL requestedURL = new LocalFilesystemURL(Uri.withAppendedPath(inputURL.URL, fileName)); File fp = new File(this.filesystemPathForURL(requestedURL)); 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 makeEntryForPath(requestedURL.fullPath, requestedURL.filesystemName, directory, Uri.fromFile(fp).toString()); } @Override public boolean removeFileAtLocalURL(LocalFilesystemURL inputURL) throws NoModificationAllowedException { String filePath = filesystemPathForURL(inputURL); File file = new File(filePath); try { this.cordova.getActivity().getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStore.Images.Media.DATA + " = ?", new String[] { filePath }); } 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. } return file.delete(); } @Override public boolean recursiveRemoveFileAtLocalURL(LocalFilesystemURL inputURL) throws NoModificationAllowedException { throw new NoModificationAllowedException("Cannot remove content url"); } @Override public JSONArray readEntriesAtLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException { // TODO Auto-generated method stub return null; } @Override public JSONObject getFileMetadataForLocalURL(LocalFilesystemURL inputURL) throws FileNotFoundException { Integer size = null; Integer lastModified = null; Cursor cursor = openCursorForURL(inputURL); try { if (cursor != null && cursor.moveToFirst()) { size = resourceSizeForCursor(cursor); lastModified = lastModifiedDateForCursor(cursor); } else { throw new FileNotFoundException(); } } finally { if (cursor != null) cursor.close(); } JSONObject metadata = new JSONObject(); try { metadata.put("size", size); metadata.put("type", resourceApi.getMimeType(inputURL.URL)); metadata.put("name", inputURL.filesystemName); metadata.put("fullPath", inputURL.fullPath); metadata.put("lastModifiedDate", lastModified); } catch (JSONException e) { return null; } return metadata; } @Override public JSONObject copyFileToURL(LocalFilesystemURL destURL, String newName, Filesystem srcFs, LocalFilesystemURL srcURL, boolean move) throws IOException, InvalidModificationException, JSONException, NoModificationAllowedException, FileExistsException { if (LocalFilesystem.class.isInstance(srcFs)) { /* Same FS, we can shortcut with CordovaResourceApi operations */ // Figure out where we should be copying to final LocalFilesystemURL destinationURL = makeDestinationURL(newName, srcURL, destURL); OutputStream os = resourceApi.openOutputStream(destURL.URL); CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(srcURL.URL); if (move && !srcFs.canRemoveFileAtLocalURL(srcURL)) { throw new NoModificationAllowedException("Cannot move file at source URL"); } try { resourceApi.copyResource(ofrr, os); } catch (IOException e) { throw new IOException("Cannot read file at source URL"); } if (move) { srcFs.removeFileAtLocalURL(srcURL); } return makeEntryForURL(destinationURL, false, destinationURL.URL.toString()); } else { // Need to copy the hard way return super.copyFileToURL(destURL, newName, srcFs, srcURL, move); } } @Override public void readFileAtURL(LocalFilesystemURL inputURL, long start, long end, ReadFileCallback readFileCallback) throws IOException { CordovaResourceApi.OpenForReadResult ofrr = resourceApi.openForRead(inputURL.URL); if (end < 0) { end = ofrr.length; } long numBytesToRead = end - start; try { if (start > 0) { ofrr.inputStream.skip(start); } LimitedInputStream inputStream = new LimitedInputStream(ofrr.inputStream, numBytesToRead); readFileCallback.handleData(inputStream, ofrr.mimeType); } finally { ofrr.inputStream.close(); } } @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(LocalFilesystemURL url) { ContentResolver contentResolver = this.cordova.getActivity().getContentResolver(); Cursor cursor = contentResolver.query(url.URL, null, null, null, null); return cursor; } protected String filesystemPathForCursor(Cursor cursor) { final String[] LOCAL_FILE_PROJECTION = { MediaStore.Images.Media.DATA }; int columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]); if (columnIndex != -1) { return cursor.getString(columnIndex); } return null; } protected Integer resourceSizeForCursor(Cursor cursor) { int columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE); if (columnIndex != -1) { String sizeStr = cursor.getString(columnIndex); if (sizeStr != null) { return Integer.parseInt(sizeStr,10); } } return null; } protected Integer lastModifiedDateForCursor(Cursor cursor) { final String[] LOCAL_FILE_PROJECTION = { MediaStore.MediaColumns.DATE_MODIFIED }; int columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]); if (columnIndex != -1) { String dateStr = cursor.getString(columnIndex); if (dateStr != null) { return Integer.parseInt(dateStr,10); } } return null; } @Override public String filesystemPathForURL(LocalFilesystemURL url) { Cursor cursor = openCursorForURL(url); try { if (cursor != null && cursor.moveToFirst()) { return filesystemPathForCursor(cursor); } } finally { if (cursor != null) cursor.close(); } return null; } @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) { String path = filesystemPathForURL(inputURL); File file = new File(path); return file.exists(); } @Override OutputStream getOutputStreamForURL(LocalFilesystemURL inputURL) throws IOException { OutputStream os = resourceApi.openOutputStream(inputURL.URL); return os; } }