1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.documentfile.provider; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.net.Uri; 23 import android.os.Build; 24 import android.provider.DocumentsContract; 25 26 import androidx.annotation.NonNull; 27 import androidx.annotation.Nullable; 28 29 import java.io.File; 30 31 /** 32 * Representation of a document backed by either a 33 * {@link android.provider.DocumentsProvider} or a raw file on disk. This is a 34 * utility class designed to emulate the traditional {@link File} interface. It 35 * offers a simplified view of a tree of documents, but it has substantial 36 * overhead. For optimal performance and a richer feature set, use the 37 * {@link android.provider.DocumentsContract} methods and constants directly. 38 * <p> 39 * There are several differences between documents and traditional files: 40 * <ul> 41 * <li>Documents express their display name and MIME type as separate fields, 42 * instead of relying on file extensions. Some documents providers may still 43 * choose to append extensions to their display names, but that's an 44 * implementation detail. 45 * <li>A single document may appear as the child of multiple directories, so it 46 * doesn't inherently know who its parent is. That is, documents don't have a 47 * strong notion of path. You can easily traverse a tree of documents from 48 * parent to child, but not from child to parent. 49 * <li>Each document has a unique identifier within that provider. This 50 * identifier is an <em>opaque</em> implementation detail of the provider, and 51 * as such it must not be parsed. 52 * </ul> 53 * <p> 54 * Before using this class, first consider if you really need access to an 55 * entire subtree of documents. The principle of least privilege dictates that 56 * you should only ask for access to documents you really need. If you only need 57 * the user to pick a single file, use {@link Intent#ACTION_OPEN_DOCUMENT} or 58 * {@link Intent#ACTION_GET_CONTENT}. If you want to let the user pick multiple 59 * files, add {@link Intent#EXTRA_ALLOW_MULTIPLE}. If you only need the user to 60 * save a single file, use {@link Intent#ACTION_CREATE_DOCUMENT}. If you use 61 * these APIs, you can pass the resulting {@link Intent#getData()} into 62 * {@link #fromSingleUri(Context, Uri)} to work with that document. 63 * <p> 64 * If you really do need full access to an entire subtree of documents, start by 65 * launching {@link Intent#ACTION_OPEN_DOCUMENT_TREE} to let the user pick a 66 * directory. Then pass the resulting {@link Intent#getData()} into 67 * {@link #fromTreeUri(Context, Uri)} to start working with the user selected 68 * tree. 69 * <p> 70 * As you navigate the tree of DocumentFile instances, you can always use 71 * {@link #getUri()} to obtain the Uri representing the underlying document for 72 * that object, for use with {@link ContentResolver#openInputStream(Uri)}, etc. 73 * <p> 74 * To simplify your code on devices running 75 * {@link android.os.Build.VERSION_CODES#KITKAT} or earlier, you can use 76 * {@link #fromFile(File)} which emulates the behavior of a 77 * {@link android.provider.DocumentsProvider}. 78 * 79 * @see android.provider.DocumentsProvider 80 * @see android.provider.DocumentsContract 81 */ 82 public abstract class DocumentFile { 83 static final String TAG = "DocumentFile"; 84 85 @Nullable 86 private final DocumentFile mParent; 87 DocumentFile(@ullable DocumentFile parent)88 DocumentFile(@Nullable DocumentFile parent) { 89 mParent = parent; 90 } 91 92 /** 93 * Create a {@link DocumentFile} representing the filesystem tree rooted at 94 * the given {@link File}. This doesn't give you any additional access to the 95 * underlying files beyond what your app already has. 96 * <p> 97 * {@link #getUri()} will return {@code file://} Uris for files explored 98 * through this tree. 99 */ 100 @NonNull fromFile(@onNull File file)101 public static DocumentFile fromFile(@NonNull File file) { 102 return new RawDocumentFile(null, file); 103 } 104 105 /** 106 * Create a {@link DocumentFile} representing the single document at the 107 * given {@link Uri}. This is only useful on devices running 108 * {@link android.os.Build.VERSION_CODES#KITKAT} or later, and will return 109 * {@code null} when called on earlier platform versions. 110 * 111 * @param singleUri the {@link Intent#getData()} from a successful 112 * {@link Intent#ACTION_OPEN_DOCUMENT} or 113 * {@link Intent#ACTION_CREATE_DOCUMENT} request. 114 */ 115 @Nullable fromSingleUri(@onNull Context context, @NonNull Uri singleUri)116 public static DocumentFile fromSingleUri(@NonNull Context context, @NonNull Uri singleUri) { 117 if (Build.VERSION.SDK_INT >= 19) { 118 return new SingleDocumentFile(null, context, singleUri); 119 } else { 120 return null; 121 } 122 } 123 124 /** 125 * Create a {@link DocumentFile} representing the document tree rooted at 126 * the given {@link Uri}. This is only useful on devices running 127 * {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later, and will return 128 * {@code null} when called on earlier platform versions. 129 * 130 * @param treeUri the {@link Intent#getData()} from a successful 131 * {@link Intent#ACTION_OPEN_DOCUMENT_TREE} request. 132 */ 133 @Nullable fromTreeUri(@onNull Context context, @NonNull Uri treeUri)134 public static DocumentFile fromTreeUri(@NonNull Context context, @NonNull Uri treeUri) { 135 if (Build.VERSION.SDK_INT >= 21) { 136 return new TreeDocumentFile(null, context, 137 DocumentsContract.buildDocumentUriUsingTree(treeUri, 138 DocumentsContract.getTreeDocumentId(treeUri))); 139 } else { 140 return null; 141 } 142 } 143 144 /** 145 * Test if given Uri is backed by a 146 * {@link android.provider.DocumentsProvider}. 147 */ isDocumentUri(@onNull Context context, @Nullable Uri uri)148 public static boolean isDocumentUri(@NonNull Context context, @Nullable Uri uri) { 149 if (Build.VERSION.SDK_INT >= 19) { 150 return DocumentsContract.isDocumentUri(context, uri); 151 } else { 152 return false; 153 } 154 } 155 156 /** 157 * Create a new document as a direct child of this directory. 158 * 159 * @param mimeType MIME type of new document, such as {@code image/png} or 160 * {@code audio/flac} 161 * @param displayName name of new document, without any file extension 162 * appended; the underlying provider may choose to append the 163 * extension 164 * @return file representing newly created document, or null if failed 165 * @throws UnsupportedOperationException when working with a single document 166 * created from {@link #fromSingleUri(Context, Uri)}. 167 * @see android.provider.DocumentsContract#createDocument(ContentResolver, 168 * Uri, String, String) 169 */ 170 @Nullable createFile(@onNull String mimeType, @NonNull String displayName)171 public abstract DocumentFile createFile(@NonNull String mimeType, @NonNull String displayName); 172 173 /** 174 * Create a new directory as a direct child of this directory. 175 * 176 * @param displayName name of new directory 177 * @return file representing newly created directory, or null if failed 178 * @throws UnsupportedOperationException when working with a single document 179 * created from {@link #fromSingleUri(Context, Uri)}. 180 * @see android.provider.DocumentsContract#createDocument(ContentResolver, 181 * Uri, String, String) 182 */ 183 @Nullable createDirectory(@onNull String displayName)184 public abstract DocumentFile createDirectory(@NonNull String displayName); 185 186 /** 187 * Return a Uri for the underlying document represented by this file. This 188 * can be used with other platform APIs to manipulate or share the 189 * underlying content. You can use {@link #isDocumentUri(Context, Uri)} to 190 * test if the returned Uri is backed by a 191 * {@link android.provider.DocumentsProvider}. 192 * 193 * @see Intent#setData(Uri) 194 * @see Intent#setClipData(android.content.ClipData) 195 * @see ContentResolver#openInputStream(Uri) 196 * @see ContentResolver#openOutputStream(Uri) 197 * @see ContentResolver#openFileDescriptor(Uri, String) 198 */ 199 @NonNull getUri()200 public abstract Uri getUri(); 201 202 /** 203 * Return the display name of this document. 204 * 205 * @see android.provider.DocumentsContract.Document#COLUMN_DISPLAY_NAME 206 */ 207 @Nullable getName()208 public abstract String getName(); 209 210 /** 211 * Return the MIME type of this document. 212 * 213 * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE 214 */ 215 @Nullable getType()216 public abstract String getType(); 217 218 /** 219 * Return the parent file of this document. Only defined inside of the 220 * user-selected tree; you can never escape above the top of the tree. 221 * <p> 222 * The underlying {@link android.provider.DocumentsProvider} only defines a 223 * forward mapping from parent to child, so the reverse mapping of child to 224 * parent offered here is purely a convenience method, and it may be 225 * incorrect if the underlying tree structure changes. 226 */ 227 @Nullable getParentFile()228 public DocumentFile getParentFile() { 229 return mParent; 230 } 231 232 /** 233 * Indicates if this file represents a <em>directory</em>. 234 * 235 * @return {@code true} if this file is a directory, {@code false} 236 * otherwise. 237 * @see android.provider.DocumentsContract.Document#MIME_TYPE_DIR 238 */ isDirectory()239 public abstract boolean isDirectory(); 240 241 /** 242 * Indicates if this file represents a <em>file</em>. 243 * 244 * @return {@code true} if this file is a file, {@code false} otherwise. 245 * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE 246 */ isFile()247 public abstract boolean isFile(); 248 249 /** 250 * Indicates if this file represents a <em>virtual</em> document. 251 * 252 * @return {@code true} if this file is a virtual document. 253 * @see android.provider.DocumentsContract.Document#FLAG_VIRTUAL_DOCUMENT 254 */ isVirtual()255 public abstract boolean isVirtual(); 256 257 /** 258 * Returns the time when this file was last modified, measured in 259 * milliseconds since January 1st, 1970, midnight. Returns 0 if the file 260 * does not exist, or if the modified time is unknown. 261 * 262 * @return the time when this file was last modified. 263 * @see android.provider.DocumentsContract.Document#COLUMN_LAST_MODIFIED 264 */ lastModified()265 public abstract long lastModified(); 266 267 /** 268 * Returns the length of this file in bytes. Returns 0 if the file does not 269 * exist, or if the length is unknown. The result for a directory is not 270 * defined. 271 * 272 * @return the number of bytes in this file. 273 * @see android.provider.DocumentsContract.Document#COLUMN_SIZE 274 */ length()275 public abstract long length(); 276 277 /** 278 * Indicates whether the current context is allowed to read from this file. 279 * 280 * @return {@code true} if this file can be read, {@code false} otherwise. 281 */ canRead()282 public abstract boolean canRead(); 283 284 /** 285 * Indicates whether the current context is allowed to write to this file. 286 * 287 * @return {@code true} if this file can be written, {@code false} 288 * otherwise. 289 * @see android.provider.DocumentsContract.Document#COLUMN_FLAGS 290 * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE 291 * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE 292 * @see android.provider.DocumentsContract.Document#FLAG_DIR_SUPPORTS_CREATE 293 */ canWrite()294 public abstract boolean canWrite(); 295 296 /** 297 * Deletes this file. 298 * <p> 299 * Note that this method does <i>not</i> throw {@code IOException} on 300 * failure. Callers must check the return value. 301 * 302 * @return {@code true} if this file was deleted, {@code false} otherwise. 303 * @see android.provider.DocumentsContract#deleteDocument(ContentResolver, 304 * Uri) 305 */ delete()306 public abstract boolean delete(); 307 308 /** 309 * Returns a boolean indicating whether this file can be found. 310 * 311 * @return {@code true} if this file exists, {@code false} otherwise. 312 */ exists()313 public abstract boolean exists(); 314 315 /** 316 * Returns an array of files contained in the directory represented by this 317 * file. 318 * 319 * @return an array of files or {@code null}. 320 * @throws UnsupportedOperationException when working with a single document 321 * created from {@link #fromSingleUri(Context, Uri)}. 322 * @see android.provider.DocumentsContract#buildChildDocumentsUriUsingTree(Uri, 323 * String) 324 */ 325 @NonNull listFiles()326 public abstract DocumentFile[] listFiles(); 327 328 /** 329 * Search through {@link #listFiles()} for the first document matching the 330 * given display name. Returns {@code null} when no matching document is 331 * found. 332 * 333 * @throws UnsupportedOperationException when working with a single document 334 * created from {@link #fromSingleUri(Context, Uri)}. 335 */ 336 @Nullable findFile(@onNull String displayName)337 public DocumentFile findFile(@NonNull String displayName) { 338 for (DocumentFile doc : listFiles()) { 339 if (displayName.equals(doc.getName())) { 340 return doc; 341 } 342 } 343 return null; 344 } 345 346 /** 347 * Renames this file to {@code displayName}. 348 * <p> 349 * Note that this method does <i>not</i> throw {@code IOException} on 350 * failure. Callers must check the return value. 351 * <p> 352 * Some providers may need to create a new document to reflect the rename, 353 * potentially with a different MIME type, so {@link #getUri()} and 354 * {@link #getType()} may change to reflect the rename. 355 * <p> 356 * When renaming a directory, children previously enumerated through 357 * {@link #listFiles()} may no longer be valid. 358 * 359 * @param displayName the new display name. 360 * @return true on success. 361 * @throws UnsupportedOperationException when working with a single document 362 * created from {@link #fromSingleUri(Context, Uri)}. 363 * @see android.provider.DocumentsContract#renameDocument(ContentResolver, 364 * Uri, String) 365 */ renameTo(@onNull String displayName)366 public abstract boolean renameTo(@NonNull String displayName); 367 } 368