From 2b69aa10d591edca214a29161ae557ba4f8da711 Mon Sep 17 00:00:00 2001 From: seven332 Date: Sun, 7 Jun 2015 02:05:52 +0800 Subject: [PATCH] Add UniFile --- .../java/com/hippo/unifile/DocumentFile.java | 166 +++++++++ .../hippo/unifile/DocumentsContractApi19.java | 200 ++++++++++ .../hippo/unifile/DocumentsContractApi21.java | 105 ++++++ .../main/java/com/hippo/unifile/RawFile.java | 194 ++++++++++ .../main/java/com/hippo/unifile/UniFile.java | 347 ++++++++++++++++++ 5 files changed, 1012 insertions(+) create mode 100644 app/src/main/java/com/hippo/unifile/DocumentFile.java create mode 100644 app/src/main/java/com/hippo/unifile/DocumentsContractApi19.java create mode 100644 app/src/main/java/com/hippo/unifile/DocumentsContractApi21.java create mode 100644 app/src/main/java/com/hippo/unifile/RawFile.java create mode 100644 app/src/main/java/com/hippo/unifile/UniFile.java diff --git a/app/src/main/java/com/hippo/unifile/DocumentFile.java b/app/src/main/java/com/hippo/unifile/DocumentFile.java new file mode 100644 index 000000000..b0f273d82 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/DocumentFile.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.unifile; + +import android.content.Context; +import android.net.Uri; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +class DocumentFile extends UniFile { + + private Context mContext; + private Uri mUri; + + DocumentFile(UniFile parent, Context context, Uri uri) { + super(parent); + mContext = context; + mUri = uri; + } + + @Override + public UniFile createFile(String displayName) { + UniFile child = contains(displayName); + + if (child != null) { + if (child.isFile()) { + return child; + } else { + return null; + } + } else { + final Uri result = DocumentsContractApi21.createFile(mContext, mUri, "application/octet-stream", displayName); + return (result != null) ? new DocumentFile(this, mContext, result) : null; + } + } + + @Override + public UniFile createDirectory(String displayName) { + UniFile child = contains(displayName); + + if (child != null) { + if (child.isDirectory()) { + return child; + } else { + return null; + } + } else { + final Uri result = DocumentsContractApi21.createDirectory(mContext, mUri, displayName); + return (result != null) ? new DocumentFile(this, mContext, result) : null; + } + } + + @Override + public Uri getUri() { + return mUri; + } + + @Override + public String getName() { + return DocumentsContractApi19.getName(mContext, mUri); + } + + @Override + public String getType() { + return DocumentsContractApi19.getType(mContext, mUri); + } + + @Override + public boolean isDirectory() { + return DocumentsContractApi19.isDirectory(mContext, mUri); + } + + @Override + public boolean isFile() { + return DocumentsContractApi19.isFile(mContext, mUri); + } + + @Override + public long lastModified() { + return DocumentsContractApi19.lastModified(mContext, mUri); + } + + @Override + public long length() { + return DocumentsContractApi19.length(mContext, mUri); + } + + @Override + public boolean canRead() { + return DocumentsContractApi19.canRead(mContext, mUri); + } + + @Override + public boolean canWrite() { + return DocumentsContractApi19.canWrite(mContext, mUri); + } + + @Override + public boolean delete() { + return DocumentsContractApi19.delete(mContext, mUri); + } + + @Override + public boolean exists() { + return DocumentsContractApi19.exists(mContext, mUri); + } + + @Override + public UniFile contains(String displayName) { + Uri childUri = DocumentsContractApi21.buildChildUri(mUri, displayName); + return DocumentsContractApi19.exists(mContext, childUri) ? + new DocumentFile(this, mContext, childUri) : null; + } + + @Override + public UniFile[] listFiles() { + final Uri[] result = DocumentsContractApi21.listFiles(mContext, mUri); + final UniFile[] resultFiles = new UniFile[result.length]; + for (int i = 0; i < result.length; i++) { + resultFiles[i] = new DocumentFile(this, mContext, result[i]); + } + return resultFiles; + } + + @Override + public boolean renameTo(String displayName) { + final Uri result = DocumentsContractApi21.renameTo(mContext, mUri, displayName); + if (result != null) { + mUri = result; + return true; + } else { + return false; + } + } + + @Override + public OutputStream openOutputStream() throws IOException { + return mContext.getContentResolver().openOutputStream(mUri); + } + + @Override + public OutputStream openOutputStream(boolean append) throws IOException { + return mContext.getContentResolver().openOutputStream(mUri, append ? "wa" : "w"); + } + + @Override + public InputStream openInputStream() throws IOException { + return mContext.getContentResolver().openInputStream(mUri); + } +} diff --git a/app/src/main/java/com/hippo/unifile/DocumentsContractApi19.java b/app/src/main/java/com/hippo/unifile/DocumentsContractApi19.java new file mode 100644 index 000000000..c4c37c97e --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/DocumentsContractApi19.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.unifile; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.text.TextUtils; +import android.util.Log; + +@TargetApi(Build.VERSION_CODES.KITKAT) +class DocumentsContractApi19 { + private static final String TAG = "UniFile"; + + public static boolean isDocumentUri(Context context, Uri self) { + return DocumentsContract.isDocumentUri(context, self); + } + + public static String getName(Context context, Uri self) { + return queryForString(context, self, DocumentsContract.Document.COLUMN_DISPLAY_NAME, null); + } + + private static String getRawType(Context context, Uri self) { + return queryForString(context, self, DocumentsContract.Document.COLUMN_MIME_TYPE, null); + } + + public static String getType(Context context, Uri self) { + final String rawType = getRawType(context, self); + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(rawType)) { + return null; + } else { + return rawType; + } + } + + public static boolean isDirectory(Context context, Uri self) { + return DocumentsContract.Document.MIME_TYPE_DIR.equals(getRawType(context, self)); + } + + public static boolean isFile(Context context, Uri self) { + final String type = getRawType(context, self); + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(type) || TextUtils.isEmpty(type)) { + return false; + } else { + return true; + } + } + + public static long lastModified(Context context, Uri self) { + return queryForLong(context, self, DocumentsContract.Document.COLUMN_LAST_MODIFIED, 0); + } + + public static long length(Context context, Uri self) { + return queryForLong(context, self, DocumentsContract.Document.COLUMN_SIZE, 0); + } + + public static boolean canRead(Context context, Uri self) { + // Ignore if grant doesn't allow read + if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_READ_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + + // Ignore documents without MIME + if (TextUtils.isEmpty(getRawType(context, self))) { + return false; + } + + return true; + } + + public static boolean canWrite(Context context, Uri self) { + // Ignore if grant doesn't allow write + if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + + final String type = getRawType(context, self); + final int flags = queryForInt(context, self, DocumentsContract.Document.COLUMN_FLAGS, 0); + + // Ignore documents without MIME + if (TextUtils.isEmpty(type)) { + return false; + } + + // Deletable documents considered writable + if ((flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0) { + return true; + } + + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(type) + && (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0) { + // Directories that allow create considered writable + return true; + } else if (!TextUtils.isEmpty(type) + && (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0) { + // Writable normal files considered writable + return true; + } + + return false; + } + + public static boolean delete(Context context, Uri self) { + return DocumentsContract.deleteDocument(context.getContentResolver(), self); + } + + public static boolean exists(Context context, Uri self) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { + DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + return c.getCount() > 0; + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + return false; + } finally { + closeQuietly(c); + } + } + + private static String queryForString(Context context, Uri self, String column, + String defaultValue) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { column }, null, null, null); + if (c.moveToFirst() && !c.isNull(0)) { + return c.getString(0); + } else { + return defaultValue; + } + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + return defaultValue; + } finally { + closeQuietly(c); + } + } + + private static int queryForInt(Context context, Uri self, String column, + int defaultValue) { + return (int) queryForLong(context, self, column, defaultValue); + } + + private static long queryForLong(Context context, Uri self, String column, + long defaultValue) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { column }, null, null, null); + if (c.moveToFirst() && !c.isNull(0)) { + return c.getLong(0); + } else { + return defaultValue; + } + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + return defaultValue; + } finally { + closeQuietly(c); + } + } + + private static void closeQuietly(AutoCloseable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } +} diff --git a/app/src/main/java/com/hippo/unifile/DocumentsContractApi21.java b/app/src/main/java/com/hippo/unifile/DocumentsContractApi21.java new file mode 100644 index 000000000..8042e169c --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/DocumentsContractApi21.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.unifile; + +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +class DocumentsContractApi21 { + private static final String TAG = "UniFile"; + + private static final String PATH_DOCUMENT = "document"; + private static final String PATH_TREE = "tree"; + + public static Uri createFile(Context context, Uri self, String mimeType, + String displayName) { + return DocumentsContract.createDocument(context.getContentResolver(), self, mimeType, + displayName); + } + + public static Uri createDirectory(Context context, Uri self, String displayName) { + return createFile(context, self, DocumentsContract.Document.MIME_TYPE_DIR, displayName); + } + + public static Uri prepareTreeUri(Uri treeUri) { + return DocumentsContract.buildDocumentUriUsingTree(treeUri, + DocumentsContract.getTreeDocumentId(treeUri)); + } + + public static String getTreeDocumentPath(Uri documentUri) { + final List paths = documentUri.getPathSegments(); + if (paths.size() >= 4 && PATH_TREE.equals(paths.get(0)) && PATH_DOCUMENT.equals(paths.get(2))) { + return paths.get(3); + } + throw new IllegalArgumentException("Invalid URI: " + documentUri); + } + + public static Uri buildChildUri(Uri uri, String displayName) { + return DocumentsContract.buildDocumentUriUsingTree(uri, + getTreeDocumentPath(uri) + "/" + displayName); + } + + public static Uri[] listFiles(Context context, Uri self) { + final ContentResolver resolver = context.getContentResolver(); + final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(self, + DocumentsContract.getDocumentId(self)); + final ArrayList results = new ArrayList(); + + Cursor c = null; + try { + c = resolver.query(childrenUri, new String[] { + DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + while (c.moveToNext()) { + final String documentId = c.getString(0); + final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(self, + documentId); + results.add(documentUri); + } + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + } finally { + closeQuietly(c); + } + + return results.toArray(new Uri[results.size()]); + } + + public static Uri renameTo(Context context, Uri self, String displayName) { + return DocumentsContract.renameDocument(context.getContentResolver(), self, displayName); + } + + private static void closeQuietly(AutoCloseable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } +} diff --git a/app/src/main/java/com/hippo/unifile/RawFile.java b/app/src/main/java/com/hippo/unifile/RawFile.java new file mode 100644 index 000000000..ab4540d4d --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/RawFile.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.unifile; + +import android.net.Uri; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; + +class RawFile extends UniFile { + private File mFile; + + RawFile(UniFile parent, File file) { + super(parent); + mFile = file; + } + + @Override + public UniFile createFile(String displayName) { + final File target = new File(mFile, displayName); + try { + target.createNewFile(); + return new RawFile(this, target); + } catch (IOException e) { + Log.w(TAG, "Failed to createFile: " + e); + return null; + } + } + + @Override + public UniFile createDirectory(String displayName) { + final File target = new File(mFile, displayName); + if (target.isDirectory() || target.mkdir()) { + return new RawFile(this, target); + } else { + return null; + } + } + + @Override + public Uri getUri() { + return Uri.fromFile(mFile); + } + + @Override + public String getName() { + return mFile.getName(); + } + + @Override + public String getType() { + if (mFile.isDirectory()) { + return null; + } else { + return getTypeForName(mFile.getName()); + } + } + + @Override + public boolean isDirectory() { + return mFile.isDirectory(); + } + + @Override + public boolean isFile() { + return mFile.isFile(); + } + + @Override + public long lastModified() { + return mFile.lastModified(); + } + + @Override + public long length() { + return mFile.length(); + } + + @Override + public boolean canRead() { + return mFile.canRead(); + } + + @Override + public boolean canWrite() { + return mFile.canWrite(); + } + + @Override + public boolean delete() { + deleteContents(mFile); + return mFile.delete(); + } + + @Override + public boolean exists() { + return mFile.exists(); + } + + @Override + public UniFile contains(String displayName) { + final File child = new File(mFile, displayName); + return child.exists() ? new RawFile(this, child) : null; + } + + @Override + public UniFile[] listFiles() { + final ArrayList results = new ArrayList(); + final File[] files = mFile.listFiles(); + if (files != null) { + for (File file : files) { + results.add(new RawFile(this, file)); + } + } + return results.toArray(new UniFile[results.size()]); + } + + @Override + public boolean renameTo(String displayName) { + final File target = new File(mFile.getParentFile(), displayName); + if (mFile.renameTo(target)) { + mFile = target; + return true; + } else { + return false; + } + } + + @Override + public OutputStream openOutputStream() throws IOException { + return new FileOutputStream(mFile); + } + + @Override + public OutputStream openOutputStream(boolean append) throws IOException { + return new FileOutputStream(mFile, append); + } + + @Override + public InputStream openInputStream() throws IOException { + return new FileInputStream(mFile); + } + + private static String getTypeForName(String name) { + final int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = name.substring(lastDot + 1).toLowerCase(); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) { + return mime; + } + } + + return "application/octet-stream"; + } + + private static boolean deleteContents(File dir) { + File[] files = dir.listFiles(); + boolean success = true; + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + success &= deleteContents(file); + } + if (!file.delete()) { + Log.w(TAG, "Failed to delete " + file); + success = false; + } + } + } + return success; + } +} diff --git a/app/src/main/java/com/hippo/unifile/UniFile.java b/app/src/main/java/com/hippo/unifile/UniFile.java new file mode 100644 index 000000000..95e832d30 --- /dev/null +++ b/app/src/main/java/com/hippo/unifile/UniFile.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hippo.unifile; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Representation of a document backed by either a + * {@link android.provider.DocumentsProvider} or a raw file on disk. This is a + * utility class designed to emulate the traditional {@link File} interface. It + * offers a simplified view of a tree of documents, but it has substantial + * overhead. For optimal performance and a richer feature set, use the + * {@link android.provider.DocumentsContract} methods and constants directly. + *

+ * There are several differences between documents and traditional files: + *

    + *
  • Documents express their display name and MIME type as separate fields, + * instead of relying on file extensions. Some documents providers may still + * choose to append extensions to their display names, but that's an + * implementation detail. + *
  • A single document may appear as the child of multiple directories, so it + * doesn't inherently know who its parent is. That is, documents don't have a + * strong notion of path. You can easily traverse a tree of documents from + * parent to child, but not from child to parent. + *
  • Each document has a unique identifier within that provider. This + * identifier is an opaque implementation detail of the provider, and + * as such it must not be parsed. + *
+ *

+ * Before using this class, first consider if you really need access to an + * entire subtree of documents. The principle of least privilege dictates that + * you should only ask for access to documents you really need. If you only need + * the user to pick a single file, use {@link Intent#ACTION_OPEN_DOCUMENT} or + * {@link Intent#ACTION_GET_CONTENT}. If you want to let the user pick multiple + * files, add {@link Intent#EXTRA_ALLOW_MULTIPLE}. If you only need the user to + * save a single file, use {@link Intent#ACTION_CREATE_DOCUMENT}. + *

+ * If you really do need full access to an entire subtree of documents, start by + * launching {@link Intent#ACTION_OPEN_DOCUMENT_TREE} to let the user pick a + * directory. Then pass the resulting {@link Intent#getData()} into + * {@link #fromTreeUri(Context, Uri)} to start working with the user selected + * tree. + *

+ * As you navigate the tree of UniFile instances, you can always use + * {@link #getUri()} to obtain the Uri representing the underlying document for + * that object, for use with {@link ContentResolver#openInputStream(Uri)}, etc. + *

+ * To simplify your code on devices running + * {@link Build.VERSION_CODES#KITKAT} or earlier, you can use + * {@link #fromFile(File)} which emulates the behavior of a + * {@link android.provider.DocumentsProvider}. + * + * @see android.provider.DocumentsProvider + * @see android.provider.DocumentsContract + */ +public abstract class UniFile { + static final String TAG = "UniFile"; + + private final UniFile mParent; + + UniFile(UniFile parent) { + mParent = parent; + } + + /** + * Create a {@link UniFile} representing the filesystem tree rooted at + * the given {@link Uri}. This doesn't give you any additional access to the + * underlying files beyond what your app already has. + *

+ * {@link #getUri()} will return {@code file://} Uris for files explored + * through this tree. + */ + public static UniFile fromFile(File file) { + return new RawFile(null, file); + } + + /** + * Create a {@link UniFile} representing the document tree rooted at + * the given {@link Uri}. This is only useful on devices running + * {@link Build.VERSION_CODES#LOLLIPOP} or later, and will return + * {@code null} when called on earlier platform versions. + * + * @param treeUri the {@link Intent#getData()} from a successful + * {@link Intent#ACTION_OPEN_DOCUMENT_TREE} request. + */ + public static UniFile fromTreeUri(Context context, Uri treeUri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return new DocumentFile(null, context, + DocumentsContractApi21.prepareTreeUri(treeUri)); + } else { + return null; + } + } + + /** + * Test if given Uri is backed by a + * {@link android.provider.DocumentsProvider}. + */ + public static boolean isDocumentUri(Context context, Uri uri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return DocumentsContractApi19.isDocumentUri(context, uri); + } else { + return false; + } + } + + /** + * Create a new document as a direct child of this directory. + * + * @param displayName name of new document, without any file extension + * appended; the underlying provider may choose to append the + * extension + * @return file representing newly created document, or null if failed + * @see android.provider.DocumentsContract#createDocument(ContentResolver, + * Uri, String, String) + */ + public abstract UniFile createFile(String displayName); + + /** + * Create a new directory as a direct child of this directory. + * + * @param displayName name of new directory + * @return file representing newly created directory, or null if failed + * @see android.provider.DocumentsContract#createDocument(ContentResolver, + * Uri, String, String) + */ + public abstract UniFile createDirectory(String displayName); + + /** + * Return a Uri for the underlying document represented by this file. This + * can be used with other platform APIs to manipulate or share the + * underlying content. You can use {@link #isDocumentUri(Context, Uri)} to + * test if the returned Uri is backed by a + * {@link android.provider.DocumentsProvider}. + * + * @see Intent#setData(Uri) + * @see Intent#setClipData(android.content.ClipData) + * @see ContentResolver#openInputStream(Uri) + * @see ContentResolver#openOutputStream(Uri) + * @see ContentResolver#openFileDescriptor(Uri, String) + */ + public abstract Uri getUri(); + + /** + * Return the display name of this document. + * + * @see android.provider.DocumentsContract.Document#COLUMN_DISPLAY_NAME + */ + public abstract String getName(); + + /** + * Return the MIME type of this document. + * + * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE + */ + public abstract String getType(); + + /** + * Return the parent file of this document. Only defined inside of the + * user-selected tree; you can never escape above the top of the tree. + *

+ * The underlying {@link android.provider.DocumentsProvider} only defines a + * forward mapping from parent to child, so the reverse mapping of child to + * parent offered here is purely a convenience method, and it may be + * incorrect if the underlying tree structure changes. + */ + public UniFile getParentFile() { + return mParent; + } + + /** + * Indicates if this file represents a directory. + * + * @return {@code true} if this file is a directory, {@code false} + * otherwise. + * @see android.provider.DocumentsContract.Document#MIME_TYPE_DIR + */ + public abstract boolean isDirectory(); + + /** + * Indicates if this file represents a file. + * + * @return {@code true} if this file is a file, {@code false} otherwise. + * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE + */ + public abstract boolean isFile(); + + /** + * Returns the time when this file was last modified, measured in + * milliseconds since January 1st, 1970, midnight. Returns 0 if the file + * does not exist, or if the modified time is unknown. + * + * @return the time when this file was last modified. + * @see android.provider.DocumentsContract.Document#COLUMN_LAST_MODIFIED + */ + public abstract long lastModified(); + + /** + * Returns the length of this file in bytes. Returns 0 if the file does not + * exist, or if the length is unknown. The result for a directory is not + * defined. + * + * @return the number of bytes in this file. + * @see android.provider.DocumentsContract.Document#COLUMN_SIZE + */ + public abstract long length(); + + /** + * Indicates whether the current context is allowed to read from this file. + * + * @return {@code true} if this file can be read, {@code false} otherwise. + */ + public abstract boolean canRead(); + + /** + * Indicates whether the current context is allowed to write to this file. + * + * @return {@code true} if this file can be written, {@code false} + * otherwise. + * @see android.provider.DocumentsContract.Document#COLUMN_FLAGS + * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE + * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE + * @see android.provider.DocumentsContract.Document#FLAG_DIR_SUPPORTS_CREATE + */ + public abstract boolean canWrite(); + + /** + * Deletes this file. + *

+ * Note that this method does not throw {@code IOException} on + * failure. Callers must check the return value. + * + * @return {@code true} if this file was deleted, {@code false} otherwise. + * @see android.provider.DocumentsContract#deleteDocument(ContentResolver, + * Uri) + */ + public abstract boolean delete(); + + /** + * Returns a boolean indicating whether this file can be found. + * + * @return {@code true} if this file exists, {@code false} otherwise. + */ + public abstract boolean exists(); + + /** + * Returns the file contained in the directory represented by this + * file. + * + * @return the file, or null if it does not exist. + */ + public abstract UniFile contains(String displayName); + + /** + * Returns an array of files contained in the directory represented by this + * file. + * + * @return an array of files or {@code null}. + * @see android.provider.DocumentsContract#buildChildDocumentsUriUsingTree(Uri, + * String) + */ + public abstract UniFile[] listFiles(); + + /** + * Search through {@link #listFiles()} for the first document matching the + * given display name. Returns {@code null} when no matching document is + * found. + */ + public UniFile findFile(String displayName) { + for (UniFile doc : listFiles()) { + if (displayName.equals(doc.getName())) { + return doc; + } + } + return null; + } + + /** + * Renames this file to {@code displayName}. + *

+ * Note that this method does not throw {@code IOException} on + * failure. Callers must check the return value. + *

+ * Some providers may need to create a new document to reflect the rename, + * potentially with a different MIME type, so {@link #getUri()} and + * {@link #getType()} may change to reflect the rename. + *

+ * When renaming a directory, children previously enumerated through + * {@link #listFiles()} may no longer be valid. + * + * @param displayName the new display name. + * @return true on success. + * @see android.provider.DocumentsContract#renameDocument(ContentResolver, + * Uri, String) + */ + public abstract boolean renameTo(String displayName); + + /** + * Open a stream on to the content associated with the file, clean it if it exists + * + * @return the {@link OutputStream} + * @throws IOException + */ + public abstract OutputStream openOutputStream() throws IOException; + + /** + * Open a stream on to the content associated with the file + * + * @param append {@code true} for do not clean it if it exists + * @return the {@link OutputStream} + * @throws IOException + */ + public abstract OutputStream openOutputStream(boolean append) throws IOException; + + /** + * Open a stream on to the content associated with the file + * + * @return the {@link InputStream} + * @throws IOException + */ + public abstract InputStream openInputStream() throws IOException; +}