package org.webinos.android.impl.mediacontent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.webinos.android.impl.mediacontent.Mapping.CompositeDbField;
import org.webinos.android.impl.mediacontent.Mapping.DbField;
import org.webinos.android.impl.mediacontent.Mapping.SingleDbField;
import org.webinos.api.DeviceAPIError;
import org.webinos.api.mediacontent.FilterValues;
import org.webinos.api.mediacontent.MediaAudio;
import org.webinos.api.mediacontent.MediaContentErrorCallback;
import org.webinos.api.mediacontent.MediaFolder;
import org.webinos.api.mediacontent.MediaFolderSuccessCallback;
import org.webinos.api.mediacontent.MediaImage;
import org.webinos.api.mediacontent.MediaItem;
import org.webinos.api.mediacontent.MediaItemCollection;
import org.webinos.api.mediacontent.MediaItemSuccessCallback;
import org.webinos.api.mediacontent.MediaSource;
import org.webinos.api.mediacontent.MediaVideo;
import org.webinos.api.mediacontent.SortMode;
import org.webinos.api.mediacontent.ThumbnailCallback;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Environment;
import android.os.FileObserver;
import android.provider.MediaStore;
import android.util.Log;
import org.blinkenlights.jid3.*;
import org.blinkenlights.jid3.v1.*;
import org.blinkenlights.jid3.v2.*;
class LocalMediaSource extends MediaSource {
private Context ctx;
private GetFoldersStrategy getFoldersStrategy;
private String volumeName;
private String storageType;
private String[] toplevelMediaFolders = null;
private Map<String, FolderObserver> folderObservers = new HashMap<String, FolderObserver>();
ContentResolver contentResolver;
Uri contentUri;
enum GetFoldersStrategy { ALL, STANDARD_FOLDERS };
public LocalMediaSource(
Context ctx,
String volumeName,
GetFoldersStrategy getFoldersStrategy) {
this.ctx = ctx;
this.volumeName = volumeName;
this.getFoldersStrategy = getFoldersStrategy;
this.contentResolver = ctx.getContentResolver();
this.contentUri = MediaStore.Files.getContentUri(this.volumeName);
if(this.volumeName.equals("internal"))
this.storageType = MediaFolder.STORAGE_TYPE_INTERNAL;
else if(this.volumeName.equals("external"))
this.storageType = MediaFolder.STORAGE_TYPE_EXTERNAL;
else
throw new DeviceAPIError(DeviceAPIError.INVALID_ERROR);
if(this.getFoldersStrategy.equals(GetFoldersStrategy.STANDARD_FOLDERS)) {
this.toplevelMediaFolders = new String[] {
Environment.DIRECTORY_MUSIC,
Environment.DIRECTORY_PICTURES,
Environment.DIRECTORY_MOVIES,
Environment.DIRECTORY_DOWNLOADS,
Environment.DIRECTORY_DCIM
};
} else if(this.getFoldersStrategy.equals(GetFoldersStrategy.ALL)) {
this.toplevelMediaFolders = new String[] {
Environment.getExternalStorageDirectory().toString()
};
}
/* observe all subfolders to reflect changes into MediaStore */
for(String folderPath : this.toplevelMediaFolders)
addFolderObservers(folderPath);
}
@Override
public void getFolders(
MediaFolderSuccessCallback successCallback,
MediaContentErrorCallback errorCallback) {
if(getFoldersStrategy.equals(GetFoldersStrategy.STANDARD_FOLDERS)) {
List<MediaFolder> folders = new ArrayList<MediaFolder>();
if(storageType.equals(MediaFolder.STORAGE_TYPE_EXTERNAL)) {
int i = 0;
for(String folderName : toplevelMediaFolders) {
File folder = Environment.getExternalStoragePublicDirectory(folderName);
if(folder != null && folder.exists() && folder.isDirectory()) {
folders.add(new MediaFolder(
String.valueOf(i++),
folder.getPath(),
folder.getName(),
storageType,
new Date(folder.lastModified())
)
);
}
}
}
if(successCallback != null)
successCallback.onSuccess(folders.toArray(new MediaFolder[folders.size()]));
} else if(getFoldersStrategy.equals(GetFoldersStrategy.ALL)) {
GetFoldersOperation op = new GetFoldersOperation(
storageType,
successCallback,
errorCallback
);
new Thread(op).start();
}
}
@Override
public void findItems(
MediaItemSuccessCallback successCallback,
MediaContentErrorCallback errorCallback,
String folderId,
FilterValues filterValues,
SortMode sortMode,
long count,
long offset) {
FindItemsOperation op = new FindItemsOperation(
successCallback,
errorCallback,
folderId,
AbstractFilter.getFilter(filterValues),
sortMode,
2,
0
);
new Thread(op).start();
}
@Override
public void getThumb(final long id, final ThumbnailCallback callback) {
GetThumbOperation op = new GetThumbOperation(
this.ctx.getContentResolver(),
id,
callback
);
new Thread(op).start();
}
/***************
* Observe folders
***************/
private class FolderObserver extends FileObserver {
private Context ctxLocalMediaSource;
public FolderObserver(String path) {
super(path);
this.ctxLocalMediaSource = ctx;
}
@Override
public void onEvent(int event, String path) {
if(event == FileObserver.MODIFY) {
Log.d("FolderObserver", "event: " + event + " path: " + path);
ctxLocalMediaSource.sendBroadcast(new Intent(
Intent.ACTION_MEDIA_MOUNTED,
Uri.parse("file://" + Environment.getExternalStorageDirectory())
)
);
}
}
}
private void addFolderObservers(String directoryName) {
File directory = new File(directoryName);
if(directory != null && directory.isDirectory()) {
FolderObserver folderObserver = new FolderObserver(directoryName);
if(!folderObservers.containsKey(directoryName))
folderObservers.put(directoryName, folderObserver);
folderObservers.get(directoryName).startWatching();
Log.d("LocalMediaSource", "add observer: " + directoryName);
File[] fList = directory.listFiles();
if(fList != null)
for(File file : fList)
if(file.isDirectory()) addFolderObservers(file.getAbsolutePath());
}
}
/***************
* Get folders
***************/
private static String commaSeparatedStringFromArray(String[] array) {
StringBuffer buf = new StringBuffer();
if(array != null && array.length > 0) {
buf.append(array[0]);
for(int i = 1; i < array.length; i++) {
buf.append(",");
buf.append(array[i]);
}
}
return buf.toString();
}
private class GetFoldersOperation implements Runnable {
private String storageType;
private MediaFolderSuccessCallback successCallback;
private MediaContentErrorCallback errorCallback;
public GetFoldersOperation(
String storageType,
MediaFolderSuccessCallback successCallback,
MediaContentErrorCallback errorCallback) {
this.storageType = storageType;
this.successCallback = successCallback;
this.errorCallback = errorCallback;
}
@Override
public void run() {
try {
Cursor cursor = null;
List<String> mediaFolderIds = new ArrayList<String>();
String[] projection = {
"DISTINCT " + MediaStore.Files.FileColumns.PARENT
};
String selection = MediaStore.Files.FileColumns.MEDIA_TYPE
+ " IN("
+ commaSeparatedStringFromArray(new String[] {
String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO),
String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO),
String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) })
+ ")";
String[] selectionArgs = null;
try {
cursor = contentResolver.query(
contentUri,
projection,
selection,
selectionArgs,
null
);
while(cursor.moveToNext()) mediaFolderIds.add(cursor.getString(0));
} finally {
if(cursor != null) cursor.close();
}
List<MediaFolder> mediaFolders = new ArrayList<MediaFolder>();
projection = new String[] {
"DISTINCT " + MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.TITLE,
MediaStore.Files.FileColumns.DATE_MODIFIED
};
selection = MediaStore.Files.FileColumns._ID
+ " IN("
+ commaSeparatedStringFromArray(
mediaFolderIds.toArray(new String[mediaFolderIds.size()])
)
+ ")";
selectionArgs = null;
try {
cursor = contentResolver.query(
contentUri,
projection,
selection,
selectionArgs,
null
);
while(cursor.moveToNext()) {
MediaFolder mediaFolder = new MediaFolder(
cursor.getString(0),
cursor.getString(1),
cursor.getString(2),
storageType,
new Date(cursor.getLong(3) * 1000)
);
mediaFolders.add(mediaFolder);
}
} finally {
if(cursor != null) cursor.close();
}
if(successCallback != null) {
successCallback.onSuccess(
mediaFolders.toArray(new MediaFolder[mediaFolders.size()])
);
}
} catch (Exception e) {
if(errorCallback != null) errorCallback.onError(e.toString());
}
}
}
/***************
* Get items
***************/
private Object getValue(Cursor cursor, SingleDbField dbField) {
Object value = null;
int index = cursor.getColumnIndex(dbField.getName());
if(index != -1) {
if(cursor.getType(index) == Cursor.FIELD_TYPE_STRING) {
value = cursor.getString(index);
} else if(cursor.getType(index) == Cursor.FIELD_TYPE_INTEGER) {
value = cursor.getLong(index);
} else if(cursor.getType(index) == Cursor.FIELD_TYPE_FLOAT) {
value = cursor.getDouble(index);
}
}
if(dbField.getTranslator() != null) {
value = dbField.getTranslator().getAttribValue(value);
}
return value;
}
private class FindItemsOperation implements Runnable {
private MediaItemSuccessCallback successCallback;
private MediaContentErrorCallback errorCallback;
private String folderId;
private AbstractFilter filter;
private SortMode sortMode;
private long count;
private long offset;
public FindItemsOperation(
MediaItemSuccessCallback successCallback,
MediaContentErrorCallback errorCallback,
String folderId,
AbstractFilter filter,
SortMode sortMode,
long count,
long offset) {
this.successCallback = successCallback;
this.errorCallback = errorCallback;
this.folderId = folderId;
this.filter = filter;
this.sortMode = sortMode;
this.count = count;
this.offset = offset;
}
@Override
public void run() {
Cursor cursor = null;
try {
String[] projection = Mapping.getProjection();
String[] selectionArgs = null;
String selection = MediaStore.Files.FileColumns.MEDIA_TYPE
+ " != "
+ MediaStore.Files.FileColumns.MEDIA_TYPE_NONE;
if(filter != null) {
SelectStatement selectStmt = QueryBuilder.getSelect(filter);
if(selectStmt != null) {
selection += " AND " + selectStmt.getStatement();
selectionArgs = selectStmt.getArgs();
}
}
if(folderId != null) {
selection += " AND ("
+ MediaStore.Files.FileColumns.PARENT
+ " = "
+ folderId + ")"
+ " OR ("
+ MediaStore.Files.FileColumns._ID
+ " = "
+ folderId + ")";
}
String sortOrder = null;
if(sortMode != null) {
DbField dbField = Mapping.getDbField(sortMode.attributeName);
if(dbField instanceof SingleDbField) {
sortOrder = ((SingleDbField)dbField).getName()
+ " "
+ sortMode.sortModeOrder;
} else {
throw new DeviceAPIError(DeviceAPIError.INVALID_VALUES_ERR);
}
}
List<MediaItem> mediaItems = new ArrayList<MediaItem>();
Map<String, Object> valueSet = new HashMap<String, Object>();
cursor = contentResolver.query(
contentUri,
projection,
selection,
selectionArgs,
sortOrder
);
while(cursor.moveToNext()) {
for(String attribute : Mapping.getAttributes()) {
DbField dbField = Mapping.getDbField(attribute);
if(dbField instanceof SingleDbField) {
SingleDbField singleDbField = (SingleDbField) dbField;
Object attribValue = getValue(cursor, singleDbField);
if(attribValue != null) {
valueSet.put(attribute, attribValue);
}
} else if(dbField instanceof CompositeDbField) {
CompositeDbField compositeDbField = (CompositeDbField) dbField;
Object[] values = new Object[compositeDbField.getDbFields().length];
int i = 0;
for(SingleDbField singleDbField : compositeDbField.getDbFields()) {
values[i++] = getValue(cursor, singleDbField);
}
if(compositeDbField.getCompositeHandler() != null) {
Object attribValue = compositeDbField.getCompositeHandler().getComposite(values);
if(attribValue != null) valueSet.put(attribute, attribValue);
} else {
throw new DeviceAPIError(DeviceAPIError.INVALID_ERROR);
}
}
}
String mediaType = (String)valueSet.get("type");
if(valueSet.containsKey("itemURI") &&
(new File((String)valueSet.get("itemURI"))).exists()) {
if(MediaItem.MEDIATYPE_AUDIO.equals(mediaType)) {
mediaItems.add(newMediaAudio(valueSet));
} else if(MediaItem.MEDIATYPE_VIDEO.equals(mediaType)) {
mediaItems.add(newMediaVideo(valueSet));
} else if(MediaItem.MEDIATYPE_IMAGE.equals(mediaType)) {
mediaItems.add(newMediaImage(valueSet));
} else if(MediaItem.MEDIATYPE_UNKNOWN.equals(mediaType)) {
/* do nothing */
}
}
}
MediaItemCollection mediaItemCollection = new MediaItemCollection();
mediaItemCollection.size = mediaItems.size();
mediaItemCollection.audios = new MediaAudio[mediaItemCollection.size];
mediaItemCollection.images = new MediaImage[mediaItemCollection.size];
mediaItemCollection.videos = new MediaVideo[mediaItemCollection.size];
for(int i = 0; i < mediaItems.size(); i++) {
if(mediaItems.get(i) instanceof MediaAudio) {
mediaItemCollection.audios[i] = (MediaAudio)mediaItems.get(i);
} else if(mediaItems.get(i) instanceof MediaImage) {
mediaItemCollection.images[i] = (MediaImage)mediaItems.get(i);
} else if(mediaItems.get(i) instanceof MediaVideo) {
mediaItemCollection.videos[i] = (MediaVideo)mediaItems.get(i);
}
}
if(successCallback != null)
successCallback.onSuccess(mediaItemCollection);
} catch (Exception e) {
if(errorCallback != null) errorCallback.onError(e.toString());
} finally {
if(cursor != null) cursor.close();
}
}
private MediaAudio newMediaAudio(Map<String, Object> valueSet) {
MediaAudio mediaAudio = new MediaAudio(valueSet);
if(mediaAudio.itemURI != null) {
String filenameArray[] = mediaAudio.itemURI.split("\\.");
if(filenameArray[filenameArray.length - 1].equalsIgnoreCase("mp3")) {
/* handling MediaMetadataRetriever limitation, MP3 file handling with
* JID3 */
File oSourceFile = new File(mediaAudio.itemURI);
MediaFile oMediaFile = new MP3File(oSourceFile);
try {
ID3Tag[] aoID3Tag = oMediaFile.getTags();
for(int i=0; i < aoID3Tag.length; i++) {
if(aoID3Tag[i] instanceof ID3V1_0Tag) {
ID3V1_0Tag oID3V1_0Tag = (ID3V1_0Tag)aoID3Tag[i];
mediaAudio.album = oID3V1_0Tag.getAlbum();
mediaAudio.artists = new String[] {oID3V1_0Tag.getArtist()};
mediaAudio.genres = new String[] {oID3V1_0Tag.getGenre().toString()};
/* composers not supported by ID3V1_0Tag */
/* lyrics not supported by ID3V1_0Tag */
/* copyright not supported by ID3V1_0Tag */
/* bitrate not supported by ID3V1_0Tag */
/* trackNumber not supported by ID3V1_0Tag */
/* duration not supported by ID3V1_0Tag */
/* playedTime not supported by ID3V1_0Tag */
/* playCount not supported by ID3V1_0Tag */
} else if(aoID3Tag[i] instanceof ID3V2_3_0Tag) {
ID3V2_3_0Tag oID3V2_3_0Tag = (ID3V2_3_0Tag)aoID3Tag[i];
mediaAudio.album = oID3V2_3_0Tag.getAlbum();
mediaAudio.artists = new String[] {oID3V2_3_0Tag.getArtist()};
mediaAudio.genres = new String[] {oID3V2_3_0Tag.getGenre()};
mediaAudio.trackNumber = oID3V2_3_0Tag.getTrackNumber();
/* composers not supported by ID3V2_3_0Tag */
/* lyrics not supported by ID3V2_3_0Tag */
/* copyright not supported by ID3V2_3_0Tag */
/* bitrate not supported by ID3V2_3_0Tag */
/* duration not supported by ID3V2_3_0Tag */
/* playedTime not supported by ID3V2_3_0Tag */
/* playCount not supported by ID3V2_3_0Tag */
}
}
} catch (ID3Exception e) {
Log.d("NewMediaAudio", "Getting mp3 tag error for " + mediaAudio.itemURI + " Exception: " + e);
}
}
/* use MediaMetadataRetriever to get media tags */
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(mediaAudio.itemURI);
try {
if(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM) != null)
mediaAudio.album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
if(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) != null)
mediaAudio.artists = new String[] {
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
};
if(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE) != null)
mediaAudio.genres = new String[] {
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_GENRE)
};
if(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER) != null)
mediaAudio.composers = new String[] {
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER)
};
mediaAudio.trackNumber = Integer.parseInt(
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER)
);
mediaAudio.duration = Long.parseLong(
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION),
10
);
/* lyrics not supported by MediaMetadataRetriever */
/* copyright not supported by MediaMetadataRetriever */
/* bitrate not supported by MediaMetadataRetriever */
/* playedTime not supported by MediaMetadataRetriever */
/* playCount not supported by MediaMetadataRetriever */
mmr.release();
} catch (Exception e) {
Log.d("NewMeidaAudio", "Metadata retrieving error for: " + mediaAudio.itemURI + " Exception: " + e);
}
/* unrecognised audio safety */
checkForNullTagsAudio(mediaAudio);
}
Log.d("api", "album of " + mediaAudio.itemURI + " is " + mediaAudio.album);
Log.d("api", "artists of " + mediaAudio.itemURI + " is " + mediaAudio.artists[0]);
return mediaAudio;
}
private void checkForNullTagsAudio(MediaAudio mediaAudio) {
if(mediaAudio.album == null)
mediaAudio.album = "Unknown";
if(mediaAudio.artists == null)
mediaAudio.artists = new String[] {"Unknown"};
else if(mediaAudio.artists[0] == null)
mediaAudio.artists[0] = "Unknown";
if(mediaAudio.genres == null)
mediaAudio.genres = new String[] {"Unknown"};
else if(mediaAudio.genres[0] == null)
mediaAudio.genres[0] = "Unknown";
}
private MediaVideo newMediaVideo(Map<String, Object> valueSet) {
MediaVideo mediaVideo = new MediaVideo(valueSet);
if(mediaVideo.itemURI != null) {
/* use MediaMetadataRetriever to get media tags */
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(mediaVideo.itemURI);
try {
mediaVideo.album = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
if(mediaVideo.album == null) mediaVideo.album = "Unknown";
mediaVideo.artists = new String[] {
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)
};
if(mediaVideo.artists[0] == null) mediaVideo.artists[0] = "Unknown";
mediaVideo.duration = Long.parseLong(
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION),
10
);
/* geolocation not supported by MediaMetadataRetriever */
/* width not supported by MediaMetadataRetriever */
/* height not supported by MediaMetadataRetriever */
/* playedTime not supported by MediaMetadataRetriever */
/* playCount not supported by MediaMetadataRetriever */
mmr.release();
} catch(Exception e) {
Log.d("NewMeidaVideo", "Metadata retrieving error for: " + mediaVideo.itemURI + " Exception: " + e);
} finally {
/* unrecognised video safety */
if(mediaVideo.album == null)
mediaVideo.album = "Unknown";
if(mediaVideo.artists == null)
mediaVideo.artists = new String[] {"Unknown"};
else if(mediaVideo.artists[0] == null)
mediaVideo.artists[0] = "Unknown";
}
}
Log.d("api", "album of " + mediaVideo.itemURI + " is " + mediaVideo.album);
Log.d("api", "artists of " + mediaVideo.itemURI + " is " + mediaVideo.artists[0]);
return mediaVideo;
}
private MediaImage newMediaImage(Map<String, Object> valueSet) {
return (new MediaImage(valueSet));
}
}
/***************
* Get thumbs
***************/
private class GetThumbOperation implements Runnable {
private ContentResolver contentResolver;
private long id;
private ThumbnailCallback callback;
public GetThumbOperation(
ContentResolver contentResolver,
long id,
ThumbnailCallback callback) {
this.contentResolver = contentResolver;
this.id = id;
this.callback = callback;
}
@Override
public void run() {
BitmapFactory.Options options=new BitmapFactory.Options();
options.inSampleSize = 1;
Bitmap curThumb = MediaStore.Images.Thumbnails.getThumbnail(
contentResolver,
id,
MediaStore.Video.Thumbnails.MICRO_KIND,
options
);
if(curThumb == null) {
callback.onSuccess(true, null);
return;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
boolean isCompressed = curThumb.compress(
Bitmap.CompressFormat.JPEG,
80,
out
);
byte[] bytes = out.toByteArray();
callback.onSuccess(!isCompressed, bytes);
}
}
}