Skip to content

Commit c55e232

Browse files
authored
Merge pull request #4 from mobitouchOS/v0.2.0-beta.4
Adds asset artwork support and notification options
2 parents cb51a84 + d34494c commit c55e232

22 files changed

Lines changed: 600 additions & 54 deletions

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
## 0.2.0-beta.4 - 2026-02-27
2+
3+
### Added
4+
5+
- `asset:///` artwork URI support — bundled Flutter assets are automatically extracted to the cache directory for use in system notifications, Android Auto, and CarPlay.
6+
- `androidNotificationOngoing` option in `MtAudioPlayerConfig` to control whether the Android notification is dismissible when paused (defaults to `false`).
7+
- `onTaskRemoved` handler to clean up playback when the app is swiped away on Android.
8+
9+
### Changed
10+
11+
- `MtArtwork` widget now supports `asset://` and `file://` URI schemes in addition to network URIs, with resolution-aware image caching (`cacheWidth`/`cacheHeight`) and `gaplessPlayback`.
12+
13+
### Fixed
14+
15+
- Skip previous/next controls are now hidden in both system notifications and the `MtTrackSkipButton` widget when the queue contains a single item.
16+
117
## 0.2.0-beta.3 - 2026-02-16
218

319
### Changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@
66
![Stability](https://img.shields.io/badge/stability-beta-orange)
77
![License](https://img.shields.io/badge/license-MIT-green)
88

9-
A stream-based audio module for Flutter. Provides background playback, system notifications, queue management, and first-class **Android Auto** & **Apple CarPlay** support -- all behind a single facade class and zero external state management dependencies.
10-
11-
This package reduces implementation overhead when combining packages such as `just_audio` and `audio_service`. It provides a simple wrapper API that captures our long-standing Flutter audio expertise in a single dependency.
9+
A stream-based audio module for Flutter that delivers **background playback**, **system notifications**, **queue management**, and **first-class Android Auto & Apple CarPlay support** — all behind a single facade API and **zero external state management dependencies**.
10+
11+
Built on top of `just_audio` + `audio_service`, mt_audio reduces the glue code and implementation overhead, packaging our production Flutter audio know-how into one dependency.
12+
13+
### Use cases
14+
- Podcast & talk apps (episode queues, resume, skip)
15+
- Online radio / live streams (background playback, simple controls)
16+
- Audiobooks (long-form playback, chapters as queue, progress)
17+
- Learning & courses (lesson playlists, quick navigation)
18+
- Any app that needs reliable background audio with minimal setup
1219

1320
<p align="center">
1421
<img src="assets/home.png" width="220" alt="Now Playing" />

android/build.gradle.kts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
group = "com.mobitouchos.mt_audio"
2+
version = "1.0"
3+
4+
buildscript {
5+
val agpVersion = if (rootProject.extra.has("agp_version")) {
6+
rootProject.extra["agp_version"] as String
7+
} else {
8+
"8.11.1"
9+
}
10+
11+
val kotlinVersion = if (rootProject.extra.has("kotlin_version")) {
12+
rootProject.extra["kotlin_version"] as String
13+
} else {
14+
"2.2.20"
15+
}
16+
17+
repositories {
18+
google()
19+
mavenCentral()
20+
}
21+
22+
dependencies {
23+
classpath("com.android.tools.build:gradle:$agpVersion")
24+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
25+
}
26+
}
27+
28+
allprojects {
29+
repositories {
30+
google()
31+
mavenCentral()
32+
}
33+
}
34+
35+
plugins {
36+
id("com.android.library")
37+
id("kotlin-android")
38+
}
39+
40+
android {
41+
namespace = "com.mobitouchos.mt_audio"
42+
compileSdk = 35
43+
44+
compileOptions {
45+
sourceCompatibility = JavaVersion.VERSION_17
46+
targetCompatibility = JavaVersion.VERSION_17
47+
}
48+
49+
kotlinOptions {
50+
jvmTarget = JavaVersion.VERSION_17.toString()
51+
}
52+
53+
defaultConfig {
54+
minSdk = 21
55+
}
56+
}

android/settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = "mt_audio"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2+
package="com.mobitouchos.mt_audio">
3+
4+
<application>
5+
<!-- Exported so Android Auto (separate process) can resolve artwork URIs.
6+
Only serves non-sensitive artwork from the sandboxed cache directory. -->
7+
<provider
8+
android:name="com.mobitouchos.mt_audio.MtAudioArtworkProvider"
9+
android:authorities="${applicationId}.mt_audio.artwork"
10+
android:exported="true"
11+
android:grantUriPermissions="false" />
12+
</application>
13+
</manifest>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.mobitouchos.mt_audio
2+
3+
import android.content.ContentProvider
4+
import android.content.ContentValues
5+
import android.database.Cursor
6+
import android.net.Uri
7+
import android.os.ParcelFileDescriptor
8+
import android.webkit.MimeTypeMap
9+
import java.io.File
10+
11+
/**
12+
* Read-only [ContentProvider] that serves cached artwork files to Android Auto.
13+
*
14+
* Android Auto runs in a separate process and cannot access `file://` URIs in the
15+
* app's private cache. This provider exposes the cached artwork via `content://`
16+
* URIs that Android Auto can resolve.
17+
*
18+
* URI format: `content://{applicationId}.mt_audio.artwork/{asset_key}`
19+
* Maps to: `{cacheDir}/mt_audio_assets/{asset_key}`
20+
*/
21+
class MtAudioArtworkProvider : ContentProvider() {
22+
23+
override fun onCreate(): Boolean = true
24+
25+
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
26+
if (mode != "r") return null
27+
28+
val assetKey = uri.path?.removePrefix("/") ?: return null
29+
val context = context ?: return null
30+
val file = File(context.cacheDir, "mt_audio_assets/$assetKey")
31+
32+
// Prevent path traversal by ensuring the resolved path stays within the cache.
33+
// The trailing separator stops prefixes like `${cacheBase}_evil/...` slipping past.
34+
val cacheBase = File(context.cacheDir, "mt_audio_assets").canonicalPath + File.separator
35+
if (!file.canonicalPath.startsWith(cacheBase)) return null
36+
37+
if (!file.exists()) return null
38+
39+
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
40+
}
41+
42+
override fun getType(uri: Uri): String? {
43+
val path = uri.path ?: return null
44+
val ext = MimeTypeMap.getFileExtensionFromUrl(path)
45+
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
46+
?: "application/octet-stream"
47+
}
48+
49+
// This is a read-only provider — write operations are not supported.
50+
51+
override fun query(
52+
uri: Uri,
53+
projection: Array<out String>?,
54+
selection: String?,
55+
selectionArgs: Array<out String>?,
56+
sortOrder: String?,
57+
): Cursor? = null
58+
59+
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
60+
61+
override fun update(
62+
uri: Uri,
63+
values: ContentValues?,
64+
selection: String?,
65+
selectionArgs: Array<out String>?,
66+
): Int = 0
67+
68+
override fun delete(
69+
uri: Uri,
70+
selection: String?,
71+
selectionArgs: Array<out String>?,
72+
): Int = 0
73+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.mobitouchos.mt_audio
2+
3+
import io.flutter.embedding.engine.plugins.FlutterPlugin
4+
import io.flutter.plugin.common.MethodCall
5+
import io.flutter.plugin.common.MethodChannel
6+
7+
/**
8+
* Minimal Flutter plugin that exposes the artwork [ContentProvider] authority
9+
* to the Dart side, allowing [MtAssetResolver] to construct `content://` URIs
10+
* at runtime without consumer configuration.
11+
*/
12+
class MtAudioPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
13+
14+
private lateinit var channel: MethodChannel
15+
private lateinit var applicationId: String
16+
17+
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
18+
applicationId = binding.applicationContext.packageName
19+
channel = MethodChannel(binding.binaryMessenger, "com.mobitouchos.mt_audio/artwork")
20+
channel.setMethodCallHandler(this)
21+
}
22+
23+
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
24+
when (call.method) {
25+
"getContentProviderAuthority" -> {
26+
result.success("$applicationId.mt_audio.artwork")
27+
}
28+
else -> result.notImplemented()
29+
}
30+
}
31+
32+
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
33+
channel.setMethodCallHandler(null)
34+
}
35+
}
6.72 MB
Loading

example/lib/sample_data/sample_data.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ final sampleTracks = [
6767
),
6868
duration: const Duration(minutes: 5, seconds: 24),
6969
),
70+
MtAudioItem(
71+
id: 'track-asset',
72+
uri: Uri.parse(
73+
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3',
74+
),
75+
title: 'SoundHelix Song 4 (Asset Art)',
76+
artist: 'T. Schürger',
77+
album: 'SoundHelix',
78+
artworkUri: Uri.parse('asset:///assets/images/sample_cover.jpg'),
79+
duration: const Duration(minutes: 7, seconds: 23),
80+
),
7081
];
7182

7283
/// Live radio streams from Public Domain Radio.

example/pubspec.lock

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,10 @@ packages:
252252
dependency: transitive
253253
description:
254254
name: matcher
255-
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
255+
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
256256
url: "https://pub.dev"
257257
source: hosted
258-
version: "0.12.18"
258+
version: "0.12.19"
259259
material_color_utilities:
260260
dependency: transitive
261261
description:
@@ -268,17 +268,17 @@ packages:
268268
dependency: transitive
269269
description:
270270
name: meta
271-
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
271+
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
272272
url: "https://pub.dev"
273273
source: hosted
274-
version: "1.17.0"
274+
version: "1.18.0"
275275
mt_audio:
276276
dependency: "direct main"
277277
description:
278278
path: ".."
279279
relative: true
280280
source: path
281-
version: "0.2.0-beta.2"
281+
version: "0.2.0-beta.3"
282282
mt_carplay:
283283
dependency: transitive
284284
description:
@@ -488,10 +488,10 @@ packages:
488488
dependency: transitive
489489
description:
490490
name: test_api
491-
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
491+
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
492492
url: "https://pub.dev"
493493
source: hosted
494-
version: "0.7.9"
494+
version: "0.7.11"
495495
typed_data:
496496
dependency: transitive
497497
description:

0 commit comments

Comments
 (0)