目前状态栏控制大部分已生效,剩余暂停到播放的链路还有问题
This commit is contained in:
@@ -1,9 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<!-- ⭐ 网络(已有) -->
|
<!-- 权限不变 -->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<!-- ⭐ 后台播放 -->
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
@@ -15,6 +13,7 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:foregroundServiceType="mediaPlayback">
|
android:foregroundServiceType="mediaPlayback">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -24,38 +23,46 @@
|
|||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
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
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme" />
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
<!-- ⭐ 关键:AudioService 必须同时声明 MediaBrowserService 和 MEDIA_BUTTON -->
|
||||||
<service
|
<service
|
||||||
android:name="com.ryanheise.audioservice.AudioService"
|
android:name="com.ryanheise.audioservice.AudioService"
|
||||||
android:foregroundServiceType="mediaPlayback"
|
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
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</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>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// lib/services/audio_player_handler.dart
|
import 'package:flutter/material.dart';
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'playback_service.dart';
|
import 'playback_service.dart';
|
||||||
import 'audio_service.dart' as local_audio;
|
import 'audio_service.dart' as local_audio;
|
||||||
@@ -7,84 +7,155 @@ class AudioPlayerHandler extends BaseAudioHandler {
|
|||||||
final PlaybackService _playback = PlaybackService();
|
final PlaybackService _playback = PlaybackService();
|
||||||
late final local_audio.AudioService _localAudio;
|
late final local_audio.AudioService _localAudio;
|
||||||
|
|
||||||
MediaItem? _currentMediaItem;
|
|
||||||
|
|
||||||
AudioPlayerHandler() {
|
AudioPlayerHandler() {
|
||||||
_localAudio = local_audio.AudioService();
|
_localAudio = local_audio.AudioService();
|
||||||
|
|
||||||
// 1. 监听播放状态(只更新 playing)
|
|
||||||
_playback.player.stream.playing.listen((playing) {
|
_playback.player.stream.playing.listen((playing) {
|
||||||
playbackState.add(playbackState.value.copyWith(
|
_updatePlaybackState(playing: playing);
|
||||||
playing: playing,
|
_syncMediaItem();
|
||||||
));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. ⭐ 进度更新:跳过,因为当前版本不支持 position 参数
|
|
||||||
// 通知栏进度条不会动,但播放/暂停/切歌功能正常
|
|
||||||
// 如果后续需要,可以升级 audio_service 版本或改用其他方案
|
|
||||||
|
|
||||||
// 3. 监听播放完成(触发下一首)
|
|
||||||
_playback.player.stream.completed.listen((_) {
|
_playback.player.stream.completed.listen((_) {
|
||||||
|
debugPrint('🎵 [handler] playback completed, auto next');
|
||||||
_localAudio.next();
|
_localAudio.next();
|
||||||
|
_updatePlaybackState();
|
||||||
|
_syncMediaItem();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. 监听时长变化,更新 MediaItem
|
|
||||||
_playback.player.stream.duration.listen((duration) {
|
_playback.player.stream.duration.listen((duration) {
|
||||||
if (_currentMediaItem != null) {
|
final current = mediaItem.value;
|
||||||
_currentMediaItem = _currentMediaItem!.copyWith(duration: duration);
|
if (current != null && current.duration != duration) {
|
||||||
mediaItem.add(_currentMediaItem);
|
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({
|
void updateNotification({
|
||||||
required String id,
|
required String id,
|
||||||
required String title,
|
required String title,
|
||||||
required String artist,
|
required String artist,
|
||||||
}) {
|
}) {
|
||||||
_currentMediaItem = MediaItem(
|
mediaItem.add(MediaItem(
|
||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: title,
|
||||||
artist: artist,
|
artist: artist,
|
||||||
duration: _playback.player.state.duration,
|
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
|
@override
|
||||||
Future<void> play() async {
|
Future<void> play() async {
|
||||||
|
debugPrint('🎵 [audio_service] play() called');
|
||||||
_playback.resume();
|
_playback.resume();
|
||||||
|
_updatePlaybackState(playing: true);
|
||||||
|
_syncMediaItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> pause() async {
|
Future<void> pause() async {
|
||||||
|
debugPrint('🎵 [audio_service] pause() called');
|
||||||
_playback.pause();
|
_playback.pause();
|
||||||
|
_updatePlaybackState(playing: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
|
debugPrint('🎵 [audio_service] stop() called');
|
||||||
_playback.stop();
|
_playback.stop();
|
||||||
|
_updatePlaybackState(playing: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> seek(Duration position) async {
|
Future<void> seek(Duration position) async {
|
||||||
|
debugPrint('🎵 [audio_service] seek() called: $position');
|
||||||
_playback.seek(position);
|
_playback.seek(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> skipToNext() async {
|
Future<void> skipToNext() async {
|
||||||
|
debugPrint('🎵 [audio_service] skipToNext() called');
|
||||||
_localAudio.next();
|
_localAudio.next();
|
||||||
|
_updatePlaybackState();
|
||||||
|
_syncMediaItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> skipToPrevious() async {
|
Future<void> skipToPrevious() async {
|
||||||
|
debugPrint('🎵 [audio_service] skipToPrevious() called');
|
||||||
_localAudio.previous();
|
_localAudio.previous();
|
||||||
|
_updatePlaybackState();
|
||||||
|
_syncMediaItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> click([MediaButton button = MediaButton.media]) async {
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user