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);
}