/***************************************************************************** * AudioUtil.java ***************************************************************************** * Copyright © 2011-2012 VLC authors and VideoLAN * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ package org.videolan.vlc.gui.audio; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.media.RingtoneManager; import android.net.Uri; import android.provider.MediaStore; import android.support.annotation.RequiresPermission; import android.util.Log; import android.widget.Toast; import org.videolan.libvlc.util.AndroidUtil; import org.videolan.vlc.BuildConfig; import org.videolan.vlc.MediaWrapper; import org.videolan.vlc.R; import org.videolan.vlc.VLCApplication; import org.videolan.vlc.util.AndroidDevices; import org.videolan.vlc.util.BitmapCache; import org.videolan.vlc.util.BitmapUtil; import org.videolan.vlc.util.MurmurHash; import org.videolan.vlc.util.Util; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; public class AudioUtil { public final static String TAG = "VLC/AudioUtil"; /** * Cache directory (/sdcard/Android/data/...) */ public static String CACHE_DIR = null; /** * VLC embedded art storage location */ public static String ART_DIR = null; /** * Cover caching directory */ public static String COVER_DIR = null; /** * User-defined playlist storage directory */ public static String PLAYLIST_DIR = null; public static final BitmapDrawable DEFAULT_COVER = new BitmapDrawable(VLCApplication.getAppResources(), BitmapCache.getFromResource(VLCApplication.getAppResources(), R.drawable.icon)); @RequiresPermission(android.Manifest.permission.WRITE_SETTINGS) public static void setRingtone(MediaWrapper song, Context context){ File newringtone = AndroidUtil.UriToFile(song.getUri()); if(newringtone == null || !newringtone.exists()) { Toast.makeText(context.getApplicationContext(),context.getString(R.string.ringtone_error), Toast.LENGTH_SHORT).show(); return; } ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DATA, newringtone.getAbsolutePath()); values.put(MediaStore.MediaColumns.TITLE, song.getTitle()); values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/*"); values.put(MediaStore.Audio.Media.ARTIST, song.getArtist()); values.put(MediaStore.Audio.Media.IS_RINGTONE, true); values.put(MediaStore.Audio.Media.IS_NOTIFICATION, false); values.put(MediaStore.Audio.Media.IS_ALARM, false); values.put(MediaStore.Audio.Media.IS_MUSIC, false); Uri uri = MediaStore.Audio.Media.getContentUriForPath(newringtone.getAbsolutePath()); Uri newUri; try { context.getContentResolver().delete(uri, MediaStore.MediaColumns.DATA + "=\"" + newringtone.getAbsolutePath() + "\"", null); newUri = context.getContentResolver().insert(uri, values); RingtoneManager.setActualDefaultRingtoneUri( context.getApplicationContext(), RingtoneManager.TYPE_RINGTONE, newUri ); } catch(Exception e) { Toast.makeText(context.getApplicationContext(), context.getString(R.string.ringtone_error), Toast.LENGTH_SHORT).show(); return; } Toast.makeText( context.getApplicationContext(), context.getString(R.string.ringtone_set, song.getTitle()), Toast.LENGTH_SHORT) .show(); } @SuppressLint("NewApi") public static void prepareCacheFolder(Context context) { try { if (AndroidUtil.isFroyoOrLater() && AndroidDevices.hasExternalStorage() && context.getExternalCacheDir() != null) CACHE_DIR = context.getExternalCacheDir().getPath(); else CACHE_DIR = AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY + "/Android/data/" + BuildConfig.APPLICATION_ID + "/cache"; } catch (Exception e) { // catch NPE thrown by getExternalCacheDir() CACHE_DIR = AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY + "/Android/data/" + BuildConfig.APPLICATION_ID + "/cache"; } ART_DIR = CACHE_DIR + "/art/"; COVER_DIR = CACHE_DIR + "/covers/"; PLAYLIST_DIR = CACHE_DIR + "/playlists/"; for(String path : Arrays.asList(ART_DIR, COVER_DIR)) { File file = new File(path); if (!file.exists()) file.mkdirs(); } } public static void clearCacheFolders() { for(String path : Arrays.asList(ART_DIR, COVER_DIR)) { File file = new File(path); if (file.exists()) deleteContent(file, false); } } private static void deleteContent(File dir, boolean deleteDir) { if (dir.isDirectory()) { File[] files = dir.listFiles(); if (files != null && files.length > 0) { for (File file : files) { deleteContent(file, true); } } } if (deleteDir) dir.delete(); } private static String getCoverFromMediaStore(Context context, MediaWrapper media) { final String album = media.getAlbum(); if (album == null) return null; ContentResolver contentResolver = context.getContentResolver(); Uri uri = android.provider.MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI; Cursor cursor = contentResolver.query(uri, new String[]{ MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ALBUM_ART}, MediaStore.Audio.Albums.ALBUM + " LIKE ?", new String[]{album}, null); if (cursor == null) { // do nothing } else if (!cursor.moveToFirst()) { // do nothing cursor.close(); } else { int titleColumn = cursor.getColumnIndex(android.provider.MediaStore.Audio.Albums.ALBUM_ART); String albumArt = cursor.getString(titleColumn); cursor.close(); return albumArt; } return null; } private static String getCoverFromVlc(Context context, MediaWrapper media) throws NoSuchAlgorithmException, UnsupportedEncodingException { String artworkURL = media.getArtworkURL(); if (artworkURL != null && artworkURL.startsWith("file://")) { return Uri.decode(artworkURL).replace("file://", ""); } else if(artworkURL != null && artworkURL.startsWith("attachment://")) { // Decode if the album art is embedded in the file String mArtist = Util.getMediaArtist(context, media); String mAlbum = Util.getMediaAlbum(context, media); /* Parse decoded attachment */ if( mArtist.length() == 0 || mAlbum.length() == 0 || mArtist.equals(VLCApplication.getAppContext().getString(R.string.unknown_artist)) || mAlbum.equals(VLCApplication.getAppContext().getString(R.string.unknown_album)) ) { /* If artist or album are missing, it was cached by title MD5 hash */ MessageDigest md = MessageDigest.getInstance("MD5"); byte[] binHash = md.digest((artworkURL + media.getTitle()).getBytes("UTF-8")); /* Convert binary hash to normal hash */ BigInteger hash = new BigInteger(1, binHash); String titleHash = hash.toString(16); while(titleHash.length() < 32) { titleHash = "0" + titleHash; } /* Use generated hash to find art */ artworkURL = ART_DIR + "/arturl/" + titleHash + "/art.png"; } else { /* Otherwise, it was cached by artist and album */ artworkURL = ART_DIR + "/artistalbum/" + mArtist + "/" + mAlbum + "/art.png"; } return artworkURL; } return null; } private static String getCoverFromFolder(MediaWrapper media) { File f = AndroidUtil.UriToFile(media.getUri()); if (f == null) return null; File folder = f.getParentFile(); if (folder == null) return null; final String[] imageExt = { ".png", ".jpeg", ".jpg"}; final String[] coverImages = { "Folder.jpg", /* Windows */ "AlbumArtSmall.jpg", /* Windows */ "AlbumArt.jpg", /* Windows */ "Album.jpg", ".folder.png", /* KDE? */ "cover.jpg", /* rockbox */ "thumb.jpg" }; /* Find the path without the extension */ int index = f.getName().lastIndexOf('.'); if (index > 0) { final String name = f.getName().substring(0, index); final String ext = f.getName().substring(index); final File[] files = folder.listFiles(new FilenameFilter() { public boolean accept(File dir, String filename) { return filename.startsWith(name) && Arrays.asList(imageExt).contains(ext); } }); if (files != null && files.length > 0) return files[0].getAbsolutePath(); } /* Find the classic cover Images */ if ( folder.listFiles() != null) { for (File file : folder.listFiles()) { for (String str : coverImages) { if (file.getAbsolutePath().endsWith(str)) return file.getAbsolutePath(); } } } return null; } private static String getCoverCachePath(Context context, MediaWrapper media, int width) { final int hash = MurmurHash.hash32(Util.getMediaArtist(context, media) + Util.getMediaAlbum(context, media)); return COVER_DIR + (hash >= 0 ? "" + hash : "m" + (-hash)) + "_" + width; } public static Bitmap getCoverFromMemCache(Context context, MediaWrapper media, int width) { if (media != null && media.getArtist() != null && media.getAlbum() != null) { final BitmapCache cache = BitmapCache.getInstance(); return cache.getBitmapFromMemCache(getCoverCachePath(context, media, width)); } else return null; } @SuppressLint("NewApi") public synchronized static Bitmap getCover(Context context, MediaWrapper media, int width) { BitmapCache cache = BitmapCache.getInstance(); String coverPath = null; Bitmap cover = null; String cachePath = null; File cacheFile = null; if (width <= 0) { Log.e(TAG, "Invalid cover width requested"); return null; } // if external storage is not available, skip covers to prevent slow audio browsing if (!AndroidDevices.hasExternalStorage()) return null; try { // try to load from cache if (media.getArtist() != null && media.getAlbum() != null) { cachePath = getCoverCachePath(context, media, width); // try to get the cover from the LRUCache first cover = cache.getBitmapFromMemCache(cachePath); if (cover != null) return cover; // try to get the cover from the storage cache cacheFile = new File(cachePath); if (cacheFile.exists()) { if (cacheFile.length() > 0) coverPath = cachePath; else return null; } } // try to get it from VLC if (coverPath == null || !cacheFile.exists()) coverPath = getCoverFromVlc(context, media); // try to get the cover from android MediaStore if (coverPath == null || !(new File(coverPath)).exists()) coverPath = getCoverFromMediaStore(context, media); // no found yet, looking in folder if (coverPath == null || !(new File(coverPath)).exists()) coverPath = getCoverFromFolder(media); // read (and scale?) the bitmap cover = readCoverBitmap(coverPath, width); // store cover into both cache if (cachePath != null) { writeBitmap(cover, cachePath); cache.addBitmapToMemCache(cachePath, cover); } } catch (Exception e) { e.printStackTrace(); } return cover; } private static void writeBitmap(Bitmap bitmap, String path) throws IOException { OutputStream out = null; try { File file = new File(path); if (file.exists() && file.length() > 0) return; out = new BufferedOutputStream(new FileOutputStream(file), 4096); if (bitmap != null) bitmap.compress(CompressFormat.JPEG, 90, out); } catch (Exception e) { Log.e(TAG, "writeBitmap failed : "+ e.getMessage()); } finally { Util.close(out); } } private static Bitmap readCoverBitmap(String path, int dipWidth) { Bitmap cover = null; BitmapFactory.Options options = new BitmapFactory.Options(); int width = Util.convertDpToPx(dipWidth); /* Get the resolution of the bitmap without allocating the memory */ options.inJustDecodeBounds = true; if (AndroidUtil.isHoneycombOrLater()) options.inMutable = true; BitmapUtil.setInBitmap(options); BitmapFactory.decodeFile(path, options); if (options.outWidth > 0 && options.outHeight > 0) { options.inJustDecodeBounds = false; options.inSampleSize = 1; // Find the best decoding scale for the bitmap while( options.outWidth / options.inSampleSize > width) options.inSampleSize = options.inSampleSize * 2; // Decode the file (with memory allocation this time) BitmapUtil.setInBitmap(options); cover = BitmapFactory.decodeFile(path, options); } return cover; } public static Bitmap getCover(Context context, ArrayList<MediaWrapper> list, int width, boolean fromMemCache) { Bitmap cover = null; LinkedList<String> testedAlbums = new LinkedList<String>(); for (MediaWrapper media : list) { /* No list cover is artist or album are null */ if (media.getAlbum() == null || media.getArtist() == null) continue; if (testedAlbums.contains(media.getAlbum())) continue; cover = fromMemCache ? AudioUtil.getCoverFromMemCache(context, media, width) : AudioUtil.getCover(context, media, width); if (cover != null) break; else if (media.getAlbum() != null) testedAlbums.add(media.getAlbum()); } return cover; } public static Bitmap getCoverFromMemCache(Context context, ArrayList<MediaWrapper> list, int width) { return getCover(context, list, width, true); } public static Bitmap getCover(Context context, ArrayList<MediaWrapper> list, int width) { return getCover(context, list, width, false); } }