Post Reply

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

OP tliebeck

3rd February 2014, 02:04 PM   |  #1  
tliebeck's Avatar
OP Senior Member
Flag Southern California
Thanks Meter: 2,204
 
10
1,090 posts
Join Date:Joined: Sep 2010
In Android 4.4 KitKat, Google/AOSP made the following change to the API specification, much to the detriment of app developers and users:

Quote:

"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/te...age/index.html

You can read my rather unhappy write-up about it here: https://plus.google.com/108338299717...ts/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/m...MediaFile.java
Eclipse project with test app: http://android.nextapp.com/content/m...MediaWrite.zip
APK of test app: http://android.nextapp.com/content/m...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) , 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 by tliebeck; 29th April 2014 at 02:54 PM. Reason: Removed no-longer-accurate comment about this being untested.
The Following 7 Users Say Thank You to tliebeck For This Useful Post: [ View ]
3rd February 2014, 02:24 PM   |  #2  
tliebeck's Avatar
OP Senior Member
Flag Southern California
Thanks Meter: 2,204
 
10
1,090 posts
Join Date:Joined: Sep 2010
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.
Last edited by tliebeck; 13th March 2014 at 03:40 AM.
3rd February 2014, 03:03 PM   |  #3  
Senior Member
Thanks Meter: 482
 
397 posts
Join Date:Joined: Jan 2013
More
Too good to be true!

Wow! This is perfect..
The Following 2 Users Say Thank You to pyler For This Useful Post: [ View ]
3rd February 2014, 09:08 PM   |  #4  
Senior Member
Thanks Meter: 193
 
1,483 posts
Join Date:Joined: Sep 2006
More
This issue has been resolved in another thread using a much simpler method...

Sent from my GT-I9505 using Tapatalk
The Following User Says Thank You to AMoosa For This Useful Post: [ View ]
3rd February 2014, 09:52 PM   |  #5  
tliebeck's Avatar
OP Senior Member
Flag Southern California
Thanks Meter: 2,204
 
10
1,090 posts
Join Date:Joined: Sep 2010
Quote:
Originally Posted by AMoosa

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.
4th February 2014, 06:32 PM   |  #6  
Senior Member
Thanks Meter: 482
 
397 posts
Join Date:Joined: Jan 2013
More
Quote:
Originally Posted by tliebeck

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 by pyler; 4th February 2014 at 06:42 PM.
The Following User Says Thank You to pyler For This Useful Post: [ View ]
4th February 2014, 08:29 PM   |  #7  
TecknoFreak's Avatar
Recognized Contributor
Flag Deltona
Thanks Meter: 832
 
1,939 posts
Join Date:Joined: Feb 2011
Donate to Me
More
I did this method when we first got kitkat a while back http://forum.xda-developers.com/show....php?t=2524277
But rooted devices. Good job bro
The Following User Says Thank You to TecknoFreak For This Useful Post: [ View ]
4th February 2014, 08:58 PM   |  #8  
Senior Member
Thanks Meter: 193
 
1,483 posts
Join Date:Joined: Sep 2006
More
Quote:
Originally Posted by tliebeck

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
The Following 2 Users Say Thank You to AMoosa For This Useful Post: [ View ]
5th February 2014, 12:32 PM   |  #9  
tliebeck's Avatar
OP Senior Member
Flag Southern California
Thanks Meter: 2,204
 
10
1,090 posts
Join Date:Joined: Sep 2010
Quote:
Originally Posted by pyler

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):
Quote:

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.
5th February 2014, 12:34 PM   |  #10  
tliebeck's Avatar
OP Senior Member
Flag Southern California
Thanks Meter: 2,204
 
10
1,090 posts
Join Date:Joined: Sep 2010
Quote:
Originally Posted by AMoosa

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

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.

Post Reply Subscribe to Thread

Tags
android, contentprovider, kitkat, sdcard, storage
Previous Thread Next Thread
Thread Tools Search this Thread
Search this Thread:

Advanced Search
Display Modes


Top Threads in Android Software and Hacking General [Developers Only] by ThreadRank