-
Notifications
You must be signed in to change notification settings - Fork 109
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
Android Auto #578
Changes from 31 commits
6a2238d
8da2b09
d168fb7
f877624
56d4f2a
05e5f72
efb14bd
0789c31
7a24508
aad1edf
99d01f2
b3656c8
ea10a21
4200036
430ee62
c85c07b
121a87e
8d2d825
0c34cfc
bcc22c0
c0355e2
72cc75a
b66d3ec
32d213f
a03199f
1febde3
4e70118
d8a11e2
294fa6f
be80fb6
ea6ec93
09df0c1
40e5bb5
c4baf85
d1aefd1
5797b16
a2cb78b
f37784d
895fe77
289f3b2
adf2a81
6d12f78
03470f9
ddc66e8
4274089
68d3b3d
1901816
ae315fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,3 +69,7 @@ android { | |
flutter { | ||
source '../..' | ||
} | ||
|
||
dependencies { | ||
implementation 'androidx.appcompat:appcompat:1.3.1' | ||
} |
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 | ||
} | ||
} |
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> |
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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<automotiveApp> | ||
<uses name="media"/> | ||
</automotiveApp> |
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'; | ||
|
@@ -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'; | ||
|
@@ -101,6 +104,20 @@ void main() async { | |
: "en_US"; | ||
await initializeDateFormatting(localeString, null); | ||
|
||
final documentsDirectory = await getApplicationDocumentsDirectory(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
} | ||
} | ||
|
@@ -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( | ||
|
@@ -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>( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -523,6 +523,24 @@ enum TabContentType { | |
return AppLocalizations.of(context)!.playlists; | ||
} | ||
} | ||
|
||
static TabContentType fromItemType(String itemType) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is used anymore? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should I be using something else? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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()); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this line used?
There was a problem hiding this comment.
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 ^^