Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.

Commit 22784d8

Browse files
Merge pull request #304 from android/CopyMoveRenameRemove
Add copy/move/rename/remove/isChildDocument
2 parents 04bac61 + dd65fb7 commit 22784d8

1 file changed

Lines changed: 205 additions & 16 deletions

File tree

StorageProvider/Application/src/main/java/com/example/android/storageprovider/MyCloudProvider.java

Lines changed: 205 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717

1818
package com.example.android.storageprovider;
1919

20+
import static android.os.Build.VERSION.SDK_INT;
21+
2022
import android.content.Context;
2123
import android.content.SharedPreferences;
2224
import android.content.res.AssetFileDescriptor;
2325
import android.content.res.TypedArray;
2426
import android.database.Cursor;
2527
import android.database.MatrixCursor;
2628
import android.graphics.Point;
29+
import android.os.Build;
2730
import android.os.CancellationSignal;
2831
import android.os.Handler;
2932
import android.os.ParcelFileDescriptor;
@@ -36,10 +39,12 @@
3639

3740
import java.io.ByteArrayOutputStream;
3841
import java.io.File;
42+
import java.io.FileInputStream;
3943
import java.io.FileNotFoundException;
4044
import java.io.FileOutputStream;
4145
import java.io.IOException;
4246
import java.io.InputStream;
47+
import java.io.OutputStream;
4348
import java.util.Collections;
4449
import java.util.Comparator;
4550
import java.util.HashSet;
@@ -95,7 +100,7 @@ public boolean onCreate() {
95100

96101
mBaseDir = getContext().getFilesDir();
97102

98-
writeDummyFilesToStorage();
103+
writeTestFilesToStorage();
99104

100105
return true;
101106
}
@@ -127,10 +132,18 @@ public Cursor queryRoots(String[] projection) throws FileNotFoundException {
127132
// FLAG_SUPPORTS_CREATE means at least one directory under the root supports creating
128133
// documents. FLAG_SUPPORTS_RECENTS means your application's most recently used
129134
// documents will show up in the "Recents" category. FLAG_SUPPORTS_SEARCH allows users
130-
// to search all documents the application shares.
131-
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
132-
Root.FLAG_SUPPORTS_RECENTS |
133-
Root.FLAG_SUPPORTS_SEARCH);
135+
// to search all documents the application shares. FLAG_SUPPORTS_IS_CHILD allows
136+
// testing parent child relationships, available after SDK 21 (Lollipop).
137+
if (SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
138+
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
139+
Root.FLAG_SUPPORTS_RECENTS |
140+
Root.FLAG_SUPPORTS_SEARCH );
141+
} else {
142+
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
143+
Root.FLAG_SUPPORTS_RECENTS |
144+
Root.FLAG_SUPPORTS_SEARCH |
145+
Root.FLAG_SUPPORTS_IS_CHILD);
146+
}
134147

135148
// COLUMN_TITLE is the root title (e.g. what will be displayed to identify your provider).
136149
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name));
@@ -330,6 +343,27 @@ public void onClose(IOException e) {
330343
// END_INCLUDE(open_document)
331344

332345

346+
public boolean isChildFile(File parentFile, File childFile){
347+
File realFileParent = childFile.getParentFile();
348+
return realFileParent == null || realFileParent.equals(parentFile);
349+
}
350+
351+
// BEGIN_INCLUDE(is_child_document)
352+
@Override
353+
public boolean isChildDocument(String parentDocumentId, String documentId) {
354+
Log.v(TAG, "isChildDocument");
355+
try {
356+
File parentFile = getFileForDocId(parentDocumentId);
357+
File childFile = getFileForDocId(documentId);
358+
return isChildFile(parentFile, childFile);
359+
} catch (FileNotFoundException e) {
360+
Log.e(TAG, "FileNotFound in isChildDocument: " + e.getMessage());
361+
e.printStackTrace();
362+
}
363+
return false;
364+
}
365+
// END_INCLUDE(is_child_document)
366+
333367
// BEGIN_INCLUDE(create_document)
334368
@Override
335369
public String createDocument(String documentId, String mimeType, String displayName)
@@ -339,9 +373,18 @@ public String createDocument(String documentId, String mimeType, String displayN
339373
File parent = getFileForDocId(documentId);
340374
File file = new File(parent.getPath(), displayName);
341375
try {
342-
file.createNewFile();
343-
file.setWritable(true);
344-
file.setReadable(true);
376+
// Create the new File to copy into
377+
boolean wasNewFileCreated = false;
378+
if (file.createNewFile()) {
379+
if (file.setWritable(true) && file.setReadable(true)) {
380+
wasNewFileCreated = true;
381+
}
382+
}
383+
384+
if (!wasNewFileCreated) {
385+
throw new FileNotFoundException("Failed to create document with name " +
386+
displayName +" and documentId " + documentId);
387+
}
345388
} catch (IOException e) {
346389
throw new FileNotFoundException("Failed to create document with name " +
347390
displayName +" and documentId " + documentId);
@@ -350,6 +393,38 @@ public String createDocument(String documentId, String mimeType, String displayN
350393
}
351394
// END_INCLUDE(create_document)
352395

396+
// BEGIN_INCLUDE(rename_document)
397+
@Override
398+
public String renameDocument(String documentId, String displayName)
399+
throws FileNotFoundException {
400+
Log.v(TAG, "renameDocument");
401+
if (displayName == null) {
402+
throw new FileNotFoundException("Failed to rename document, new name is null");
403+
}
404+
405+
// Create the destination file in the same directory as the source file
406+
File sourceFile = getFileForDocId(documentId);
407+
File sourceParentFile = sourceFile.getParentFile();
408+
if (sourceParentFile == null) {
409+
throw new FileNotFoundException("Failed to rename document. File has no parent.");
410+
}
411+
File destFile = new File(sourceParentFile.getPath(), displayName);
412+
413+
// Try to do the rename
414+
try {
415+
boolean renameSucceeded = sourceFile.renameTo(destFile);
416+
if (!renameSucceeded) {
417+
throw new FileNotFoundException("Failed to rename document. Renamed failed.");
418+
}
419+
} catch (Exception e) {
420+
Log.w(TAG, "Rename exception : " + e.getLocalizedMessage() + e.getCause());
421+
throw new FileNotFoundException("Failed to rename document. Error: " + e.getMessage());
422+
}
423+
424+
return getDocIdForFile(destFile);
425+
}
426+
// END_INCLUDE(rename_document)
427+
353428
// BEGIN_INCLUDE(delete_document)
354429
@Override
355430
public void deleteDocument(String documentId) throws FileNotFoundException {
@@ -363,6 +438,113 @@ public void deleteDocument(String documentId) throws FileNotFoundException {
363438
}
364439
// END_INCLUDE(delete_document)
365440

441+
// BEGIN_INCLUDE(remove_document)
442+
@Override
443+
public void removeDocument(String documentId, String parentDocumentId)
444+
throws FileNotFoundException {
445+
Log.v(TAG, "removeDocument");
446+
File parent = getFileForDocId(parentDocumentId);
447+
File file = getFileForDocId(documentId);
448+
449+
if (file == null) {
450+
throw new FileNotFoundException("Failed to delete document with id " + documentId);
451+
}
452+
453+
// removeDocument is the same as deleteDocument but allows the parent to be specified
454+
// Check here if the specified parentDocumentId matches the true parent of documentId
455+
boolean doesFileParentMatch = false;
456+
File fileParent = file.getParentFile();
457+
458+
if (fileParent == null || fileParent.equals(parent)) {
459+
doesFileParentMatch = true;
460+
}
461+
462+
// Remove the file if parent matches or file and parent are equal
463+
if (parent.equals(file) || doesFileParentMatch) {
464+
if (file.delete()) {
465+
Log.i(TAG, "Deleted file with id " + documentId);
466+
} else {
467+
throw new FileNotFoundException("Failed to delete document with id " + documentId);
468+
}
469+
} else {
470+
throw new FileNotFoundException("Failed to delete document with id " + documentId);
471+
}
472+
}
473+
// END_INCLUDE(remove_document)
474+
475+
/**
476+
* overload copyDocument to insist that the parent matches
477+
*/
478+
public String copyDocument(String sourceDocumentId, String sourceParentDocumentId,
479+
String targetParentDocumentId) throws FileNotFoundException {
480+
Log.v(TAG, "copyDocument with document parent");
481+
if (!isChildDocument(sourceParentDocumentId, sourceDocumentId)) {
482+
throw new FileNotFoundException("Failed to copy document with id " +
483+
sourceDocumentId + ". Parent is not: " + sourceParentDocumentId);
484+
}
485+
return copyDocument(sourceDocumentId, targetParentDocumentId);
486+
}
487+
488+
// BEGIN_INCLUDE(copyDocument)
489+
@Override
490+
public String copyDocument(String sourceDocumentId, String targetParentDocumentId)
491+
throws FileNotFoundException {
492+
Log.v(TAG, "copyDocument");
493+
494+
File parent = getFileForDocId(targetParentDocumentId);
495+
File oldFile = getFileForDocId(sourceDocumentId);
496+
File newFile = new File(parent.getPath(), oldFile.getName());
497+
try {
498+
// Create the new File to copy into
499+
boolean wasNewFileCreated = false;
500+
if (newFile.createNewFile()) {
501+
if (newFile.setWritable(true) && newFile.setReadable(true)) {
502+
wasNewFileCreated = true;
503+
}
504+
}
505+
506+
if (!wasNewFileCreated) {
507+
throw new FileNotFoundException("Failed to copy document " + sourceDocumentId +
508+
". Could not create new file.");
509+
}
510+
511+
// Copy the bytes into the new file
512+
try (InputStream inStream = new FileInputStream(oldFile)) {
513+
try (OutputStream outStream = new FileOutputStream(newFile)) {
514+
// Transfer bytes from in to out
515+
byte[] buf = new byte[4096]; // ideal range for network: 2-8k, disk: 8-64k
516+
int len;
517+
while ((len = inStream.read(buf)) > 0) {
518+
outStream.write(buf, 0, len);
519+
}
520+
}
521+
}
522+
} catch (IOException e) {
523+
throw new FileNotFoundException("Failed to copy document: " + sourceDocumentId +
524+
". " + e.getMessage());
525+
}
526+
return getDocIdForFile(newFile);
527+
}
528+
// END_INCLUDE(copyDocument)
529+
530+
// BEGIN_INCLUDE(moveDocument)
531+
@Override
532+
public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
533+
String targetParentDocumentId) throws FileNotFoundException {
534+
Log.v(TAG, "moveDocument");
535+
try {
536+
// Copy document, insisting that the parent is correct
537+
String newDocumentId = copyDocument(sourceDocumentId, sourceParentDocumentId,
538+
targetParentDocumentId);
539+
// Remove old document
540+
removeDocument(sourceDocumentId,sourceParentDocumentId);
541+
return newDocumentId;
542+
} catch (FileNotFoundException e) {
543+
throw new FileNotFoundException("Failed to move document " + sourceDocumentId);
544+
}
545+
}
546+
// END_INCLUDE(moveDocument)
547+
366548

367549
@Override
368550
public String getDocumentType(String documentId) throws FileNotFoundException {
@@ -471,7 +653,7 @@ private String getDocIdForFile(File file) {
471653
* @param result the cursor to modify
472654
* @param docId the document ID representing the desired file (may be null if given file)
473655
* @param file the File object representing the desired file (may be null if given docID)
474-
* @throws java.io.FileNotFoundException
656+
* @throws FileNotFoundException
475657
*/
476658
private void includeFile(MatrixCursor result, String docId, File file)
477659
throws FileNotFoundException {
@@ -497,6 +679,16 @@ private void includeFile(MatrixCursor result, String docId, File file)
497679
// FLAG_SUPPORTS_DELETE
498680
flags |= Document.FLAG_SUPPORTS_WRITE;
499681
flags |= Document.FLAG_SUPPORTS_DELETE;
682+
683+
// Add SDK specific flags if appropriate
684+
if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
685+
flags |= Document.FLAG_SUPPORTS_RENAME;
686+
}
687+
if (SDK_INT >= Build.VERSION_CODES.N) {
688+
flags |= Document.FLAG_SUPPORTS_REMOVE;
689+
flags |= Document.FLAG_SUPPORTS_MOVE;
690+
flags |= Document.FLAG_SUPPORTS_COPY;
691+
}
500692
}
501693

502694
final String displayName = file.getName();
@@ -544,13 +736,12 @@ private File getFileForDocId(String docId) throws FileNotFoundException {
544736
}
545737
}
546738

547-
548739
/**
549740
* Preload sample files packaged in the apk into the internal storage directory. This is a
550-
* dummy function specific to this demo. The MyCloud mock cloud service doesn't actually
741+
* test function specific to this demo. The MyCloud mock cloud service doesn't actually
551742
* have a backend, so it simulates by reading content from the device's internal storage.
552743
*/
553-
private void writeDummyFilesToStorage() {
744+
private void writeTestFilesToStorage() {
554745
if (mBaseDir.list().length > 0) {
555746
return;
556747
}
@@ -572,7 +763,7 @@ private void writeDummyFilesToStorage() {
572763
}
573764

574765
/**
575-
* Write a file to internal storage. Used to set up our dummy "cloud server".
766+
* Write a file to internal storage. Used to set up our simple "cloud server".
576767
*
577768
* @param resId the resource ID of the file to write to internal storage
578769
* @param extension the file extension (ex. .png, .mp3)
@@ -610,14 +801,12 @@ private int[] getResourceIdArray(int arrayResId) {
610801
}
611802

612803
/**
613-
* Dummy function to determine whether the user is logged in.
804+
* Placeholder function to determine whether the user is logged in.
614805
*/
615806
private boolean isUserLoggedIn() {
616807
final SharedPreferences sharedPreferences =
617808
getContext().getSharedPreferences(getContext().getString(R.string.app_name),
618809
Context.MODE_PRIVATE);
619810
return sharedPreferences.getBoolean(getContext().getString(R.string.key_logged_in), false);
620811
}
621-
622-
623812
}

0 commit comments

Comments
 (0)