Compare commits

...

39 commits

Author SHA1 Message Date
Jan Christian Grünhage 51c1fc4d26 Merge pull request #41 from J05HI/wrong-confirm-button
Fix wrong confirm button label
2017-08-11 22:45:15 +02:00
Jan Christian Grünhage e78c69bc6e Merge pull request #40 from J05HI/update-translations
Updated some translations.
2017-08-11 21:39:47 +02:00
Joshua Ott 75f5520759 Fix wrong confirm button label 2017-08-11 20:45:53 +02:00
Joshua Ott eb7a2ed319 Update app name translation 2017-08-11 20:35:35 +02:00
Joshua Ott 1c8177622d Update some translations 2017-08-11 20:13:43 +02:00
Jan Christian Grünhage 204829ac5b
Fixes for #30 and #25, as well as some other little things:
- Disabled JACK Toolchain, moved to native Java 8 support instead.
 - Updated gradle plugin from 2.3 to 2.4-alpha6
 - Removed the paragraph about bithub from the README
 - Rephrased some things about why 5.0 is minimum
 - Made analytics opt-in on first launch
 - Extracted strings from the intro
 - Added [LeakCanary](https://github.com/square/leakcanary)
2017-04-22 15:40:29 +02:00
Jan Christian Grünhage 1861adfd22
Merge branch 'release/v0.9.6.0' into develop 2017-03-29 00:31:32 +02:00
Jan Christian Grünhage 2f17819dbb
Version Bump, changing a lot of minor stuff suggested by Android Studio. 2017-03-29 00:31:10 +02:00
Jan Christian Grünhage 3374b3f8d0
Added exception reporting for tag writing failures. 2017-03-28 17:50:21 +02:00
Jan Christian Grünhage bd7cd4d8ef
This should fix #24. 2017-03-28 15:34:35 +02:00
Jan Christian Grünhage 9625d9c302
Merge branch 'develop' of ssh://github.com:22/jcgruenhage/PlayMusicExporter into develop 2017-03-28 14:56:51 +02:00
Jan Christian Grünhage f43a8f546d
Merge branch 'feature/countly' into develop 2017-03-28 14:55:48 +02:00
Jan Christian Grünhage ac9c89ce6f
Removed countly maven repo, since it is available from jcenter too. Also updated library version in the framework. 2017-03-28 14:55:24 +02:00
Jan Christian Grünhage a76cf7e3e4
Added countly for crash reporting/analytics. Updated library versions. 2017-03-28 14:28:07 +02:00
Jan Christian Grünhage 61991eab3e Added donation infos. 2017-03-23 08:42:07 +01:00
Jan Christian Grünhage 2f7369abd8
Merge branch 'release/v0.9.5.2' into develop 2017-03-19 15:05:43 +01:00
Jan Christian Grünhage a7e2c1de89
Version Bump 2017-03-19 15:05:25 +01:00
Jan Christian Grünhage e016799616
Updated libs, since F-Droid now seems to support these newer versions. 2017-03-19 15:04:39 +01:00
Jan Christian Grünhage 65d5dceec3 Merge pull request #21 from jcgruenhage/master
Not sure why this commit is only on master and not on develop.
2017-03-19 14:59:22 +01:00
Jan Christian Grünhage 1d8ffc81aa Added F-Droid Badge 2017-03-19 14:49:47 +01:00
Jan Christian Grünhage 0bb4a5d586
Version bump and library downgrade, to get working F-Droid build. 2017-03-14 09:39:18 +01:00
Jan Christian Grünhage afc5d3fe2a Merge branch 'develop' 2017-03-11 14:19:08 +01:00
Jan Christian Grünhage fb620132e8
Version Bump 2017-03-11 14:17:12 +01:00
Jan Christian Grünhage d1f0823f3c
Automated "About" preference screen values (version and build date) 2017-03-11 14:16:40 +01:00
Jan Christian Grünhage 6c7832f1e6 Merge pull request #16 from jcgruenhage/update-libs
Updated libraries and replaced mp3agic jar with gradle dependency.
2017-03-11 11:15:04 +01:00
Jan Christian Grünhage a0fda16193
Updated libraries and replaced mp3agic jar with gradle dependency. 2017-03-11 11:11:26 +01:00
Jan Christian Grünhage 9ba16eba8b Merge pull request #14 from jcgruenhage/CrashHandlerDeviceDetails
Device Info and SDK in crash handler.
2017-03-10 11:57:14 +01:00
Jan Christian Grünhage 6ecfb05391
Added some more info and a debug setting to test the crash handler. 2017-03-10 11:51:40 +01:00
Mark Gillespie 9c6e32a537 Device Info and SDK in crash handler. 2017-03-10 10:16:41 +00:00
Jan Christian Grünhage 387b2e2c0a Merge pull request #13 from jcgruenhage/IntroGraphics
Intro graphics
2017-03-10 00:43:50 +01:00
Mark Gillespie 017768e88b Removed stray artefact in the corner (I used online PNG to SVG converter) 2017-03-09 23:21:43 +00:00
Mark Gillespie b47c907919 Remove cloud download (not referenced). superuser to SVG. 2017-03-09 22:34:19 +00:00
Mark Gillespie 587a14a67c Folder access and copyright warning to SVG. 2017-03-09 22:15:35 +00:00
Mark Gillespie bcf951e6d1 Playmusicexporter icon to SVG 2017-03-09 21:34:22 +00:00
Mark Gillespie a5a26ee2b6 Use Android Studio to convert anything than can be, to the more efficient and preferred WebP format. 2017-03-09 18:57:58 +00:00
Mark Gillespie 7dafd21843 Refresh button (#11)
* Added a refresh button to bin the previous database, copy the new one, and reload it into the UI.   Saves having to kill the app and re-open to see the latest changes. Should address "Issue #8 Reload Play Music Files"

* Pull Request actions.

* Fixed Typo and Translation
2017-03-09 15:08:51 +00:00
Jan Christian Grünhage 67d6fd5ffb Merge pull request #10 from jcgruenhage/AndroidStudio23
Just tested this, works.
2017-03-08 11:06:02 +01:00
Mark Gillespie 59e90a7ab6 Gradle 2.3.0 2017-03-06 19:26:45 +00:00
Jan Christian Grünhage c33c463327 Added Matrix/IRC Chatroom 2017-02-27 22:43:33 +01:00
77 changed files with 667 additions and 510 deletions

View file

@ -4,6 +4,10 @@ This Android app exports your Play Music mp3 files directly to your sdcard
or a documents provider using root permissions.
You can also setup automatic export, that exports all currently cached not yet exported music.
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/app/re.jcg.playmusicexporter)
## About
This AndroidStudio project allow you to access the database from Google's PlayMusic
@ -15,8 +19,9 @@ There is also a nice library you can simply use in your projects.
**This app and the included library will require root access to your device!
If your device is not rooted you can neither use this app nor the library.**
This app uses API Level 21, which has been introduced by Android Lollipop. If you use KitKat
or lower, this app will not work.
This app uses multiple features introduced in API Level 21 (Android 5.0 Lollipop), the JobScheduler
and the the folder selection of the storage access framework.
If you use KitKat or lower, this app will not work.
### Credits
@ -30,9 +35,28 @@ by [Michael Patricios (mpatric)](https://github.com/mpatric).
### Contributing
#### What can you contribute?
Implement features you want to see, or take a look at the issue tracker and fix broken things. Before making bigger changes, see that a contributor is supporting the inclusion, so that you can be sure it will be merged. (See Discuss Ideas below)
#### How to contribute:
If you want to contribute to this project, fork off of develop,
implement what you want to implement, and submit a pull request back into develop.
After testing it, if enough of the collaborators like it, we will merge it.
You might want to discuss your ideas with us first though:
#### Discuss Ideas
If you have any questions, the easiest way to get help is asking in the #playmusicexporter IRC channel on freenode. You can join that on webchat.freenode.net, or preferably, if you are not really an IRC guy anyway, try the corresponding matrix room [#playmusicexporter:jcg.re](https://matrix.to/#/#playmusicexporter:jcg.re). They are bridged together, so everything that happens in one of the rooms gets automatically transfered to the other one too.
### Supporting this project
If you want to support this project, the best way to do so is contributing source code to the project (see above), or help with illustrations, if that is something you're good at! (I am most certainly not good at creating icons and such things)
If you are unable to do that, but you still want to support this project, you could send me some bitcoins instead: 1NdzpDWPQ53xWT5fraGPZX5F9XrKiPBXjp
I will distribute bitcoins send to me to others helping on the project too, so that other people contributing also have a piece of the cake.
### Copyright

View file

@ -20,8 +20,6 @@
* THE SOFTWARE.
*/
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
@ -29,7 +27,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0-beta3'
classpath 'com.android.tools.build:gradle:2.4.0-alpha6'
}
}
@ -38,5 +36,4 @@ allprojects {
repositories {
jcenter()
}
}

View file

@ -42,5 +42,5 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.1.1'
compile 'com.android.support:appcompat-v7:25.3.1'
}

View file

@ -24,7 +24,9 @@ package de.arcus.framework.crashhandler;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import de.arcus.framework.BuildConfig;
import de.arcus.framework.activities.CrashActivity;
import de.arcus.framework.logger.Logger;
@ -70,10 +72,25 @@ public class CrashHandler implements Thread.UncaughtExceptionHandler {
StringBuilder logBuilder = new StringBuilder();
//Device Information
logBuilder.append("---------- Device Information -----------\n");
logBuilder.append("Manufacturer: ").append(Build.MANUFACTURER).append("\n");
logBuilder.append("Model: ").append(Build.MODEL).append("\n");
logBuilder.append("Device API: ").append(Build.VERSION.RELEASE).append("\n");
logBuilder.append("Product: ").append(Build.PRODUCT).append("\n");
logBuilder.append("Build Number: ").append(Build.DISPLAY).append("\n");
logBuilder.append("Build Tags: ").append(Build.TAGS).append("\n");
logBuilder.append("\n");
// Information
logBuilder.append("---------- Information -----------\n");
logBuilder.append("---------- App Information -----------\n");
logBuilder.append("PackageName: ").append(mActivity.getPackageName()).append("\n");
logBuilder.append("Crashed activity: ").append(mActivity.getLocalClassName()).append("\n");
logBuilder.append("Version number: ").append(BuildConfig.VERSION_NAME).append("\n");
logBuilder.append("Version code: ").append(BuildConfig.VERSION_CODE).append("\n");
logBuilder.append("\n");
logBuilder.append("----------- Exception ------------\n");
logBuilder.append(ex.getMessage()).append("\n");

View file

@ -1,9 +1,5 @@
package de.arcus.framework.superuser;
/**
* Created by jcgruenhage on 1/18/17.
*/
public interface SuperUserPermissionRequestListener {
void superUserGranted(boolean granted);
}

View file

@ -22,6 +22,8 @@
package de.arcus.framework.utils;
import android.os.Environment;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@ -29,6 +31,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -41,10 +44,12 @@ public class FileTools {
/**
* Private constructor
*/
private FileTools() {}
private FileTools() {
}
/**
* Creates a directory if it not exists
*
* @param dir Directory path
* @return Returns true if the directory was created or already exists
*/
@ -74,6 +79,7 @@ public class FileTools {
/**
* Checks if the directory exists
*
* @param dir Path of the file
* @return Return whether the directory exists
*/
@ -86,6 +92,7 @@ public class FileTools {
/**
* Creates an empty file
*
* @param file File path
* @return Returns true if the file was successfully created
*/
@ -97,7 +104,7 @@ public class FileTools {
return (new File(file)).createNewFile();
} catch (IOException e) {
// Failed
Logger.getInstance().logError("FileCreate", "Could not create file: " + e.getMessage());
Logger.getInstance().logError("FileCreate", "Could not create file: " + e.getMessage());
return false;
}
@ -105,7 +112,8 @@ public class FileTools {
/**
* Moves a file
* @param src Soruce path
*
* @param src Soruce path
* @param dest Destination path
* @return Return whether the moving was successful
*/
@ -121,7 +129,8 @@ public class FileTools {
/**
* Copies a stream
* @param inputStream Source stream
*
* @param inputStream Source stream
* @param outputStream Destination stream
* @return Return whether the stream was copied successful
*/
@ -154,7 +163,8 @@ public class FileTools {
/**
* Copies a file
* @param src Source path
*
* @param src Source path
* @param dest Destination path
* @return Return whether the file was copied successful
*/
@ -194,6 +204,7 @@ public class FileTools {
/**
* Deletes a file
*
* @param file Path of the file
* @return Returns whether the deleting was successful
*/
@ -204,6 +215,7 @@ public class FileTools {
/**
* Checks if the file exists
*
* @param file Path of the file
* @return Return whether the file exists
*/
@ -216,6 +228,7 @@ public class FileTools {
/**
* Checks whether the file or directory is a link
*
* @param path Path of the file / directory
* @return Returns whether the file or directory is a link
*/
@ -234,6 +247,7 @@ public class FileTools {
/**
* Gets the root canonical file of a symbolic link
*
* @param path The path
* @return The root file
*/
@ -243,6 +257,7 @@ public class FileTools {
/**
* Gets the root canonical file of a symbolic link
*
* @param file The file
* @return The root file
*/
@ -268,19 +283,20 @@ public class FileTools {
/**
* Gets all storages; eg. all sdcards
*
* @return List of all storages
*/
public static String[] getStorages() {
List<String> storages = new ArrayList<>();
// Hard coded mount points
final String[] mountPointBlacklist = new String[] { "/mnt/tmp", "/mnt/factory", "/mnt/obb", "/mnt/asec", "/mnt/secure", "/mnt/media_rw", "/mnt/shell", "/storage/emulated" };
final String[] mountPointDirectories = new String[] { "/mnt", "/storage" };
final String[] mountPoints = new String[] { "/sdcard", "/external_sd" };
final String[] mountPointBlacklist = new String[]{"/mnt/tmp", "/mnt/factory", "/mnt/obb", "/mnt/asec", "/mnt/secure", "/mnt/media_rw", "/mnt/shell", "/storage/emulated"};
final String[] mountPointDirectories = new String[]{"/mnt", "/storage"};
final String[] mountPoints = new String[]{Environment.getExternalStorageDirectory().getAbsolutePath(), "/external_sd"};
// Adds all mount point directories
for(String mountPointDirectory : mountPointDirectories) {
for (String mountPointDirectory : mountPointDirectories) {
// Checks all subdirectories
File dir = getRootCanonicalFile(mountPointDirectory);
if (dir.exists() && dir.isDirectory()) {
@ -300,7 +316,7 @@ public class FileTools {
}
// Adds all direct mount points
for(String mountPoint : mountPoints) {
for (String mountPoint : mountPoints) {
File file = getRootCanonicalFile(mountPoint);
if (file.isDirectory() && file.canRead()) {
if (!storages.contains(file.getAbsolutePath()))
@ -309,9 +325,7 @@ public class FileTools {
}
// Remove all blacklisted paths
for (String blacklistPath : mountPointBlacklist) {
storages.remove(blacklistPath);
}
storages.removeAll(Arrays.asList(mountPointBlacklist));
// Sort the list
Collections.sort(storages);

View file

@ -1,6 +1,6 @@
#Sun Jan 22 21:19:51 CET 2017
#Tue Mar 28 12:22:37 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip

View file

@ -20,8 +20,6 @@
* THE SOFTWARE.
*/
apply plugin: 'com.android.application'
android {
@ -33,13 +31,11 @@ android {
minSdkVersion 21
targetSdkVersion 25
// TODO Change Version with releases
versionCode 106
versionName '0.9.4'
versionCode 110
versionName '0.9.6.0'
vectorDrawables.useSupportLibrary = true
jackOptions {
enabled true
}
buildConfigField "java.util.Date", "BUILD_TIME", "new java.util.Date(" + System.currentTimeMillis() + "L)"
}
buildTypes {
release {
@ -57,9 +53,13 @@ dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':framework')
compile project(':playmusiclib')
compile 'com.android.support:appcompat-v7:25.1.1'
compile 'com.android.support:support-v4:25.1.1'
compile 'com.android.support:design:25.1.1'
compile 'com.android.support:support-vector-drawable:25.1.1'
compile "com.github.paolorotolo:appintro:4.1.0"
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:support-v4:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.android.support:support-vector-drawable:25.3.1'
compile 'com.github.paolorotolo:appintro:4.1.0'
compile 'ly.count.android:sdk:16.12.2'
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
}

View file

@ -24,15 +24,17 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:name=".PlayMusicExporter">
<activity
android:name=".activities.MusicContainerListActivity"
android:label="@string/app_name">
@ -51,7 +53,7 @@
</activity>
<activity
android:name=".activities.Intro"
android:label="Play Music Exporter Intro"/>
android:label="Play Music Exporter Intro" />
<service android:name=".services.ExportService" />
<service android:name=".services.ExportAllService" />

View file

@ -0,0 +1,24 @@
package re.jcg.playmusicexporter;
import android.app.Application;
import com.squareup.leakcanary.LeakCanary;
/**
* Android Application.
* Normally, we would not need to extend this, but it is required for
* the leak detection library we use.
* See {@link LeakCanary}
*/
public class PlayMusicExporter extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}

View file

@ -1,10 +1,9 @@
package re.jcg.playmusicexporter.activities;
import android.Manifest;
import android.app.Dialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
@ -30,49 +29,41 @@ public class Intro extends AppIntro {
Fragment warning;
Fragment storage;
Fragment superuser;
Fragment error;
Fragment finish;
private void initFragments() {
int color = ContextCompat.getColor(this, R.color.application_main);
welcome = AppIntroFragment.newInstance(
"Welcome!",
"This is the Play Music Exporter. It can export songs from Play Music " +
"and save them as MP3 files where you want them to be.",
getString(R.string.intro_welcome_title),
getString(R.string.intro_welcome_description),
R.drawable.ic_launcher_transparent,
Color.parseColor("#ef6c00"));
color);
warning = AppIntroFragment.newInstance(
"Warning!",
"You are responsible for what you do with this app. Depending on where you live " +
"it might be illegal to use this app. We discourage piracy of music " +
"and other intellectual property. Sharing music you exported with " +
"this tool might be a very bad idea, Google could put an invisible " +
"watermark on the music, so that people can trace the MP3s back to " +
"the owner of the Google account that was used.",
getString(R.string.intro_warning_title),
getString(R.string.intro_warning_description),
R.drawable.ic_warning_white,
Color.parseColor("#ef6c00"));
color);
storage = AppIntroFragment.newInstance(
"We need access to your storage.",
"We need to access the external storage, " +
"for copying the Play Music database to a folder," +
"where we have the right to work with it. " +
"We also need access to the external storage," +
"to finish up the MP3s, from encrypted without ID3 tags," +
"to decrypted with ID3 tags, before we save them to your export path.",
getString(R.string.intro_storage_title),
getString(R.string.intro_storage_description),
R.drawable.ic_folder_white,
Color.parseColor("#ef6c00"));
color);
superuser = AppIntroFragment.newInstance(
"We need root access.",
"Some of the files we need to access are in the private folders of Play Music. " +
"Android prevents apps from accessing the private folders " +
"of other apps, but luckily, you can circumvent this protection " +
"with root access. Without root access this app can't do anything.",
getString(R.string.intro_superuser_title),
getString(R.string.intro_superuser_description),
R.drawable.ic_superuser,
Color.parseColor("#ef6c00"));
color);
error = AppIntroFragment.newInstance(
getString(R.string.intro_error_title),
getString(R.string.intro_error_description),
R.drawable.ic_error_white,
color);
finish = AppIntroFragment.newInstance(
"Tutorial finished!",
"One note: Should you revoke any of these permission, the tutorial will be " +
"shown again on the next launch.",
getString(R.string.intro_finish_title),
getString(R.string.intro_finish_description),
R.drawable.ic_launcher_transparent,
Color.parseColor("#ef6c00"));
color);
}
@Override
@ -88,6 +79,7 @@ public class Intro extends AppIntro {
addSlide(warning);
addSlide(storage);
addSlide(superuser);
addSlide(error);
addSlide(finish);
pager.setPagingEnabled(true);
@ -101,7 +93,7 @@ public class Intro extends AppIntro {
promptAcceptWarning();
} else if (storage.equals(oldFragment) && superuser.equals(newFragment)) {
requestStoragePermission();
} else if (superuser.equals(oldFragment) && finish.equals(newFragment)) {
} else if (superuser.equals(oldFragment) && error.equals(newFragment)) {
SuperUser.askForPermissionInBackground(granted -> {
if (!granted) {
AlertDialog.Builder builder =
@ -114,9 +106,30 @@ public class Intro extends AppIntro {
builder.show();
}
});
} else if (error.equals(oldFragment) && finish.equals(newFragment)) {
promptEnableErrorReporting();
}
}
private void promptEnableErrorReporting() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
Dialog.OnClickListener enable = (dialog, which) -> {
PlayMusicExporterPreferences.setReportStats(true);
dialog.dismiss();
};
Dialog.OnClickListener disable = (dialog, which) -> {
PlayMusicExporterPreferences.setReportStats(false);
dialog.dismiss();
};
builder.setTitle(R.string.error_alert_dialog_title);
builder.setMessage(R.string.error_alert_dialog_message);
builder.setCancelable(false);
builder.setNegativeButton(R.string.no, disable);
builder.setNeutralButton(R.string.whatever, enable);
builder.setPositiveButton(R.string.yes, enable);
builder.show();
}
private void requestStoragePermission() {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
@ -152,12 +165,12 @@ public class Intro extends AppIntro {
private void promptAcceptWarning() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Understood?");
builder.setMessage("Have you read and understood this?");
builder.setTitle(R.string.warning_alert_dialog_title);
builder.setMessage(R.string.warning_alert_dialog_message);
builder.setCancelable(false);
builder.setNegativeButton("No", ((dialog, which)
builder.setNegativeButton(getString(R.string.no), ((dialog, which)
-> pager.setCurrentItem(pager.getCurrentItem() - 1)));
builder.setPositiveButton("Yes", (((dialog, which) -> dialog.dismiss())));
builder.setPositiveButton(getString(R.string.yes), (((dialog, which) -> dialog.dismiss())));
builder.show();
}

View file

@ -40,9 +40,13 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.Toast;
import de.arcus.framework.logger.Logger;
import de.arcus.framework.crashhandler.CrashHandler;
import de.arcus.playmusiclib.exceptions.CouldNotOpenDatabaseException;
import de.arcus.playmusiclib.exceptions.NoSuperUserException;
import ly.count.android.sdk.Countly;
import ly.count.android.sdk.DeviceId;
import re.jcg.playmusicexporter.R;
import re.jcg.playmusicexporter.fragments.MusicTrackListFragment;
import re.jcg.playmusicexporter.fragments.MusicContainerListFragment;
@ -104,16 +108,15 @@ public class MusicContainerListActivity extends AppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Adds the crash handler to this class
CrashHandler.addCrashHandler(this);
PlayMusicExporterPreferences.init(this);
if (!PlayMusicExporterPreferences.getSetupDone()) {
startActivity(new Intent(this, Intro.class));
finish();
} else {
if (PlayMusicExporterPreferences.getReportStats()) {
Countly.sharedInstance().init(this, getString(R.string.countly_url), getString(R.string.countly_token), null, DeviceId.Type.OPEN_UDID);
Countly.sharedInstance().enableCrashReporting();
}
setContentView(R.layout.activity_track_list);
@ -192,11 +195,11 @@ public class MusicContainerListActivity extends AppCompatActivity
}
}
}
/**
* Loads the PlayMusicExporter lib and shows the list
*/
private void loadPlayMusicExporter()
{
private void loadPlayMusicExporter() {
// Gets the running instance
mPlayMusicManager = PlayMusicManager.getInstance();
@ -258,7 +261,7 @@ public class MusicContainerListActivity extends AppCompatActivity
MusicContainerListFragment musicTrackListFragment = (MusicContainerListFragment) getSupportFragmentManager()
.findFragmentById(R.id.fragment_main);
switch(mViewType) {
switch (mViewType) {
case Album:
// Load all albums to the list
AlbumDataSource dataSourceAlbum = new AlbumDataSource(mPlayMusicManager);
@ -325,9 +328,24 @@ public class MusicContainerListActivity extends AppCompatActivity
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.music_track_list, menu);
MenuItem itemRefreshLibrary = menu.findItem(R.id.action_refresh);
itemRefreshLibrary.setOnMenuItemClickListener(item ->
{
try {
mPlayMusicManager.reloadDatabase();
mPlayMusicManager = null;
loadPlayMusicExporter();
Toast.makeText(this, R.string.database_reloaded, Toast.LENGTH_SHORT).show();
} catch (NoSuperUserException | CouldNotOpenDatabaseException e) {
Toast.makeText(this, R.string.dialog_superuser_access_denied_title, Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
return true;
});
// Finds the search item and create the search view
MenuItem itemSearch = menu.findItem(R.id.action_search);
mSearchView = (SearchView)MenuItemCompat.getActionView(itemSearch);
mSearchView = (SearchView) MenuItemCompat.getActionView(itemSearch);
if (mSearchView != null) {
// Sets the search listener
@ -355,4 +373,18 @@ public class MusicContainerListActivity extends AppCompatActivity
return false;
}
@Override
public void onStart() {
super.onStart();
if (PlayMusicExporterPreferences.getReportStats())
Countly.sharedInstance().onStart(this);
}
@Override
public void onStop() {
if (PlayMusicExporterPreferences.getReportStats())
Countly.sharedInstance().onStop();
super.onStop();
}
}

View file

@ -23,6 +23,7 @@ import re.jcg.playmusicexporter.services.ExportAllJob;
import re.jcg.playmusicexporter.services.ExportAllService;
import re.jcg.playmusicexporter.settings.PlayMusicExporterPreferences;
import java.text.SimpleDateFormat;
import java.util.List;
/**
@ -45,31 +46,28 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
* A preference value change listener that updates the preference's summary
* to reflect its new value.
*/
private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
String stringValue = value.toString();
private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
String stringValue = value.toString();
if (preference instanceof ListPreference) {
// For list preferences, look up the correct display value in
// the preference's 'entries' list.
ListPreference listPreference = (ListPreference) preference;
int index = listPreference.findIndexOfValue(stringValue);
if (preference instanceof ListPreference) {
// For list preferences, look up the correct display value in
// the preference's 'entries' list.
ListPreference listPreference = (ListPreference) preference;
int index = listPreference.findIndexOfValue(stringValue);
// Set the summary to reflect the new value.
preference.setSummary(
index >= 0
? listPreference.getEntries()[index]
: null);
// Set the summary to reflect the new value.
preference.setSummary(
index >= 0
? listPreference.getEntries()[index]
: null);
} else {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.setSummary(stringValue);
}
return true;
} else {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.setSummary(stringValue);
}
return true;
};
/**
@ -126,15 +124,8 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.pref_headers, target);
// Remove the Debug Fragment
if (!BuildConfig.DEBUG) {
for (int i = 0; i < target.size(); i++) {
if ("Debug".equals(target.get(i).title)) {
target.remove(i);
break;
}
}
if (BuildConfig.DEBUG) {
loadHeadersFromResource(R.xml.pref_debug_header, target);
}
}
@ -159,19 +150,13 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
setHasOptionsMenu(true);
findPreference("preference_alba_export_path").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startActivityForResult(new Intent("android.intent.action.OPEN_DOCUMENT_TREE"), REQUEST_CODE_OPEN_DOCUMENT_TREE_ALBA_PATH);
return true;
}
findPreference("preference_alba_export_path").setOnPreferenceClickListener(preference -> {
startActivityForResult(new Intent("android.intent.action.OPEN_DOCUMENT_TREE"), REQUEST_CODE_OPEN_DOCUMENT_TREE_ALBA_PATH);
return true;
});
findPreference("preference_groups_export_path").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startActivityForResult(new Intent("android.intent.action.OPEN_DOCUMENT_TREE"), REQUEST_CODE_OPEN_DOCUMENT_TREE_GROUPS_PATH);
return true;
}
findPreference("preference_groups_export_path").setOnPreferenceClickListener(preference -> {
startActivityForResult(new Intent("android.intent.action.OPEN_DOCUMENT_TREE"), REQUEST_CODE_OPEN_DOCUMENT_TREE_GROUPS_PATH);
return true;
});
}
@ -245,12 +230,9 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
private void setupOnClickListeners() {
findPreference(PlayMusicExporterPreferences.AUTO_EXPORT_PATH).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startActivityForResult(new Intent("android.intent.action.OPEN_DOCUMENT_TREE"), REQUEST_CODE_OPEN_DOCUMENT_TREE_AUTO_PATH);
return true;
}
findPreference(PlayMusicExporterPreferences.AUTO_EXPORT_PATH).setOnPreferenceClickListener(preference -> {
startActivityForResult(new Intent("android.intent.action.OPEN_DOCUMENT_TREE"), REQUEST_CODE_OPEN_DOCUMENT_TREE_AUTO_PATH);
return true;
});
//For every setting that is used to define the export job, setup a listener that
@ -261,12 +243,9 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
private void setupRescheduler(String preference) {
findPreference(preference).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
ExportAllJob.scheduleExport(getContext());
return false;
}
findPreference(preference).setOnPreferenceClickListener(preference1 -> {
ExportAllJob.scheduleExport(getContext());
return false;
});
}
@ -305,6 +284,10 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_about);
setHasOptionsMenu(true);
findPreference("about_version_number").setSummary(BuildConfig.VERSION_NAME);
findPreference("about_build_date")
.setSummary(SimpleDateFormat.getDateInstance().format(BuildConfig.BUILD_TIME));
}
@Override
@ -325,13 +308,12 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
addPreferencesFromResource(R.xml.pref_debug);
setHasOptionsMenu(true);
findPreference("debug_trigger_export_all").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
Log.i(TAG, "Debug Trigger Export All Click registered.");
ExportAllService.startExport(getActivity());
return true;
}
findPreference("debug_trigger_export_all").setOnPreferenceClickListener(preference -> {
ExportAllService.startExport(getActivity());
return true;
});
findPreference("debug_test_crash_handler").setOnPreferenceClickListener(preference -> {
throw new IllegalStateException("Test for the crash handler.");
});
}

View file

@ -79,10 +79,7 @@ public class MusicContainerListFragment extends ListFragment {
* A dummy implementation of the {@link Callbacks} interface that does
* nothing. Used only when this fragment is not attached to an activity.
*/
private static Callbacks sDummyCallbacks = new Callbacks() {
@Override
public void onItemSelected(MusicTrackList musicTrackList) {
}
private static Callbacks sDummyCallbacks = musicTrackList -> {
};
private MusicContainerListAdapter mMusicTrackListAdapter;
@ -112,9 +109,7 @@ public class MusicContainerListFragment extends ListFragment {
// Null check
if (list != null) {
// Copy the list
for (MusicTrackList musicTrackList : list) {
newList.add(musicTrackList);
}
newList.addAll(list);
}
// Set the list in the adapter
@ -145,7 +140,8 @@ public class MusicContainerListFragment extends ListFragment {
setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
}
}
// DEPRECATED
// DEPRECATED
@Override
public void onAttach(Context context) {
super.onAttach(context);

View file

@ -32,7 +32,6 @@ import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
@ -197,43 +196,36 @@ public class MusicTrackListFragment extends Fragment {
mListView.setAdapter(mMusicTrackAdapter);
// Click on one list item
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// The header is not clicked
if (position > 0) {
// We need to subtract the header view
position -= 1;
mListView.setOnItemClickListener((parent, view, position, id) -> {
// The header is not clicked
if (position > 0) {
// We need to subtract the header view
position -= 1;
// Gets the selected track
MusicTrack musicTrack = mMusicTrackAdapter.getItem(position);
// Gets the selected track
MusicTrack musicTrack = mMusicTrackAdapter.getItem(position);
// Toggle the track
selectTrack(musicTrack, view, TrackSelectionState.Toggle);
}
// Toggle the track
selectTrack(musicTrack, view, TrackSelectionState.Toggle);
}
});
// The floating action button
mFloatingButtonExport = (FloatingActionButton) rootView.findViewById(R.id.floating_button_export);
mFloatingButtonExport.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v)
{
m_CPULock.acquire();
// Export all selected tracks
for (SelectedTrack selectedTrack : SelectedTrackList.getInstance().getSelectedItems()) {
selectedTrack.export(getActivity());
}
if ( m_CPULock.isHeld())
{
m_CPULock.release();
}
// Clear the selection
SelectedTrackList.getInstance().clear(true);
mFloatingButtonExport.setOnClickListener(v -> {
m_CPULock.acquire();
// Export all selected tracks
for (SelectedTrack selectedTrack : SelectedTrackList.getInstance().getSelectedItems()) {
selectedTrack.export(getActivity());
}
if ( m_CPULock.isHeld())
{
m_CPULock.release();
}
// Clear the selection
SelectedTrackList.getInstance().clear(true);
});
updateFloatingButton();
}

View file

@ -22,7 +22,6 @@
package re.jcg.playmusicexporter.fragments;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
@ -160,47 +159,24 @@ public class NavigationDrawerFragment extends Fragment {
setViewType(mViewType);
// Click on album
mButtonTypeAlbum.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setViewType(ViewType.Album);
}
});
mButtonTypeAlbum.setOnClickListener(v -> setViewType(ViewType.Album));
// Click on artist
mButtonTypeArtist.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setViewType(ViewType.Artist);
}
});
mButtonTypeArtist.setOnClickListener(v -> setViewType(ViewType.Artist));
// Click on playlist
mButtonTypePlaylist.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setViewType(ViewType.Playlist);
}
});
mButtonTypePlaylist.setOnClickListener(v -> setViewType(ViewType.Playlist));
// Click on rated
mButtonTypeRated.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setViewType(ViewType.Rated);
}
});
mButtonTypeRated.setOnClickListener(v -> setViewType(ViewType.Rated));
// Click on settings
mButtonSettings.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intentSettings = new Intent(getActivity(), SettingsActivity.class);
startActivity(intentSettings);
mButtonSettings.setOnClickListener(v -> {
Intent intentSettings = new Intent(getActivity(), SettingsActivity.class);
startActivity(intentSettings);
// Close the drawer
mDrawerLayout.closeDrawers();
}
// Close the drawer
mDrawerLayout.closeDrawers();
});
// Color the settings button
@ -307,12 +283,7 @@ public class NavigationDrawerFragment extends Fragment {
}
// Defer code dependent on restoration of previous instance state.
mDrawerLayout.post(new Runnable() {
@Override
public void run() {
mDrawerToggle.syncState();
}
});
mDrawerLayout.post(mDrawerToggle::syncState);
mDrawerLayout.setDrawerListener(mDrawerToggle);
}

View file

@ -9,6 +9,7 @@ import android.util.Log;
import java.util.List;
import ly.count.android.sdk.Countly;
import re.jcg.playmusicexporter.settings.PlayMusicExporterPreferences;
import re.jcg.playmusicexporter.utils.MusicPathBuilder;
import de.arcus.playmusiclib.PlayMusicManager;
@ -34,8 +35,7 @@ public class ExportAllService extends IntentService {
Log.i(TAG, "Intent sent!");
}
public ExportAllService()
{
public ExportAllService() {
super("AutoGPME-ExportService");
}
@ -75,10 +75,12 @@ public class ExportAllService extends IntentService {
try {
if (lPlayMusicManager.exportMusicTrack(lTrack, lUri, lPath, PlayMusicExporterPreferences.getFileOverwritePreference())) {
Log.i(TAG, "Exported Music Track: " + getStringForTrack(lTrack));
if (PlayMusicExporterPreferences.getReportStats())
Countly.sharedInstance().recordEvent("Exported Song", 1);
} else {
Log.i(TAG, "Failed to export Music Track: " + getStringForTrack(lTrack));
}
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("Invalid URI:")) {
/*
@ -88,19 +90,15 @@ public class ExportAllService extends IntentService {
*/
Log.i(TAG, "Automatic export failed, because the URI is invalid.");
} else throw e;
}
finally
{
if ( CPULock.isHeld())
{
CPULock.release();
}
} catch (Exception e) {
if (PlayMusicExporterPreferences.getReportStats())
Countly.sharedInstance().logException(e);
e.printStackTrace();
}
}
}
}
if ( CPULock.isHeld())
{
if (CPULock.isHeld()) {
CPULock.release();
}
}

View file

@ -31,6 +31,7 @@ import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import de.arcus.framework.logger.Logger;
import ly.count.android.sdk.Countly;
import re.jcg.playmusicexporter.R;
import de.arcus.playmusiclib.PlayMusicManager;
import de.arcus.playmusiclib.datasources.MusicTrackDataSource;
@ -200,9 +201,18 @@ public class ExportService extends IntentService {
updateNotification();
// Exports the song
if(!playMusicManager.exportMusicTrack(mTrackCurrent, uri, path, PlayMusicExporterPreferences.getFileOverwritePreference())) {
// Export failed
mTracksFailed ++;
try {
if (playMusicManager.exportMusicTrack(mTrackCurrent, uri, path, PlayMusicExporterPreferences.getFileOverwritePreference())) {
if (PlayMusicExporterPreferences.getReportStats())
Countly.sharedInstance().recordEvent("Exported Song", 1);
} else {
// Export failed
mTracksFailed ++;
}
} catch (Exception e) {
if (PlayMusicExporterPreferences.getReportStats())
Countly.sharedInstance().logException(e);
e.printStackTrace();
}
} else {
// Export failed

View file

@ -7,6 +7,7 @@ import android.net.Uri;
import android.os.Environment;
import android.preference.PreferenceManager;
import re.jcg.playmusicexporter.BuildConfig;
import re.jcg.playmusicexporter.fragments.NavigationDrawerFragment;
public class PlayMusicExporterPreferences {
@ -49,6 +50,9 @@ public class PlayMusicExporterPreferences {
public static final String SETUP_DONE = "preference_setup_done";
public static final boolean SETUP_DONE_DEFAULT = false;
public static final String REPORT_STATS = "preference_report_stats";
public static final boolean REPORT_STATS_DEFAULT = true;
private PlayMusicExporterPreferences() {
}
@ -175,4 +179,13 @@ public class PlayMusicExporterPreferences {
public static void setAlbumArtSize(int size) {
preferences.edit().putString(EXPORT_ALBUM_ART_SIZE, "" + size).apply();
}
public static boolean getReportStats() {
//Never report stats in debug builds
return preferences.getBoolean(REPORT_STATS, REPORT_STATS_DEFAULT) && !BuildConfig.DEBUG;
}
public static void setReportStats(boolean reportStats) {
preferences.edit().putBoolean(REPORT_STATS, reportStats).apply();
}
}

View file

@ -22,14 +22,12 @@
package re.jcg.playmusicexporter.utils;
import android.graphics.Bitmap;
import android.widget.ImageView;
import java.lang.ref.WeakReference;
import re.jcg.playmusicexporter.R;
import de.arcus.playmusiclib.ArtworkLoader;
import de.arcus.playmusiclib.ArtworkLoaderCallback;
import de.arcus.playmusiclib.items.ArtworkEntry;
/**
@ -121,13 +119,10 @@ public class ArtworkViewLoader {
maximalArtworkSize = imageViewDefault.getContext().getResources().getDimensionPixelSize(R.dimen.music_track_artwork_loading_size);
// Sets the bitmap in the UI thread
Runnable runnable = new Runnable() {
@Override
public void run() {
// Default icon
imageViewDefault.setImageResource(mDefaultImage);
Runnable runnable = () -> {
// Default icon
imageViewDefault.setImageResource(mDefaultImage);
}
};
imageViewDefault.post(runnable);
}
@ -138,36 +133,30 @@ public class ArtworkViewLoader {
mIsLoading = true;
// Load the artwork
ArtworkLoader.loadArtworkAsync(mArtworkEntry, maximalArtworkSize, new ArtworkLoaderCallback() {
@Override
public void onFinished(final Bitmap bitmap) {
final ImageView imageView = mImageView.get();
ArtworkLoader.loadArtworkAsync(mArtworkEntry, maximalArtworkSize, bitmap -> {
final ImageView imageView = mImageView.get();
if (imageViewDefault != null) {
// Sets the bitmap in the UI thread
Runnable runnable = new Runnable() {
@Override
public void run() {
// Bitmap is valid
if (bitmap != null)
imageView.setImageBitmap(bitmap);
else
imageView.setImageResource(mDefaultImage);
}
};
imageView.post(runnable);
}
if (imageViewDefault != null) {
// Sets the bitmap in the UI thread
Runnable runnable = () -> {
// Bitmap is valid
if (bitmap != null)
imageView.setImageBitmap(bitmap);
else
imageView.setImageResource(mDefaultImage);
};
imageView.post(runnable);
}
// Loading is done
mIsLoading = false;
// Loading is done
mIsLoading = false;
// Loads the next image
if (mNewArtworkEntry != null) {
mArtworkEntry = mNewArtworkEntry;
mNewArtworkEntry = null;
// Loads the next image
if (mNewArtworkEntry != null) {
mArtworkEntry = mNewArtworkEntry;
mNewArtworkEntry = null;
loadImage();
}
loadImage();
}
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/button_navigation_drawer_hover">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/button_navigation_drawer_normal"/>
</shape>
</item>
</ripple>

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/button_navigation_drawer_hover">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/button_navigation_drawer_selected"/>
</shape>
</item>
</ripple>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 877 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

View file

@ -21,7 +21,11 @@
~ THE SOFTWARE.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/button_navigation_drawer_normal"/>
</shape>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/button_navigation_drawer_hover">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/button_navigation_drawer_normal"/>
</shape>
</item>
</ripple>

View file

@ -21,7 +21,11 @@
~ THE SOFTWARE.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/button_navigation_drawer_selected"/>
</shape>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/button_navigation_drawer_hover">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/button_navigation_drawer_selected"/>
</shape>
</item>
</ripple>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="240dp"
android:height="240dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -0,0 +1,13 @@
<vector android:height="240dp" android:viewportHeight="1000.0"
android:viewportWidth="1000.0" android:width="240dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="M110.9,67.5C79.5,70.9 48.7,97.3 37,131.2L33.4,141.5l0,302.6 0,302.6l2.6,7.9c11.4,34.5 43.1,61.5 76.4,65 12.9,1.4 764.6,1.4 777.5,0 35.7,-3.8 68.4,-33.6 78,-71.3 1.4,-5.4 1.6,-43.3 1.3,-260.3L968.9,234.1l-3.1,-9c-8.9,-26.4 -34.1,-51.7 -60.4,-60.7L898.2,161.8 700.5,161.5 502.8,161.2 455.6,113.8 408.5,66.4 263.6,66.6C183.9,66.6 115.2,67.1 110.9,67.5" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M110.9,67.5C79.5,70.9 48.7,97.3 37,131.2L33.4,141.5l0,302.6 0,302.6l2.6,7.9c11.4,34.5 43.1,61.5 76.4,65 12.9,1.4 764.6,1.4 777.5,0 35.7,-3.8 68.4,-33.6 78,-71.3 1.4,-5.4 1.6,-43.3 1.3,-260.3L968.9,234.1l-3.1,-9c-8.9,-26.4 -34.1,-51.7 -60.4,-60.7L898.2,161.8 700.5,161.5 502.8,161.2 455.6,113.8 408.5,66.4 263.6,66.6C183.9,66.6 115.2,67.1 110.9,67.5" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M110.9,67.5C79.5,70.9 48.7,97.3 37,131.2L33.4,141.5l0,302.6 0,302.6l2.6,7.9c11.4,34.5 43.1,61.5 76.4,65 12.9,1.4 764.6,1.4 777.5,0 35.7,-3.8 68.4,-33.6 78,-71.3 1.4,-5.4 1.6,-43.3 1.3,-260.3L968.9,234.1l-3.1,-9c-8.9,-26.4 -34.1,-51.7 -60.4,-60.7L898.2,161.8 700.5,161.5 502.8,161.2 455.6,113.8 408.5,66.4 263.6,66.6C183.9,66.6 115.2,67.1 110.9,67.5" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M110.9,67.5C79.5,70.9 48.7,97.3 37,131.2L33.4,141.5l0,302.6 0,302.6l2.6,7.9c11.4,34.5 43.1,61.5 76.4,65 12.9,1.4 764.6,1.4 777.5,0 35.7,-3.8 68.4,-33.6 78,-71.3 1.4,-5.4 1.6,-43.3 1.3,-260.3L968.9,234.1l-3.1,-9c-8.9,-26.4 -34.1,-51.7 -60.4,-60.7L898.2,161.8 700.5,161.5 502.8,161.2 455.6,113.8 408.5,66.4 263.6,66.6C183.9,66.6 115.2,67.1 110.9,67.5" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M110.9,67.5C79.5,70.9 48.7,97.3 37,131.2L33.4,141.5l0,302.6 0,302.6l2.6,7.9c11.4,34.5 43.1,61.5 76.4,65 12.9,1.4 764.6,1.4 777.5,0 35.7,-3.8 68.4,-33.6 78,-71.3 1.4,-5.4 1.6,-43.3 1.3,-260.3L968.9,234.1l-3.1,-9c-8.9,-26.4 -34.1,-51.7 -60.4,-60.7L898.2,161.8 700.5,161.5 502.8,161.2 455.6,113.8 408.5,66.4 263.6,66.6C183.9,66.6 115.2,67.1 110.9,67.5" android:strokeColor="#00000000"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

View file

@ -0,0 +1,5 @@
<vector android:height="240dp" android:viewportHeight="2540.0"
android:viewportWidth="2440.0" android:width="240dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="M422,2416c-11,-15 -26,-64 -37,-128 -10,-57 -20,-109 -22,-115 -4,-9 -27,-13 -68,-13 -38,0 -66,-5 -72,-12 -6,-7 -40,-173 -77,-368l-68,-355 5,-160c3,-124 9,-179 27,-245 109,-407 426,-724 835,-831 79,-21 112,-24 275,-24 163,0 196,3 275,24 409,107 726,424 835,831 18,66 24,121 27,245l5,160 -68,355c-37,195 -71,361 -77,368 -6,7 -34,12 -72,12 -41,0 -64,4 -68,13 -2,6 -12,58 -22,115 -28,159 -16,152 -257,152 -200,0 -201,0 -235,-26 -70,-54 -75,-91 -34,-299 10,-55 29,-152 41,-215 72,-392 79,-414 140,-445 52,-27 102,-18 268,46 86,34 165,68 174,77 15,12 18,12 22,1 8,-26 25,-134 32,-201l6,-68 -90,0c-83,0 -93,-2 -121,-26 -17,-14 -31,-35 -31,-45 0,-49 -35,-180 -65,-245 -78,-171 -188,-285 -352,-368 -29,-14 -55,-26 -58,-26 -3,0 -5,92 -5,205l0,205 95,0c70,0 95,3 95,13 0,7 -103,115 -229,241l-229,229 -95,-89c-128,-120 -367,-369 -367,-383 0,-7 32,-11 90,-11l90,0 0,-205 0,-205 -52,26c-166,84 -274,197 -353,367 -30,66 -65,197 -65,246 0,10 -14,31 -31,45 -28,24 -38,26 -121,26l-90,0 6,68c7,67 24,175 32,201 4,11 7,11 22,-1 9,-9 88,-43 174,-77 166,-64 216,-73 268,-46 61,31 68,53 140,445 12,63 31,160 41,215 41,208 36,245 -34,299 -34,26 -35,26 -235,26 -200,0 -201,0 -220,-24z" android:strokeColor="#00000000"/>
</vector>

View file

@ -0,0 +1,25 @@
<!--
Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:pathData="M17.6,6.4C16.2,4.9 14.2,4 12,4c-4.4,0 -8,3.6 -8,8s3.6,8 8,8c3.7,0 6.8,-2.6 7.7,-6l-2.1,0c-0.8,2.3 -3,4 -5.6,4c-3.3,0 -6,-2.7 -6,-6s2.7,-6 6,-6c1.7,0 3.1,0.7 4.2,1.8L13,11l7,0L20,4L17.6,6.4z"
android:fillColor="@color/white"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,6 @@
<vector android:height="240dp" android:viewportHeight="978.0"
android:viewportWidth="956.0" android:width="240dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#FFFFFF"
android:pathData="M925.9,377.9L697,377.9L633.8,591.6L845.5,591.6l0,76.7L609.7,668.3L538.3,907.6L449.6,907.6L521.1,668.3L335.5,668.3L264.1,907.6L175.4,907.6L246.9,668.3L42.1,668.3l0,-76.7L270.9,591.6L334.2,377.9L122.5,377.9l0,-76.7L358.2,301.1L430.4,61.2l88.7,0L446.9,301.1l185.6,0L704.6,61.2l88.7,0L721.1,301.1L925.9,301.1ZM610.4,376.7L422.1,376.7l-64.6,216l188.3,0z"
android:strokeColor="#00000000" android:strokeWidth="32.00193405"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,4 @@
<vector android:height="240dp" android:viewportHeight="512.0"
android:viewportWidth="512.0" android:width="240dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="m507.6,431.9l-224,-384c-5.7,-9.8 -16.3,-15.9 -27.6,-15.9 -11.4,0 -21.9,6 -27.6,15.9l-224,384c-5.8,9.9 -5.8,22.1 -0.1,32.1 5.7,9.9 16.3,16.1 27.8,16.1h448c11.5,0 22,-6.1 27.8,-16.1 5.7,-9.9 5.7,-22.2 -0.1,-32.1zM256,416c-17.7,0 -32,-14.3 -32,-32 0,-17.7 14.3,-32 32,-32 17.7,0 32,14.3 32,32 0,17.7 -14.3,32 -32,32zM288,288c0,17.7 -14.3,32 -32,32s-32,-14.3 -32,-32v-96c0,-17.7 14.3,-32 32,-32s32,14.3 32,32v96z"/>
</vector>

View file

@ -29,4 +29,10 @@
app:showAsAction="always"
app:actionViewClass="android.support.v7.widget.SearchView"
/>
<item android:id="@+id/action_refresh"
android:title="@string/action_refesh"
android:icon="@drawable/ic_menu_refresh"
app:showAsAction="always" />
</menu>

View file

@ -22,18 +22,17 @@
-->
<resources>
<string name="app_name">Play Music Exporter (beta)</string>
<string name="navigation_drawer_close">Drawer schließen</string>
<string name="view_type_selection_title">Ansicht</string>
<string name="title_view_rated">Positiv bewertet</string>
<string name="title_view_playlist">Playlists</string>
<string name="title_view_playlist">Playlisten</string>
<string name="title_view_artist">Interpreten</string>
<string name="title_view_album">Alben</string>
<string name="navigation_drawer_open">Drawer öffnen</string>
<string name="title_track_detail">Titeldetails</string>
<string name="title_settings">Einstellungen</string>
<string name="settings_category_about_me">Über mich</string>
<string name="settings_category_export">Export-Einstellungen</string>
<string name="settings_category_export">Export Einstellungen</string>
<string name="settings_category_extra">Extras</string>
<string name="settings_category_thanks">Dank an</string>
<string name="settings_export_path">Speicherort</string>
@ -54,13 +53,13 @@
<string name="action_share">Teilen</string>
<string name="action_select_all">Alles markieren</string>
<string name="settings_export_path_custom">Benutzerdefinierter Pfad</string>
<string name="settings_export_id3_artwork_size">Maximale Artworkgröße</string>
<string name="settings_export_id3_artwork_size">Maximale Covergröße</string>
<string name="settings_export_id3_artwork_size_summary">Wenn die Größe des Artworks größer als der eingestellte Wert ist, wird das Artwork auf diesen Wert runter skalliert.</string>
<string name="action_mode_track_selection">%d Titel ausgewählt</string>
<string name="action_deselect_all">Auswahl aufheben</string>
<string name="toast_error_track_not_offline">Sie können nur offline Titel exportieren. Versuchen Sie das Album in Play Music offline herunterzuladen.</string>
<string name="toast_error_track_not_offline">Du kannst nur offline Titel exportieren. Versuche das Album in Play Music offline herunterzuladen.</string>
<string name="settings_build_date">Kompilationsdatum</string>
<string name="settings_category_develop">Entwicklung</string>
<string name="settings_category_develop">Über diese Version</string>
<string name="dialog_storage_access_denied_title">Speicher-Zugriff verweigert</string>
<string name="dialog_storage_access_denied">Der Play Music Exporter benötigt Schreibzugriff auf den externen Speicher, ohne diesen kann die App nicht genutzt werden.</string>
<string name="text_okay">OK</string>
@ -81,8 +80,8 @@
<string name="settings_export_subdirectory_structure_group_example">Beispiel: Great Songs/4. Beatles - Yesterday.mp3</string>
<string name="settings_version_number">Versionsnummer</string>
<string name="debug_trigger_export_all_title">Jetzt den ExportAllService starten</string>
<string name="settings_category_debug">Debug</string>
<string name="pref_header_debug">Debug</string>
<string name="settings_category_debug">Debuggen</string>
<string name="pref_header_debug">Debuggen</string>
<string name="settings_auto_export_different_path">Export Pfad</string>
<string name="settings_auto_export_different_path_switch">Nutze einen anderen Export Pfad</string>
<string name="settings_auto_export_different_path_switch_summary">Wenn dieser Schalter aus ist nutzt der automatische Export den gleichen Pfad wie der manuelle Export von Alben</string>
@ -91,19 +90,44 @@
<string name="settings_auto_export_different_structure_switch_summary">Wenn dieser Schalter aus ist nutzt der automatische Export den gleichen Verzeichnisbaum wie der manuelle Export von Alben</string>
<string name="settings_auto_export_enable_checkbox">Automatischer Export aktiviert</string>
<string name="settings_auto_export_interval_list">Export Interval</string>
<string name="settings_auto_export_require_charging_checkbox">Setze laden voraus</string>
<string name="settings_auto_export_require_charging_checkbox">Setze Laden des Geräts voraus</string>
<string name="settings_auto_export_require_unmetered_checkbox">Setze eine unbeschränkte Netzwerkverbindung voraus</string>
<string name="settings_category_alba_export">Export Speichereinstellungen Alben</string>
<string name="settings_category_alba_export">Export Speichereinstellungen für Alben</string>
<string name="settings_category_auto_export_conditions">Export Bedingungen</string>
<string name="settings_category_auto_export_path_subdir">Export Pfad und Verzeichnisbaum</string>
<string name="settings_category_groups_export">Export Speichereinstellungen für Playlists</string>
<string name="settings_export_path_alba">Export Pfad für Alben</string>
<string name="settings_export_path_groups">Export Pfad für Playlists</string>
<string name="dialog_superuser_access_denied_title">Administrator-Zugriff verweigert</string>
<string name="dialog_superuser_access_denied_title">Root-Zugriff verweigert</string>
<string name="dialog_superuser_access_denied">Der Play Music Exporter benötigt Administrator Rechte, ohne diese Rechte kann die App nicht genutzt werden.</string>
<string name="settings_auto_export_different_structure_dialog_title">Verzeichnisbaum</string>
<string name="settings_export_overwrite_existing">Bestehende Dateien überschreiben</string>
<string name="file_handling_category">Dateiverarbeitung</string>
<string name="overwrite_summary">Überschreiben Sie die exportierte Datei immer, auch wenn sie bereits existiert.</string>
<string name="overwrite_title">Bestehende Dateien überschreiben.</string>
<string name="overwrite_summary">Überschreibe die exportierte Datei immer, auch wenn sie bereits existiert</string>
<string name="overwrite_title">Bestehende Dateien überschreiben</string>
<string name="action_refesh">Datenbank neu laden</string>
<string name="database_reloaded">Datenbank neu geladen</string>
<string name="debug_test_crash_handler">Teste Fehlerbehandlung</string>
<string name="settings_donation_summery">An den Entwickler über Bitcoin spenden. Dies erfordert ein Bitcoin Wallet auf deinem Handy. Besuche die Homepage für Spenden von einem Computer.</string>
<string name="settings_donation_title">Unterstütze mich</string>
<string name="intro_welcome_title">Willkommen!</string>
<string name="intro_welcome_description">Das ist der Play Music Exporter. Er ermöglicht dir Export und die Speicherung von Liedern aus Play Music als MP3 Dateien.</string>
<string name="intro_warning_title">Warnung!</string>
<string name="intro_warning_description">Du bist verantwortlich für das, was du mit dieser App machst. Die App könnte in deinem Land möglicherweiße illegal sein. Wir raten von Musikpiraterie und dem Stehlen anderen geistigen Eigentums ab. Das Teilen von Musik, die du mit diesem Tool exportiert hast, wäre keine gute Idee, da Google möglicherweise ein unsichtbares Wasserzeichen auf die Musik setzt, um das Google Konto zurück verfolgen zu können.</string>
<string name="intro_storage_title">Wir benötigen Zugriff auf deinen Speicher.</string>
<string name="intro_storage_description">Wir benötigen Zugriff auf den externen Speicher, um die Play Music Datenbank in einen Ordner zu kopieren, in dem wir das Recht haben, mit ihr zu arbeiten. Außerdem benötigen wir den Zugriff auch, um die verschlüsselten MP3s ohne ID3-Tags zu entschlüsseln und mit ID3-Tags im Exportpfad zu speichern.</string>
<string name="intro_superuser_description">Einige der Dateien, auf die wir zugreifen müssen, befinden sich in den privaten Ordnern von Play Music. Android verhindert, dass andere Apps auf diese zugreifen können. Glücklicherweise kannst du diesen Schutz mit Root-Zugriff umgehen. Ohne Root-Zugriff können wir leider nichts machen.</string>
<string name="intro_superuser_title">Wir benötigen Root-Zugriff.</string>
<string name="intro_error_description">Keine Software ist perfekt. Wir haben eine automatisierte Fehlerberichterstattung hinzugefügt, um die App zu verbessern. Aktiviere sie, wenn du gerne dazu beitragen möchtest, die App zu verbessern. Falls du dies nicht aktivierst, und etwas nicht funktioniert, bist du auf eigene Faust angewiesen, da wir dir ohne Logs nicht helfen können. Die gesammelten Daten enthalten nichts, was zur Identifizierung deiner Identität verwendet werden könnte. (Auf der nächsten Seite wirst du aufgefordert werden, dies zu akzeptieren oder zu verbieten)</string>
<string name="intro_error_title">Anonyme Fehlerbericht Erstattung.</string>
<string name="intro_finish_description">Hinweis: Solltest du eine dieser Berechtigungen deaktivieren, wird das Tutorial erneut angezeigt.</string>
<string name="intro_finish_title">Einführung abgeschlossen!</string>
<string name="error_alert_dialog_title">Anonyme Fehlerbericht Erstattung?</string>
<string name="error_alert_dialog_message">Aktiviere bitte die automatische Fehlerberichterstattung, um die App zu verbessern. Du wirst keine Hilfe bekommen, falls du dies deaktivierst, da wir keine Fehler ohne die dazugehörigen Logs beheben können. Die gesammelten Daten enthalten nichts, was zur Identifizierung deiner Identität verwendet werden könnte.</string>
<string name="no">Nein</string>
<string name="yes">Ja</string>
<string name="whatever">Mir egal, lass mich einfach die App nutzen!</string>
<string name="warning_alert_dialog_title">Verstanden?</string>
<string name="warning_alert_dialog_message">Hast du das gelesen und verstanden?</string>
<string name="settings_open_homepage_title">Webseite</string>
</resources>

View file

@ -44,4 +44,6 @@
<color name="button_navigation_drawer_active">#11000000</color>
<color name="button_navigation_drawer_text">#88000000</color>
<color name="button_navigation_drawer_text_active">#ef6c00</color>
<color name="white">#ffffffff</color>
<color name="black">#ff000000</color>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="countly_url" translatable="false">https://countly.jcg.re/</string>
<string name="countly_token" translatable="false">82a1d9405388c4dd73dc9835f84c59cf4274086d</string>
</resources>

View file

@ -22,7 +22,7 @@
<resources>
<string name="app_name">Play Music Exporter (beta)</string>
<string name="app_name" translatable="false">Play Music Exporter</string>
<string name="title_track_detail">Track Detail</string>
<string name="action_select_all">Select all</string>
@ -52,7 +52,7 @@
<string name="dialog_storage_access_denied_title">Storage access denied</string>
<string name="dialog_storage_access_denied">The Play Music Exporter does not work without write access to the external storage. You can not use this app without granting it write access.</string>
<string name="dialog_superuser_access_denied_title">Superuser access denied</string>
<string name="dialog_superuser_access_denied_title">Root access denied</string>
<string name="dialog_superuser_access_denied">The Play Music Exporter does not work without superuser access. You can not use this app without granting it superuser access.</string>
@ -72,8 +72,8 @@
<string name="settings_category_auto_export_conditions">Export conditions</string>
<string name="settings_category_auto_export_path_subdir">Export path and subdirectory structure</string>
<string name="settings_category_debug">Debug</string>
<string name="settings_category_alba_export">Export location albums</string>
<string name="settings_category_groups_export">Export location playlists</string>
<string name="settings_category_alba_export">Export location for albums</string>
<string name="settings_category_groups_export">Export location for playlists</string>
<string name="settings_version_number">Version Number</string>
@ -121,8 +121,8 @@
<string name="settings_open_old_homepage_title" translatable="false">David-Schulte.de</string>
<string name="settings_open_old_homepage_url" translatable="false"><![CDATA[http://www.david-schulte.de/]]></string>
<string name="settings_open_homepage_title" translatable="false">Website</string>
<string name="settings_open_homepage_url" translatable="false"><![CDATA[https://jcg.re/]]></string>
<string name="settings_open_homepage_title">Website</string>
<string name="settings_open_homepage_url" translatable="false"><![CDATA[https://github.com/jcgruenhage/PlayMusicExporter]]></string>
<string name="settings_mp3agic_title" translatable="false">Mp3agic ID3 Libary</string>
<string name="settings_mp3agic_summery" translatable="false">Michael Patricios &#169; 2006&#8211;2013</string>
@ -144,6 +144,31 @@
<string name="debug_trigger_export_all_title">Trigger ExportAllService now</string>
<string name="file_handling_category">File Handling</string>
<string name="overwrite_summary">Always overwrite the exported file, even if it already exists.</string>
<string name="overwrite_summary">Always overwrite the exported file, even if it already exists</string>
<string name="overwrite_title">Overwrite Existing Files</string>
<string name="action_refesh">Refresh</string>
<string name="database_reloaded">Music Database Reloaded</string>
<string name="debug_test_crash_handler">Test Crash Handler</string>
<string name="settings_donation_title">Support me</string>
<string name="settings_donation_summery">Donate to the current developer via Bitcoin. This requires that you have installed a bitcoin wallet on your phone. For donations from a computer, see the homepage.</string>
<string name="settings_donation_url" translatable="false"><![CDATA[bitcoin:1NdzpDWPQ53xWT5fraGPZX5F9XrKiPBXjp]]></string>
<string name="intro_welcome_title">Welcome!</string>
<string name="intro_welcome_description">This is the Play Music Exporter. It can export songs from Play Music and save them as MP3 files where you want them to be.</string>
<string name="intro_warning_title">Warning!</string>
<string name="intro_warning_description">You are responsible for what you do with this app. Depending on where you live it might be illegal to use this app. We discourage piracy of music and other intellectual property. Sharing music you exported with this tool might be a very bad idea, Google could put an invisible watermark on the music, so that people can trace the MP3s back to the owner of the Google account that was used.</string>
<string name="intro_storage_title">We need access to your storage.</string>
<string name="intro_storage_description">We need to access the external storage, for copying the Play Music database to a folder, where we have the right to work with it. We also need access to the external storage, to finish up the MP3s, from encrypted without ID3 tags, to decrypted with ID3 tags, before we save them to your export path.</string>
<string name="intro_superuser_description">Some of the files we need to access are in the private folders of Play Music. Android prevents apps from accessing the private folders of other apps, but luckily, you can circumvent this protection with root access. Without root access this app can\'t do anything.</string>
<string name="intro_superuser_title">We need root access.</string>
<string name="intro_error_description">No single piece of software is perfect. To get this closer though, we have added automated error reporting to this app. If you would like to help make this app better, let us enable that. If you don\'t enable this and something does not work, you are on your own, since we can\'t help you without having the logs this provides us with. The data collected by this does not include anything that could be used to identify you. (You will be prompted to either accept or deny that when you go to the next slide)</string>
<string name="intro_error_title">Anonymous error reporting.</string>
<string name="intro_finish_description">One note: Should you revoke any of these permission, the tutorial will be shown again on the next launch.</string>
<string name="intro_finish_title">Introduction finished!</string>
<string name="error_alert_dialog_title">Anonymous error reporting?</string>
<string name="error_alert_dialog_message">In order to make this app better, please enable automatic crash reporting. You wont ever get any help, if you disable this, since we can\'t fix bugs without the logs provided by this. The data collected by this does not include anything that could be used to identify you.</string>
<string name="no">No</string>
<string name="yes">Yes</string>
<string name="whatever">I don\'t care, just let me use the app!</string>
<string name="warning_alert_dialog_title">Understood?</string>
<string name="warning_alert_dialog_message">Have you read and understood this?</string>
</resources>

View file

@ -12,7 +12,7 @@
</Preference>
</PreferenceCategory>
<!-- About David Schulte -->
<!-- About this project -->
<PreferenceCategory android:title="@string/settings_category_about_me">
<!-- Homepage -->
<Preference
@ -22,6 +22,15 @@
android:action="android.intent.action.VIEW"
android:data="@string/settings_open_homepage_url" />
</Preference>
<!-- Donation -->
<Preference
android:summary="@string/settings_donation_summery"
android:title="@string/settings_donation_title">
<intent
android:action="android.intent.action.VIEW"
android:data="@string/settings_donation_url" />
</Preference>
</PreferenceCategory>
<!-- About David Schulte -->
@ -50,11 +59,11 @@
<!-- TODO change Version number and build date with releases -->
<!-- Version number-->
<Preference
android:summary="0.9.4"
android:key="about_version_number"
android:title="@string/settings_version_number" />
<!-- Build date-->
<Preference
android:summary="01.02.2017"
android:key="about_build_date"
android:title="@string/settings_build_date" />
</PreferenceCategory>

View file

@ -2,5 +2,7 @@
<PreferenceCategory android:title="@string/settings_category_debug">
<Preference android:title="@string/debug_trigger_export_all_title"
android:key="debug_trigger_export_all"/>
<Preference android:title="@string/debug_test_crash_handler"
android:key="debug_test_crash_handler"/>
</PreferenceCategory>
</PreferenceScreen>

View file

@ -0,0 +1,7 @@
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header
android:fragment="re.jcg.playmusicexporter.activities.SettingsActivity$DebugPreferenceFragment"
android:icon="@drawable/ic_action_settings"
android:title="@string/pref_header_debug" />
</preference-headers>

View file

@ -16,9 +16,5 @@
android:fragment="re.jcg.playmusicexporter.activities.SettingsActivity$AboutPreferenceFragment"
android:icon="@drawable/ic_info_black_24dp"
android:title="@string/pref_header_about" />
<header
android:fragment="re.jcg.playmusicexporter.activities.SettingsActivity$DebugPreferenceFragment"
android:icon="@drawable/ic_action_settings"
android:title="@string/pref_header_debug" />
</preference-headers>

View file

@ -41,6 +41,6 @@ android {
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':framework')
compile 'com.mpatric:mp3agic:0.9.0'
}

Binary file not shown.

View file

@ -25,7 +25,6 @@ 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;
@ -33,10 +32,8 @@ 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;
@ -44,7 +41,10 @@ import com.mpatric.mp3agic.ID3v2;
import com.mpatric.mp3agic.ID3v22Tag;
import com.mpatric.mp3agic.ID3v23Tag;
import com.mpatric.mp3agic.ID3v24Tag;
import com.mpatric.mp3agic.InvalidDataException;
import com.mpatric.mp3agic.Mp3File;
import com.mpatric.mp3agic.NotSupportedException;
import com.mpatric.mp3agic.UnsupportedTagException;
import java.io.ByteArrayOutputStream;
import java.io.File;
@ -138,6 +138,7 @@ public class PlayMusicManager {
/**
* 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() {
@ -297,6 +298,7 @@ public class PlayMusicManager {
/**
* Creates a new PlayMusic manager
*
* @param context App context
*/
public PlayMusicManager(Context context) {
@ -307,8 +309,9 @@ public class PlayMusicManager {
/**
* Loads all needed information and opens the database
* @throws PlayMusicNotFoundException PlayMusic is not installed
* @throws NoSuperUserException No super user permissions
*
* @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 {
@ -345,7 +348,8 @@ public class PlayMusicManager {
/**
* Copies the database to a temp directory and opens it
* @throws NoSuperUserException No super user permissions
*
* @throws NoSuperUserException No super user permissions
* @throws de.arcus.playmusiclib.exceptions.CouldNotOpenDatabaseException Could not open the database
*/
private void loadDatabase() throws NoSuperUserException, CouldNotOpenDatabaseException {
@ -370,10 +374,11 @@ public class PlayMusicManager {
/**
* Reloads the database from PlayMusic
* @throws NoSuperUserException No super user permissions
*
* @throws NoSuperUserException No super user permissions
* @throws CouldNotOpenDatabaseException Could not open the database
*/
public void realoadDatabase() throws NoSuperUserException, CouldNotOpenDatabaseException {
public void reloadDatabase() throws NoSuperUserException, CouldNotOpenDatabaseException {
// Reload database
loadDatabase();
}
@ -403,6 +408,7 @@ public class PlayMusicManager {
/**
* Gets the path to the music track
*
* @param localCopyPath The local copy path
* @return The path to the music file
*/
@ -437,6 +443,7 @@ public class PlayMusicManager {
/**
* Gets the full path to the artwork
*
* @param artworkPath The artwork path
* @return The full path to the artwork
*/
@ -469,12 +476,13 @@ public class PlayMusicManager {
/**
* 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
*
* @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 ) {
public boolean exportMusicTrack(MusicTrack musicTrack, String dest, boolean forceOverwrite) throws InvalidDataException, IOException, UnsupportedTagException, NotSupportedException {
// Creates the destination directory
File directory = new File(dest).getParentFile();
@ -487,12 +495,13 @@ public class PlayMusicManager {
/**
* 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
*
* @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) {
public boolean exportMusicTrack(MusicTrack musicTrack, Uri uri, String path, boolean forceOverwrite) throws InvalidDataException, IOException, UnsupportedTagException, NotSupportedException {
// Check for null
if (musicTrack == null) return false;
@ -504,10 +513,9 @@ public class PlayMusicManager {
String uniqueID = UUID.randomUUID().toString();
if ( forceOverwrite || !isAlreadyThere(uri, path) )
{
if (forceOverwrite || !isAlreadyThere(uri, path)) {
String fileTmp = getTempPath() + uniqueID +"_tmp.mp3";
String fileTmp = getTempPath() + uniqueID + "_tmp.mp3";
// Copy to temp path failed
if (!SuperUserTools.fileCopy(srcFile, fileTmp))
@ -515,7 +523,7 @@ public class PlayMusicManager {
// Encrypt the file
if (musicTrack.isEncoded()) {
String fileTmpCrypt = getTempPath() + uniqueID +"_crypt.mp3";
String fileTmpCrypt = getTempPath() + uniqueID + "_crypt.mp3";
// Encrypts the file
if (trackEncrypt(musicTrack, fileTmp, fileTmpCrypt)) {
@ -530,7 +538,6 @@ public class PlayMusicManager {
}
String dest;
Uri copyUri = null;
if (uri.toString().startsWith("file://")) {
@ -541,19 +548,19 @@ public class PlayMusicManager {
FileTools.directoryCreate(parentDirectory);
} else {
// Complex uri (Lollipop)
dest = getTempPath() + uniqueID +"_final.mp3";
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<directories.length - 1; i++) {
String[] directories = path.split("/");
for (int i = 0; i < directories.length - 1; i++) {
String directoryName = directories[i];
boolean found = false;
// Search all sub elements
for (DocumentFile subDocument: document.listFiles()) {
for (DocumentFile subDocument : document.listFiles()) {
// Directory exists
if (subDocument.isDirectory() && subDocument.getName().equalsIgnoreCase(directoryName)) {
document = subDocument;
@ -571,12 +578,12 @@ public class PlayMusicManager {
// Gets the filename
String filename = directories[directories.length - 1];
for (DocumentFile subDocument: document.listFiles()) {
for (DocumentFile subDocument : document.listFiles()) {
// Directory exists
if (subDocument.isFile() ){
if ( filename != null && subDocument.getName().equalsIgnoreCase(filename)) {
if (subDocument.isFile()) {
if (filename != null && subDocument.getName().equalsIgnoreCase(filename)) {
// Delete the file
if ( forceOverwrite ) {
if (forceOverwrite) {
Logger.getInstance().logWarning("ExportMusicTrack", "(forceOverwrite) Deleting original file: " + filename);
}
subDocument.delete();
@ -660,7 +667,7 @@ public class PlayMusicManager {
//new MediaScanner(mContext, dest);
} else {
Logger.getInstance().logInfo("exportMusicTrack", path + " already exists, skipping." );
Logger.getInstance().logInfo("exportMusicTrack", path + " already exists, skipping.");
}
// Done
@ -669,9 +676,10 @@ public class PlayMusicManager {
/**
* Checks if the destination file already exists
* @param pUri the source file
*
* @param pUri the source file
* @param pPath The destination
* return true if the file already exists
* return true if the file already exists
*/
private boolean isAlreadyThere(Uri pUri, String pPath) {
if (pUri.toString().startsWith("file://")) {
@ -683,8 +691,8 @@ public class PlayMusicManager {
for (String lDisplayName : pPath.split("/")) {
if (lDocumentFile.findFile(lDisplayName) != null) {
lDocumentFile = lDocumentFile.findFile(lDisplayName);
if ( lDocumentFile.length() == 0 ) {
if ( !lDocumentFile.isDirectory() ) {
if (lDocumentFile.length() == 0) {
if (!lDocumentFile.isDirectory()) {
Logger.getInstance().logInfo("isAlreadyThere", pPath + " File exists, but is 0 bytes in size.");
}
}
@ -699,122 +707,117 @@ public class PlayMusicManager {
/**
* 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
* @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);
private boolean trackWriteID3(MusicTrack musicTrack, String src, String dest) throws InvalidDataException, IOException, UnsupportedTagException, NotSupportedException {
// 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<ID3v1Genres.GENRES.length; n++) {
// Genre found
if (ID3v1Genres.GENRES[n].equals(musicTrack.getGenre())) {
tagID3v1.setGenre(n);
break;
}
}
mp3File.setId3v1Tag(tagID3v1);
}
// It can't be null
final ID3v2 tagID3v2;
// Creates the requested version
switch(mID3v2Version) {
case ID3v22:
tagID3v2 = new ID3v22Tag();
break;
case ID3v23:
tagID3v2 = new ID3v23Tag();
break;
case ID3v24:
tagID3v2 = new ID3v24Tag();
break;
default:
tagID3v2 = null;
break;
}
// 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
tagID3v2.setTitle(musicTrack.getTitle());
tagID3v2.setArtist(musicTrack.getArtist());
tagID3v2.setAlbum(musicTrack.getAlbum());
tagID3v2.setAlbumArtist(musicTrack.getAlbumArtist());
tagID3v2.setTrack("" + musicTrack.getTrackNumber());
tagID3v2.setPartOfSet("" + musicTrack.getDiscNumber());
tagID3v2.setYear(musicTrack.getYear());
tagID3v1.setTrack(musicTrack.getTitle());
tagID3v1.setArtist(musicTrack.getArtist());
tagID3v1.setAlbum(musicTrack.getAlbum());
tagID3v1.setYear(musicTrack.getYear());
if (!TextUtils.isEmpty(musicTrack.getGenre())) {
try {
// Maybe the genre is not supported
tagID3v2.setGenreDescription(musicTrack.getGenre());
} catch (IllegalArgumentException e) {
Logger.getInstance().logWarning("TrackWriteID3", e.getMessage());
// Search the genre
for (int n = 0; n < ID3v1Genres.GENRES.length; n++) {
// Genre found
if (ID3v1Genres.GENRES[n].equals(musicTrack.getGenre())) {
tagID3v1.setGenre(n);
break;
}
}
// Add the artwork to the meta data
if (mID3EnableArtwork) {
// Load the artwork
Bitmap bitmap = ArtworkLoader.loadArtwork(musicTrack, mID3ArtworkMaximumSize);
if (bitmap != null) {
// JPEG is default
String mimeType = "image/jpeg";
// Load the bitmap into a byte array
ByteArrayOutputStream artworkDataStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, artworkDataStream);
// Adds the artwork to the meta data
tagID3v2.setAlbumImage(artworkDataStream.toByteArray(), mimeType);
}
}
mp3File.setId3v2Tag(tagID3v2);
// Save the file
mp3File.save(dest);
// Done
return true;
} catch (Exception e) {
Logger.getInstance().logError("TrackWriteId3", e.toString());
mp3File.setId3v1Tag(tagID3v1);
}
// Failed
return false;
// It can't be null
final ID3v2 tagID3v2;
// Creates the requested version
switch (mID3v2Version) {
case ID3v22:
tagID3v2 = new ID3v22Tag();
break;
case ID3v23:
tagID3v2 = new ID3v23Tag();
break;
case ID3v24:
tagID3v2 = new ID3v24Tag();
break;
default:
tagID3v2 = null;
break;
}
// Set all tag values
tagID3v2.setTitle(musicTrack.getTitle());
tagID3v2.setArtist(musicTrack.getArtist());
tagID3v2.setAlbum(musicTrack.getAlbum());
tagID3v2.setAlbumArtist(musicTrack.getAlbumArtist());
tagID3v2.setTrack("" + musicTrack.getTrackNumber());
tagID3v2.setPartOfSet("" + musicTrack.getDiscNumber());
tagID3v2.setYear(musicTrack.getYear());
if (!TextUtils.isEmpty(musicTrack.getGenre())) {
try {
// Maybe the genre is not supported
tagID3v2.setGenreDescription(musicTrack.getGenre());
} catch (IllegalArgumentException e) {
Logger.getInstance().logWarning("TrackWriteID3", e.getMessage());
}
}
// Add the artwork to the meta data
if (mID3EnableArtwork) {
// Load the artwork
Bitmap bitmap = ArtworkLoader.loadArtwork(musicTrack, mID3ArtworkMaximumSize);
if (bitmap != null) {
// JPEG is default
String mimeType = "image/jpeg";
// Load the bitmap into a byte array
ByteArrayOutputStream artworkDataStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, artworkDataStream);
// Adds the artwork to the meta data
tagID3v2.setAlbumImage(artworkDataStream.toByteArray(), mimeType);
}
}
mp3File.setId3v2Tag(tagID3v2);
// Save the file
mp3File.save(dest);
// Done
return true;
}
/**
* Encrypts a track and save it to a new path
*
* @param musicTrack The music track
* @param src The source mp3 file
* @param dest The destination path
* @param src The source mp3 file
* @param dest The destination path
* @return Return if the operation was successful
*/
private boolean trackEncrypt(MusicTrack musicTrack, String src, String dest) {
@ -843,8 +846,8 @@ public class PlayMusicManager {
* Deletes all cache files
*/
private void cleanUp(String theUniqueID) {
FileTools.fileDelete( getTempPath() + theUniqueID +"_final.mp3");
FileTools.fileDelete( getTempPath() + theUniqueID +"_tmp.mp3");
FileTools.fileDelete( getTempPath() + theUniqueID +"_crypt.mp3");
FileTools.fileDelete(getTempPath() + theUniqueID + "_final.mp3");
FileTools.fileDelete(getTempPath() + theUniqueID + "_tmp.mp3");
FileTools.fileDelete(getTempPath() + theUniqueID + "_crypt.mp3");
}
}