Initial commit
8
android/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
key.properties
|
76
android/app/build.gradle
Normal file
|
@ -0,0 +1,76 @@
|
|||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "f.f.freezer"
|
||||
minSdkVersion 20
|
||||
targetSdkVersion 28
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig signingConfigs.release
|
||||
shrinkResources false
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation group: 'org', name: 'jaudiotagger', version: '2.0.3'
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
7
android/app/src/debug/AndroidManifest.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="f.f.freezer">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
68
android/app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,68 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="f.f.freezer">
|
||||
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
|
||||
calls FlutterMain.startInitialization(this); in its onCreate method.
|
||||
In most cases you can leave this as-is, but you if you want to provide
|
||||
additional functionality it is fine to subclass or reimplement
|
||||
FlutterApplication and put your custom class here. -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<application
|
||||
android:name="io.flutter.app.FlutterApplication"
|
||||
android:label="Freezer"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<!-- Displays an Android View that continues showing the launch screen
|
||||
Drawable until Flutter paints its first frame, then this splash
|
||||
screen fades out. A splash screen is useful to avoid any visual
|
||||
gap between the end of Android's launch screen and the painting of
|
||||
Flutter's first frame. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
|
||||
<service android:name="com.ryanheise.audioservice.AudioService">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
</manifest>
|
167
android/app/src/main/java/f/f/freezer/MainActivity.java
Normal file
|
@ -0,0 +1,167 @@
|
|||
package f.f.freezer;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jaudiotagger.audio.AudioFile;
|
||||
import org.jaudiotagger.audio.AudioFileIO;
|
||||
import org.jaudiotagger.tag.FieldKey;
|
||||
import org.jaudiotagger.tag.Tag;
|
||||
import org.jaudiotagger.tag.TagOptionSingleton;
|
||||
import org.jaudiotagger.tag.datatype.Artwork;
|
||||
import org.jaudiotagger.tag.flac.FlacTag;
|
||||
import org.jaudiotagger.tag.id3.ID3v23Tag;
|
||||
import org.jaudiotagger.tag.id3.valuepair.ImageFormats;
|
||||
import org.jaudiotagger.tag.reference.PictureTypes;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
import io.flutter.embedding.engine.FlutterEngine;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
private static final String CHANNEL = "f.f.freezer/native";
|
||||
|
||||
@Override
|
||||
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine);
|
||||
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
|
||||
.setMethodCallHandler(((call, result) -> {
|
||||
//Decrypt track
|
||||
if (call.method.equals("decryptTrack")) {
|
||||
String tid = call.argument("id").toString();
|
||||
String path = call.argument("path");
|
||||
decryptTrack(path, tid);
|
||||
result.success(0);
|
||||
}
|
||||
//Add tags to track
|
||||
if (call.method.equals("tagTrack")) {
|
||||
try {
|
||||
//Tag
|
||||
TagOptionSingleton.getInstance().setAndroid(true);
|
||||
AudioFile f = AudioFileIO.read(new File(call.argument("path").toString()));
|
||||
boolean isFlac = true;
|
||||
if (f.getAudioHeader().getFormat().contains("MPEG")) {
|
||||
f.setTag(new ID3v23Tag());
|
||||
isFlac = false;
|
||||
}
|
||||
|
||||
Tag tag = f.getTag();
|
||||
tag.setField(FieldKey.TITLE, call.argument("title").toString());
|
||||
tag.setField(FieldKey.ALBUM, call.argument("album").toString());
|
||||
tag.setField(FieldKey.ARTIST, call.argument("artists").toString());
|
||||
tag.setField(FieldKey.TRACK, call.argument("trackNumber").toString());
|
||||
|
||||
//Album art
|
||||
String cover = call.argument("cover").toString();
|
||||
if (isFlac) {
|
||||
//FLAC requires different cover adding
|
||||
RandomAccessFile imageFile = new RandomAccessFile(new File(cover), "r");
|
||||
byte[] imageData = new byte[(int) imageFile.length()];
|
||||
imageFile.read(imageData);
|
||||
tag.setField(((FlacTag) tag).createArtworkField(
|
||||
imageData,
|
||||
PictureTypes.DEFAULT_ID,
|
||||
ImageFormats.MIME_TYPE_JPG,
|
||||
"cover",
|
||||
1400,
|
||||
1400,
|
||||
24,
|
||||
0
|
||||
));
|
||||
} else {
|
||||
//MP3
|
||||
Artwork art = Artwork.createArtworkFromFile(new File(cover));
|
||||
tag.addField(art);
|
||||
}
|
||||
//Save
|
||||
AudioFileIO.write(f);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
result.success(null);
|
||||
}
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
public static void decryptTrack(String path, String tid) {
|
||||
try {
|
||||
//Load file
|
||||
File inputFile = new File(path + ".ENC");
|
||||
BufferedInputStream buffin = new BufferedInputStream(new FileInputStream(inputFile));
|
||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
||||
byte[] key = getKey(tid);
|
||||
for (int i=0; i<inputFile.length()/2048; i++) {
|
||||
byte[] tmp = new byte[2048];
|
||||
buffin.read(tmp, 0, tmp.length);
|
||||
if ((i%3) == 0) {
|
||||
tmp = decryptChunk(key, tmp);
|
||||
}
|
||||
buf.write(tmp);
|
||||
}
|
||||
//Save
|
||||
FileOutputStream outputStream = new FileOutputStream(new File(path));
|
||||
outputStream.write(buf.toByteArray());
|
||||
outputStream.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
//Calculate decryption key from track id
|
||||
public static byte[] getKey(String id) {
|
||||
String secret = "g4el58wc0zvf9na1";
|
||||
String key = "";
|
||||
try {
|
||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||
//md5.update(id.getBytes());
|
||||
byte[] md5id = md5.digest(id.getBytes());
|
||||
String idmd5 = bytesToHex(md5id).toLowerCase();
|
||||
|
||||
for(int i=0; i<16; i++) {
|
||||
int s0 = idmd5.charAt(i);
|
||||
int s1 = idmd5.charAt(i+16);
|
||||
int s2 = secret.charAt(i);
|
||||
key += (char)(s0^s1^s2);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
return key.getBytes();
|
||||
}
|
||||
|
||||
//Decrypt 2048b chunk
|
||||
public static byte[] decryptChunk(byte[] key, byte[] data) throws Exception{
|
||||
byte[] IV = {00, 01, 02, 03, 04, 05, 06, 07};
|
||||
SecretKeySpec Skey = new SecretKeySpec(key, "Blowfish");
|
||||
Cipher cipher = Cipher.getInstance("Blowfish/CBC/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, Skey, new javax.crypto.spec.IvParameterSpec(IV));
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
}
|
BIN
android/app/src/main/res/drawable-hdpi/ic_logo.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
android/app/src/main/res/drawable-hdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 196 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_play_arrow.png
Normal file
After Width: | Height: | Size: 390 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_skip_next.png
Normal file
After Width: | Height: | Size: 415 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_skip_previous.png
Normal file
After Width: | Height: | Size: 401 B |
BIN
android/app/src/main/res/drawable-hdpi/ic_stop.png
Normal file
After Width: | Height: | Size: 137 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_logo.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
android/app/src/main/res/drawable-mdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 144 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_play_arrow.png
Normal file
After Width: | Height: | Size: 220 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_skip_next.png
Normal file
After Width: | Height: | Size: 251 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_skip_previous.png
Normal file
After Width: | Height: | Size: 250 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_stop.png
Normal file
After Width: | Height: | Size: 122 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_logo.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 220 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_play_arrow.png
Normal file
After Width: | Height: | Size: 410 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_skip_next.png
Normal file
After Width: | Height: | Size: 456 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_skip_previous.png
Normal file
After Width: | Height: | Size: 440 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_stop.png
Normal file
After Width: | Height: | Size: 181 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_logo.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 302 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_play_arrow.png
Normal file
After Width: | Height: | Size: 683 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_skip_next.png
Normal file
After Width: | Height: | Size: 734 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_skip_previous.png
Normal file
After Width: | Height: | Size: 723 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_stop.png
Normal file
After Width: | Height: | Size: 228 B |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_logo.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 316 B |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_play_arrow.png
Normal file
After Width: | Height: | Size: 807 B |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_skip_next.png
Normal file
After Width: | Height: | Size: 983 B |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_skip_previous.png
Normal file
After Width: | Height: | Size: 953 B |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_stop.png
Normal file
After Width: | Height: | Size: 304 B |
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
18
android/app/src/main/res/values/styles.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">@android:color/white</item>
|
||||
</style>
|
||||
</resources>
|
7
android/app/src/profile/AndroidManifest.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="f.f.freezer">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
29
android/build.gradle
Normal file
|
@ -0,0 +1,29 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
4
android/gradle.properties
Normal file
|
@ -0,0 +1,4 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
#android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
#Sat May 23 20:49:20 CEST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
15
android/settings.gradle
Normal file
|
@ -0,0 +1,15 @@
|
|||
include ':app'
|
||||
|
||||
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
|
||||
|
||||
def plugins = new Properties()
|
||||
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
|
||||
if (pluginsFile.exists()) {
|
||||
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
|
||||
}
|
||||
|
||||
plugins.each { name, path ->
|
||||
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
|
||||
include ":$name"
|
||||
project(":$name").projectDir = pluginDirectory
|
||||
}
|
1
android/settings_aar.gradle
Normal file
|
@ -0,0 +1 @@
|
|||
include ':app'
|