[HOWTO] Write to media card (secondary storage) from an app under Android 4.4 KitKat

Search This thread

tliebeck

Senior Member
Sep 15, 2010
1,849
4,434
Southern California
In Android 4.4 KitKat, Google/AOSP made the following change to the API specification, much to the detriment of app developers and users:

"The WRITE_EXTERNAL_STORAGE permission must only grant write access to the primary external storage on a device. Apps must not be allowed to write to secondary external storage devices, except in their package-specific directories as allowed by synthesized permissions."
Source: http://source.android.com/devices/tech/storage/index.html

You can read my rather unhappy write-up about it here: https://plus.google.com/108338299717386673901/posts/gjnmuaDM8sn

This only applies to dual-storage devices, i.e., devices with a user-writable internal flash storage AND a removable SD card. But if your device has both, as of Android 4.4, apps will no longer be able to write arbitrarily to the "secondary" storage (the SD card).

There is however still an API exposed that will allow you to write to secondary storage, notably the media content provider. This is far from an ideal solution, and I imagine that someday it will not be possible.

I've written a tiny bit of code to let your applications continue to work with files on the SD card using the media content provider. This code should only be used to write to the secondary storage on Android 4.4+ devices if all else fails. I would strongly recommend that you NEVER rely on this code. This code DOES NOT use root access.

The class:
Code:
/*
 * Copyright (C) 2014 NextApp, Inc.
 * 
 * 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 nextapp.mediafile;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import android.provider.MediaStore;

/**
 * Wrapper for manipulating files via the Android Media Content Provider. As of Android 4.4 KitKat, applications can no longer write
 * to the "secondary storage" of a device. Write operations using the java.io.File API will thus fail. This class restores access to
 * those write operations by way of the Media Content Provider.
 * 
 * Note that this class relies on the internal operational characteristics of the media content provider API, and as such is not
 * guaranteed to be future-proof. Then again, we did all think the java.io.File API was going to be future-proof for media card
 * access, so all bets are off.
 * 
 * If you're forced to use this class, it's because Google/AOSP made a very poor API decision in Android 4.4 KitKat.
 * Read more at https://plus.google.com/+TodLiebeck/posts/gjnmuaDM8sn
 *
 * Your application must declare the permission "android.permission.WRITE_EXTERNAL_STORAGE".
 */
public class MediaFile {

    private final File file;
    private final ContentResolver contentResolver;
    private final Uri filesUri;
    private final Uri imagesUri;

    public MediaFile(ContentResolver contentResolver, File file) {
        this.file = file;
        this.contentResolver = contentResolver;
        filesUri = MediaStore.Files.getContentUri("external");
        imagesUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    }

    /**
     * Deletes the file. Returns true if the file has been successfully deleted or otherwise does not exist. This operation is not
     * recursive.
     */
    public boolean delete()
            throws IOException {
        if (!file.exists()) {
            return true;
        }

        boolean directory = file.isDirectory();
        if (directory) {
            // Verify directory does not contain any files/directories within it.
            String[] files = file.list();
            if (files != null && files.length > 0) {
                return false;
            }
        }

        String where = MediaStore.MediaColumns.DATA + "=?";
        String[] selectionArgs = new String[] { file.getAbsolutePath() };

        // Delete the entry from the media database. This will actually delete media files (images, audio, and video).
        contentResolver.delete(filesUri, where, selectionArgs);

        if (file.exists()) {
            // If the file is not a media file, create a new entry suggesting that this location is an image, even
            // though it is not.
            ContentValues values = new ContentValues();
            values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
            contentResolver.insert(imagesUri, values);

            // Delete the created entry, such that content provider will delete the file.
            contentResolver.delete(filesUri, where, selectionArgs);
        }

        return !file.exists();
    }

    public File getFile() {
        return file;
    }

    /**
     * Creates a new directory. Returns true if the directory was successfully created or exists.
     */
    public boolean mkdir()
            throws IOException {
        if (file.exists()) {
            return file.isDirectory();
        }

        ContentValues values;
        Uri uri;

        // Create a media database entry for the directory. This step will not actually cause the directory to be created.
        values = new ContentValues();
        values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
        contentResolver.insert(filesUri, values);

        // Create an entry for a temporary image file within the created directory.
        // This step actually causes the creation of the directory.
        values = new ContentValues();
        values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath() + "/temp.jpg");
        uri = contentResolver.insert(imagesUri, values);

        // Delete the temporary entry.
        contentResolver.delete(uri, null, null);

        return file.exists();
    }

    /**
     * Returns an OutputStream to write to the file. The file will be truncated immediately.
     */
    public OutputStream write()
            throws IOException {
        if (file.exists() && file.isDirectory()) {
            throw new IOException("File exists and is a directory.");
        }

        // Delete any existing entry from the media database.
        // This may also delete the file (for media types), but that is irrelevant as it will be truncated momentarily in any case.
        String where = MediaStore.MediaColumns.DATA + "=?";
        String[] selectionArgs = new String[] { file.getAbsolutePath() };
        contentResolver.delete(filesUri, where, selectionArgs);

        ContentValues values = new ContentValues();
        values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
        Uri uri = contentResolver.insert(filesUri, values);

        if (uri == null) {
            // Should not occur.
            throw new IOException("Internal error.");
        }

        return contentResolver.openOutputStream(uri);
    }
}

Source download (or cut/paste the above): http://android.nextapp.com/content/mediawrite/v1/MediaFile.java
Eclipse project with test app: http://android.nextapp.com/content/mediawrite/v1/MediaWrite.zip
APK of test app: http://android.nextapp.com/content/mediawrite/v1/MediaWrite.apk

The test project is currently configured to target the path /storage/extSdCard/MediaWriteTest (this is correct for a Note3, at least on 4.3...make sure you don't have anything there). Edit MainActivity.java in the Eclipse project to change it.

And again, let me stress that the above code might not work in the future should Google dislike it. I wouldn't recommend that the average app developer make use of this code, but if you're writing a file manager (or something else that competes with any of my other apps) :D, it might be useful to you. And actually at the time of writing, this functionality is NOT in FX File Explorer or WebSharing.
 
Last edited:

tliebeck

Senior Member
Sep 15, 2010
1,849
4,434
Southern California
Follow up:

My understanding is that creating and deleting files with this method does work on Android 4.4, but making new directories does not.

Creating directories does work on Android 4.3 (e.g. Google Play Edition) devices that have crippled secondary storage, so that mkdir() method will be useful for all five people who own GPE devices who are still using 4.3. :D
 
Last edited:

tliebeck

Senior Member
Sep 15, 2010
1,849
4,434
Southern California
This issue has been resolved in another thread using a much simpler method...

Sent from my GT-I9505 using Tapatalk

If that solution is for non-root app developers, can you provide a link?

Bear in mind this solution is for app developers writing apps for users who may be on stock firmware without root access. I'm familiar with the proper solution for rooted users, where media_rw is added for the WRITE_EXTERNAL_STORAGE permission in /etc/permissions/platform.xml.
 

pyler

Senior Member
Jan 13, 2013
1,279
2,372
If that solution is for non-root app developers, can you provide a link?

Bear in mind this solution is for app developers writing apps for users who may be on stock firmware without root access. I'm familiar with the proper solution for rooted users, where media_rw is added for the WRITE_EXTERNAL_STORAGE permission in /etc/permissions/platform.xml.

Root solution... Possible to grant WRITE_MEDIA_STORAGE via pm grant in shell (non root via adb shell?)? Samsung File Manager uses it and it can access to extsdcard. Better than modifying system file.

But I agree that we need Fixer app for that (to fix things in etc/permissions/platform.xml)
 
Last edited:
  • Like
Reactions: tliebeck

AMoosa

Senior Member
Sep 24, 2006
2,119
349
Samsung Galaxy Z Fold2
If that solution is for non-root app developers, can you provide a link?

Bear in mind this solution is for app developers writing apps for users who may be on stock firmware without root access. I'm familiar with the proper solution for rooted users, where media_rw is added for the WRITE_EXTERNAL_STORAGE permission in /etc/permissions/platform.xml.

Hi. Yes it's the one you have cited. Isn't everyone on xda rooted these days? Lol.

Sent from my GT-I9505 using Tapatalk
 

tliebeck

Senior Member
Sep 15, 2010
1,849
4,434
Southern California
Root solution... Possible to grant WRITE_MEDIA_STORAGE via pm grant in shell (non root via adb shell?)? Samsung File Manager uses it and it can access to extsdcard. Better than modifying system file.

But I agree that we need Fixer app for that (to fix things in etc/permissions/platform.xml)

The Samsung file manager declares it in its manifest, and I believe it's available to any system app that does so. I'm not sure on the method by which these permissions are granted (i.e., if it's based on having a certain signature, or if just being in /system is enough). I didn't think there was a means to grant it to any non-system app without using root though.

I did try just try declaring that in the manifest, and it pm grant returns the following (for shell and root):
Operation not allowed: java.lang.SecurityException: Permission android.permission.WRITE_MEDIA_STORAGE is not a changeable permission type
Thanks for the suggestion though, was worth a try.
 

dadda

Senior Member
Yeah, this certainly isn't going to be *personally* useful to anyone here...just posted it for app devs catering to users on non-rooted stock stuff.
I have a question, if I install the kitkat 4.4.2 update for sprint can I then install apk or is there a diffrent way also im not rooted. Cause this is the only thing stopping me from installing the update.
 

tliebeck

Senior Member
Sep 15, 2010
1,849
4,434
Southern California
I have a question, if I install the kitkat 4.4.2 update for sprint can I then install apk or is there a diffrent way also im not rooted. Cause this is the only thing stopping me from installing the update.

Would not recommend it, this is still not confirmed to work, and only affects applications that implement it. Only real solution is still rooting.
 

virtualdj

Member
Mar 28, 2006
31
10
Apparently the latest update of ES File Explorer should give write access to the external SD card without root, here's the changelog:
Code:
V3.1.1
-Samsung external card write issue on Android 4.4 Kitkat
-Multi-thread download & copy setting
-Video/audio thumbnails
-Download Facebook video when playing(use FB link in homepage)
It would be interesting to know the hack the app used to write and delete files there!
 

tliebeck

Senior Member
Sep 15, 2010
1,849
4,434
Southern California
Apparently the latest update of ES File Explorer should give write access to the external SD card without root, here's the changelog:
Code:
V3.1.1
-Samsung external card write issue on Android 4.4 Kitkat
-Multi-thread download & copy setting
-Video/audio thumbnails
-Download Facebook video when playing(use FB link in homepage)
It would be interesting to know the hack the app used to write and delete files there!

The above code should work for creating and deleting files.

My question would be whether or not they are able to create new folders? If they can, they've found a new workaround.
 

jimmod

Senior Member
Nov 9, 2010
254
334
The above code should work for creating and deleting files.

My question would be whether or not they are able to create new folders? If they can, they've found a new workaround.

ES Explorer can create directory on ext sdcard without root.
Anyone get updated with how to do that?
 

dburckh

Senior Member
Jan 26, 2011
260
91
ES Explorer can create directory on ext sdcard without root.
Anyone get updated with how to do that?

I just got 4.4.2 on m G Tab 8.3. I can't do a File.mkdir(), but I could do it from the command line. I could also do a cp. Maybe they are doing a Runtime.exec()?

Update. Just tried that. Did not work, permission denied. Weird that that shell user can, but an app user can't.
 
Last edited:

jimmod

Senior Member
Nov 9, 2010
254
334
I just got 4.4.2 on m G Tab 8.3. I can't do a File.mkdir(), but I could do it from the command line. I could also do a cp. Maybe they are doing a Runtime.exec()?

Update. Just tried that. Did not work, permission denied. Weird that that shell user can, but an app user can't.

Yes, adb shell (shell user) is able to create folder.
But if using terminal emulator app it permission denied.

Most likely shell user have write permission. But to switch to shell user from app will need root access.
 

tliebeck

Senior Member
Sep 15, 2010
1,849
4,434
Southern California
Sorry for not updating this thread promptly with this information, but there is now a 4.4 folder creation workaround, courtesy of the developer of X-plore File Manager. Works on Samsung devices, but apparently doesn't on Sony or HTC stuff. Not sure why this is the case.

I haven't had time to update the original standalone sample app/project, but here's the complete workaround class straight out of FX beta. This code is designed to be built against Android 2.1 (API level 7), hence the use of reflection. It's using the media API to generate album art for an MP3 track and place it in the folder you want created. Tiny MP3 file is attached, place that in /res/raw.

Code:
package nextapp.maui.io;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import nextapp.maui.AndroidEnvironment;
import nextapp.maui.Maui;
import nextapp.maui.R;
import nextapp.maui.storage.ContentUriUtil;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.provider.MediaStore;
import android.util.Log;

/**
 * Wrapper for manipulating files via the Android Media Content Provider. As of Android 4.4 KitKat, applications can no longer write
 * to the "secondary storage" of a device. Write operations using the java.io.File API will thus fail. This class restores access to
 * those write operations by way of the Media Content Provider.
 * 
 * Note that this class relies on the internal operational characteristics of the media content provider API, and as such is not
 * guaranteed to be future-proof. Then again, we did all think the java.io.File API was going to be future-proof for media card
 * access, so all bets are off.
 * 
 * If you're forced to use this class, it's because Google/AOSP made a very poor API decision in Android 4.4 KitKat.
 * Read more at https://plus.google.com/+TodLiebeck/posts/gjnmuaDM8sn
 *
 * Your application must declare the permission "android.permission.WRITE_EXTERNAL_STORAGE".
 */
public class MediaFile {
    
    private static final String NO_MEDIA = ".nomedia";
    private static final String ALBUM_ART_URI = "content://media/external/audio/albumart";
    private static final String[] ALBUM_PROJECTION = { BaseColumns._ID, MediaStore.Audio.AlbumColumns.ALBUM_ID, "media_type" };
        
    private static File getExternalFilesDir(Context context) {
        if (AndroidEnvironment.SDK < AndroidEnvironment.FROYO) {
            return null;
        }
        
        try {
            Method method = Context.class.getMethod("getExternalFilesDir", String.class);
            return (File) method.invoke(context, (String) null);
        } catch (SecurityException ex) {
            Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
            return null;
        } catch (NoSuchMethodException ex) {
            Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
            return null;
        } catch (IllegalArgumentException ex) {
            Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
            return null;
        } catch (IllegalAccessException ex) {
            Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
            return null;
        } catch (InvocationTargetException ex) {
            Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
            return null;
        }
    }
    
    public static boolean SUPPORTED = ContentUriUtil.FILES_URI != null;
    
    private final File file;
    private final Context context;
    private final ContentResolver contentResolver;

    public MediaFile(Context context, File file) {
        this.file = file;
        this.context = context;
        contentResolver = context.getContentResolver();
    }

    /**
     * Deletes the file. Returns true if the file has been successfully deleted or otherwise does not exist. This operation is not
     * recursive.
     */
    public boolean delete()
            throws IOException {
        if (!SUPPORTED) {
            throw new IOException("MediaFile API not supported by device.");
        }
        
        if (!file.exists()) {
            return true;
        }

        boolean directory = file.isDirectory();
        if (directory) {
            // Verify directory does not contain any files/directories within it.
            String[] files = file.list();
            if (files != null && files.length > 0) {
                return false;
            }
        }

        String where = MediaStore.MediaColumns.DATA + "=?";
        String[] selectionArgs = new String[] { file.getAbsolutePath() };

        // Delete the entry from the media database. This will actually delete media files (images, audio, and video).
        contentResolver.delete(ContentUriUtil.FILES_URI, where, selectionArgs);

        if (file.exists()) {
            // If the file is not a media file, create a new entry suggesting that this location is an image, even
            // though it is not.
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
            contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

            // Delete the created entry, such that content provider will delete the file.
            contentResolver.delete(ContentUriUtil.FILES_URI, where, selectionArgs);
        }

        return !file.exists();
    }

    public File getFile() {
        return file;
    }
    
    private int getTemporaryAlbumId() {
        final File temporaryTrack;
        try {
            temporaryTrack = installTemporaryTrack();
        } catch (IOException ex) {
            return 0;
        }
        
        final String[] selectionArgs = { temporaryTrack.getAbsolutePath() };
        Cursor cursor = contentResolver.query(ContentUriUtil.FILES_URI, ALBUM_PROJECTION, MediaStore.MediaColumns.DATA + "=?", 
                selectionArgs, null);
        if (cursor == null || !cursor.moveToFirst()) {
            if (cursor != null) {
                cursor.close();
                cursor = null;
            }
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.DATA, temporaryTrack.getAbsolutePath());
            values.put(MediaStore.MediaColumns.TITLE, "{MediaWrite Workaround}");
            values.put(MediaStore.MediaColumns.SIZE, temporaryTrack.length());
            values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/mpeg");
            values.put(MediaStore.Audio.AudioColumns.IS_MUSIC, true);
            contentResolver.insert(ContentUriUtil.FILES_URI, values);
        }
        cursor = contentResolver.query(ContentUriUtil.FILES_URI, ALBUM_PROJECTION, MediaStore.MediaColumns.DATA + "=?", 
                selectionArgs, null);
        if (cursor == null) {
            return 0;
        }
        if (!cursor.moveToFirst()) {
            cursor.close();
            return 0;
        }
        int id = cursor.getInt(0);
        int albumId = cursor.getInt(1);
        int mediaType = cursor.getInt(2);
        cursor.close();
        
        ContentValues values = new ContentValues();
        boolean updateRequired = false;
        if (albumId == 0) {
            values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, 13371337);
            updateRequired = true;
        }
        if (mediaType != 2) {
            values.put("media_type", 2);
            updateRequired = true;
        }
        if (updateRequired) {
            contentResolver.update(ContentUriUtil.FILES_URI, values, BaseColumns._ID + "=" + id, null);
        }
        cursor = contentResolver.query(ContentUriUtil.FILES_URI, ALBUM_PROJECTION, MediaStore.MediaColumns.DATA + "=?", 
                selectionArgs, null);
        if (cursor == null) {
            return 0;
        }
        
        try {
            if (!cursor.moveToFirst()) {
                return 0;
            }
            return cursor.getInt(1);
        } finally {
            cursor.close();
        }
    }
    
    
    private File installTemporaryTrack() 
    throws IOException {
        File externalFilesDir = getExternalFilesDir(context);
        if (externalFilesDir == null) {
            return null;
        }
        File temporaryTrack = new File(externalFilesDir, "temptrack.mp3");
        if (!temporaryTrack.exists()) {
            InputStream in = null;
            OutputStream out = null;
            try {
                in = context.getResources().openRawResource(R.raw.temptrack);
                out = new FileOutputStream(temporaryTrack);
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);
                }
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException ex) {
                        return null;
                    }
                }
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException ex) {
                        return null;
                    }
                }
            }
        }
        return temporaryTrack;
    }

    /**
     * Creates a new directory. Returns true if the directory was successfully created or exists.
     */
    public boolean mkdir()
            throws IOException {
        if (file.exists()) {
            return file.isDirectory();
        }
        
        File tmpFile = new File(file, ".MediaWriteTemp");
        int albumId = getTemporaryAlbumId();
        
        if (albumId == 0) {
            throw new IOException("Fail");
        }
        
        Uri albumUri = Uri.parse(ALBUM_ART_URI + '/' + albumId);
        ContentValues values = new ContentValues();
        values.put(MediaStore.MediaColumns.DATA, tmpFile.getAbsolutePath());
        
        if (contentResolver.update(albumUri, values, null, null) == 0) {
            values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, albumId);
            contentResolver.insert(Uri.parse(ALBUM_ART_URI), values);
        }
        
        try {
            ParcelFileDescriptor fd = contentResolver.openFileDescriptor(albumUri, "r");
            fd.close();
        } finally {
            MediaFile tmpMediaFile = new MediaFile(context, tmpFile);
            tmpMediaFile.delete();
        }
        
        return file.exists();
    }
    
    /**
     * Returns an OutputStream to write to the file. The file will be truncated immediately.
     */
    public OutputStream write(long size)
    throws IOException {
        if (!SUPPORTED) {
            throw new IOException("MediaFile API not supported by device.");
        }
        
        if (NO_MEDIA.equals(file.getName().trim())) {
            throw new IOException("Unable to create .nomedia file via media content provider API.");
        }

        if (file.exists() && file.isDirectory()) {
            throw new IOException("File exists and is a directory.");
        }

        // Delete any existing entry from the media database.
        // This may also delete the file (for media types), but that is irrelevant as it will be truncated momentarily in any case.
        String where = MediaStore.MediaColumns.DATA + "=?";
        String[] selectionArgs = new String[] { file.getAbsolutePath() };
        contentResolver.delete(ContentUriUtil.FILES_URI, where, selectionArgs);

        ContentValues values = new ContentValues();
        values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
        values.put(MediaStore.MediaColumns.SIZE, size);
        Uri uri = contentResolver.insert(ContentUriUtil.FILES_URI, values);

        if (uri == null) {
            // Should not occur.
            throw new IOException("Internal error.");
        }

        return contentResolver.openOutputStream(uri);
    }
}
 

Attachments

  • temptrack.mp3.zip
    625 bytes · Views: 477

Top Liked Posts

  • There are no posts matching your filters.
  • 8
    In Android 4.4 KitKat, Google/AOSP made the following change to the API specification, much to the detriment of app developers and users:

    "The WRITE_EXTERNAL_STORAGE permission must only grant write access to the primary external storage on a device. Apps must not be allowed to write to secondary external storage devices, except in their package-specific directories as allowed by synthesized permissions."
    Source: http://source.android.com/devices/tech/storage/index.html

    You can read my rather unhappy write-up about it here: https://plus.google.com/108338299717386673901/posts/gjnmuaDM8sn

    This only applies to dual-storage devices, i.e., devices with a user-writable internal flash storage AND a removable SD card. But if your device has both, as of Android 4.4, apps will no longer be able to write arbitrarily to the "secondary" storage (the SD card).

    There is however still an API exposed that will allow you to write to secondary storage, notably the media content provider. This is far from an ideal solution, and I imagine that someday it will not be possible.

    I've written a tiny bit of code to let your applications continue to work with files on the SD card using the media content provider. This code should only be used to write to the secondary storage on Android 4.4+ devices if all else fails. I would strongly recommend that you NEVER rely on this code. This code DOES NOT use root access.

    The class:
    Code:
    /*
     * Copyright (C) 2014 NextApp, Inc.
     * 
     * 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 nextapp.mediafile;
    
    import java.io.File;
    import java.io.IOException;
    import java.io.OutputStream;
    
    import android.content.ContentResolver;
    import android.content.ContentValues;
    import android.net.Uri;
    import android.provider.MediaStore;
    
    /**
     * Wrapper for manipulating files via the Android Media Content Provider. As of Android 4.4 KitKat, applications can no longer write
     * to the "secondary storage" of a device. Write operations using the java.io.File API will thus fail. This class restores access to
     * those write operations by way of the Media Content Provider.
     * 
     * Note that this class relies on the internal operational characteristics of the media content provider API, and as such is not
     * guaranteed to be future-proof. Then again, we did all think the java.io.File API was going to be future-proof for media card
     * access, so all bets are off.
     * 
     * If you're forced to use this class, it's because Google/AOSP made a very poor API decision in Android 4.4 KitKat.
     * Read more at https://plus.google.com/+TodLiebeck/posts/gjnmuaDM8sn
     *
     * Your application must declare the permission "android.permission.WRITE_EXTERNAL_STORAGE".
     */
    public class MediaFile {
    
        private final File file;
        private final ContentResolver contentResolver;
        private final Uri filesUri;
        private final Uri imagesUri;
    
        public MediaFile(ContentResolver contentResolver, File file) {
            this.file = file;
            this.contentResolver = contentResolver;
            filesUri = MediaStore.Files.getContentUri("external");
            imagesUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        }
    
        /**
         * Deletes the file. Returns true if the file has been successfully deleted or otherwise does not exist. This operation is not
         * recursive.
         */
        public boolean delete()
                throws IOException {
            if (!file.exists()) {
                return true;
            }
    
            boolean directory = file.isDirectory();
            if (directory) {
                // Verify directory does not contain any files/directories within it.
                String[] files = file.list();
                if (files != null && files.length > 0) {
                    return false;
                }
            }
    
            String where = MediaStore.MediaColumns.DATA + "=?";
            String[] selectionArgs = new String[] { file.getAbsolutePath() };
    
            // Delete the entry from the media database. This will actually delete media files (images, audio, and video).
            contentResolver.delete(filesUri, where, selectionArgs);
    
            if (file.exists()) {
                // If the file is not a media file, create a new entry suggesting that this location is an image, even
                // though it is not.
                ContentValues values = new ContentValues();
                values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
                contentResolver.insert(imagesUri, values);
    
                // Delete the created entry, such that content provider will delete the file.
                contentResolver.delete(filesUri, where, selectionArgs);
            }
    
            return !file.exists();
        }
    
        public File getFile() {
            return file;
        }
    
        /**
         * Creates a new directory. Returns true if the directory was successfully created or exists.
         */
        public boolean mkdir()
                throws IOException {
            if (file.exists()) {
                return file.isDirectory();
            }
    
            ContentValues values;
            Uri uri;
    
            // Create a media database entry for the directory. This step will not actually cause the directory to be created.
            values = new ContentValues();
            values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
            contentResolver.insert(filesUri, values);
    
            // Create an entry for a temporary image file within the created directory.
            // This step actually causes the creation of the directory.
            values = new ContentValues();
            values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath() + "/temp.jpg");
            uri = contentResolver.insert(imagesUri, values);
    
            // Delete the temporary entry.
            contentResolver.delete(uri, null, null);
    
            return file.exists();
        }
    
        /**
         * Returns an OutputStream to write to the file. The file will be truncated immediately.
         */
        public OutputStream write()
                throws IOException {
            if (file.exists() && file.isDirectory()) {
                throw new IOException("File exists and is a directory.");
            }
    
            // Delete any existing entry from the media database.
            // This may also delete the file (for media types), but that is irrelevant as it will be truncated momentarily in any case.
            String where = MediaStore.MediaColumns.DATA + "=?";
            String[] selectionArgs = new String[] { file.getAbsolutePath() };
            contentResolver.delete(filesUri, where, selectionArgs);
    
            ContentValues values = new ContentValues();
            values.put(MediaStore.Files.FileColumns.DATA, file.getAbsolutePath());
            Uri uri = contentResolver.insert(filesUri, values);
    
            if (uri == null) {
                // Should not occur.
                throw new IOException("Internal error.");
            }
    
            return contentResolver.openOutputStream(uri);
        }
    }

    Source download (or cut/paste the above): http://android.nextapp.com/content/mediawrite/v1/MediaFile.java
    Eclipse project with test app: http://android.nextapp.com/content/mediawrite/v1/MediaWrite.zip
    APK of test app: http://android.nextapp.com/content/mediawrite/v1/MediaWrite.apk

    The test project is currently configured to target the path /storage/extSdCard/MediaWriteTest (this is correct for a Note3, at least on 4.3...make sure you don't have anything there). Edit MainActivity.java in the Eclipse project to change it.

    And again, let me stress that the above code might not work in the future should Google dislike it. I wouldn't recommend that the average app developer make use of this code, but if you're writing a file manager (or something else that competes with any of my other apps) :D, it might be useful to you. And actually at the time of writing, this functionality is NOT in FX File Explorer or WebSharing.
    4
    Sorry for not updating this thread promptly with this information, but there is now a 4.4 folder creation workaround, courtesy of the developer of X-plore File Manager. Works on Samsung devices, but apparently doesn't on Sony or HTC stuff. Not sure why this is the case.

    I haven't had time to update the original standalone sample app/project, but here's the complete workaround class straight out of FX beta. This code is designed to be built against Android 2.1 (API level 7), hence the use of reflection. It's using the media API to generate album art for an MP3 track and place it in the folder you want created. Tiny MP3 file is attached, place that in /res/raw.

    Code:
    package nextapp.maui.io;
    
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    import nextapp.maui.AndroidEnvironment;
    import nextapp.maui.Maui;
    import nextapp.maui.R;
    import nextapp.maui.storage.ContentUriUtil;
    
    import android.content.ContentResolver;
    import android.content.ContentValues;
    import android.content.Context;
    import android.database.Cursor;
    import android.net.Uri;
    import android.os.ParcelFileDescriptor;
    import android.provider.BaseColumns;
    import android.provider.MediaStore;
    import android.util.Log;
    
    /**
     * Wrapper for manipulating files via the Android Media Content Provider. As of Android 4.4 KitKat, applications can no longer write
     * to the "secondary storage" of a device. Write operations using the java.io.File API will thus fail. This class restores access to
     * those write operations by way of the Media Content Provider.
     * 
     * Note that this class relies on the internal operational characteristics of the media content provider API, and as such is not
     * guaranteed to be future-proof. Then again, we did all think the java.io.File API was going to be future-proof for media card
     * access, so all bets are off.
     * 
     * If you're forced to use this class, it's because Google/AOSP made a very poor API decision in Android 4.4 KitKat.
     * Read more at https://plus.google.com/+TodLiebeck/posts/gjnmuaDM8sn
     *
     * Your application must declare the permission "android.permission.WRITE_EXTERNAL_STORAGE".
     */
    public class MediaFile {
        
        private static final String NO_MEDIA = ".nomedia";
        private static final String ALBUM_ART_URI = "content://media/external/audio/albumart";
        private static final String[] ALBUM_PROJECTION = { BaseColumns._ID, MediaStore.Audio.AlbumColumns.ALBUM_ID, "media_type" };
            
        private static File getExternalFilesDir(Context context) {
            if (AndroidEnvironment.SDK < AndroidEnvironment.FROYO) {
                return null;
            }
            
            try {
                Method method = Context.class.getMethod("getExternalFilesDir", String.class);
                return (File) method.invoke(context, (String) null);
            } catch (SecurityException ex) {
                Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
                return null;
            } catch (NoSuchMethodException ex) {
                Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
                return null;
            } catch (IllegalArgumentException ex) {
                Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
                return null;
            } catch (IllegalAccessException ex) {
                Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
                return null;
            } catch (InvocationTargetException ex) {
                Log.d(Maui.LOG_TAG, "Unexpected reflection error.", ex);
                return null;
            }
        }
        
        public static boolean SUPPORTED = ContentUriUtil.FILES_URI != null;
        
        private final File file;
        private final Context context;
        private final ContentResolver contentResolver;
    
        public MediaFile(Context context, File file) {
            this.file = file;
            this.context = context;
            contentResolver = context.getContentResolver();
        }
    
        /**
         * Deletes the file. Returns true if the file has been successfully deleted or otherwise does not exist. This operation is not
         * recursive.
         */
        public boolean delete()
                throws IOException {
            if (!SUPPORTED) {
                throw new IOException("MediaFile API not supported by device.");
            }
            
            if (!file.exists()) {
                return true;
            }
    
            boolean directory = file.isDirectory();
            if (directory) {
                // Verify directory does not contain any files/directories within it.
                String[] files = file.list();
                if (files != null && files.length > 0) {
                    return false;
                }
            }
    
            String where = MediaStore.MediaColumns.DATA + "=?";
            String[] selectionArgs = new String[] { file.getAbsolutePath() };
    
            // Delete the entry from the media database. This will actually delete media files (images, audio, and video).
            contentResolver.delete(ContentUriUtil.FILES_URI, where, selectionArgs);
    
            if (file.exists()) {
                // If the file is not a media file, create a new entry suggesting that this location is an image, even
                // though it is not.
                ContentValues values = new ContentValues();
                values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
                contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    
                // Delete the created entry, such that content provider will delete the file.
                contentResolver.delete(ContentUriUtil.FILES_URI, where, selectionArgs);
            }
    
            return !file.exists();
        }
    
        public File getFile() {
            return file;
        }
        
        private int getTemporaryAlbumId() {
            final File temporaryTrack;
            try {
                temporaryTrack = installTemporaryTrack();
            } catch (IOException ex) {
                return 0;
            }
            
            final String[] selectionArgs = { temporaryTrack.getAbsolutePath() };
            Cursor cursor = contentResolver.query(ContentUriUtil.FILES_URI, ALBUM_PROJECTION, MediaStore.MediaColumns.DATA + "=?", 
                    selectionArgs, null);
            if (cursor == null || !cursor.moveToFirst()) {
                if (cursor != null) {
                    cursor.close();
                    cursor = null;
                }
                ContentValues values = new ContentValues();
                values.put(MediaStore.MediaColumns.DATA, temporaryTrack.getAbsolutePath());
                values.put(MediaStore.MediaColumns.TITLE, "{MediaWrite Workaround}");
                values.put(MediaStore.MediaColumns.SIZE, temporaryTrack.length());
                values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/mpeg");
                values.put(MediaStore.Audio.AudioColumns.IS_MUSIC, true);
                contentResolver.insert(ContentUriUtil.FILES_URI, values);
            }
            cursor = contentResolver.query(ContentUriUtil.FILES_URI, ALBUM_PROJECTION, MediaStore.MediaColumns.DATA + "=?", 
                    selectionArgs, null);
            if (cursor == null) {
                return 0;
            }
            if (!cursor.moveToFirst()) {
                cursor.close();
                return 0;
            }
            int id = cursor.getInt(0);
            int albumId = cursor.getInt(1);
            int mediaType = cursor.getInt(2);
            cursor.close();
            
            ContentValues values = new ContentValues();
            boolean updateRequired = false;
            if (albumId == 0) {
                values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, 13371337);
                updateRequired = true;
            }
            if (mediaType != 2) {
                values.put("media_type", 2);
                updateRequired = true;
            }
            if (updateRequired) {
                contentResolver.update(ContentUriUtil.FILES_URI, values, BaseColumns._ID + "=" + id, null);
            }
            cursor = contentResolver.query(ContentUriUtil.FILES_URI, ALBUM_PROJECTION, MediaStore.MediaColumns.DATA + "=?", 
                    selectionArgs, null);
            if (cursor == null) {
                return 0;
            }
            
            try {
                if (!cursor.moveToFirst()) {
                    return 0;
                }
                return cursor.getInt(1);
            } finally {
                cursor.close();
            }
        }
        
        
        private File installTemporaryTrack() 
        throws IOException {
            File externalFilesDir = getExternalFilesDir(context);
            if (externalFilesDir == null) {
                return null;
            }
            File temporaryTrack = new File(externalFilesDir, "temptrack.mp3");
            if (!temporaryTrack.exists()) {
                InputStream in = null;
                OutputStream out = null;
                try {
                    in = context.getResources().openRawResource(R.raw.temptrack);
                    out = new FileOutputStream(temporaryTrack);
                    byte[] buffer = new byte[4096];
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) != -1) {
                        out.write(buffer, 0, bytesRead);
                    }
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException ex) {
                            return null;
                        }
                    }
                    if (out != null) {
                        try {
                            out.close();
                        } catch (IOException ex) {
                            return null;
                        }
                    }
                }
            }
            return temporaryTrack;
        }
    
        /**
         * Creates a new directory. Returns true if the directory was successfully created or exists.
         */
        public boolean mkdir()
                throws IOException {
            if (file.exists()) {
                return file.isDirectory();
            }
            
            File tmpFile = new File(file, ".MediaWriteTemp");
            int albumId = getTemporaryAlbumId();
            
            if (albumId == 0) {
                throw new IOException("Fail");
            }
            
            Uri albumUri = Uri.parse(ALBUM_ART_URI + '/' + albumId);
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.DATA, tmpFile.getAbsolutePath());
            
            if (contentResolver.update(albumUri, values, null, null) == 0) {
                values.put(MediaStore.Audio.AlbumColumns.ALBUM_ID, albumId);
                contentResolver.insert(Uri.parse(ALBUM_ART_URI), values);
            }
            
            try {
                ParcelFileDescriptor fd = contentResolver.openFileDescriptor(albumUri, "r");
                fd.close();
            } finally {
                MediaFile tmpMediaFile = new MediaFile(context, tmpFile);
                tmpMediaFile.delete();
            }
            
            return file.exists();
        }
        
        /**
         * Returns an OutputStream to write to the file. The file will be truncated immediately.
         */
        public OutputStream write(long size)
        throws IOException {
            if (!SUPPORTED) {
                throw new IOException("MediaFile API not supported by device.");
            }
            
            if (NO_MEDIA.equals(file.getName().trim())) {
                throw new IOException("Unable to create .nomedia file via media content provider API.");
            }
    
            if (file.exists() && file.isDirectory()) {
                throw new IOException("File exists and is a directory.");
            }
    
            // Delete any existing entry from the media database.
            // This may also delete the file (for media types), but that is irrelevant as it will be truncated momentarily in any case.
            String where = MediaStore.MediaColumns.DATA + "=?";
            String[] selectionArgs = new String[] { file.getAbsolutePath() };
            contentResolver.delete(ContentUriUtil.FILES_URI, where, selectionArgs);
    
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.DATA, file.getAbsolutePath());
            values.put(MediaStore.MediaColumns.SIZE, size);
            Uri uri = contentResolver.insert(ContentUriUtil.FILES_URI, values);
    
            if (uri == null) {
                // Should not occur.
                throw new IOException("Internal error.");
            }
    
            return contentResolver.openOutputStream(uri);
        }
    }
    2
    Too good to be true!

    Wow! This is perfect..
    2
    If that solution is for non-root app developers, can you provide a link?

    Bear in mind this solution is for app developers writing apps for users who may be on stock firmware without root access. I'm familiar with the proper solution for rooted users, where media_rw is added for the WRITE_EXTERNAL_STORAGE permission in /etc/permissions/platform.xml.

    Hi. Yes it's the one you have cited. Isn't everyone on xda rooted these days? Lol.

    Sent from my GT-I9505 using Tapatalk
    1
    This issue has been resolved in another thread using a much simpler method...

    Sent from my GT-I9505 using Tapatalk