0

I am trying to create code that allows my music app to delete a song from Storage. So far, is succeeds in deleting the file if the file is on the internal(emulated) storage (that is, not the app's internal storage, but the internal shared storage of the phone). But, as soon as the song is on the external SD card, the file.delete() does not delete the file and returns false.

Here is my code so far:

//Remove selected tracks from the database 
activity.getContentResolver()
     .delete(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, selection.toString(), null);

//Delete File from storage
File file = new File(song.getFilePath);
if(!file.delete()){
   Log.e("MusicFunctions", "Failed to delete file: " + song.getFilePath());
}

When I select a song that is on the SD Card, is does not get deleted, but only gets removed from the database; here is a logcat output:

E/MusicFunctions: Failed to delete file: /storage/3138-3763/Music/Test/Odesza/In Return/Always This Late.mp3

I have also tried context.deleteFile(file) but I also had no luck.

As I said, it only fails to delete when the file is on the SD card. When it is saved on internal storage, it deletes fine. Why does it not delete, and what is the proper way to delete files from the SD card on Android 5.0+?

Many Thanks in advance.

EDIT: I forgot to mention that I have already added the permissions:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE"/>

and I do get the required storage permission during run time:

ActivityCompat.requestPermissions(thisActivity,
                    new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                    MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL);

ANOTHER EDIT: I have noticed that file manager apps need to be granted an additional permission, following the steps like at https://metactrl.com/docs/sdcard-on-lollipop/

How can I achieve this?

4 Answers4

2

I have noticed that the question has had some interest again. I am happy to say that I have indeed found a solution to the problem. I did extensive online research and found some source code files (although I'm sorry I cannot remember where I found them) which solved my problem

The problem with Android 4.4 and up is that you need extra privileges through the Storage Access Framework in order for a third party app to delete / modify external SD Card files.

In order to get these privileges, you need to get the document's URI or one of its parent files' (directories') URIs. To do this you need to open Android's built in file browser. It is best it the user chooses the SD card root directory through the File Browser, so that your app can modify/delete any file on the SD card. To do this you can follow this code:

private int REQUEST_CODE = 42;
private void getSDCardAccess(){
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, REQUEST_CODE);
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    if (resultCode == RESULT_OK) {
        Uri treeUri = resultData.getData();
        DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);

        getContentResolver().takePersistableUriPermission(treeUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION |
                        Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

        if(shrdPref == null){
            shrdPref = getSharedPreferences(PREF_MAIN_FILE, MODE_PRIVATE);
        }
        //Takes the access so that we can use it again after the app reopens
        shrdPref.edit().putString(KEY_SDCARDSTORAGE, treeUri.toString()).apply();
    }
}

Further you the need a file's 'Document File' in order to modify and delete a file. The following snippets of code can help you do that, as well as detect if a file/directory is writable using the document file method... (I know its a lot of code, most of which came from another source. I truly regret forgetting where I got it as they do deserve a lot of credit). The functions you would want to take note of is: isWritableNormalOrSAF(), deleteFile() and maybe copyFile(). Note that most of the other functions are required for these to work)

public static boolean isWritable(@NonNull final File file) {
    boolean isExisting = file.exists();

    try {
        FileOutputStream output = new FileOutputStream(file, true);
        try {
            output.close();
        }
        catch (IOException e) {
            // do nothing.
        }
    }
    catch (FileNotFoundException e) {
        return false;
    }
    boolean result = file.canWrite();

    // Ensure that file is not created during this process.
    if (!isExisting) {
        // noinspection ResultOfMethodCallIgnored
        file.delete();
    }

    return result;
}


public static boolean isWritableNormalOrSaf(@Nullable final File folder, Context context) {
    // Verify that this is a directory.
    Log.e("StorageHelper", "start");
    if (folder == null || !folder.exists() || !folder.isDirectory()) {
        Log.e("StorageHelper", "return 1");
        return false;
    }

    // Find a non-existing file in this directory.
    int i = 0;
    File file;
    do {
        String fileName = "AugendiagnoseDummyFile" + (++i);
        file = new File(folder, fileName);
        //Log.e("StorageHelper", "file:" + fileName);
    }
    while (file.exists());

    // First check regular writability
    if (isWritable(file)) {
        //Log.e("StorageHelper", "return 2 true");
        return true;
    }

    // Next check SAF writability.
    Log.e("StorageHelper", "start 2");
    DocumentFile document;
    try {
        document = getDocumentFile(file, false, false, context);
    }
    catch (Exception e) {
        //Log.e("StorageHelper", "return 3 exception");
        return false;
    }

    if (document == null) {
        //Log.e("StorageHelper", "return 4 doc null");
        return false;
    }

    // This should have created the file - otherwise something is wrong with access URL.
    boolean result = document.canWrite() && file.exists();

    // Ensure that the dummy file is not remaining.
    document.delete();

    //Log.e("StorageHelper", "return end");
    return result;
}


public static boolean deleteFile(@NonNull final File file, Context context) {
    // First try the normal deletion.
    if (file.delete()) {
        return true;
    }

    // Try with Storage Access Framework.
        DocumentFile document = getDocumentFile(file, false, true, context);
        return document != null && document.delete();

}




private static DocumentFile getDocumentFile(@NonNull final File file, final boolean isDirectory, final boolean createDirectories, Context context) {
    SharedPreferences sharedPreferences = context.getSharedPreferences(PREF_MAIN_FILE, Context.MODE_PRIVATE);
    String uriString = sharedPreferences.getString(KEY_SDCARDSTORAGE, null);
    if(uriString == null){
        return null;
    }

    Uri treeUri = Uri.parse(uriString);

    String fullPath;
    try {
        fullPath = file.getCanonicalPath();
    }
    catch (IOException e) {
        return null;
    }

    String baseFolder = null;

    // First try to get the base folder via unofficial StorageVolume API from the URIs.
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
        StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
        StorageVolume volume = storageManager.getStorageVolume(file);
        String uuid = volume.getUuid();

        String volumeId = getVolumeIdFromTreeUri(treeUri);
        if (uuid.equals(volumeId)) {
            // Use parcel to get the hidden path field from StorageVolume
            Parcel parcel = Parcel.obtain();
            volume.writeToParcel(parcel, 0);
            parcel.setDataPosition(0);
            parcel.readString();
            parcel.readInt();
            String volumeBasePath = parcel.readString();
            parcel.recycle();
            baseFolder = getFullPathFromTreeUri(treeUri, volumeBasePath);
        }

    }
    else {
        // Use Java Reflection to access hidden methods from StorageVolume
        String treeBase = getFullPathFromTreeUri(treeUri, getVolumePath(getVolumeIdFromTreeUri(treeUri), context));
        if (treeBase != null && fullPath.startsWith(treeBase)) {
            treeUri = treeUri;
            baseFolder = treeBase;
        }
    }




    if (baseFolder == null) {
        // Alternatively, take root folder from device and assume that base URI works.
        baseFolder = getExtSdCardFolder(file, context);
    }

    if (baseFolder == null) {
        return null;
    }

    String relativePath = fullPath.substring(baseFolder.length() + 1);

    // start with root of SD card and then parse through document tree.
    DocumentFile document = DocumentFile.fromTreeUri(context, treeUri);

    String[] parts = relativePath.split("\\/");
    for (int i = 0; i < parts.length; i++) {
        DocumentFile nextDocument = document.findFile(parts[i]);

        if (nextDocument == null) {
            if (i < parts.length - 1) {
                if (createDirectories) {
                    nextDocument = document.createDirectory(parts[i]);
                }
                else {
                    return null;
                }
            }
            else if (isDirectory) {
                nextDocument = document.createDirectory(parts[i]);
            }
            else {
                nextDocument = document.createFile("image", parts[i]);
            }
        }
        document = nextDocument;
    }

    return document;
}









@Nullable
private static String getFullPathFromTreeUri(@Nullable final Uri treeUri, final String volumeBasePath) {
    if (treeUri == null) {
        return null;
    }
    if (volumeBasePath == null) {
        return File.separator;
    }
    String volumePath = volumeBasePath;
    if (volumePath.endsWith(File.separator)) {
        volumePath = volumePath.substring(0, volumePath.length() - 1);
    }

    String documentPath = getDocumentPathFromTreeUri(treeUri);
    if (documentPath.endsWith(File.separator)) {
        documentPath = documentPath.substring(0, documentPath.length() - 1);
    }

    if (documentPath.length() > 0) {
        if (documentPath.startsWith(File.separator)) {
            return volumePath + documentPath;
        }
        else {
            return volumePath + File.separator + documentPath;
        }
    }
    else {
        return volumePath;
    }
}


private static String getVolumeIdFromTreeUri(final Uri treeUri) {
    final String docId = DocumentsContract.getTreeDocumentId(treeUri);
    final String[] split = docId.split(":");

    if (split.length > 0) {
        return split[0];
    }
    else {
        return null;
    }
}

private static final String PRIMARY_VOLUME_NAME = "primary";
private static String getVolumePath(final String volumeId, Context context) {
    try {
        StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);

        Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");

        Method getVolumeList = storageManager.getClass().getMethod("getVolumeList");
        Method getUuid = storageVolumeClazz.getMethod("getUuid");
        Method getPath = storageVolumeClazz.getMethod("getPath");
        Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
        Object result = getVolumeList.invoke(storageManager);

        final int length = Array.getLength(result);
        for (int i = 0; i < length; i++) {
            Object storageVolumeElement = Array.get(result, i);
            String uuid = (String) getUuid.invoke(storageVolumeElement);
            Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);

            // primary volume?
            if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {
                return (String) getPath.invoke(storageVolumeElement);
            }

            // other volumes?
            if (uuid != null) {
                if (uuid.equals(volumeId)) {
                    return (String) getPath.invoke(storageVolumeElement);
                }
            }
        }

        // not found.
        return null;
    }
    catch (Exception ex) {
        return null;
    }
}


private static String getDocumentPathFromTreeUri(final Uri treeUri) {
    final String docId = DocumentsContract.getTreeDocumentId(treeUri);
    final String[] split = docId.split(":");
    if ((split.length >= 2) && (split[1] != null)) {
        return split[1];
    }
    else {
        return File.separator;
    }
}


public static String getExtSdCardFolder(@NonNull final File file, Context context) {
    String[] extSdPaths = getExtSdCardPaths(context);
    try {
        for (String extSdPath : extSdPaths) {
            if (file.getCanonicalPath().startsWith(extSdPath)) {
                return extSdPath;
            }
        }
    }
    catch (IOException e) {
        return null;
    }
    return null;
}

private static String[] getExtSdCardPaths(Context context) {
    List<String> paths = new ArrayList<>();
    for (File file : context.getExternalFilesDirs("external")) {
        if (file != null && !file.equals(context.getExternalFilesDir("external"))) {
            int index = file.getAbsolutePath().lastIndexOf("/Android/data");
            if (index < 0) {
                Log.w("StorageHelper", "Unexpected external file dir: " + file.getAbsolutePath());
            }
            else {
                String path = file.getAbsolutePath().substring(0, index);
                try {
                    path = new File(path).getCanonicalPath();
                }
                catch (IOException e) {
                    // Keep non-canonical path.
                }
                paths.add(path);
            }
        }
    }
    return paths.toArray(new String[paths.size()]);
}








public static boolean copyFile(@NonNull final File source, @NonNull final File target, Context context) {
    FileInputStream inStream = null;
    OutputStream outStream = null;
    FileChannel inChannel = null;
    FileChannel outChannel = null;
    try {
        inStream = new FileInputStream(source);

        // First try the normal way
        if (isWritable(target)) {
            // standard way
            outStream = new FileOutputStream(target);
            inChannel = inStream.getChannel();
            outChannel = ((FileOutputStream) outStream).getChannel();
            inChannel.transferTo(0, inChannel.size(), outChannel);
        }
        else {
            // Storage Access Framework
            DocumentFile targetDocument = getDocumentFile(target, false, true, context);
            if (targetDocument != null) {
                outStream = context.getContentResolver().openOutputStream(targetDocument.getUri());
            }

            if (outStream != null) {
                // Both for SAF and for Kitkat, write to output stream.
                byte[] buffer = new byte[4096]; // MAGIC_NUMBER
                int bytesRead;
                while ((bytesRead = inStream.read(buffer)) != -1) {
                    outStream.write(buffer, 0, bytesRead);
                }
            }

        }
    }
    catch (Exception e) {
        Log.e("StorageHelper",
                "Error when copying file from " + source.getAbsolutePath() + " to " + target.getAbsolutePath(), e);
        return false;
    }
    finally {
        try {
            inStream.close();
        }
        catch (Exception e) {
            // ignore exception
        }
        try {
            outStream.close();
        }
        catch (Exception e) {
            Log.e("StorageHelper", "OutStreamClose: " + e.toString());
            // ignore exception
        }
        try {
            ((FileChannel) inChannel).close();
        }
        catch (Exception e) {
            // ignore exception
        }
        try {
            outChannel.close();
        }
        catch (Exception e) {
            Log.e("StorageHelper", "OutChannelClose: " + e.toString());
            // ignore exception
        }
    }
    return true;
}


    public static String getExtensionFromName(String fileName){
    String extension = "";

    int i = fileName.lastIndexOf('.');
    if (i > 0) {
        extension = fileName.substring(i+1);
    }

    return extension;
}
  • DocumentFile document = getDocumentFile(file, false, true, context); what do the values isDirectory false and createDirectories true do, do i just keep these values when i only want to delete file from sdcard? – Vince VD Feb 08 '19 at 22:53
  • DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri); is also never used so what does this line do? – Vince VD Feb 09 '19 at 02:04
  • 1
    @Vince As for the pickedDir, you can remove that, it was just used for debugging code. Using the "delteFile()" function will try to delete any file and return if it was successfully deleted. As for the values "isDirectory" and "createDirectory", it can be left like that. – user9656809 Feb 09 '19 at 04:55
0

try this one

File file = new File(song.getFilePath);
  if(!file.delete()){
   if(file.exists()){
      file.getCanonicalFile().delete();
      if(file.exists()){
        getApplicationContext().deleteFile(file.getName());
     }
    }
  }
Zahoor Saleem
  • 614
  • 7
  • 15
0

You can try using canonical file delete method

File file = new File(uri.getPath());
file.delete();
if(file.exists()){
      file.getCanonicalFile().delete();
      if(file.exists()){
           getApplicationContext().deleteFile(file.getName());
      }
}
adityakamble49
  • 1,971
  • 18
  • 27
0

Your file path was wrong,you should query absolute path from ContentProvider by uri,for how to get absolute path by uri check this question,How to get the Full file path from URI

aolphn
  • 2,950
  • 2
  • 21
  • 30