/* * Copyright (c) 2015 David Schulte * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package de.arcus.playmusiclib; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.support.v4.content.FileProvider; import android.support.v4.provider.DocumentFile; import android.text.TextUtils; import android.util.Log; import com.mpatric.mp3agic.ID3v1Genres; import com.mpatric.mp3agic.ID3v1Tag; import com.mpatric.mp3agic.ID3v2; import com.mpatric.mp3agic.ID3v22Tag; import com.mpatric.mp3agic.ID3v23Tag; import com.mpatric.mp3agic.ID3v24Tag; import com.mpatric.mp3agic.Mp3File; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.UUID; import de.arcus.framework.logger.Logger; import de.arcus.framework.superuser.SuperUser; import de.arcus.framework.superuser.SuperUserTools; import de.arcus.framework.utils.FileTools; import de.arcus.playmusiclib.enums.ID3v2Version; import de.arcus.playmusiclib.exceptions.CouldNotOpenDatabaseException; import de.arcus.playmusiclib.exceptions.NoSuperUserException; import de.arcus.playmusiclib.exceptions.PlayMusicNotFoundException; import de.arcus.playmusiclib.items.MusicTrack; /** * Connects to the PlayMusic data */ public class PlayMusicManager { /** * PlayMusic package id */ public static final String PLAYMUSIC_PACKAGE_ID = "com.google.android.music"; /** * The last created instance */ private static PlayMusicManager instance; /** * @return Gets the last created instance or returns null if there is no instance */ public static PlayMusicManager getInstance() { return instance; } /** * Context of the app, needed to access to the package manager */ private Context mContext; /** * @return Gets the app context */ public Context getContext() { return mContext; } /** * Play Music database */ private SQLiteDatabase mDatabase; /** * @return Gets the database */ public SQLiteDatabase getDatabase() { return mDatabase; } /** * Path to the private app data * Eg.: /data/data/com.google.android.music/ */ private String mPathPrivateData; /** * Paths to all possible public app data * Eg.: /sdcard/Android/data/com.google.android.music/ */ private String[] mPathPublicData; /** * Application info from PlayMusic */ private ApplicationInfo mPlayMusicApplicationInfo; /** * @return Gets the path to the database */ private String getDatabasePath() { return mPathPrivateData + "/databases/music.db"; } /** * The database will be copied to a temp folder to access from the app * @return Gets the temp path to the database */ private String getTempDatabasePath() { return getTempPath() + "music.db"; } /** * @return Gets the temp path to the exported music */ private String getTempPath() { // Marshmallow hack if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Use the internal storage instead String path = Environment.getExternalStorageDirectory() + "/PlayMusicExporter/"; FileTools.directoryCreate(path); return path; } else { return mContext.getCacheDir().getAbsolutePath(); } } /** * If this is set the data source will only load offline tracks */ private boolean mOfflineOnly; /** * @return Returns whether the data source should only load offline tracks */ public boolean getOfflineOnly() { return mOfflineOnly; } /** * @param offlineOnly Sets whether the data source should only load offline tracks */ public void setOfflineOnly(boolean offlineOnly) { mOfflineOnly = offlineOnly; } /** * If this is set the exporter will add ID3 tag to the mp3 files */ private boolean mID3Enable = true; /** * @return Gets whether the exporter adds ID3 tags to the mp3 files */ public boolean getID3Enable() { return mID3Enable; } /** * @param id3Enable Sets whether the exporter adds ID3 tags to the mp3 files */ public void setID3Enable(boolean id3Enable) { mID3Enable = id3Enable; } /** * If this is set the exporter will add the artwork to the ID2v2 tag */ private boolean mID3EnableArtwork = true; /** * @return Gets whether the exporter adds the artwork image */ public boolean getID3EnableArtwork() { return mID3EnableArtwork; } /** * @param id3EnableArtwork Sets whether the exporter adds the artwork image */ public void setID3EnableArtwork(boolean id3EnableArtwork) { mID3EnableArtwork = id3EnableArtwork; } /** * The ID3 artwork format (eg. JPEG or PNG) */ private Bitmap.CompressFormat mID3ArtworkFormat = Bitmap.CompressFormat.JPEG; /** * @return Gets the current artwork format */ public Bitmap.CompressFormat getID3ArtworkFormat() { return mID3ArtworkFormat; } /** * @param id3ArtworkFormat Sets the artwork format for the id3 tag */ public void setID3ArtworkFormat(Bitmap.CompressFormat id3ArtworkFormat) { mID3ArtworkFormat = id3ArtworkFormat; } /** * The ID3 artwork maximum size (0 = use original size) */ private int mID3ArtworkMaximumSize = 512; /** * @return Gets the current artwork format */ public int getID3ArtworkMaximumSize() { return mID3ArtworkMaximumSize; } /** * @param id3ArtworkMaximumSize Sets the artwork maximum size of the artwork. * If the original artwork is larger than this value the app will * sample it down. (0 = use original size) */ public void setID3ArtworkMaximumSize(int id3ArtworkMaximumSize) { mID3ArtworkMaximumSize = id3ArtworkMaximumSize; } /** * If this is set the exporter will also adds ID3v1 tags */ private boolean mID3EnableFallback = true; /** * @return Gets whether the exporter adds ID3v1 tags as fallback */ public boolean getID3EnableFallback() { return mID3EnableFallback; } /** * @param id3EnableFallback Sets whether the exporter adds ID3v1 tags as fallback */ public void setID3EnableFallback(boolean id3EnableFallback) { mID3EnableFallback = id3EnableFallback; } /** * The sub version of ID3v2 * Use 2.3 for default to fix issues with the Windows Windows Media Player */ private ID3v2Version mID3v2Version = ID3v2Version.ID3v23; /** * @return Gets the sub version of ID3v2 */ public ID3v2Version getID3v2Version() { return mID3v2Version; } /** * @param id3v2Version Sets the sub version of ID3v2 */ public void setID3v2Version(ID3v2Version id3v2Version) { mID3v2Version = id3v2Version; } /** * Creates a new PlayMusic manager * @param context App context */ public PlayMusicManager(Context context) { mContext = context; instance = this; } /** * Loads all needed information and opens the database * @throws PlayMusicNotFoundException PlayMusic is not installed * @throws NoSuperUserException No super user permissions * @throws CouldNotOpenDatabaseException Could not open the database */ public void startUp() throws PlayMusicNotFoundException, NoSuperUserException, CouldNotOpenDatabaseException { // Gets the package manager PackageManager packageManager = mContext.getPackageManager(); try { // Loads the application info mPlayMusicApplicationInfo = packageManager.getApplicationInfo(PLAYMUSIC_PACKAGE_ID, 0); } catch (PackageManager.NameNotFoundException e) { // No PlayMusic throw new PlayMusicNotFoundException(); } // Path to the private data mPathPrivateData = mPlayMusicApplicationInfo.dataDir; List publicDataList = new ArrayList<>(); // Search on all sdcards for (String storage : FileTools.getStorages()) { String publicData = storage + "/Android/data/com.google.android.music"; // Directory exists if (FileTools.directoryExists(publicData)) publicDataList.add(publicData); } // Convert to array mPathPublicData = publicDataList.toArray(new String[publicDataList.size()]); // Loads the database loadDatabase(); } /** * Copies the database to a temp directory and opens it * @throws NoSuperUserException No super user permissions * @throws de.arcus.playmusiclib.exceptions.CouldNotOpenDatabaseException Could not open the database */ private void loadDatabase() throws NoSuperUserException, CouldNotOpenDatabaseException { // Ask for super user if (!SuperUser.askForPermissions()) throw new NoSuperUserException(); // Close the database closeDatabase(); // Copy the database to the temp folder if (!SuperUserTools.fileCopy(getDatabasePath(), getTempDatabasePath())) throw new CouldNotOpenDatabaseException(); // Opens the database try { mDatabase = SQLiteDatabase.openDatabase(getTempDatabasePath(), null, SQLiteDatabase.OPEN_READONLY); } catch (SQLException e) { throw new CouldNotOpenDatabaseException(); } } /** * Reloads the database from PlayMusic * @throws NoSuperUserException No super user permissions * @throws CouldNotOpenDatabaseException Could not open the database */ public void realoadDatabase() throws NoSuperUserException, CouldNotOpenDatabaseException { // Reload database loadDatabase(); } /** * Closes the database if it's open */ private void closeDatabase() { if (mDatabase == null) return; mDatabase.close(); } /** * Debug function to get the database */ public void copyDatabaseToSdCard() { FileTools.fileCopy(getTempDatabasePath(), Environment.getExternalStorageDirectory() + "/music.db"); } /** * @return Gets the path to the private music */ public String getPrivateMusicPath() { return mPathPrivateData + "/files/music"; } /** * Gets the path to the music track * @param localCopyPath The local copy path * @return The path to the music file */ public String getMusicFile(String localCopyPath) { // LocalCopyPath is empty if (TextUtils.isEmpty(localCopyPath)) return null; String path; // Search in the public data for (String publicData : mPathPublicData) { path = publicData + "/files/music/" + localCopyPath; if (FileTools.fileExists(path)) return path; } // Private music path path = getPrivateMusicPath() + "/" + localCopyPath; // Don't check if the file exists, this will freeze the UI thread //if (SuperUserTools.fileExists(path)) return path; return path; } /** * @return Gets the path to the private files */ public String getPrivateFilesPath() { return mPathPrivateData + "/files"; } /** * Gets the full path to the artwork * @param artworkPath The artwork path * @return The full path to the artwork */ public String getArtworkPath(String artworkPath) { // Artwork path is empty if (TextUtils.isEmpty(artworkPath)) return null; String path; // Fix the path for Play Music 5.9.1854 if (!artworkPath.startsWith("artwork/")) artworkPath = "artwork/" + artworkPath; // Search in the public data for (String publicData : mPathPublicData) { path = publicData + "/files/" + artworkPath; if (FileTools.fileExists(path)) return path; } // Private artwork path path = getPrivateFilesPath() + "/" + artworkPath; // Don't check if the file exists, this will freeze the UI thread // if (SuperUserTools.fileExists(path)) return path; return path; } /** * Exports a track to the sd card * @param musicTrack The music track you want to export * @param dest The destination path * @param forceOverwrite Forces overwrite of the destination file * @return Returns whether the export was successful */ public boolean exportMusicTrack(MusicTrack musicTrack, String dest, boolean forceOverwrite ) { // Creates the destination directory File directory = new File(dest).getParentFile(); // Filename String filename = new File(dest).getName(); return exportMusicTrack(musicTrack, Uri.fromFile(directory), filename, forceOverwrite); } /** * Exports a track to the sd card * @param musicTrack The music track you want to export * @param uri The document tree * @param forceOverwrite Forces overwrite of the destination file * @return Returns whether the export was successful */ public boolean exportMusicTrack(MusicTrack musicTrack, Uri uri, String path, boolean forceOverwrite) { // Check for null if (musicTrack == null) return false; String srcFile = musicTrack.getSourceFile(); // Could not find the source file if (srcFile == null) return false; String uniqueID = UUID.randomUUID().toString(); if ( forceOverwrite || !isAlreadyThere(uri, path) ) { String fileTmp = getTempPath() + uniqueID +"_tmp.mp3"; // Copy to temp path failed if (!SuperUserTools.fileCopy(srcFile, fileTmp)) return false; // Encrypt the file if (musicTrack.isEncoded()) { String fileTmpCrypt = getTempPath() + uniqueID +"_crypt.mp3"; // Encrypts the file if (trackEncrypt(musicTrack, fileTmp, fileTmpCrypt)) { // Remove the old tmp file FileTools.fileDelete(fileTmp); // New tmp file fileTmp = fileTmpCrypt; } else { Logger.getInstance().logWarning("ExportMusicTrack", "Encrypting failed! Continue with decrypted file."); } } String dest; Uri copyUri = null; if (uri.toString().startsWith("file://")) { // Build the full path dest = uri.buildUpon().appendPath(path).build().getPath(); String parentDirectory = new File(dest).getParent(); FileTools.directoryCreate(parentDirectory); } else { // Complex uri (Lollipop) dest = getTempPath() + uniqueID +"_final.mp3"; // The root DocumentFile document = DocumentFile.fromTreeUri(mContext, uri); // Creates the subdirectories String[] directories = path.split("\\/"); for(int i=0; i= Build.VERSION_CODES.LOLLIPOP) { try { // Gets the file descriptor ParcelFileDescriptor parcelFileDescriptor = mContext.getContentResolver().openFileDescriptor(copyUri, "w"); // Gets the output stream FileOutputStream fileOutputStream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor()); // Gets the input stream FileInputStream fileInputStream = new FileInputStream(dest); // Copy the stream FileTools.fileCopy(fileInputStream, fileOutputStream); // Close all streams fileOutputStream.close(); fileInputStream.close(); parcelFileDescriptor.close(); } catch (FileNotFoundException e) { Logger.getInstance().logError("ExportMusicTrack", "File not found!"); // Could not copy the file return false; } catch (IOException e) { Logger.getInstance().logError("ExportMusicTrack", "Failed to write the document: " + e.toString()); // Could not copy the file return false; } } } // Delete temp files cleanUp(uniqueID); // Adds the file to the media system //new MediaScanner(mContext, dest); } else { Logger.getInstance().logInfo("exportMusicTrack", path + " already exists, skipping." ); } // Done return true; } /** * Checks if the destination file already exists * @param pUri the source file * @param pPath The destination * return true if the file already exists */ private boolean isAlreadyThere(Uri pUri, String pPath) { if (pUri.toString().startsWith("file://")) { //Old sdcard URI return FileTools.fileExists(pUri.buildUpon().appendPath(pPath).build().toString()); } else { //Documents Provider URI DocumentFile lDocumentFile = DocumentFile.fromTreeUri(mContext, pUri); for (String lDisplayName : pPath.split("/")) { if (lDocumentFile.findFile(lDisplayName) != null) { lDocumentFile = lDocumentFile.findFile(lDisplayName); if ( lDocumentFile.length() == 0 ) { if ( !lDocumentFile.isDirectory() ) { Logger.getInstance().logInfo("isAlreadyThere", pPath + " File exists, but is 0 bytes in size."); } } } else { Logger.getInstance().logInfo("isAlreadyThere", pPath + " does not exist yet."); return false; } } return true; } } /** * Copies the music file to a new path and adds the mp3 meta data * @param musicTrack Track information * @param src The source mp3 file * @param dest The destination path * return Return if the operation was successful */ private boolean trackWriteID3(MusicTrack musicTrack, String src, String dest) { try { // Opens the mp3 Mp3File mp3File = new Mp3File(src); // Removes all existing tags mp3File.removeId3v1Tag(); mp3File.removeId3v2Tag(); mp3File.removeCustomTag(); // We want to add a fallback ID3v1 tag if (mID3EnableFallback) { // Create a new tag with ID3v1 ID3v1Tag tagID3v1 = new ID3v1Tag(); // Set all tag values tagID3v1.setTrack(musicTrack.getTitle()); tagID3v1.setArtist(musicTrack.getArtist()); tagID3v1.setAlbum(musicTrack.getAlbum()); tagID3v1.setYear(musicTrack.getYear()); // Search the genre for(int n=0; n