diff --git a/.vscode/launch.json b/.vscode/launch.json
index 815c87652..e6f542a9b 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,33 +5,47 @@
"version": "0.2.0",
"configurations": [
{
- "name": "Flutter Profile",
+ "name": "dev",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
- "flutterMode": "profile"
+ "args": [
+ // TODO: remove this when intl_translation supports nnbd https://github.com/dart-lang/intl_translation/issues/134
+ "--no-sound-null-safety"
+ ],
+ "codeLens": {
+ "for": [
+ "run-file",
+ "debug-file",
+ "run-test",
+ "run-test-file",
+ "debug-test",
+ "debug-test-file",
+ ],
+ // TODO: remove this intl_translation supports nnbd
+ // See also https://github.com/Dart-Code/Dart-Code/issues/3356
+ "title": "${debugType} (--no-sound-null-safety)"
+ }
},
{
- "name": "Flutter Release",
+ "name": "profile",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
- "flutterMode": "release"
- },
- {
- "name": "Flutter Dev",
- "type": "dart",
- "request": "launch",
- "program": "lib/main.dart"
+ "flutterMode": "profile",
+ "args": [
+ "--no-sound-null-safety"
+ ]
},
{
- "name": "Flutter Dev (fast-start)",
+ "name": "release",
"type": "dart",
"request": "launch",
"program": "lib/main.dart",
+ "flutterMode": "release",
"args": [
- "--fast-start"
+ "--no-sound-null-safety"
]
- },
+ }
]
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 766d3e778..87a533825 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 1.0.4
+
+- Added playlists and artists
+- Added more selection actions
+- In deletion prompts added a preview of the arts of the content that is being deleted
+- Other fixes and cosmetic changes
+
## 1.0.3
- Fix issue with current song not updated on search route
@@ -13,7 +20,7 @@
was saved only for the current app session, when it was added, and was not restored.
- Optimized scrollbar and listviews
- Became media browser service, support for Android Auto
-- Fixed that album arts were not recahced (below Android 11)
+- Fixed that album arts were not recached (below Android 10)
- Refactored a lot of code and fixed a lot of other bugs
- Changed player backend (non-UX)
diff --git a/analysis_options.yaml b/analysis_options.yaml
index a075b40b9..6cf90d90b 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -51,7 +51,7 @@ linter:
# - avoid_setters_without_getters # not yet tested
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- # - avoid_slow_async_io # DISABLED_BY_ME
+ - avoid_slow_async_io
- avoid_type_to_string
- avoid_types_as_parameter_names
# - avoid_types_on_closure_parameters # conflicts with always_specify_types
@@ -187,7 +187,7 @@ linter:
- use_is_even_rather_than_modulo
- use_key_in_widget_constructors
- use_late_for_private_fields_and_variables
- # - use_named_constants # not yet tested
+ - use_named_constants
- use_raw_strings
- use_rethrow_when_possible
# - use_setters_to_change_properties # not yet tested
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
deleted file mode 100644
index b1d8733d3..000000000
--- a/android/app/proguard-rules.pro
+++ /dev/null
@@ -1,4 +0,0 @@
--keepclassmembers class com.nt4f04und.sweyer.player.Album { *; }
--keepclassmembers class com.nt4f04und.sweyer.player.Artist { *; }
--keepclassmembers class com.nt4f04und.sweyer.player.Playlist { *; }
--keepclassmembers class com.nt4f04und.sweyer.player.Song { *; }
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 236cf94b5..76f41ad19 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -69,14 +69,7 @@
-
-
-
-
+
diff --git a/android/app/src/main/java/com/nt4f04und/sweyer/Constants.java b/android/app/src/main/java/com/nt4f04und/sweyer/Constants.java
index 01184ad6d..94c41197e 100644
--- a/android/app/src/main/java/com/nt4f04und/sweyer/Constants.java
+++ b/android/app/src/main/java/com/nt4f04und/sweyer/Constants.java
@@ -8,7 +8,13 @@
public class Constants {
public static final String LogTag = "com.nt4f04und.sweyer";
- public static final class intents {
- public static final int PERMANENT_DELETION_REQUEST = 0;
+ public enum intents {
+ FAVORITE_REQUEST(0),
+ PERMANENT_DELETION_REQUEST(1);
+
+ public final int value;
+ intents(int value) {
+ this.value = value;
+ }
}
}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/nt4f04und/sweyer/MainActivity.java b/android/app/src/main/java/com/nt4f04und/sweyer/MainActivity.java
index 4f2151a7c..48c452729 100644
--- a/android/app/src/main/java/com/nt4f04und/sweyer/MainActivity.java
+++ b/android/app/src/main/java/com/nt4f04und/sweyer/MainActivity.java
@@ -9,16 +9,15 @@
import android.content.Intent;
import android.os.Bundle;
-import com.nt4f04und.sweyer.channels.GeneralChannel;
+import androidx.annotation.Nullable;
+
import com.nt4f04und.sweyer.channels.ContentChannel;
import com.nt4f04und.sweyer.handlers.GeneralHandler;
-
-import androidx.annotation.Nullable;
+import com.ryanheise.audioservice.AudioServicePlugin;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.BinaryMessenger;
-import com.ryanheise.audioservice.AudioServicePlugin;
public class MainActivity extends FlutterActivity {
@@ -27,8 +26,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneralHandler.init(getApplicationContext());
BinaryMessenger messenger = getBinaryMessenger();
- GeneralChannel.instance.init(messenger, this);
- ContentChannel.instance.init(messenger);
+ ContentChannel.instance.init(messenger, this);
}
BinaryMessenger getBinaryMessenger() {
@@ -43,15 +41,14 @@ public FlutterEngine provideFlutterEngine(Context context) {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == Constants.intents.PERMANENT_DELETION_REQUEST) {
- // Report deletion intent result on android R
- ContentChannel.instance.sendDeletionResult(resultCode == RESULT_OK);
+ if (requestCode == Constants.intents.PERMANENT_DELETION_REQUEST.value ||
+ requestCode == Constants.intents.FAVORITE_REQUEST.value) {
+ ContentChannel.instance.sendResultFromIntent(resultCode == RESULT_OK);
}
}
@Override
protected void onDestroy() {
- GeneralChannel.instance.destroy();
ContentChannel.instance.destroy();
super.onDestroy();
}
diff --git a/android/app/src/main/java/com/nt4f04und/sweyer/channels/ContentChannel.java b/android/app/src/main/java/com/nt4f04und/sweyer/channels/ContentChannel.java
index 71da75346..893b11125 100644
--- a/android/app/src/main/java/com/nt4f04und/sweyer/channels/ContentChannel.java
+++ b/android/app/src/main/java/com/nt4f04und/sweyer/channels/ContentChannel.java
@@ -5,8 +5,14 @@
package com.nt4f04und.sweyer.channels;
+import android.app.Activity;
+import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
@@ -14,26 +20,28 @@
import android.os.Handler;
import android.os.Looper;
import android.os.OperationCanceledException;
+import android.provider.MediaStore;
import android.util.Log;
import android.util.Size;
-import com.nt4f04und.sweyer.BuildConfig;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
import com.nt4f04und.sweyer.Constants;
import com.nt4f04und.sweyer.handlers.FetchHandler;
import com.nt4f04und.sweyer.handlers.GeneralHandler;
+import com.nt4f04und.sweyer.services.DeletionService;
import org.jetbrains.annotations.NotNull;
-import androidx.annotation.Nullable;
-
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import io.flutter.embedding.android.FlutterActivity;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
@@ -41,8 +49,9 @@
public enum ContentChannel {
instance;
- public void init(BinaryMessenger messenger) {
+ public void init(BinaryMessenger messenger, FlutterActivity activity) {
if (channel == null) {
+ this.activity = activity;
channel = new MethodChannel(messenger, "content_channel");
channel.setMethodCallHandler(this::onMethodCall);
}
@@ -50,26 +59,34 @@ public void init(BinaryMessenger messenger) {
public void destroy() {
channel = null;
+ activity = null;
}
@Nullable
MethodChannel channel;
@Nullable
private MethodChannel.Result result;
+ @Nullable
+ public Activity activity;
private final HashMap loadingSignals = new HashMap<>();
+ private static final String UNEXPECTED_ERROR = "UNEXPECTED_ERROR";
+ private static final String INTENT_SENDER_ERROR = "INTENT_SENDER_ERROR";
+ private static final String IO_ERROR = "IO_ERROR";
+ private static final String SDK_ERROR = "SDK_ERROR";
+ private static final String PLAYLIST_NOT_EXISTS_ERROR = "PLAYLIST_NOT_EXISTS_ERROR";
+
public void onMethodCall(MethodCall call, @NotNull MethodChannel.Result result) {
try {
switch (call.method) {
case "loadAlbumArt": {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
- ContentResolver contentResolver = GeneralHandler.getAppContext().getContentResolver();
+ ContentResolver contentResolver = getContentResolver();
CancellationSignal signal = new CancellationSignal();
String id = call.argument("id");
loadingSignals.put(id, signal);
- executor.execute(() -> {
+ Executors.newSingleThreadExecutor().execute(() -> {
byte[] bytes = null;
try {
Bitmap bitmap = contentResolver.loadThumbnail(
@@ -83,9 +100,8 @@ public void onMethodCall(MethodCall call, @NotNull MethodChannel.Result result)
} catch (OperationCanceledException ex) {
// do nothing
} catch (IOException e) {
- Log.e(Constants.LogTag,"loadThumbnail failed");
- e.printStackTrace();
- } finally {
+ result.error(IO_ERROR, "loadThumbnail failed", Log.getStackTraceString(e));
+ } finally {
byte[] finalBytes = bytes;
handler.post(() -> {
loadingSignals.remove(id);
@@ -94,7 +110,7 @@ public void onMethodCall(MethodCall call, @NotNull MethodChannel.Result result)
}
});
} else {
- result.error("0", "This method requires Android Q and above", "");
+ result.error(SDK_ERROR, "This method requires Android 29 and above", "");
}
break;
}
@@ -108,63 +124,344 @@ public void onMethodCall(MethodCall call, @NotNull MethodChannel.Result result)
break;
}
case "fixAlbumArt": {
- ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
- executor.execute(() -> {
- Object rawId = call.argument("id");
- Long id;
- if (rawId instanceof Long) {
- id = (Long) rawId;
- } else if (rawId instanceof Integer) {
- id = Long.valueOf((Integer) rawId);
- } else {
- throw new IllegalArgumentException();
- }
- Uri songCover = Uri.parse("content://media/external/audio/albumart");
- Uri uriSongCover = ContentUris.withAppendedId(songCover, id);
- ContentResolver res = GeneralChannel.instance.activity.getContentResolver();
+ Executors.newSingleThreadExecutor().execute(() -> {
try {
- InputStream is = res.openInputStream(uriSongCover);
- is.close();
- } catch (Exception ex) {
- // do nothing
- ex.printStackTrace();
+ Long id = GeneralHandler.getLong(call.argument("id"));
+ Uri songCover = Uri.parse("content://media/external/audio/albumart");
+ Uri uriSongCover = ContentUris.withAppendedId(songCover, id);
+ ContentResolver res = getContentResolver();
+ try {
+ InputStream is = res.openInputStream(uriSongCover);
+ is.close();
+ } catch (Exception ex) {
+ // do nothing
+ }
+ handler.post(() -> {
+ result.success(null);
+ });
+ } catch (Exception e) {
+ handler.post(() -> {
+ result.error(UNEXPECTED_ERROR, e.getMessage(), Log.getStackTraceString(e));
+ });
}
- handler.post(() -> {
- result.success(null);
- });
});
break;
}
case "retrieveSongs": {
- ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
- executor.execute(() -> {
- ArrayList> res = FetchHandler.retrieveSongs();
- handler.post(() -> {
- result.success(res);
- });
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ ArrayList> res = FetchHandler.retrieveSongs();
+ handler.post(() -> {
+ result.success(res);
+ });
+ } catch (Exception e) {
+ handler.post(() -> {
+ result.error(UNEXPECTED_ERROR, e.getMessage(), Log.getStackTraceString(e));
+ });
+ }
});
break;
}
case "retrieveAlbums": {
- ExecutorService executor = Executors.newSingleThreadExecutor();
Handler handler = new Handler(Looper.getMainLooper());
- executor.execute(() -> {
- ArrayList> res = FetchHandler.retrieveAlbums();
- handler.post(() -> {
- result.success(res);
- });
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ ArrayList> res = FetchHandler.retrieveAlbums();
+ handler.post(() -> {
+ result.success(res);
+ });
+ } catch (Exception e) {
+ handler.post(() -> {
+ result.error(UNEXPECTED_ERROR, e.getMessage(), Log.getStackTraceString(e));
+ });
+ }
});
break;
}
- case "deleteSongs": {
- FetchHandler.deleteSongs(call.argument("songs"));
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- // Save the result to report to the flutter code later in `sendDeletionResult`
+ case "retrievePlaylists": {
+ Handler handler = new Handler(Looper.getMainLooper());
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ ArrayList> res = FetchHandler.retrievePlaylists();
+ handler.post(() -> {
+ result.success(res);
+ });
+ } catch (Exception e) {
+ handler.post(() -> {
+ result.error(UNEXPECTED_ERROR, e.getMessage(), Log.getStackTraceString(e));
+ });
+ }
+ });
+ break;
+ }
+ case "retrieveArtists": {
+ Handler handler = new Handler(Looper.getMainLooper());
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ ArrayList> res = FetchHandler.retrieveArtists();
+ handler.post(() -> {
+ result.success(res);
+ });
+ } catch (Exception e) {
+ handler.post(() -> {
+ result.error(UNEXPECTED_ERROR, e.getMessage(), Log.getStackTraceString(e));
+ });
+ }
+ });
+ break;
+ }
+ case "retrieveGenres": {
+ Handler handler = new Handler(Looper.getMainLooper());
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ ArrayList> res = FetchHandler.retrieveGenres();
+ handler.post(() -> {
+ result.success(res);
+ });
+ } catch (Exception e) {
+ handler.post(() -> {
+ result.error(UNEXPECTED_ERROR, e.getMessage(), Log.getStackTraceString(e));
+ });
+ }
+ });
+ break;
+ }
+ case "setSongsFavorite": {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
this.result = result;
+ Boolean value = call.argument("value");
+ ArrayList> songs = call.argument("songs");
+ ArrayList uris = new ArrayList<>();
+ for (HashMap song : songs) {
+ Long id = GeneralHandler.getLong(song.get("id"));
+ uris.add(ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id));
+ }
+ PendingIntent pendingIntent = MediaStore.createFavoriteRequest(
+ GeneralHandler.getAppContext().getContentResolver(),
+ uris,
+ value
+ );
+ startIntentSenderForResult(pendingIntent, Constants.intents.FAVORITE_REQUEST);
+ } else {
+ result.error(SDK_ERROR, "This method requires Android 30 and above", "");
+ }
+ break;
+ }
+ case "deleteSongs": {
+ Intent serviceIntent = new Intent(GeneralHandler.getAppContext(), DeletionService.class);
+ serviceIntent.putExtra("songs", (ArrayList>) call.argument("songs"));
+ GeneralHandler.getAppContext().startService(serviceIntent);
+ // Save the result to report to the flutter code later in `sendResultFromIntent(`
+ this.result = result;
+ break;
+ }
+ case "createPlaylist": {
+ Handler handler = new Handler(Looper.getMainLooper());
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ String name = call.argument("name");
+ ContentResolver resolver = getContentResolver();
+ ContentValues values = new ContentValues(1);
+ values.put(MediaStore.Audio.Playlists.NAME, name);
+
+ Uri uri = resolver.insert(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, values);
+ if (uri != null) {
+ resolver.notifyChange(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null);
+ }
+ handler.post(() -> {
+ result.success(null);
+ });
+ } catch (Exception e) {
+ handler.post(() -> {
+ result.error(UNEXPECTED_ERROR, e.getMessage(), Log.getStackTraceString(e));
+ });
+
+ }
+ });
+ break;
+ }
+ case "renamePlaylist": {
+ Handler handler = new Handler(Looper.getMainLooper());
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ Long id = GeneralHandler.getLong(call.argument("id"));
+ ContentResolver resolver = getContentResolver();
+ if (playlistExists(id)) {
+ String name = call.argument("name");
+ ContentValues values = new ContentValues(1);
+ values.put(MediaStore.Audio.Playlists.NAME, name);
+
+ int rows = resolver.update(
+ ContentUris.withAppendedId(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, id),
+ values,
+ null,
+ null
+ );
+ if (rows > 0) {
+ resolver.notifyChange(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null);
+ }
+ handler.post(() -> {
+ result.success(null);
+ });
+ } else {
+ handler.post(() -> {
+ result.error(PLAYLIST_NOT_EXISTS_ERROR, "No playlists with such id", id);
+ });
+ }
+ } catch (Exception e) {
+ handler.post(() -> {
+ result.error(UNEXPECTED_ERROR, e.getMessage(), Log.getStackTraceString(e));
+ });
+ }
+ });
+ break;
+ }
+ case "removePlaylists": {
+ Handler handler = new Handler(Looper.getMainLooper());
+ Executors.newSingleThreadExecutor().execute(() -> {
+ try {
+ ArrayList