/** * MicroEmulator * Copyright (C) 2006-2007 Bartek Teodorczyk <barteo@barteo.net> * Copyright (C) 2006-2007 Vlad Skarzhevskyy * * It is licensed under the following two licenses as alternatives: * 1. GNU Lesser General Public License (the "LGPL") version 2.1 or any newer version * 2. Apache License (the "AL") Version 2.0 * * You may not use this file except in compliance with at least one of * the above two licenses. * * You may obtain a copy of the LGPL at * http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt * * You may obtain a copy of the AL 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 LGPL or the AL for the specific language governing permissions and * limitations. * * @version $Id: FileSystemFileConnection.java 2198 2009-11-09 11:28:41Z barteo $ */ package org.microemu.cldc.file; import android.os.Environment; import jimm.modules.DebugLog; import java.io.*; import java.lang.reflect.Method; import java.security.*; import java.util.Enumeration; import java.util.Vector; import java.util.regex.Pattern; import javax.microedition.io.file.ConnectionClosedException; import javax.microedition.io.file.FileConnection; public class FileSystemFileConnection implements FileConnection { private File fsRoot; private String host; private String fullPath; private File file; private boolean isRoot; private boolean isDirectory; private Throwable locationClosedFrom = null; private FileSystemConnectorImpl notifyClosed; private InputStream opendInputStream; private OutputStream opendOutputStream; private final static char DIR_SEP = '/'; private final static String DIR_SEP_STR = "/"; /* The context to be used when acessing filesystem */ private AccessControlContext acc; private static boolean java15 = false; FileSystemFileConnection(String name, FileSystemConnectorImpl notifyClosed) throws IOException { // <host>/<path> int hostEnd = name.indexOf(DIR_SEP); if (hostEnd == -1) { throw new IOException("Invalid path " + name); } this.notifyClosed = notifyClosed; host = name.substring(0, hostEnd); fullPath = name.substring(hostEnd + 1); if (fullPath.length() == 0) { throw new IOException("Invalid path " + name); } // application path // "e:/jimm-multi/" is symlink to home if (fullPath.startsWith("e:/jimm-multi/")) { fullPath = fullPath.replaceFirst("e:/jimm-multi/", "~/"); } int rootEnd = fullPath.indexOf(DIR_SEP); isRoot = ((rootEnd == -1) || (rootEnd == fullPath.length() - 1)); if (DIR_SEP == fullPath.charAt(fullPath.length() - 1)) { fullPath = fullPath.substring(0, fullPath.length() - 1); } acc = AccessController.getContext(); doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { String dir = fullPath; String path = ""; if (-1 != dir.indexOf(DIR_SEP)) { path = dir.substring(dir.indexOf(DIR_SEP) + 1); dir = dir.substring(0, dir.indexOf(DIR_SEP)); } fsRoot = "~".equals(dir) ? getHome() : getRoot(dir); if (null == fsRoot) { throw new FileNotFoundException("file '" + dir + "' not found"); } file = "".equals(path) ? fsRoot : new File(fsRoot, path); isDirectory = file.isDirectory(); return null; } }); } private File getRoot(String dir) { for (File file : lsRoot()) { if (file.getName().equals(dir)) { return file; } } return null; } private File getHome() { File home; home = getExistHome("ext_card"); if (null != home) return home; home = getExistHome("sdcard"); if (null != home) return home; File root = getRoot("ext_card"); root = (null == root) ? getRoot("sdcard") : root; if (null != root) { home = new File(root, "jimm-multi"); if (!home.exists()) home.mkdir(); return home; } return null; //return new File(root, "Android/data/" + JimmActivity.PACKAGE_NAME + "/files/"); } private File getExistHome(String rootName) { File root = getRoot(rootName); if (null != root) { File home = new File(root, "jimm-multi"); return home.exists() ? home : null; } return null; } private <T> T doPrivilegedIO(PrivilegedExceptionAction<T> action) throws IOException { return FileSystemConnectorImpl.doPrivilegedIO(action, acc); } private abstract class PrivilegedBooleanAction implements PrivilegedAction { public Object run() { return getBoolean(); } abstract boolean getBoolean(); } private boolean doPrivilegedBoolean(PrivilegedBooleanAction action) { return (Boolean) AccessController.doPrivileged(action); } private static File create(File root, String dirName) { File dir = new File(root, dirName); if (!dir.exists()) { dir.mkdirs(); } return dir; } private static Vector<File> getLocalRoots() { Vector<File> ls = new Vector<File>(); try { File fsRoot = new File(System.getProperty("user.home") + "/.jimm/filesystem"); if (!fsRoot.exists()) { if (!fsRoot.mkdirs()) { throw new RuntimeException("Can't create filesystem root " + fsRoot.getAbsolutePath()); } // Create real roots C and E add(ls, create(fsRoot, "c")); add(ls, create(fsRoot, "e")); } } catch (Exception e) { System.out.println("Cannot access user.home " + e); } return ls; } private static Vector<File> lsRoot() { Vector<File> ls = new Vector<File>(); try { add(ls, Environment.getExternalStorageDirectory()); add(ls, new File("/sdcard")); add(ls, new File("/mnt/sdcard")); add(ls, getWritableDir("/mnt/ext_card")); File sdcard = getCardDir(); if (null != sdcard) { add(ls, new File(sdcard, "DCIM")); add(ls, new File(sdcard, "DCIM/Camera")); add(ls, new File(sdcard, "Books")); add(ls, new File(sdcard, "Download")); } } catch (Exception ignored) { } if (ls.isEmpty()) { try { ls = getLocalRoots(); } catch (Exception ignored) { } } return ls; } private static File getCardDir() { File ext = Environment.getExternalStorageDirectory(); if (isAccessible(ext)) return ext; if (isAccessible(new File("/sdcard"))) return new File("/sdcard"); if (isAccessible(new File("/mnt/sdcard"))) return new File("/mnt/sdcard"); if (isAccessible(new File("/mnt/ext_card"))) return new File("/mnt/ext_card"); return null; } private static File getWritableDir(String path) { File dir = new File(path); return (dir.canWrite() && dir.canRead() && dir.canExecute()) ? dir : null; } private static boolean isAccessible(File file) { if ((null == file) || !file.exists() || file.isHidden() || !file.isDirectory() || !file.canRead()) { return false; } return true; } private static void add(Vector<File> ls, File file) { if (!isAccessible(file)) { return; } for (File has : ls) { if (has.equals(file)) { return; } } ls.add(file); } static Enumeration listRoots() { Vector<String> list = new Vector<String>(); for (File file : lsRoot()) { list.add(file.getName() + DIR_SEP); } return list.elements(); } public long availableSize() { throwClosed(); if (fsRoot == null) { return -1; } return getFileValueJava6("getFreeSpace"); } public long totalSize() { throwClosed(); if (fsRoot == null) { return -1; } return getFileValueJava6("getTotalSpace"); } public boolean canRead() { throwClosed(); return doPrivilegedBoolean(new PrivilegedBooleanAction() { public boolean getBoolean() { return file.canRead(); } }); } public boolean canWrite() { throwClosed(); return doPrivilegedBoolean(new PrivilegedBooleanAction() { public boolean getBoolean() { return file.canWrite(); } }); } public void create() throws IOException { throwClosed(); doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { if (!file.createNewFile()) { throw new IOException("File already exists " + file.getAbsolutePath()); } return null; } }); } public void delete() throws IOException { throwClosed(); doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { if (!file.delete()) { throw new IOException("Unable to delete " + file.getAbsolutePath()); } return null; } }); } public long directorySize(final boolean includeSubDirs) throws IOException { throwClosed(); return (Long) doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { if (!file.isDirectory()) { throw new IOException("Not a directory " + file.getAbsolutePath()); } return directorySize(file, includeSubDirs); } }); } private static long directorySize(File dir, boolean includeSubDirs) throws IOException { long size = 0; File[] files = dir.listFiles(); if (files == null) { // null if security restricted return 0L; } for (int i = 0; i < files.length; i++) { File child = files[i]; if (includeSubDirs && child.isDirectory()) { size += directorySize(child, true); } else { size += child.length(); } } return size; } public boolean exists() { throwClosed(); return doPrivilegedBoolean(new PrivilegedBooleanAction() { public boolean getBoolean() { return file.exists(); } }); } public long fileSize() throws IOException { throwClosed(); return (Long) doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { return file.length(); } }); } public String getName() { // TODO test on real device. Not declared throwClosed(); if (isRoot) { return ""; } if (this.isDirectory) { return this.file.getName() + DIR_SEP; } else { return this.file.getName(); } } public String getPath() { // TODO test on real device. Not declared throwClosed(); // returns Parent directory // /<root>/<directory>/ if (isRoot) { return DIR_SEP + fullPath + DIR_SEP; } int pathEnd = fullPath.lastIndexOf(DIR_SEP); if (pathEnd == -1) { return DIR_SEP_STR; } return DIR_SEP + fullPath.substring(0, pathEnd + 1); } public String getURL() { // TODO test on real device. Not declared throwClosed(); // file://<host>/<root>/<directory>/<filename.extension> // or // file://<host>/<root>/<directory>/<directoryname>/ return FileSystem.PROTOCOL + this.host + DIR_SEP + fullPath + ((this.isDirectory) ? DIR_SEP_STR : ""); } public boolean isDirectory() { throwClosed(); return this.isDirectory; } public boolean isHidden() { throwClosed(); return doPrivilegedBoolean(new PrivilegedBooleanAction() { public boolean getBoolean() { return file.isHidden(); } }); } public long lastModified() { throwClosed(); return AccessController.doPrivileged(new PrivilegedAction<Long>() { public Long run() { return file.lastModified(); } }, acc); } public void mkdir() throws IOException { throwClosed(); doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { if (!file.mkdir()) { throw new IOException("Can't create directory " + file.getAbsolutePath()); } return null; } }); } public Enumeration list() throws IOException { return this.list(null, false); } public Enumeration list(final String filter, final boolean includeHidden) throws IOException { throwClosed(); return (Enumeration) doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { return listPrivileged(filter, includeHidden); } }); } private Enumeration listPrivileged(final String filter, boolean includeHidden) throws IOException { if (!this.file.isDirectory()) { throw new IOException("Not a directory " + this.file.getAbsolutePath()); } FilenameFilter filenameFilter = null; if (filter != null) { filenameFilter = new FilenameFilter() { private Pattern pattern; { /* convert simple search pattern to regexp */ pattern = Pattern.compile(filter.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*")); } public boolean accept(File dir, String name) { return pattern.matcher(name).matches(); } }; } File[] files = this.file.listFiles(filenameFilter); if (files == null) { // null if security restricted return (new Vector()).elements(); } Vector<String> list = new Vector<String>(); for (File child : files) { if ((!includeHidden) && (child.isHidden())) { continue; } if (child.isDirectory()) { list.add(child.getName() + DIR_SEP); } else { list.add(child.getName()); } } return list.elements(); } public boolean isOpen() { return (this.file != null); } private void throwOpenDirectory() throws IOException { if (this.isDirectory) { throw new IOException("Unable to open Stream on directory"); } } public InputStream openInputStream() throws IOException { throwClosed(); throwOpenDirectory(); if (this.opendInputStream != null) { throw new IOException("InputStream already opened"); } /** * Trying to open more than one InputStream or more than one * OutputStream from a StreamConnection causes an IOException. */ this.opendInputStream = (InputStream) doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { return new FileInputStream(file) { public void close() throws IOException { FileSystemFileConnection.this.opendInputStream = null; super.close(); } }; } }); return this.opendInputStream; } public String getAbsolutePath() throws IOException { return (String) doPrivilegedIO(new PrivilegedExceptionAction<String>() { public String run() throws IOException { if (!file.exists()) throw new FileNotFoundException(); return file.getAbsolutePath(); } }); } public DataInputStream openDataInputStream() throws IOException { return new DataInputStream(openInputStream()); } public OutputStream openOutputStream() throws IOException { return openOutputStream(false); } public OutputStream openOutputStream(final boolean append) throws IOException { throwClosed(); throwOpenDirectory(); if (this.opendOutputStream != null) { throw new IOException("OutputStream already opened"); } /** * Trying to open more than one InputStream or more than one * OutputStream from a StreamConnection causes an IOException. */ this.opendOutputStream = (OutputStream) doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { return new FileOutputStream(file, append) { public void close() throws IOException { FileSystemFileConnection.this.opendOutputStream = null; super.close(); } }; } }); return this.opendOutputStream; } public DataOutputStream openDataOutputStream() throws IOException { return new DataOutputStream(openOutputStream()); } public OutputStream openOutputStream(long byteOffset) throws IOException { throwClosed(); throwOpenDirectory(); if (this.opendOutputStream != null) { throw new IOException("OutputStream already opened"); } // we cannot truncate the file here since it could already have content // which should be overridden instead of wiped. return openOutputStream(true, byteOffset); } private OutputStream openOutputStream(boolean appendToFile, final long byteOffset) throws IOException { throwClosed(); throwOpenDirectory(); if (this.opendOutputStream != null) { throw new IOException("OutputStream already opened"); } /** * Trying to open more than one InputStream or more than one * OutputStream from a StreamConnection causes an IOException. */ this.opendOutputStream = (OutputStream) doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { RandomAccessFile raf = new RandomAccessFile(file, "rw"); raf.seek(byteOffset); return new FileOutputStream(raf.getFD()) { public void close() throws IOException { FileSystemFileConnection.this.opendOutputStream = null; super.close(); } }; } }); return this.opendOutputStream; } public void rename(final String newName) throws IOException { throwClosed(); if (newName.indexOf(DIR_SEP) != -1) { throw new IllegalArgumentException("Name contains path specification " + newName); } doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { File newFile = new File(file.getParentFile(), newName); if (!file.renameTo(newFile)) { throw new IOException("Unable to rename " + file.getAbsolutePath() + " to " + newFile.getAbsolutePath()); } return null; } }); this.fullPath = this.getPath() + newName; } public void setFileConnection(String s) throws IOException { throwClosed(); // TODO Auto-generated method stub } public void setHidden(boolean hidden) throws IOException { throwClosed(); } private void fileSetJava16(String mehtodName, final Boolean param) throws IOException { if (java15) { throw new IOException("Not supported on Java version < 6"); } // Use Java6 function in reflection. try { final Method setWritable = file.getClass().getMethod(mehtodName, boolean.class); doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { try { setWritable.invoke(file, param); } catch (Exception e) { throw new IOException(e.getCause().getMessage()); } file.setReadOnly(); return null; } }); } catch (NoSuchMethodException e) { java15 = true; throw new IOException("Not supported on Java version < 6"); } } private long getFileValueJava6(String methodName) throws SecurityException { if (java15) { throw new SecurityException("Not supported on Java version < 6"); } // Use Java6 function in reflection. try { final Method getter = file.getClass().getMethod(methodName); Long rc = (Long) doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { try { return getter.invoke(file); } catch (Exception e) { throw new IOException(e.getCause().getMessage()); } } }); return rc.longValue(); } catch (IOException e) { throw new SecurityException(e.getMessage()); } catch (NoSuchMethodException e) { java15 = true; throw new SecurityException("Not supported on Java version < 6"); } } public void setReadable(boolean readable) throws IOException { throwClosed(); fileSetJava16("setReadable", readable); } public void setWritable(boolean writable) throws IOException { throwClosed(); if (!writable) { doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { file.setReadOnly(); return null; } }); } else { fileSetJava16("setWritable", writable); } } public void truncate(final long byteOffset) throws IOException { throwClosed(); doPrivilegedIO(new PrivilegedExceptionAction() { public Object run() throws IOException { RandomAccessFile raf = new RandomAccessFile(file, "rw"); try { raf.setLength(byteOffset); } finally { raf.close(); } return null; } }); } public long usedSize() { try { return fileSize(); } catch (IOException e) { return -1; } } public void close() throws IOException { if (this.file != null) { if (this.notifyClosed != null) { this.notifyClosed.notifyClosed(this); } locationClosedFrom = new Throwable(); locationClosedFrom.fillInStackTrace(); this.file = null; } } private void throwClosed() throws ConnectionClosedException { if (this.file == null) { if (locationClosedFrom != null) { locationClosedFrom.printStackTrace(); } throw new ConnectionClosedException("Connection already closed"); } } }