marina
marina

Reputation: 1680

Android CRUD MediaStore API 26+

I have an application for API26+

With Android 10 and above (API29+) should be used MediaStore to access files, instead of Environment.getExternalStoragePublicDirectory

normally making of new approach would be maked in creation of if-block

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
    // deprecated code
} else {
    // new approach
}

But:

CRUD:

Is it possible to work with MediaStorage with API26+?

If yes, how? Many properties are added first in API29

Upvotes: 0

Views: 1914

Answers (1)

marina
marina

Reputation: 1680

After some own research and testing i have found the way to work with MediaStore and on old devices in same time.

First of all some helper classes needed:

With FileType we can support different file types in application at same time.

public enum FileType {

    File(Environment.DIRECTORY_DOCUMENTS, Constants.VERSION_29_ABOVE ? MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "text/plain"),
    Download(Environment.DIRECTORY_DOWNLOADS, Constants.VERSION_29_ABOVE ? MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "text/plain"),
    Image(Environment.DIRECTORY_PICTURES, Constants.VERSION_29_ABOVE ? MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "image/jpeg"),
    Audio(Environment.DIRECTORY_MUSIC, Constants.VERSION_29_ABOVE ? MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "audio/mpeg"),
    Video(Environment.DIRECTORY_MOVIES, Constants.VERSION_29_ABOVE ? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "video/mpeg");

    private final String directory;
    private final Uri contentUri;
    private String mimeType;

    FileType(String directory, Uri contentUri, String mimeType) {
        this.directory = directory;
        this.contentUri = contentUri;
        this.mimeType = mimeType;
    }

    public String getDirectory() {
        return directory;
    }

    public Uri getContentUri() {
        return contentUri;
    }

    public String getMimeType() {
        return mimeType;
    }

    public void setMimeType(String mimeType) {
        this.mimeType = mimeType;
    }
}

with setMimeType we can change current file extension and still use other settings

And we need simple callback class to get different results

public interface ObjectCallback<T> {

    void result(T object);
}

When we want write some data in a file, we need an OutputStream

With internal storage, the way to get a file has not changed

/**
 * opens OutputStream to write data to file
 *
 * @param context    activity context
 * @param fileName   relative file name
 * @param fileType   file type to get folder specific values for access
 * @param fileStream callback with file output stream to requested file
 * @return true if output stream successful opened, false otherwise
 */
public boolean openOutputStream(Context context, String fileName, FileType fileType, ObjectCallback<OutputStream> fileStream) {
    File internalFolder = context.getExternalFilesDir(fileType.getDirectory());
    File absFileName = new File(internalFolder, fileName);

    try {
        FileOutputStream fOut = new FileOutputStream(absFileName);
        fileStream.result(fOut);
    } catch (Exception e) {
        e.printStackTrace();

        return false;
    }

    return true;
}

By external storage, the way to access a file on API 28- and API 29+ is completely different. On API 28- we could just use a file for it. On API 29+ we shoould first check, if file already exists. If it (say text.txt) exists and we would make ContentResolver.insert, it would create text-1.txt and in worst case (endless loop) in could quick fill whole smartphone storage.

/**
 * @param context  application context
 * @param fileName relative file name
 * @return uri to file or null if file not exist
 */
public Uri getFileUri(Context context, String fileName) {
    // remove folders from file name
    fileName = fileName.contains("/") ? fileName.substring(fileName.lastIndexOf("/") + 1) : fileName;

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
        // Deprecated in API 29
        File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), Constants.PUBLIC_STORAGE_FOLDER);

        File file = new File(storageDir, fileName);

        return file.exists() ? Uri.fromFile(file) : null;
    } else {
        String folderName = Environment.DIRECTORY_PICTURES + File.separator + Constants.PUBLIC_STORAGE_FOLDER + File.separator;

        // get content resolver that can interact with public storage
        ContentResolver resolver = context.getContentResolver();

        String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";
        String[] selectionArgs = new String[]{folderName};

        Cursor cursor = resolver.query(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), null, selection, selectionArgs, null);

        Uri uri = null;

        if (cursor.getCount() > 0) {
            while (cursor.moveToNext()) {
                String itemFileName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));

                if (itemFileName.equals(fileName)) {
                    long id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID));

                    uri = ContentUris.withAppendedId(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), id);

                    break;
                }
            }
        }

        cursor.close();

        return uri;
    }
}

/**
 * opens OutputStream to write data to file
 *
 * @param context    activity context
 * @param fileName   relative file name
 * @param fileType   file type to get folder specific values for access
 * @param fileStream callback with file output stream to requested file
 * @return true if output stream successful opened, false otherwise
 */
public boolean openOutputStream(Context context, String fileName, FileType fileType, ObjectCallback<OutputStream> fileStream) {
    // remove folders from file name
    fileName = fileName.contains("/") ? fileName.substring(fileName.lastIndexOf("/") + 1) : fileName;

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
        File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), Constants.PUBLIC_STORAGE_FOLDER);

        if (!storageDir.exists() && !storageDir.mkdir()) {
            // directory for file not exists and not created. return false
            return false;
        }

        File file = new File(storageDir, fileName);

        try {
            FileOutputStream fOut = new FileOutputStream(file);
            fileStream.result(fOut);
        } catch (Exception e) {
            e.printStackTrace();

            return false;
        }
    } else {
        // get content resolver that can interact with public storage
        ContentResolver resolver = context.getContentResolver();

        // always check first if file already exist
        Uri existFile = getFileUri(context, fileName);

        if (existFile == null) {
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.MIME_TYPE, fileType.getMimeType());
            // absolute folder name
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, fileType.getDirectory() + File.separator + Constants.PUBLIC_STORAGE_FOLDER);
            // file name
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);

            // create uri to the file. If folder not exists, it would be created automatically
            Uri uri = resolver.insert(fileType.getContentUri(), values);

            // open stream from uri and write data to file
            try (OutputStream outputStream = resolver.openOutputStream(uri)) {
                fileStream.result(outputStream);

                // end changing of file
                // when this point reached, no exception was thrown and file write was successful
                values.put(MediaStore.MediaColumns.IS_PENDING, false);
                resolver.update(uri, values, null, null);
            } catch (Exception e) {
                e.printStackTrace();

                if (uri != null) {
                    // Don't leave an orphan entry in the MediaStore
                    resolver.delete(uri, null, null);
                }

                return false;
            }
        } else {
            // open stream from uri and write data to file
            try (OutputStream outputStream = resolver.openOutputStream(existFile)) {
                fileStream.result(outputStream);
            } catch (Exception e) {
                e.printStackTrace();

                // Don't leave an orphan entry in the MediaStore
                resolver.delete(existFile, null, null);

                return false;
            }
        }
    }

    return true;
}

In same way could be opened InputStream on all devices and different storage.

To delete file:

  • internal could be still deleted with file.delete on every API
  • external storage with API29+ requires user interaction to delete some file

FileProvider

To use Intent for showing some picture, crop picture or whatever, where we use setDataAndType

Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(photoUri, "image/*");
// allow to read the file
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

we need in some cases the FileProvider. Specially:

enter image description here

To show external file created with the app, the photoUri would look this way

 Uri photoUri = Constants.VERSION_29_ABOVE ? uriToFile : 
      FileProvider.getUriForFile(context,
           context.getApplicationContext().getPackageName() + ".provider",
           new File(uriToFile.getPath()));

Upvotes: 2

Related Questions