目前状态栏控制大部分已生效,剩余暂停到播放的链路还有问题

This commit is contained in:
2026-08-21 00:03:04 +08:00
parent bb8e4caf48
commit 8ecca4ac01
2 changed files with 118 additions and 40 deletions
+23 -16
View File
@@ -1,9 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ⭐ 网络(已有) -->
<!-- 权限不变 -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- ⭐ 后台播放 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
@@ -15,6 +13,7 @@
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"
android:foregroundServiceType="mediaPlayback">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -24,34 +23,42 @@
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"
/>
android:resource="@style/NormalTheme" />
<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 -->
<!-- ⭐ 关键:AudioService 必须同时声明 MediaBrowserService 和 MEDIA_BUTTON -->
<service
android:name="com.ryanheise.audioservice.AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
android:exported="true">
<intent-filter>
<!-- 媒体浏览器服务(MediaButtonReceiver 需要) -->
<action android:name="android.media.browse.MediaBrowserService" />
<!-- 媒体按钮(通知栏、蓝牙、耳机按键) -->
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<!-- ⭐ MediaButtonReceiver 接收系统媒体按钮事件 -->
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
+93 -22
View File
@@ -1,4 +1,4 @@
// lib/services/audio_player_handler.dart
import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart';
import 'playback_service.dart';
import 'audio_service.dart' as local_audio;
@@ -7,84 +7,155 @@ class AudioPlayerHandler extends BaseAudioHandler {
final PlaybackService _playback = PlaybackService();
late final local_audio.AudioService _localAudio;
MediaItem? _currentMediaItem;
AudioPlayerHandler() {
_localAudio = local_audio.AudioService();
// 1. 监听播放状态(只更新 playing)
_playback.player.stream.playing.listen((playing) {
playbackState.add(playbackState.value.copyWith(
playing: playing,
));
_updatePlaybackState(playing: playing);
_syncMediaItem();
});
// 2. ⭐ 进度更新:跳过,因为当前版本不支持 position 参数
// 通知栏进度条不会动,但播放/暂停/切歌功能正常
// 如果后续需要,可以升级 audio_service 版本或改用其他方案
// 3. 监听播放完成(触发下一首)
_playback.player.stream.completed.listen((_) {
debugPrint('🎵 [handler] playback completed, auto next');
_localAudio.next();
_updatePlaybackState();
_syncMediaItem();
});
// 4. 监听时长变化,更新 MediaItem
_playback.player.stream.duration.listen((duration) {
if (_currentMediaItem != null) {
_currentMediaItem = _currentMediaItem!.copyWith(duration: duration);
mediaItem.add(_currentMediaItem);
final current = mediaItem.value;
if (current != null && current.duration != duration) {
mediaItem.add(current.copyWith(duration: duration));
}
});
}
/// 外部调用更新通知栏
void _syncMediaItem() {
final song = _localAudio.currentSong;
debugPrint(
'🎵 [handler] _syncMediaItem: currentSong = ${song?.title ?? "null"}');
if (song != null) {
mediaItem.add(MediaItem(
id: song.id,
title: song.title,
artist: song.artist,
duration: _playback.player.state.duration,
));
}
}
void updateNotification({
required String id,
required String title,
required String artist,
}) {
_currentMediaItem = MediaItem(
mediaItem.add(MediaItem(
id: id,
title: title,
artist: artist,
duration: _playback.player.state.duration,
);
mediaItem.add(_currentMediaItem);
));
_updatePlaybackState();
}
void _updatePlaybackState({bool? playing}) {
final isPlaying = playing ?? _playback.player.state.playing;
debugPrint('🎵 _updatePlaybackState: isPlaying=$isPlaying');
final controls = [
MediaControl.skipToPrevious,
if (isPlaying) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
];
debugPrint('🎵 controls: ${controls.map((c) => c.action).join(', ')}');
playbackState.add(PlaybackState(
controls: controls,
processingState: AudioProcessingState.ready,
playing: isPlaying,
androidCompactActionIndices: const [0, 1, 2],
updateTime: DateTime.now(),
));
}
// ---- AudioHandler 接口实现 ----
@override
Future<void> play() async {
debugPrint('🎵 [audio_service] play() called');
_playback.resume();
_updatePlaybackState(playing: true);
_syncMediaItem();
}
@override
Future<void> pause() async {
debugPrint('🎵 [audio_service] pause() called');
_playback.pause();
_updatePlaybackState(playing: false);
}
@override
Future<void> stop() async {
debugPrint('🎵 [audio_service] stop() called');
_playback.stop();
_updatePlaybackState(playing: false);
}
@override
Future<void> seek(Duration position) async {
debugPrint('🎵 [audio_service] seek() called: $position');
_playback.seek(position);
}
@override
Future<void> skipToNext() async {
debugPrint('🎵 [audio_service] skipToNext() called');
_localAudio.next();
_updatePlaybackState();
_syncMediaItem();
}
@override
Future<void> skipToPrevious() async {
debugPrint('🎵 [audio_service] skipToPrevious() called');
_localAudio.previous();
_updatePlaybackState();
_syncMediaItem();
}
@override
Future<void> click([MediaButton button = MediaButton.media]) async {
// 默认行为:打开 App
debugPrint(
'🎵 [audio_service] click(): ${button.name} (index: ${button.index})');
final name = button.name.toLowerCase();
if (name.contains('play') || name.contains('media')) {
debugPrint('🎵 → play() via click');
await play();
return;
}
if (name.contains('pause')) {
debugPrint('🎵 → pause() via click');
await pause();
return;
}
if (name.contains('next') || name.contains('skip_next')) {
debugPrint('🎵 → skipToNext() via click');
await skipToNext();
return;
}
if (name.contains('previous') || name.contains('skip_previous')) {
debugPrint('🎵 → skipToPrevious() via click');
await skipToPrevious();
return;
}
debugPrint('🎵 → fallback toggle');
if (_playback.player.state.playing) {
await pause();
} else {
await play();
}
}
}