Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android Auto #578

Merged
merged 48 commits into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6a2238d
Move _includeItemTypes() to TabContentType
puff Jan 17, 2024
8da2b09
Basic Android Auto support
puff Jan 17, 2024
d168fb7
Try using downloaded parent before going online in Android Auto
puff Jan 17, 2024
f877624
Add shuffle to Android Auto
puff Jan 18, 2024
56d4f2a
Use correct shuffle status for Android Auto
puff Jan 18, 2024
05e5f72
Artists now start instant mix in Android Auto
puff Jan 20, 2024
efb14bd
synchronize internal queue and Android media queue
Chaphasilor Jan 21, 2024
0789c31
fix adding to next up when shuffle is active
Chaphasilor Jan 21, 2024
7a24508
Add sorting to Android Auto and make offline sort by name case-insens…
puff Jan 22, 2024
aad1edf
Remove unused parameter in Android Auto getBaseItems
puff Jan 22, 2024
99d01f2
Only sort downloaded items if not playing them in Android Auto
puff Jan 22, 2024
b3656c8
Use a ContentProvider to resolve artwork for Android Auto
puff Jan 23, 2024
ea10a21
Basic voice search for songs in Android Auto
puff Feb 4, 2024
4200036
Merge branch 'redesign' into pr/puff/578
Chaphasilor Feb 6, 2024
430ee62
enable showing search results for android auto voice search
Chaphasilor Feb 6, 2024
c85c07b
rename use parent instead of category
Chaphasilor Feb 6, 2024
121a87e
implement starting instant mixes from search results
Chaphasilor Feb 6, 2024
8d2d825
improved playBySearch using extras
Chaphasilor Feb 7, 2024
0c34cfc
Download images in MediaItemContentProvider
puff Feb 16, 2024
bcc22c0
replaced manual passing and parsing of media IDs with serializable Me…
Chaphasilor Feb 16, 2024
c0355e2
use item ID instead of parent ID
Chaphasilor Feb 16, 2024
72cc75a
Remove redundant TabContentType resolves in Android Auto code
puff Feb 17, 2024
b66d3ec
Merge branch 'redesign' into pr/puff/578
Chaphasilor Feb 17, 2024
32d213f
shuffle all for empty search query ("play some music")
Chaphasilor Feb 17, 2024
a03199f
increase timeout for http requests
Chaphasilor Feb 19, 2024
1febde3
Re-implement MediaItemContentProvider in Kotlin
puff Mar 8, 2024
4e70118
improve search using metadata where possible
Chaphasilor Mar 9, 2024
d8a11e2
prefer playlists for voice search
Chaphasilor Mar 9, 2024
294fa6f
Merge branch 'redesign' into pr/puff/578
Chaphasilor Mar 9, 2024
be80fb6
make voice search artist match case insensitive
Chaphasilor Mar 9, 2024
ea6ec93
Use correct item type id in Android Auto
puff Mar 10, 2024
09df0c1
only resolve songs from downloads in online mode, add artist playback…
Chaphasilor Mar 10, 2024
40e5bb5
also use grid layout in Android Auto if enabled
Chaphasilor Mar 10, 2024
c4baf85
improve offline mode for Android Auto
Chaphasilor Mar 10, 2024
d1aefd1
merge branch 'redesign'
Chaphasilor Mar 23, 2024
5797b16
disable artwork preloading in media session due to issues with custom…
Chaphasilor Mar 23, 2024
a2cb78b
Merge branch 'redesign' into pr/puff/578
Chaphasilor May 24, 2024
f37784d
added offline support for voice commands
Chaphasilor May 26, 2024
895fe77
explicitly handle request for recent ("For you") root
Chaphasilor May 26, 2024
289f3b2
fix syntax error
Chaphasilor May 26, 2024
adf2a81
improvements based on review
Chaphasilor May 27, 2024
6d12f78
fix errors, properly join file paths
Chaphasilor May 27, 2024
03470f9
fix issues with content provider custom artwork URIs
Chaphasilor May 27, 2024
ddc66e8
fix album name always being shown
Chaphasilor May 27, 2024
4274089
search multiple item types
Chaphasilor May 30, 2024
68d3b3d
added like button to media notification, added settings for customizi…
Chaphasilor May 30, 2024
1901816
Merge branch 'redesign' into pr/puff/578
Chaphasilor May 30, 2024
ae315fb
Merge branch 'redesign' into pr/puff/578
Chaphasilor Jun 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,7 @@ android {
flutter {
source '../..'
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1'
}
28 changes: 26 additions & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,16 @@
<category android:name="android.intent.category.APP_MUSIC"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service android:name="com.ryanheise.audioservice.AudioService" android:foregroundServiceType="mediaPlayback"
android:exported="true">
<provider
android:name=".MediaItemContentProvider"
android:authorities="com.unicornsonlsd.finamp"
android:exported="true" />
<service android:name="com.ryanheise.audioservice.AudioService" android:foregroundServiceType="mediaPlayback" android:exported="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
Expand All @@ -43,8 +50,25 @@
</intent-filter>
</receiver>

<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />

<!-- 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" />
</application>

<uses-feature
android:name="android.hardware.type.automotive"
android:required="true" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.landscape"
android:required="false" />
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package com.unicornsonlsd.finamp
import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.unicornsonlsd.finamp

import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.util.LruCache
import java.io.File
import java.io.FileOutputStream
import java.net.URL

class MediaItemContentProvider : ContentProvider() {

private lateinit var memoryCache : LruCache<String, ByteArray>

override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
if (uri.fragment != null) {
// we store the original scheme://host in fragment since it should be unused
val origin = Uri.parse(uri.fragment)
val fixedUri = uri.buildUpon().fragment(null).scheme(origin.scheme).authority(origin.authority).toString()

// check if we already cached the image
val bytes = memoryCache.get(fixedUri)
if (bytes != null) {
return openPipeHelper(uri, "application/octet-stream", null, bytes) {output, _, _, _, b ->
FileOutputStream(output.fileDescriptor).write(b)
}
}

val response = URL(fixedUri).readBytes()
memoryCache.put(fixedUri, response)
return openPipeHelper(uri, "application/octet-stream", null, response) {output, _, _, _, b ->
FileOutputStream(output.fileDescriptor).write(b)
}
}

// this means it's a local image (downloaded or placeholder art)
return ParcelFileDescriptor.open(File(uri.path!!), ParcelFileDescriptor.MODE_READ_ONLY)
}

override fun onCreate(): Boolean {
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()

// Use 1/8th of the available memory for this memory cache.
val cacheSize = maxMemory / 8
memoryCache = object : LruCache<String, ByteArray>(cacheSize) {

override fun sizeOf(key: String, value: ByteArray): Int {
// The cache size will be measured in kilobytes rather than
// number of items.
return value.size / 1024
}
}

return true
}

override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
return null
}

override fun getType(uri: Uri): String? {
return null
}

override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}

override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions android/app/src/main/res/drawable-mdpi/baseline_shuffle_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<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:fillColor="@android:color/white"
android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z"/>
</vector>
11 changes: 11 additions & 0 deletions android/app/src/main/res/drawable-mdpi/baseline_shuffle_on_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<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:fillColor="@android:color/white"
android:pathData="M21,1L3,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,3c0,-1.1 -0.9,-2 -2,-2zM10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z"
android:fillType="evenOdd"/>
</vector>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions android/app/src/main/res/xml/automotive_app_desc.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<automotiveApp>
<uses name="media"/>
</automotiveApp>
Binary file added images/album_white.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion lib/components/MusicScreen/music_screen_tab_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class _MusicScreenTabViewState extends State<MusicScreenTabView>

final _jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();
final _isarDownloader = GetIt.instance<DownloadsService>();
final _finampUserHelper = GetIt.instance<FinampUserHelper>();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line used?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not anymore, at least according to the linter ^^

StreamSubscription<void>? _refreshStream;

ScrollController? controller;
Expand All @@ -73,7 +74,8 @@ class _MusicScreenTabViewState extends State<MusicScreenTabView>
settings.tabSortOrder[widget.tabContentType]?.toString() ??
SortOrder.ascending.toString();
final newItems = await _jellyfinApiHelper.getItems(
parentItem: widget.view,
parentItem: widget.view ??
_finampUserHelper.currentUser?.currentView,
includeItemTypes: widget.tabContentType.itemType.idString,

// If we're on the songs tab, sort by "Album,SortName". This is what the
Expand Down
2 changes: 1 addition & 1 deletion lib/components/PlayerScreen/queue_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ class _CurrentTrackState extends State<CurrentTrack> {
width: (screenSize.width -
2 * horizontalPadding -
albumImageSize) *
(playbackPosition!.inMilliseconds /
((playbackPosition?.inMilliseconds ?? 0) /
(mediaState?.mediaItem
?.duration ??
const Duration(
Expand Down
22 changes: 22 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';

import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
Expand All @@ -9,6 +11,7 @@ import 'package:finamp/screens/interaction_settings_screen.dart';
import 'package:finamp/screens/login_screen.dart';
import 'package:finamp/screens/playback_history_screen.dart';
import 'package:finamp/screens/queue_restore_screen.dart';
import 'package:finamp/services/android_auto_helper.dart';
import 'package:finamp/services/downloads_service.dart';
import 'package:finamp/services/downloads_service_backend.dart';
import 'package:finamp/services/finamp_settings_helper.dart';
Expand Down Expand Up @@ -101,6 +104,20 @@ void main() async {
: "en_US";
await initializeDateFormatting(localeString, null);

final documentsDirectory = await getApplicationDocumentsDirectory();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't put this in documents. Put it in application support. Also, this whole block should probably be in one of the submethods where we've still got the errorApp wrapper.

final albumImageFile = File('${documentsDirectory.absolute.path}/images/album_white.png');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think constructing files from full pathstrings is considered good practice? I'm not sure how much that matters though.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/images/album_white.png could be moved to a constant. Not really sure what else to do here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I basically mean using the path_helper to combine the segments. Like I said, I'm not sure how much it matters though.

if (!(await albumImageFile.exists())) {
final albumImageBytes = await rootBundle.load("images/album_white.png");
final albumBuffer = albumImageBytes.buffer;
await albumImageFile.create(recursive: true);
await albumImageFile.writeAsBytes(
albumBuffer.asUint8List(
albumImageBytes.offsetInBytes,
albumImageBytes.lengthInBytes,
),
);
}

runApp(const Finamp());
}
}
Expand Down Expand Up @@ -223,6 +240,8 @@ Future<void> _setupPlaybackServices() async {
final session = await AudioSession.instance;
session.configure(const AudioSessionConfiguration.music());

GetIt.instance.registerSingleton<AndroidAutoHelper>(AndroidAutoHelper());

final audioHandler = await AudioService.init(
builder: () => MusicPlayerBackgroundTask(),
config: AudioServiceConfig(
Expand All @@ -231,6 +250,9 @@ Future<void> _setupPlaybackServices() async {
androidNotificationChannelName: "Playback",
androidNotificationIcon: "mipmap/white",
androidNotificationChannelId: "com.unicornsonlsd.finamp.audio",
androidBrowsableRootExtras: <String, dynamic>{
"android.media.browse.SEARCH_SUPPORTED" : true, // support showing search button on Android Auto as well as alternative search results on the player screen after voice search
}
),
);
// GetIt.instance.registerSingletonAsync<AudioHandler>(
Expand Down
63 changes: 63 additions & 0 deletions lib/models/finamp_models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,24 @@ enum TabContentType {
return AppLocalizations.of(context)!.playlists;
}
}

static TabContentType fromItemType(String itemType) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is used anymore?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this was based off some other code that got factored out completely. This seems fine, although I'm wondering if using BaseItemDto types would make more sense than tab types for all this? I guess it is mapping to basically music screen tabs though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that is the case, it really is closer to the tab types (so the browsable collections containing the items)

switch (itemType) {
case "Audio":
return TabContentType.songs;
case "MusicAlbum":
return TabContentType.albums;
case "MusicArtist":
return TabContentType.artists;
case "MusicGenre":
return TabContentType.genres;
case "Playlist":
return TabContentType.playlists;
default:
throw const FormatException("Unsupported itemType");
}
}

}

@HiveType(typeId: 39)
Expand Down Expand Up @@ -1661,3 +1679,48 @@ enum TranscodeDownloadsSetting {
@HiveField(2)
ask;
}

@HiveType(typeId: 67)
enum MediaItemParentType {
@HiveField(0)
collection,
@HiveField(1)
rootCollection,
@HiveField(2)
instantMix,
}

@JsonSerializable()
@HiveType(typeId: 68)
class MediaItemId {

MediaItemId({
required this.contentType,
required this.parentType,
this.itemId,
this.parentId,
});

@HiveField(0)
TabContentType contentType;

@HiveField(1)
MediaItemParentType parentType;

@HiveField(2)
String? itemId;

@HiveField(3)
String? parentId;

factory MediaItemId.fromJson(Map<String, dynamic> json) =>
_$MediaItemIdFromJson(json);

Map<String, dynamic> toJson() => _$MediaItemIdToJson(this);

@override
String toString() {
return jsonEncode(toJson());
}

}