From 00a8ac89aa9e764c7e3f9afa57dccdc9b78ced4b Mon Sep 17 00:00:00 2001 From: hacker1024 Date: Wed, 18 Dec 2019 11:55:20 +1100 Subject: [PATCH 01/30] Add connected boolean --- lib/audio_service.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/audio_service.dart b/lib/audio_service.dart index e758689c..01bf5ef5 100644 --- a/lib/audio_service.dart +++ b/lib/audio_service.dart @@ -431,6 +431,7 @@ class AudioService { } }); await _channel.invokeMethod("connect"); + _connected = true; } /// Disconnects your UI from the service. @@ -439,8 +440,13 @@ class AudioService { static Future disconnect() async { _channel.setMethodCallHandler(null); await _channel.invokeMethod("disconnect"); + _connected = false; } + /// True if the UI is connected. + static bool get connected => _connected; + static bool _connected = false; + /// True if the background audio task is running. static Future get running async { return await _channel.invokeMethod("isRunning"); From f815de3ad410697928eca6f47bdc4fcd9c95f238 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Wed, 18 Dec 2019 12:21:51 +1100 Subject: [PATCH 02/30] Add MediaItem.copyWith --- lib/audio_service.dart | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/audio_service.dart b/lib/audio_service.dart index e758689c..47ede4f9 100644 --- a/lib/audio_service.dart +++ b/lib/audio_service.dart @@ -258,6 +258,35 @@ class MediaItem { this.rating, }); + MediaItem copyWith({ + String id, + String album, + String title, + String artist, + String genre, + int duration, + String artUri, + bool playable, + String displayTitle, + String displaySubtitle, + String displayDescription, + Rating rating, + }) => + MediaItem( + id: id ?? this.id, + album: album ?? this.album, + title: title ?? this.title, + artist: artist ?? this.artist, + genre: genre ?? this.genre, + duration: duration ?? this.duration, + artUri: artUri ?? this.artUri, + playable: playable ?? this.playable, + displayTitle: displayTitle ?? this.displayTitle, + displaySubtitle: displaySubtitle ?? this.displaySubtitle, + displayDescription: displayDescription ?? this.displayDescription, + rating: rating ?? this.rating, + ); + @override int get hashCode => id.hashCode; From 26f56e139100c4971499326cde3ef64e67d21d9a Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Wed, 18 Dec 2019 13:03:23 +1100 Subject: [PATCH 03/30] Add systemActions option to setState --- .../audioservice/AudioServicePlugin.java | 15 ++++++++++----- example/lib/main.dart | 1 + ios/Classes/AudioServicePlugin.m | 10 +++++----- lib/audio_service.dart | 14 ++++++++++++-- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java index 0f6f952e..df1f673a 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java @@ -586,11 +586,12 @@ public void onMethodCall(MethodCall call, Result result) { case "setState": List args = (List)call.arguments; List> rawControls = (List>)args.get(0); - int playbackState = (Integer)args.get(1); - long position = getLong(args.get(2)); - float speed = (float)((double)((Double)args.get(3))); - long updateTimeSinceEpoch = args.get(4) == null ? System.currentTimeMillis() : getLong(args.get(4)); - List compactActionIndexList = (List)args.get(5); + List rawSystemActions = (List)args.get(1); + int playbackState = (Integer)args.get(2); + long position = getLong(args.get(3)); + float speed = (float)((double)((Double)args.get(4))); + long updateTimeSinceEpoch = args.get(5) == null ? System.currentTimeMillis() : getLong(args.get(5)); + List compactActionIndexList = (List)args.get(6); // On the flutter side, we represent the update time relative to the epoch. // On the native side, we must represent the update time relative to the boot time. @@ -604,6 +605,10 @@ public void onMethodCall(MethodCall call, Result result) { actionBits |= actionCode; actions.add(AudioService.instance.action(resource, (String)rawControl.get("label"), actionCode)); } + for (Integer rawSystemAction : rawSystemActions) { + int actionCode = 1 << rawSystemAction; + actionBits |= actionCode; + } int[] compactActionIndices = null; if (compactActionIndexList != null) { compactActionIndices = new int[Math.min(AudioService.MAX_COMPACT_ACTIONS, compactActionIndexList.size())]; diff --git a/example/lib/main.dart b/example/lib/main.dart index 2b5b28ee..15a881b9 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -375,6 +375,7 @@ class AudioPlayerTask extends BackgroundAudioTask { void _setState({@required BasicPlaybackState state, int position = 0}) { AudioServiceBackground.setState( controls: getControls(state), + systemActions: [MediaAction.seekTo], basicState: state, position: position, ); diff --git a/ios/Classes/AudioServicePlugin.m b/ios/Classes/AudioServicePlugin.m index 500dde4e..8ff5c175 100644 --- a/ios/Classes/AudioServicePlugin.m +++ b/ios/Classes/AudioServicePlugin.m @@ -219,15 +219,15 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { result(@YES); } else if ([@"setState" isEqualToString:call.method]) { long long msSinceEpoch; - if (call.arguments[4] != [NSNull null]) { - msSinceEpoch = [call.arguments[4] longLongValue]; + if (call.arguments[5] != [NSNull null]) { + msSinceEpoch = [call.arguments[5] longLongValue]; } else { msSinceEpoch = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0); } - state = call.arguments[1]; - position = call.arguments[2]; + state = call.arguments[2]; + position = call.arguments[3]; updateTime = [NSNumber numberWithLongLong: msSinceEpoch]; - speed = call.arguments[3]; + speed = call.arguments[4]; [channel invokeMethod:@"onPlaybackStateChanged" arguments:@[ // state state, diff --git a/lib/audio_service.dart b/lib/audio_service.dart index 47ede4f9..7599235e 100644 --- a/lib/audio_service.dart +++ b/lib/audio_service.dart @@ -812,8 +812,15 @@ class AudioServiceBackground { } /// Sets the current playback state and dictates which media actions can be - /// controlled by clients and which media controls should be visible in the - /// notification, Wear OS and Android Auto. + /// controlled by clients and which media controls and actions should be + /// enabled in the notification, Wear OS and Android Auto. Each control + /// listed in [controls] will appear as a button in the notification and its + /// action will also be made available to all clients such as Wear OS and + /// Android Auto. Any additional actions that you would like to enable for + /// clients that do not correspond to a button can be listed in + /// [systemActions]. For example, include [MediaAction.seekTo] in + /// [systemActions] and the system will provide a seek bar in the + /// notification. /// /// All clients will be notified so they can update their display. /// @@ -826,6 +833,7 @@ class AudioServiceBackground { /// The playback [speed] is given as a double where 1.0 means normal speed. static Future setState({ @required List controls, + List systemActions = const [], @required BasicPlaybackState basicState, int position = 0, double speed = 1.0, @@ -846,8 +854,10 @@ class AudioServiceBackground { 'action': control.action.index, }) .toList(); + final rawSystemActions = systemActions.map((action) => action.index).toList(); await _backgroundChannel.invokeMethod('setState', [ rawControls, + rawSystemActions, basicState.index, position, speed, From fc3b46499e7ea7331d00febf1768bc2d45df3ec8 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Wed, 18 Dec 2019 13:50:46 +1100 Subject: [PATCH 04/30] Upgrade sdk and rxdart dependencies --- example/.flutter-plugins-dependencies | 1 + example/lib/main.dart | 4 ++-- example/pubspec.yaml | 2 +- pubspec.yaml | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 example/.flutter-plugins-dependencies diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies new file mode 100644 index 00000000..766e13a4 --- /dev/null +++ b/example/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"audio_service","dependencies":["flutter_isolate"]},{"name":"audioplayer","dependencies":[]},{"name":"flutter_isolate","dependencies":[]},{"name":"flutter_tts","dependencies":[]},{"name":"path_provider","dependencies":[]}]} \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 15a881b9..416a0bc2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -199,9 +199,9 @@ class _MyAppState extends State with WidgetsBindingObserver { Widget positionIndicator(MediaItem mediaItem, PlaybackState state) { return StreamBuilder( - stream: Observable.combineLatest2( + stream: Rx.combineLatest2( _dragPositionSubject.stream, - Observable.periodic(Duration(milliseconds: 200), + Stream.periodic(Duration(milliseconds: 200), (_) => state.currentPosition.toDouble()), (dragPosition, statePosition) => dragPosition ?? statePosition), builder: (context, snapshot) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9fb753a4..9dc93494 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: path_provider: ^1.4.0 audioplayer: ^0.5.2 flutter_tts: ^0.7.0 - rxdart: ^0.22.6 + rxdart: ^0.23.1 audio_service: path: ../ diff --git a/pubspec.yaml b/pubspec.yaml index c63ee50b..4826f49a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,10 +4,10 @@ version: 0.5.4 homepage: https://github.com/ryanheise/audio_service environment: - sdk: ">=2.1.0 <3.0.0" + sdk: '>=2.6.0 <3.0.0' dependencies: - rxdart: ^0.22.6 + rxdart: ^0.23.1 flutter_isolate: ^1.0.0+10 flutter: sdk: flutter From a46fa9cfcee5dabdeae282240e54f6f4f2f7a03f Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Wed, 18 Dec 2019 21:47:44 +1100 Subject: [PATCH 05/30] Combine state streams. --- example/lib/main.dart | 133 +++++++++++++++---------------- ios/Classes/AudioServicePlugin.m | 6 +- lib/audio_service.dart | 6 +- 3 files changed, 74 insertions(+), 71 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 416a0bc2..308869eb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -84,73 +84,64 @@ class _MyAppState extends State with WidgetsBindingObserver { title: const Text('Audio Service Demo'), ), body: new Center( - child: StreamBuilder>( - stream: AudioService.queueStream, + child: StreamBuilder( + stream: Rx.combineLatest3, MediaItem, PlaybackState, ScreenState>(AudioService.queueStream, AudioService.currentMediaItemStream, AudioService.playbackStateStream, (queue, mediaItem, playbackState) => ScreenState(queue, mediaItem, playbackState)), builder: (context, snapshot) { - final queue = snapshot.data; - return StreamBuilder( - stream: AudioService.currentMediaItemStream, - builder: (context, snapshot) { - final mediaItem = snapshot.data; - return StreamBuilder( - stream: AudioService.playbackStateStream, - builder: (context, snapshot) { - final state = snapshot.data; - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (queue != null && queue.isNotEmpty) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.skip_previous), - iconSize: 64.0, - onPressed: mediaItem == queue.first - ? null - : AudioService.skipToPrevious, - ), - IconButton( - icon: Icon(Icons.skip_next), - iconSize: 64.0, - onPressed: mediaItem == queue.last - ? null - : AudioService.skipToNext, - ), - ], - ), - if (mediaItem?.title != null) Text(mediaItem.title), - if (state?.basicState == - BasicPlaybackState.connecting) ...[ - stopButton(), - Text("Connecting..."), - ] else if (state?.basicState == - BasicPlaybackState.skippingToNext) ...[ - stopButton(), - Text("Skipping..."), - ] else if (state?.basicState == - BasicPlaybackState.skippingToPrevious) ...[ - stopButton(), - Text("Skipping..."), - ] else if (state?.basicState == - BasicPlaybackState.playing) ...[ - pauseButton(), - stopButton(), - positionIndicator(mediaItem, state), - ] else if (state?.basicState == - BasicPlaybackState.paused) ...[ - playButton(), - stopButton(), - positionIndicator(mediaItem, state), - ] else ...[ - audioPlayerButton(), - textToSpeechButton(), - ], - ], - ); - }, - ); - }, + final screenState = snapshot.data; + final queue = screenState?.queue; + final mediaItem = screenState?.mediaItem; + final state = screenState?.playbackState; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (queue != null && queue.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.skip_previous), + iconSize: 64.0, + onPressed: mediaItem == queue.first + ? null + : AudioService.skipToPrevious, + ), + IconButton( + icon: Icon(Icons.skip_next), + iconSize: 64.0, + onPressed: mediaItem == queue.last + ? null + : AudioService.skipToNext, + ), + ], + ), + if (mediaItem?.title != null) Text(mediaItem.title), + if (state?.basicState == + BasicPlaybackState.connecting) ...[ + stopButton(), + Text("Connecting..."), + ] else if (state?.basicState == + BasicPlaybackState.skippingToNext) ...[ + stopButton(), + Text("Skipping..."), + ] else if (state?.basicState == + BasicPlaybackState.skippingToPrevious) ...[ + stopButton(), + Text("Skipping..."), + ] else if (state?.basicState == + BasicPlaybackState.playing) ...[ + pauseButton(), + stopButton(), + positionIndicator(mediaItem, state), + ] else if (state?.basicState == + BasicPlaybackState.paused) ...[ + playButton(), + stopButton(), + positionIndicator(mediaItem, state), + ] else ...[ + audioPlayerButton(), + textToSpeechButton(), + ], + ], ); }, ), @@ -230,6 +221,14 @@ class _MyAppState extends State with WidgetsBindingObserver { } } +class ScreenState { + final List queue; + final MediaItem mediaItem; + final PlaybackState playbackState; + + ScreenState(this.queue, this.mediaItem, this.playbackState); +} + void _audioPlayerTaskEntrypoint() async { AudioServiceBackground.run(() => AudioPlayerTask()); } @@ -284,9 +283,9 @@ class AudioPlayerTask extends BackgroundAudioTask { } }); + _setState(state: BasicPlaybackState.connecting, position: 0); AudioServiceBackground.setQueue(_queue); AudioServiceBackground.setMediaItem(mediaItem); - _setState(state: BasicPlaybackState.connecting, position: 0); onPlay(); await _completer.future; playerStateSubscription.cancel(); diff --git a/ios/Classes/AudioServicePlugin.m b/ios/Classes/AudioServicePlugin.m index 8ff5c175..2a3e6968 100644 --- a/ios/Classes/AudioServicePlugin.m +++ b/ios/Classes/AudioServicePlugin.m @@ -18,6 +18,7 @@ @implementation AudioServicePlugin static BOOL _running = NO; static FlutterResult startResult = nil; static MPRemoteCommandCenter *commandCenter = nil; +static NSArray *queue = nil; static NSMutableDictionary *mediaItem = nil; static NSNumber *state = nil; static NSNumber *position = nil; @@ -78,6 +79,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { // update time since epoch updateTime ]]; + [channel invokeMethod:@"onMediaChanged" arguments:@[mediaItem ? mediaItem : [NSNull null]]]; + [channel invokeMethod:@"onQueueChanged" arguments:@[queue ? queue : [NSNull null]]]; result(nil); } else if ([@"disconnect" isEqualToString:call.method]) { @@ -243,7 +246,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self updateNowPlayingInfo]; result(@(YES)); } else if ([@"setQueue" isEqualToString:call.method]) { - [channel invokeMethod:@"onQueueChanged" arguments:@[call.arguments]]; + queue = call.arguments; + [channel invokeMethod:@"onQueueChanged" arguments:@[queue]]; result(@YES); } else if ([@"setMediaItem" isEqualToString:call.method]) { mediaItem = call.arguments; diff --git a/lib/audio_service.dart b/lib/audio_service.dart index d21c484b..dc8747d6 100644 --- a/lib/audio_service.dart +++ b/lib/audio_service.dart @@ -439,12 +439,12 @@ class AudioService { _playbackStateSubject.add(_playbackState); break; case 'onMediaChanged': - _currentMediaItem = _raw2mediaItem(call.arguments[0]); + _currentMediaItem = call.arguments[0] != null ? _raw2mediaItem(call.arguments[0]) : null; _currentMediaItemSubject.add(_currentMediaItem); break; case 'onQueueChanged': - final List args = List.from(call.arguments[0]); - _queue = args.map(_raw2mediaItem).toList(); + final List args = call.arguments[0] != null ? List.from(call.arguments[0]) : null; + _queue = args?.map(_raw2mediaItem)?.toList(); _queueSubject.add(_queue); break; case 'onStopped': From cfbca033102ab26088384b1ad4b3c906647ed76e Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Wed, 18 Dec 2019 22:56:43 +1100 Subject: [PATCH 06/30] Notify all streams on connect --- .../java/com/ryanheise/audioservice/AudioServicePlugin.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java index df1f673a..b3944b53 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java @@ -127,9 +127,8 @@ public void onConnected() { PlaybackStateCompat state = mediaController.getPlaybackState(); controllerCallback.onPlaybackStateChanged(state); MediaMetadataCompat metadata = mediaController.getMetadata(); - if (metadata != null) - controllerCallback.onMetadataChanged(metadata); controllerCallback.onQueueChanged(mediaController.getQueue()); + controllerCallback.onMetadataChanged(metadata); synchronized (this) { if (playPending) { @@ -672,6 +671,7 @@ private static List> mediaItems2raw(List } private static List> queue2raw(List queue) { + if (queue == null) return null; List> rawQueue = new ArrayList>(); for (MediaSessionCompat.QueueItem queueItem : queue) { MediaDescriptionCompat description = queueItem.getDescription(); @@ -734,6 +734,7 @@ private static HashMap rating2raw(RatingCompat rating) { } private static Map mediaMetadata2raw(MediaMetadataCompat mediaMetadata) { + if (mediaMetadata == null) return null; MediaDescriptionCompat description = mediaMetadata.getDescription(); Map raw = new HashMap(); raw.put("id", description.getMediaId()); From dbb5ba8e7424f69f12d65c96143d21c656997685 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Wed, 18 Dec 2019 22:57:48 +1100 Subject: [PATCH 07/30] Format --- example/lib/main.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 308869eb..2383667a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -85,7 +85,13 @@ class _MyAppState extends State with WidgetsBindingObserver { ), body: new Center( child: StreamBuilder( - stream: Rx.combineLatest3, MediaItem, PlaybackState, ScreenState>(AudioService.queueStream, AudioService.currentMediaItemStream, AudioService.playbackStateStream, (queue, mediaItem, playbackState) => ScreenState(queue, mediaItem, playbackState)), + stream: Rx.combineLatest3, MediaItem, + PlaybackState, ScreenState>( + AudioService.queueStream, + AudioService.currentMediaItemStream, + AudioService.playbackStateStream, + (queue, mediaItem, playbackState) => + ScreenState(queue, mediaItem, playbackState)), builder: (context, snapshot) { final screenState = snapshot.data; final queue = screenState?.queue; @@ -115,8 +121,7 @@ class _MyAppState extends State with WidgetsBindingObserver { ], ), if (mediaItem?.title != null) Text(mediaItem.title), - if (state?.basicState == - BasicPlaybackState.connecting) ...[ + if (state?.basicState == BasicPlaybackState.connecting) ...[ stopButton(), Text("Connecting..."), ] else if (state?.basicState == @@ -329,8 +334,8 @@ class AudioPlayerTask extends BackgroundAudioTask { if (!hasPrevious) return; if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing) { _audioPlayer.stop(); - _queueIndex--; - _position = null; + _queueIndex--; + _position = null; } _setState(state: BasicPlaybackState.skippingToPrevious, position: 0); AudioServiceBackground.setMediaItem(mediaItem); From c6e5dba20529fd28f1e3ad2d2d0391e255cafe90 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 26 Dec 2019 02:54:38 +1100 Subject: [PATCH 08/30] Version 0.5.5 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80f72dbc..f352b0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.5 + +* Bump sdk version to 2.6.0. + ## 0.5.4 * Fix Android memory leak. diff --git a/pubspec.yaml b/pubspec.yaml index 4826f49a..22f71010 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: audio_service description: Flutter plugin to play audio in the background while the screen is off. -version: 0.5.4 +version: 0.5.5 homepage: https://github.com/ryanheise/audio_service environment: From a04ef2a8782ba697996d86cc3cef0ed3306476a8 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 26 Dec 2019 03:36:04 +1100 Subject: [PATCH 09/30] Support Flutter 1.12 --- README.md | 17 +++++++++++++++++ android/build.gradle | 2 +- android/gradle.properties | 3 +++ example/android/app/build.gradle | 1 + example/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/lib/main.dart | 7 +++++++ pubspec.yaml | 2 +- 8 files changed, 32 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b966d762..bfac6e1c 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,23 @@ drawable-xxxhdpi You can use [Android Asset Studio](https://romannurik.github.io/AndroidAssetStudio/) to generate these different subdirectories for any standard material design icon. +Starting from Flutter 1.12, you will also need to disable the `shrinkResources` setting in your `android/app/build.gradle` file, otherwise your icon resources will be removed during the build: + +``` +android { + compileSdkVersion 28 + + ... + + buildTypes { + release { + signingConfig ... + shrinkResources false // ADD THIS LINE + } + } +} +``` + *NOTE: Most Flutter plugins today were written before Flutter added support for running Dart code in a headless environment (without an Android Activity present). As such, a number of plugins assume there is an activity and run into a `NullPointerException`. Fortunately, it is very easy for plugin authors to update their plugins remove this assumption. If you encounter such a plugin, see the bottom of this README file for a sample bug report you can send to the relevant plugin author.* ## iOS setup diff --git a/android/build.gradle b/android/build.gradle index f293e0b0..7aed7f72 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } diff --git a/android/gradle.properties b/android/gradle.properties index 8bd86f68..38c8d454 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1 +1,4 @@ org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 65925567..edf99ef5 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -45,6 +45,7 @@ android { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug + shrinkResources false } } } diff --git a/example/android/build.gradle b/example/android/build.gradle index 541636cc..e0d7ae2c 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 2819f022..63ab3ae0 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/example/lib/main.dart b/example/lib/main.dart index 2383667a..2fd6395e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -8,6 +8,13 @@ import 'package:audio_service/audio_service.dart'; import 'package:flutter_tts/flutter_tts.dart'; import 'package:rxdart/rxdart.dart'; +// NOTE: Since Flutter 1.12, the audioplayer plugin will crash your release +// builds. As an alternative, I am developing a new audio player plugin called +// just_audio which I will switch to once the iOS implementation catches up to +// the Android implementation. In the meantime, or if you really want to use +// audioplayer, you may need to fork it and update compileSdkVersion = 28 and +// update the gradle wrapper to the latest. + MediaControl playControl = MediaControl( androidIcon: 'drawable/ic_action_play_arrow', label: 'Play', diff --git a/pubspec.yaml b/pubspec.yaml index 22f71010..1d23fde7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ environment: dependencies: rxdart: ^0.23.1 - flutter_isolate: ^1.0.0+10 + flutter_isolate: ^1.0.0+11 flutter: sdk: flutter From 15b04fc749b70db52ec8cfa84b0ec50031884d46 Mon Sep 17 00:00:00 2001 From: ryanheise Date: Fri, 27 Dec 2019 18:48:51 +1100 Subject: [PATCH 10/30] Update bug template --- .github/ISSUE_TEMPLATE/bug_report.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8f7623cf..bd0b7af4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,8 +10,17 @@ assignees: '' **Describe the bug** A clear and concise description of what the bug is. +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Error messages** + +``` +If applicable, copy & paste error message here. +``` + **Minimal reproduction project** -If the example project doesn't itself exhibit the bug, please fork this project and modify the example to reproduce the bug. Provide the link to your repository here. +If the example project exhibits the bug, please mention that here, otherwise fork this project and modify the example to reproduce the bug. Provide the link to your repository here. **To Reproduce** Steps to reproduce the behavior: @@ -23,12 +32,10 @@ Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Runtime Environment (please complete the following information):** +**Runtime Environment (please complete the following information if relevant):** - Device: [e.g. Samsung Galaxy Note 8] - Android version: [e.g. 8.0.0] + - iOS version: [e.g. 13.3] **Flutter SDK version** ``` From aa88e07ac89d37ebd1a8375415b360942ae41c01 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Sat, 28 Dec 2019 12:53:08 +1100 Subject: [PATCH 11/30] Version 0.5.6 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f352b0b9..2022a248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.6 + +* Support Flutter 1.12. + ## 0.5.5 * Bump sdk version to 2.6.0. diff --git a/pubspec.yaml b/pubspec.yaml index 1d23fde7..331f6d81 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: audio_service description: Flutter plugin to play audio in the background while the screen is off. -version: 0.5.5 +version: 0.5.6 homepage: https://github.com/ryanheise/audio_service environment: From 910e21e30fddf8ec9c6f088950fcc084010ed187 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Sat, 28 Dec 2019 12:56:26 +1100 Subject: [PATCH 12/30] Destroy FlutterNativeView --- .../java/com/ryanheise/audioservice/AudioServicePlugin.java | 1 + lib/audio_service.dart | 3 +++ 2 files changed, 4 insertions(+) diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java index b3944b53..c513b99a 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java @@ -622,6 +622,7 @@ public void onMethodCall(MethodCall call, Result result) { if (silenceAudioTrack != null) silenceAudioTrack.release(); if (clientHandler != null) clientHandler.invokeMethod("onStopped"); + backgroundFlutterView.destroy(); backgroundFlutterView = null; backgroundHandler = null; result.success(true); diff --git a/lib/audio_service.dart b/lib/audio_service.dart index dc8747d6..dd8ceac3 100644 --- a/lib/audio_service.dart +++ b/lib/audio_service.dart @@ -813,6 +813,9 @@ class AudioServiceBackground { await _backgroundChannel.invokeMethod('ready'); await task.onStart(); await _backgroundChannel.invokeMethod('stopped'); + if (Platform.isIOS) { + FlutterIsolate.current.kill(); + } _backgroundChannel.setMethodCallHandler(null); _state = _noneState; } From 1f699e51cdc94bb0f789bc4478c94430ec4283c8 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Sat, 28 Dec 2019 13:24:10 +1100 Subject: [PATCH 13/30] Version 0.5.7 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2022a248..a7d3bd1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.7 + +* Destroy isolates after use. + ## 0.5.6 * Support Flutter 1.12. diff --git a/pubspec.yaml b/pubspec.yaml index 331f6d81..90829211 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: audio_service description: Flutter plugin to play audio in the background while the screen is off. -version: 0.5.6 +version: 0.5.7 homepage: https://github.com/ryanheise/audio_service environment: From cf385e6426711433e40865392a5eb153228ccebd Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Sun, 29 Dec 2019 18:19:54 +1100 Subject: [PATCH 14/30] Update gradle version --- .../android/gradle/wrapper/gradle-wrapper.properties | 2 +- example/pubspec.yaml | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 63ab3ae0..296b146b 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9dc93494..5f12e6be 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,13 +1,9 @@ name: audio_service_example description: Demonstrates how to use the audio_service plugin. +publish_to: 'none' -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# Read more about versioning at semver.org. -version: 1.0.0+1 +environment: + sdk: ">=2.1.0 <3.0.0" dependencies: flutter: From a3e0623328b32811456f48724a14e816958a85a4 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Sun, 29 Dec 2019 19:35:00 +1100 Subject: [PATCH 15/30] Update sdk version --- example/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5f12e6be..01e0b8ca 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the audio_service plugin. publish_to: 'none' environment: - sdk: ">=2.1.0 <3.0.0" + sdk: '>=2.6.0 <3.0.0' dependencies: flutter: From 40ded026d57a5bba5279524eb51679c1ca1fa7ee Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 2 Jan 2020 13:17:17 +1100 Subject: [PATCH 16/30] Update example to use just_audio --- example/.flutter-plugins-dependencies | 2 +- example/.gitignore | 5 +- .../res/drawable-hdpi/ic_action_skip_next.png | Bin 0 -> 251 bytes .../drawable-hdpi/ic_action_skip_previous.png | Bin 0 -> 257 bytes .../res/drawable-mdpi/ic_action_skip_next.png | Bin 0 -> 156 bytes .../drawable-mdpi/ic_action_skip_previous.png | Bin 0 -> 166 bytes .../drawable-xhdpi/ic_action_skip_next.png | Bin 0 -> 260 bytes .../ic_action_skip_previous.png | Bin 0 -> 265 bytes .../drawable-xxhdpi/ic_action_skip_next.png | Bin 0 -> 460 bytes .../ic_action_skip_previous.png | Bin 0 -> 450 bytes .../drawable-xxxhdpi/ic_action_skip_next.png | Bin 0 -> 509 bytes .../ic_action_skip_previous.png | Bin 0 -> 522 bytes example/lib/main.dart | 232 +++++++++++------- example/pubspec.yaml | 2 +- 14 files changed, 145 insertions(+), 96 deletions(-) create mode 100644 example/android/app/src/main/res/drawable-hdpi/ic_action_skip_next.png create mode 100644 example/android/app/src/main/res/drawable-hdpi/ic_action_skip_previous.png create mode 100644 example/android/app/src/main/res/drawable-mdpi/ic_action_skip_next.png create mode 100644 example/android/app/src/main/res/drawable-mdpi/ic_action_skip_previous.png create mode 100644 example/android/app/src/main/res/drawable-xhdpi/ic_action_skip_next.png create mode 100644 example/android/app/src/main/res/drawable-xhdpi/ic_action_skip_previous.png create mode 100644 example/android/app/src/main/res/drawable-xxhdpi/ic_action_skip_next.png create mode 100644 example/android/app/src/main/res/drawable-xxhdpi/ic_action_skip_previous.png create mode 100644 example/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_next.png create mode 100644 example/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_previous.png diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 766e13a4..9aea328f 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"audio_service","dependencies":["flutter_isolate"]},{"name":"audioplayer","dependencies":[]},{"name":"flutter_isolate","dependencies":[]},{"name":"flutter_tts","dependencies":[]},{"name":"path_provider","dependencies":[]}]} \ No newline at end of file +{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"audio_service","dependencies":["flutter_isolate"]},{"name":"flutter_isolate","dependencies":[]},{"name":"flutter_tts","dependencies":[]},{"name":"just_audio","dependencies":["path_provider"]},{"name":"path_provider","dependencies":[]}]} \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore index 47e0b4d6..66cc6440 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -1,6 +1,5 @@ # Miscellaneous *.class -*.lock *.log *.pyc *.swp @@ -23,10 +22,11 @@ **/doc/api/ .dart_tool/ .flutter-plugins +.flutter-plugins-dependencies .packages .pub-cache/ .pub/ -build/ +/build/ # Android related **/android/**/gradle-wrapper.jar @@ -60,6 +60,7 @@ build/ **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* diff --git a/example/android/app/src/main/res/drawable-hdpi/ic_action_skip_next.png b/example/android/app/src/main/res/drawable-hdpi/ic_action_skip_next.png new file mode 100644 index 0000000000000000000000000000000000000000..28a76a3e4e4cb49dfe8c4aa7485c4b80c7f3711d GIT binary patch literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB4tcsbhD5Z!y|&S-DN&^TW4vcV zvs-g;_YTtv&e+CoZzmJa3lAQtL2*y%=tV?#dDI( zQDrOlE3u1Yo}6D{a9BWGu;odeZ%1C>gTe~DWM4fv=e6! literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/drawable-hdpi/ic_action_skip_previous.png b/example/android/app/src/main/res/drawable-hdpi/ic_action_skip_previous.png new file mode 100644 index 0000000000000000000000000000000000000000..d60ff0848b3204f290427d933131a2d91160ba49 GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBPI$UFhD5Z!y|y=t$x)=?;^vNo zKblEX-3+Ea5v^$r5eo}bOi*0HEwOrnfY8^K!7mOexVV&*D|~O()R&o&nAWjKqw8GO z>@~+EKim2gF^EC~>-ZW|TwjnGavtml*{x zf3L7+TFG-|tMv|f^@MeNQV&?~t0w{JN22yS8&2e$J~c}saVqzUHO=bB&xb5kFxo9~ zn(I!YUh{J0wW;5q{s{T}c-FM5-<98rUG{DQI^m!1{<+_yPRN)oz5(^jumgXpakjBrq$6l&e#rRkEEWW2_lDW+OOloS96}HoK zJlmBY6!78r43R535^TJb?vmtO z@i-wNp<~HZ)-E0%o~Z$)AzK?d8ygQcH#RP0_LRMQ(QfC_XYc$VK_ z%41_|tB_&i=jBdlNONrA$&fl};BYymizj2+#R-fzU5{!=upVJ#;EB+P57OQ74QMfg Mr>mdKI;Vst0F>b~&j0`b literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/drawable-xhdpi/ic_action_skip_next.png b/example/android/app/src/main/res/drawable-xhdpi/ic_action_skip_next.png new file mode 100644 index 0000000000000000000000000000000000000000..164718d770ba4e607bb27113b17194497ac075c5 GIT binary patch literal 260 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUt)1EGlAsLNtZ|M3nJBqkHRA;@| z!V=oNW|h<93oCwUi3qi31qC{CoLTR-Hk0YEiuLq8>N~%wsNcLhb9ak?6Nh3;aztX4 z;&stm=l)poW}nf2Af>zDp|JmqIS-{w6J~^;@hd46yzrr~T{z}03(x1C*~&3@Sxs^_ zSX{W8>Tp?&t;uc GLK6VX18O$_ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/drawable-xhdpi/ic_action_skip_previous.png b/example/android/app/src/main/res/drawable-xhdpi/ic_action_skip_previous.png new file mode 100644 index 0000000000000000000000000000000000000000..b33307eee7eb658e825f0b7229a319d184118e51 GIT binary patch literal 265 zcmV+k0rvihP)aU#@OYZXZ|&WxZ9Rzyvrv^NNcs&;gdf)#McyRbT|{oV?;93e17ClU03z0kCm0 zQWTg1M`wKX1$w{=xH)61FE9r7&X^Ph7Qn^1zxo10VEe_yt0*u7PAw*{0&NRG@+D-Y zeTc2puhB;851EyAD`BPH%vou-(^l#OO(XS@(n@=%w|@3pMMOkI!3t_~fcrSJrOv?%s__%gaBd5oMF?v1341uvV`ouJr&iB+= zoS+&<_<-v@bQUS7yPEE5e&nVUEvUPJU)!*=ctP#Vbb#A9zGmkr1htuHHt=<3rcnuM z^By<-**GvtK^4B@fz^-PC^vtUHjz{u0KQ|c^7i6Nj>|f`uXcU4fe8C+#tj;9Y9ZT}V-@oxF zQqV**+wAqwS)8C;+Aqmx+%SJ4-jpH)P4Gg0tES7~1?AOqhw0homF4HCJ{X*!O0y9^ zk3@qZef1aam{ZD3g#g&g|!hZ2xd7u^dxZ2%V{8IV?L%9b6_I2lF=h!is z&UmoXJ~8d~gA&;c=ItBp)89QPk$fQ{_qaF_$P&!po&Is1!TMVc=|1{L+*>2BvhC_H z{}DAKICVkkolF0sgF_eOzIo*R@J1+GR)K8wzrEX6+Ff9K_b~clMJSup!&xt_vThn^ zi)Xa#{*kaVI7^i|_CWWceOKA68fQ=2FR(skL2*Tse{1b3K9hsS-FHR9MLXss9cN3b za^BG!-dekw&&2ppZOxSVKX(=_zHuqiX3A2xz-DQS2}iR6<;u4h9Fwg~C|l!Qk-ahd z@`FF;9iA+#)qcIB>3eHsDElnS-sPuW?>zX@NZ{w|om1JOD>U`>MS6YXXZ@IES|(>Ld%mXw1`g=k{mFi8w;(_R7&i=_u6{1-oD!M<%NoWl literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_next.png b/example/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_next.png new file mode 100644 index 0000000000000000000000000000000000000000..ea1a197c750db6d71dfb8cd59e97422898c1f222 GIT binary patch literal 509 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U_9dK;uumf=k1--?khEGW-pRo ztFwT?uHo>Ug}d|Go6C=%Cr*XO3r0(;vBGBf<=oMmczCV0bV##tt}GvWnv zG-k8<%!p1fEi&VtVI-SipJc|ZVQBk+DWYS-SthMB!W%>_X0wLO(JNp&!*VK((eycA z1UFDd>Wp>41%uhFEHktdR;HP8bC~EK@axSH6DX)UAZZY;IGfdF27AKd6f^D)llcb% zJApElc@0xJZ|W!<+{0L%yeZP5X=6jrV|Rf{vG>zI=&zjq{`>{Sm16JZHyQ@%+z%EM zJ`%T2{qeGntrh<6(?ybh#!XnM@Z`~6l}P6mPpbp>EnC+8ecP0_Svvch&C^W7%75OQ zCE;=PXmxR3Lg=*jZ(j;d`Mc;VW3R_D*#-R?u4XSE)OhGO2C_d*Ud72PKY>3>-ONHD pK6HX-qyFm?r>V7%z*;uumf=j|O^?72%cauWIjShu~4h@YTjlWjws^}P>J8nI5_MI3@{v{@r z4-cG~aYuTBisvLaT5@u4zvZ*fy=Iqe3Z_(dhhKqlu0 z?i~+a*BsSWJizzi$b+9<*Pk+UA2>N-C(nfqybtsY;y>;ZOW?C$xyQ6wQemV0fzSjs zW|IW|3eGdh4M(OmZayHxmT^GrgTRb~44r>j{t5nHWa{9w`atP}U2F=u4U!4-@BOx5 zTQHYF`#|M`C^m@#AL#_?3Z9tb49>@EUOcQ9XO! zo7o_;;r@Zt2Q_RjCMzA#`ygX6osn&Z+5w>gDT{VSw-wR}!aMl?>}5P=;E-0wl4|DL z&=r3u*h7%vk7QKzDuxI4Nn4lj1bhrT!ZM44;a(4SwwA-u+ZU5fWEpA{jDw^Z54cZq z-YUA_k@HWxTy}=(OTU~DT=3|S&L=j8>1t{G0fmL{CLDhHlJ|8GLqW6KHwMw|8WYVK zc6jPnH+0<&u|CjyOZLHPE~`V+OG^%HuZ-=5hdGe3q^y=9UA#TtE_~Z|U>q}ey85}S Ib4q9e045&RwEzGB literal 0 HcmV?d00001 diff --git a/example/lib/main.dart b/example/lib/main.dart index 2fd6395e..c0f43a61 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,20 +1,13 @@ import 'dart:math'; -import 'package:audioplayer/audioplayer.dart'; import 'package:flutter/material.dart'; import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:flutter_tts/flutter_tts.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:rxdart/rxdart.dart'; -// NOTE: Since Flutter 1.12, the audioplayer plugin will crash your release -// builds. As an alternative, I am developing a new audio player plugin called -// just_audio which I will switch to once the iOS implementation catches up to -// the Android implementation. In the meantime, or if you really want to use -// audioplayer, you may need to fork it and update compileSdkVersion = 28 and -// update the gradle wrapper to the latest. - MediaControl playControl = MediaControl( androidIcon: 'drawable/ic_action_play_arrow', label: 'Play', @@ -25,6 +18,16 @@ MediaControl pauseControl = MediaControl( label: 'Pause', action: MediaAction.pause, ); +MediaControl skipToNextControl = MediaControl( + androidIcon: 'drawable/ic_action_skip_next', + label: 'Next', + action: MediaAction.skipToNext, +); +MediaControl skipToPreviousControl = MediaControl( + androidIcon: 'drawable/ic_action_skip_previous', + label: 'Previous', + action: MediaAction.skipToPrevious, +); MediaControl stopControl = MediaControl( androidIcon: 'drawable/ic_action_stop', label: 'Stop', @@ -104,6 +107,7 @@ class _MyAppState extends State with WidgetsBindingObserver { final queue = screenState?.queue; final mediaItem = screenState?.mediaItem; final state = screenState?.playbackState; + final basicState = state?.basicState ?? BasicPlaybackState.none; return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -128,31 +132,38 @@ class _MyAppState extends State with WidgetsBindingObserver { ], ), if (mediaItem?.title != null) Text(mediaItem.title), - if (state?.basicState == BasicPlaybackState.connecting) ...[ - stopButton(), - Text("Connecting..."), - ] else if (state?.basicState == - BasicPlaybackState.skippingToNext) ...[ - stopButton(), - Text("Skipping..."), - ] else if (state?.basicState == - BasicPlaybackState.skippingToPrevious) ...[ - stopButton(), - Text("Skipping..."), - ] else if (state?.basicState == - BasicPlaybackState.playing) ...[ - pauseButton(), - stopButton(), - positionIndicator(mediaItem, state), - ] else if (state?.basicState == - BasicPlaybackState.paused) ...[ - playButton(), - stopButton(), - positionIndicator(mediaItem, state), - ] else ...[ + if (basicState == BasicPlaybackState.none) ...[ audioPlayerButton(), textToSpeechButton(), - ], + ] else + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (basicState == BasicPlaybackState.playing) + pauseButton() + else if (basicState == BasicPlaybackState.paused) + playButton() + else if (basicState == BasicPlaybackState.buffering || + basicState == BasicPlaybackState.skippingToNext || + basicState == + BasicPlaybackState.skippingToPrevious) + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 64.0, + height: 64.0, + child: CircularProgressIndicator(), + ), + ), + stopButton(), + ], + ), + if (basicState != BasicPlaybackState.none && + basicState != BasicPlaybackState.stopped) ...[ + positionIndicator(mediaItem, state), + Text("State: " + + "$basicState".replaceAll(RegExp(r'^.*\.'), '')), + ] ], ); }, @@ -201,14 +212,14 @@ class _MyAppState extends State with WidgetsBindingObserver { ); Widget positionIndicator(MediaItem mediaItem, PlaybackState state) { + double seekPos; return StreamBuilder( stream: Rx.combineLatest2( _dragPositionSubject.stream, - Stream.periodic(Duration(milliseconds: 200), - (_) => state.currentPosition.toDouble()), - (dragPosition, statePosition) => dragPosition ?? statePosition), + Stream.periodic(Duration(milliseconds: 200)), + (dragPosition, _) => dragPosition), builder: (context, snapshot) { - var position = snapshot.data ?? 0.0; + double position = snapshot.data ?? state.currentPosition.toDouble(); double duration = mediaItem?.duration?.toDouble(); return Column( children: [ @@ -216,12 +227,19 @@ class _MyAppState extends State with WidgetsBindingObserver { Slider( min: 0.0, max: duration, - value: max(0.0, min(position, duration)), + value: seekPos ?? max(0.0, min(position, duration)), onChanged: (value) { _dragPositionSubject.add(value); }, onChangeEnd: (value) { AudioService.seekTo(value.toInt()); + // Due to a delay in platform channel communication, there is + // a brief moment after releasing the Slider thumb before the + // new position is broadcast from the platform side. This + // hack is to hold onto seekPos until the next state update + // comes through. + // TODO: Improve this code. + seekPos = value; _dragPositionSubject.add(null); }, ), @@ -266,10 +284,11 @@ class AudioPlayerTask extends BackgroundAudioTask { "https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg", ), ]; - int _queueIndex = 0; + int _queueIndex = -1; AudioPlayer _audioPlayer = new AudioPlayer(); Completer _completer = Completer(); - int _position; + BasicPlaybackState _skipState; + bool _playing; bool get hasNext => _queueIndex + 1 < _queue.length; @@ -277,35 +296,49 @@ class AudioPlayerTask extends BackgroundAudioTask { MediaItem get mediaItem => _queue[_queueIndex]; + BasicPlaybackState _stateToBasicState(AudioPlaybackState state) { + switch (state) { + case AudioPlaybackState.none: + return BasicPlaybackState.none; + case AudioPlaybackState.stopped: + return BasicPlaybackState.stopped; + case AudioPlaybackState.paused: + return BasicPlaybackState.paused; + case AudioPlaybackState.playing: + return BasicPlaybackState.playing; + case AudioPlaybackState.buffering: + return BasicPlaybackState.buffering; + case AudioPlaybackState.connecting: + return _skipState ?? BasicPlaybackState.connecting; + case AudioPlaybackState.completed: + return BasicPlaybackState.stopped; + default: + throw Exception("Illegal state"); + } + } + @override Future onStart() async { - var playerStateSubscription = _audioPlayer.onPlayerStateChanged - .where((state) => state == AudioPlayerState.COMPLETED) + var playerStateSubscription = _audioPlayer.playbackStateStream + .where((state) => state == AudioPlaybackState.completed) .listen((state) { _handlePlaybackCompleted(); }); - var audioPositionSubscription = - _audioPlayer.onAudioPositionChanged.listen((when) { - final wasConnecting = _position == null; - _position = when.inMilliseconds; - if (wasConnecting) { - // After a delay, we finally start receiving audio positions from the - // AudioPlayer plugin, so we can broadcast the playing state. - _setPlayState(); + var eventSubscription = _audioPlayer.playbackEventStream.listen((event) { + final state = _stateToBasicState(event.state); + if (state != BasicPlaybackState.stopped) { + _setState( + state: state, + position: event.position.inMilliseconds, + ); } }); - _setState(state: BasicPlaybackState.connecting, position: 0); AudioServiceBackground.setQueue(_queue); - AudioServiceBackground.setMediaItem(mediaItem); - onPlay(); + await onSkipToNext(); await _completer.future; playerStateSubscription.cancel(); - audioPositionSubscription.cancel(); - } - - void _setPlayState() { - _setState(state: BasicPlaybackState.playing, position: _position); + eventSubscription.cancel(); } void _handlePlaybackCompleted() { @@ -324,51 +357,56 @@ class AudioPlayerTask extends BackgroundAudioTask { } @override - void onSkipToNext() { - if (!hasNext) return; - if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing) { - _audioPlayer.stop(); - } - _queueIndex++; - _position = null; - _setState(state: BasicPlaybackState.skippingToNext, position: 0); - AudioServiceBackground.setMediaItem(mediaItem); - onPlay(); - } + Future onSkipToNext() => _skip(1); @override - void onSkipToPrevious() { - if (!hasPrevious) return; - if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing) { - _audioPlayer.stop(); - _queueIndex--; - _position = null; + Future onSkipToPrevious() => _skip(-1); + + Future _skip(int offset) async { + final newPos = _queueIndex + offset; + if (!(newPos >= 0 && newPos < _queue.length)) return; + if (_playing == null) { + // First time, we want to start playing + _playing = true; + } else if (_playing) { + // Stop current item + await _audioPlayer.stop(); } - _setState(state: BasicPlaybackState.skippingToPrevious, position: 0); + // Load next item + _queueIndex = newPos; AudioServiceBackground.setMediaItem(mediaItem); - onPlay(); + _skipState = offset > 0 + ? BasicPlaybackState.skippingToNext + : BasicPlaybackState.skippingToPrevious; + await _audioPlayer.setUrl(mediaItem.id); + _skipState = null; + // Resume playback if we were playing + if (_playing) { + onPlay(); + } else { + _setState(state: BasicPlaybackState.paused); + } } @override void onPlay() { - _audioPlayer.play(mediaItem.id); - if (_position != null) { - _setPlayState(); - // Otherwise we are still loading the audio. + if (_skipState == null) { + _playing = true; + _audioPlayer.play(); } } @override void onPause() { - _audioPlayer.pause(); - _setState(state: BasicPlaybackState.paused, position: _position); + if (_skipState == null) { + _playing = false; + _audioPlayer.pause(); + } } @override void onSeekTo(int position) { - _audioPlayer.seek(position / 1000.0); - final state = AudioServiceBackground.state.basicState; - _setState(state: state, position: position); + _audioPlayer.seek(Duration(milliseconds: position)); } @override @@ -383,7 +421,10 @@ class AudioPlayerTask extends BackgroundAudioTask { _completer.complete(); } - void _setState({@required BasicPlaybackState state, int position = 0}) { + void _setState({@required BasicPlaybackState state, int position}) { + if (position == null) { + position = _audioPlayer.playbackEvent.position.inMilliseconds; + } AudioServiceBackground.setState( controls: getControls(state), systemActions: [MediaAction.seekTo], @@ -393,13 +434,20 @@ class AudioPlayerTask extends BackgroundAudioTask { } List getControls(BasicPlaybackState state) { - switch (state) { - case BasicPlaybackState.playing: - return [pauseControl, stopControl]; - case BasicPlaybackState.paused: - return [playControl, stopControl]; - default: - return [stopControl]; + if (_playing) { + return [ + skipToPreviousControl, + pauseControl, + stopControl, + skipToNextControl + ]; + } else { + return [ + skipToPreviousControl, + playControl, + stopControl, + skipToNextControl + ]; } } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 01e0b8ca..5cf0391b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: sdk: flutter path_provider: ^1.4.0 - audioplayer: ^0.5.2 + just_audio: ^0.0.6 flutter_tts: ^0.7.0 rxdart: ^0.23.1 audio_service: From 665e0185d4bd11d50741325073e3647161abcd35 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 2 Jan 2020 13:37:47 +1100 Subject: [PATCH 17/30] Update Copyright dates --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 603f9fb8..03aae637 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2019 Ryan Heise and the project contributors. +Copyright (c) 2018-2020 Ryan Heise and the project contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From c3c52c5c38843d5f646207fc2f3d40134e3caf91 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 2 Jan 2020 16:09:49 +1100 Subject: [PATCH 18/30] Remove try-with-resource --- README.md | 6 +++--- .../com/ryanheise/audioservice/AudioService.java | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bfac6e1c..3d4a156a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Play audio in the background. This plugin wraps around your existing Dart audio code to allow it to run in the background, and also respond to media button clicks on the lock screen, notifications, control center, headphone buttons and other supported remote control devices. This is necessary for a whole range of media applications such as music and podcast players, text-to-speech readers, navigators, etc. -This plugin is audio agnostic. It is designed to allow you to use your favourite audio plugins, such as [audioplayer](https://pub.dartlang.org/packages/audioplayer), [flutter_radio](https://pub.dev/packages/flutter_radio), [flutter_tts](https://pub.dartlang.org/packages/flutter_tts), and others. It simply wraps a special isolate around your existing audio code so that it can run in the background and enable remote control interfaces. +This plugin is audio agnostic. It is designed to allow you to use your favourite audio plugins, such as [just_audio](https://pub.dartlang.org/packages/just_audio), [flutter_radio](https://pub.dev/packages/flutter_radio), [flutter_tts](https://pub.dartlang.org/packages/flutter_tts), and others. It simply wraps a special isolate around your existing audio code so that it can run in the background and enable remote control interfaces. Note that because your app's UI and your background audio task will run in separate isolates, they do not share memory. They communicate through the message passing APIs provided by audio_service. @@ -24,8 +24,8 @@ Note that because your app's UI and your background audio task will run in separ | FF/rewind | ✅ | ✅ | | rate | ✅ | ✅ | | custom actions | ✅ | (untested) | -| notifications/control center | ✅ | ✅ | -| lock screen controls | ✅ | ✅ | +| notifications/control center | ✅ | (partial) | +| lock screen controls | ✅ | (partial) | | album art | ✅ | ✅ | | queue management | ✅ | ✅ | | runs in background | ✅ | ✅ | diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioService.java b/android/src/main/java/com/ryanheise/audioservice/AudioService.java index 2de9ceb3..bea21cd4 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioService.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioService.java @@ -414,16 +414,25 @@ synchronized void loadArtBitmap(MediaMetadataCompat mediaMetadata) { Uri artUri = mediaMetadata.getDescription().getIconUri(); Bitmap bitmap = artBitmapCache.get(artUri.toString()); if (bitmap == null) { - try (InputStream in = new URL(artUri.toString()).openConnection().getInputStream()) { + InputStream in = null; + try { + in = new URL(artUri.toString()).openConnection().getInputStream(); bitmap = BitmapFactory.decodeStream(in); if (!running) return; artBitmapCache.put(artUri.toString(), bitmap); - } - catch (IOException e) { + } catch (IOException e) { artUriBlacklist.add(artUri.toString()); e.printStackTrace(); return; + } finally { + if (in != null) { + try { + in.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } } } String mediaId = mediaMetadata.getDescription().getMediaId(); From 7578d7745ee7546302f5f7317575be9740279518 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 2 Jan 2020 16:16:13 +1100 Subject: [PATCH 19/30] Update dependencies --- example/pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5cf0391b..80375248 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -9,9 +9,9 @@ dependencies: flutter: sdk: flutter - path_provider: ^1.4.0 + path_provider: ^1.5.1 just_audio: ^0.0.6 - flutter_tts: ^0.7.0 + flutter_tts: ^0.8.5 rxdart: ^0.23.1 audio_service: path: ../ From 6d582b22afff1c78bd887f58a2bab640b03a7ee4 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Sun, 26 Jan 2020 00:35:55 +1100 Subject: [PATCH 20/30] enable queue for audio player task --- example/lib/main.dart | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index c0f43a61..14476422 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -174,18 +174,24 @@ class _MyAppState extends State with WidgetsBindingObserver { ); } - RaisedButton audioPlayerButton() => - startButton('AudioPlayer', _audioPlayerTaskEntrypoint); - - RaisedButton textToSpeechButton() => - startButton('TextToSpeech', _textToSpeechTaskEntrypoint); + RaisedButton audioPlayerButton() => startButton( + 'AudioPlayer', + () { + AudioService.start( + backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint, + androidNotificationChannelName: 'Audio Service Demo', + notificationColor: 0xFF2196f3, + androidNotificationIcon: 'mipmap/ic_launcher', + enableQueue: true, + ); + }, + ); - RaisedButton startButton(String label, Function entrypoint) => RaisedButton( - child: Text(label), - onPressed: () { + RaisedButton textToSpeechButton() => startButton( + 'TextToSpeech', + () { AudioService.start( - backgroundTaskEntrypoint: entrypoint, - resumeOnClick: true, + backgroundTaskEntrypoint: _textToSpeechTaskEntrypoint, androidNotificationChannelName: 'Audio Service Demo', notificationColor: 0xFF2196f3, androidNotificationIcon: 'mipmap/ic_launcher', @@ -193,6 +199,12 @@ class _MyAppState extends State with WidgetsBindingObserver { }, ); + RaisedButton startButton(String label, VoidCallback onPressed) => + RaisedButton( + child: Text(label), + onPressed: onPressed, + ); + IconButton playButton() => IconButton( icon: Icon(Icons.play_arrow), iconSize: 64.0, From 19547f4defa2a112f89832153bb662b3c7547851 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Mon, 27 Jan 2020 00:32:52 +1100 Subject: [PATCH 21/30] Support Flutter 1.12's new v2 embedding model --- README.md | 58 +------ android/build.gradle | 4 +- .../audioservice/AudioServicePlugin.java | 141 ++++++++++++------ .../android/app/src/main/AndroidManifest.xml | 12 +- .../audioserviceexample/MainActivity.java | 9 +- .../audioserviceexample/MainApplication.java | 20 --- 6 files changed, 107 insertions(+), 137 deletions(-) delete mode 100644 example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainApplication.java diff --git a/README.md b/README.md index 3d4a156a..eb0fe488 100644 --- a/README.md +++ b/README.md @@ -93,43 +93,16 @@ The full example on GitHub demonstrates how to fill in these callbacks to do aud ## Android setup -1. You will need to create a custom `MainApplication` class as follows: - -```java -// Insert your package name here instead of com.example.yourpackagename. -// You can find your package name at the top of your AndroidManifest file -// after package="... -package com.example.yourpackagename; - -import io.flutter.plugin.common.PluginRegistry; -import io.flutter.app.FlutterApplication; -import io.flutter.plugins.GeneratedPluginRegistrant; -import com.ryanheise.audioservice.AudioServicePlugin; - -public class MainApplication extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback { - @Override - public void onCreate() { - super.onCreate(); - AudioServicePlugin.setPluginRegistrantCallback(this); - } +These instructions assume that your project follows the new project template introduced in Flutter 1.12. If your project was created prior to 1.12 and uses the old project structure, you can either view a previous version of this README on GitHub, or update your project to follow the [new project template](https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects). - @Override - public void registerWith(PluginRegistry registry) { - GeneratedPluginRegistrant.registerWith(registry); - } -} -``` - -2. Edit your project's `AndroidManifest.xml` file to reference your `MainApplication` class, declare the permission to create a wake lock, and add component entries for the `` and ``: +1. Edit your project's `AndroidManifest.xml` file to declare the permission to create a wake lock, and add component entries for the `` and ``: ```xml - + ... @@ -148,7 +121,7 @@ public class MainApplication extends FlutterApplication implements PluginRegistr ``` -3. Any icons that you want to appear in the notification (see the `MediaControl` class) should be defined as Android resources in `android/app/src/main/res`. Here you will find a subdirectory for each different resolution: +2. Any icons that you want to appear in the notification (see the `MediaControl` class) should be defined as Android resources in `android/app/src/main/res`. Here you will find a subdirectory for each different resolution: ``` drawable-hdpi @@ -177,8 +150,6 @@ android { } ``` -*NOTE: Most Flutter plugins today were written before Flutter added support for running Dart code in a headless environment (without an Android Activity present). As such, a number of plugins assume there is an activity and run into a `NullPointerException`. Fortunately, it is very easy for plugin authors to update their plugins remove this assumption. If you encounter such a plugin, see the bottom of this README file for a sample bug report you can send to the relevant plugin author.* - ## iOS setup Insert this in your `Info.plist` file: @@ -191,24 +162,3 @@ Insert this in your `Info.plist` file: ``` The example project may be consulted for context. - -### Sample bug report - -If you encounter a Flutter plugin that gives a `NullPointerException` on Android, it is likely that the plugin has assumed the existence of an activity when there is none. If that is the case, you can submit a bug report to the author of that plugin and suggest the simple fix that should get it to work. - -Here is a sample bug report. - -> Flutter's new background execution feature (described here: https://medium.com/flutter-io/executing-dart-in-the-background-with-flutter-plugins-and-geofencing-2b3e40a1a124) allows plugins to be registered in a background context (e.g. a Service). The problem is that the wifi plugin assumes that the context for plugin registration is an activity with this line of code: -> -> ` WifiManager wifiManager = (WifiManager) registrar.activity().getApplicationContext().getSystemService(Context.WIFI_SERVICE);` -> -> `registrar.activity()` may now return null, and this leads to a `NullPointerException`: -> -> ``` -> E/AndroidRuntime( 2453): at com.ly.wifi.WifiPlugin.registerWith(WifiPlugin.java:23) -> E/AndroidRuntime( 2453): at io.flutter.plugins.GeneratedPluginRegistrant.registerWith(GeneratedPluginRegistrant.java:30) -> ``` -> -> The solution is to change the above line of code to this: -> -> ` WifiManager wifiManager = (WifiManager) registrar.activeContext().getApplicationContext().getSystemService(Context.WIFI_SERVICE);` diff --git a/android/build.gradle b/android/build.gradle index 7aed7f72..de54c7d4 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -34,6 +34,6 @@ android { } dependencies { - implementation 'androidx.core:core:1.0.0' - implementation 'androidx.media:media:1.0.0' + implementation 'androidx.core:core:1.1.0' + implementation 'androidx.media:media:1.1.0' } diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java index c513b99a..73588c87 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java @@ -1,5 +1,6 @@ package com.ryanheise.audioservice; +import io.flutter.embedding.engine.plugins.service.*; import android.app.Activity; import android.content.ComponentName; import android.content.Context; @@ -35,18 +36,29 @@ import io.flutter.plugin.common.PluginRegistry.ViewDestroyListener; import io.flutter.view.FlutterCallbackInformation; import io.flutter.view.FlutterMain; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.plugin.common.BinaryMessenger; +import android.app.Service; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.dart.DartExecutor.DartCallback; +import android.content.res.AssetManager; import io.flutter.view.FlutterNativeView; import io.flutter.view.FlutterRunArguments; /** AudioservicePlugin */ -public class AudioServicePlugin { +public class AudioServicePlugin implements FlutterPlugin, ActivityAware { private static final String CHANNEL_AUDIO_SERVICE = "ryanheise.com/audioService"; private static final String CHANNEL_AUDIO_SERVICE_BACKGROUND = "ryanheise.com/audioServiceBackground"; private static PluginRegistrantCallback pluginRegistrantCallback; private static ClientHandler clientHandler; private static BackgroundHandler backgroundHandler; - private static FlutterNativeView backgroundFlutterView; + private static FlutterEngine backgroundFlutterEngine; private static int nextQueueItemId = 0; private static List queueMediaIds = new ArrayList(); private static Map queueItemIds = new HashMap(); @@ -63,14 +75,65 @@ public static void setPluginRegistrantCallback(PluginRegistrantCallback pluginRe AudioServicePlugin.pluginRegistrantCallback = pluginRegistrantCallback; } - /** Plugin registration. */ + /** v1 plugin registration. */ public static void registerWith(Registrar registrar) { - if (registrar.activity() != null) - clientHandler = new ClientHandler(registrar); - else - backgroundHandler.init(registrar); + if (registrar.activity() != null) { + clientHandler = new ClientHandler(registrar.messenger()); + clientHandler.activity = registrar.activity(); + registrar.addViewDestroyListener(new ViewDestroyListener() { + @Override + public boolean onViewDestroy(FlutterNativeView view) { + clientHandler = null; + return false; + } + }); + } else { + backgroundHandler.init(registrar.messenger()); + } + } + + private FlutterPluginBinding flutterPluginBinding; + + // + // FlutterPlugin callbacks + // + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + flutterPluginBinding = binding; } + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + flutterPluginBinding = null; + } + + // + // ActivityAware callbacks + // + + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + clientHandler = new ClientHandler(flutterPluginBinding.getFlutterEngine().getDartExecutor()); + clientHandler.activity = binding.getActivity(); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + clientHandler.activity = binding.getActivity(); + } + + @Override + public void onDetachedFromActivity() { + clientHandler = null; + } + + + private static void sendConnectResult(boolean result) { connectResult.success(result); connectResult = null; @@ -81,8 +144,8 @@ private static void sendStartResult(boolean result) { startResult = null; } - private static class ClientHandler implements MethodCallHandler, ViewDestroyListener { - private Registrar registrar; + private static class ClientHandler implements MethodCallHandler { + Activity activity; private MethodChannel channel; private boolean playPending; public MediaBrowserCompat mediaBrowser; @@ -119,7 +182,7 @@ public void onChildrenLoaded(String parentId, List @Override public void onConnected() { try { - Activity activity = registrar.activity(); + //Activity activity = registrar.activity(); MediaSessionCompat.Token token = mediaBrowser.getSessionToken(); mediaController = new MediaControllerCompat(activity, token); MediaControllerCompat.setMediaController(activity, mediaController); @@ -155,23 +218,14 @@ public void onConnectionFailed() { } }; - public ClientHandler(Registrar registrar) { - this.registrar = registrar; - channel = new MethodChannel(registrar.messenger(), CHANNEL_AUDIO_SERVICE); + public ClientHandler(BinaryMessenger messenger) { + channel = new MethodChannel(messenger, CHANNEL_AUDIO_SERVICE); channel.setMethodCallHandler(this); - registrar.addViewDestroyListener(this); - } - - @Override - public boolean onViewDestroy(FlutterNativeView view) { - clientHandler = null; - return false; } @Override public void onMethodCall(MethodCall call, final Result result) { - Context context = registrar.activeContext(); - FlutterApplication application = (FlutterApplication)context.getApplicationContext(); + Context context = activity; switch (call.method) { case "isRunning": result.success(AudioService.isRunning()); @@ -195,8 +249,7 @@ public void onMethodCall(MethodCall call, final Result result) { final boolean enableQueue = (Boolean)arguments.get("enableQueue"); final boolean androidStopForegroundOnPause = (Boolean)arguments.get("androidStopForegroundOnPause"); - final String appBundlePath = FlutterMain.findAppBundlePath(application); - Activity activity = application.getCurrentActivity(); + final String appBundlePath = FlutterMain.findAppBundlePath(context.getApplicationContext()); backgroundHandler = new BackgroundHandler(callbackHandle, appBundlePath, enableQueue); AudioService.init(activity, resumeOnClick, androidNotificationChannelName, androidNotificationChannelDescription, notificationColor, androidNotificationIcon, androidNotificationClickStartsActivity, androidNotificationOngoing, shouldPreloadArtwork, androidStopForegroundOnPause, backgroundHandler); @@ -393,7 +446,6 @@ private static class BackgroundHandler implements MethodCallHandler, AudioServic private long callbackHandle; private String appBundlePath; private boolean enableQueue; - private Registrar registrar; public MethodChannel channel; private AudioTrack silenceAudioTrack; private static final int SILENCE_SAMPLE_RATE = 44100; @@ -405,9 +457,9 @@ public BackgroundHandler(long callbackHandle, String appBundlePath, boolean enab this.enableQueue = enableQueue; } - public void init(Registrar registrar) { - this.registrar = registrar; - channel = new MethodChannel(registrar.messenger(), CHANNEL_AUDIO_SERVICE_BACKGROUND); + public void init(BinaryMessenger messenger) { + if (channel != null) return; + channel = new MethodChannel(messenger, CHANNEL_AUDIO_SERVICE_BACKGROUND); channel.setMethodCallHandler(this); } @@ -477,25 +529,26 @@ public void onPrepareFromMediaId(String mediaId) { } @Override public void onPlay() { - if (backgroundFlutterView == null) { + if (backgroundFlutterEngine == null) { + Context context = AudioService.instance; + backgroundFlutterEngine = new FlutterEngine(context); FlutterCallbackInformation cb = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); if (cb == null || appBundlePath == null) { sendStartResult(false); return; } - backgroundFlutterView = new FlutterNativeView(AudioService.instance, true); - if (pluginRegistrantCallback == null) { - sendStartResult(false); - throw new IllegalStateException("No pluginRegistrantCallback has been set. Make sure you call AudioServicePlugin.setPluginRegistrantCallback(this) from your application's onCreate."); - } if (enableQueue) AudioService.instance.enableQueue(); - pluginRegistrantCallback.registerWith(backgroundFlutterView.getPluginRegistry()); - FlutterRunArguments args = new FlutterRunArguments(); - args.bundlePath = appBundlePath; - args.entrypoint = cb.callbackName; - args.libraryPath = cb.callbackLibraryPath; - backgroundFlutterView.runFromBundle(args); + // Register plugins in background isolate if app is using v1 embedding + if (pluginRegistrantCallback != null) { + pluginRegistrantCallback.registerWith(new ShimPluginRegistry(backgroundFlutterEngine)); + } + + DartExecutor executor = backgroundFlutterEngine.getDartExecutor(); + init(executor); + DartCallback dartCallback = new DartCallback(context.getAssets(), appBundlePath, cb); + + executor.executeDartCallback(dartCallback); } else invokeMethod("onPlay"); @@ -554,11 +607,9 @@ public void onSetRating(RatingCompat rating, Bundle extras) { invokeMethod("onSetRating", rating2raw(rating), extras.getSerializable("extrasMap")); } - @Override public void onMethodCall(MethodCall call, Result result) { - Context context = registrar.activeContext(); - FlutterApplication application = (FlutterApplication)context.getApplicationContext(); + Context context = AudioService.instance; switch (call.method) { case "ready": result.success(true); @@ -622,8 +673,8 @@ public void onMethodCall(MethodCall call, Result result) { if (silenceAudioTrack != null) silenceAudioTrack.release(); if (clientHandler != null) clientHandler.invokeMethod("onStopped"); - backgroundFlutterView.destroy(); - backgroundFlutterView = null; + backgroundFlutterEngine.destroy(); + backgroundFlutterEngine = null; backgroundHandler = null; result.success(true); break; diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index ef7a0180..ef621284 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -15,7 +15,6 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> - - @@ -50,5 +42,9 @@ + + diff --git a/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainActivity.java b/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainActivity.java index 0551245a..4a9c79d5 100644 --- a/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainActivity.java +++ b/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainActivity.java @@ -1,13 +1,6 @@ package com.ryanheise.audioserviceexample; -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; public class MainActivity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } } diff --git a/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainApplication.java b/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainApplication.java deleted file mode 100644 index 0cb7aa5a..00000000 --- a/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainApplication.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.ryanheise.audioserviceexample; - -import io.flutter.plugin.common.PluginRegistry; -import io.flutter.app.FlutterApplication; - -import io.flutter.plugins.GeneratedPluginRegistrant; -import com.ryanheise.audioservice.AudioServicePlugin; - -public class MainApplication extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback { - @Override - public void onCreate() { - super.onCreate(); - AudioServicePlugin.setPluginRegistrantCallback(this); - } - - @Override - public void registerWith(PluginRegistry registry) { - GeneratedPluginRegistrant.registerWith(registry); - } -} From 8e949d9d95c36a36600ac71141b04ff1b91bfb3e Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Wed, 29 Jan 2020 00:36:45 +1100 Subject: [PATCH 22/30] iOS: prevent double start --- ios/Classes/AudioServicePlugin.m | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/Classes/AudioServicePlugin.m b/ios/Classes/AudioServicePlugin.m index 2a3e6968..c026fb81 100644 --- a/ios/Classes/AudioServicePlugin.m +++ b/ios/Classes/AudioServicePlugin.m @@ -88,6 +88,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else if ([@"start" isEqualToString:call.method]) { if (_running) { result(@NO); + return; } _running = YES; // The result will be sent after the background task actually starts. From 31c85d539f973ced7d34d60a6199388b614041ac Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 30 Jan 2020 20:24:24 +1100 Subject: [PATCH 23/30] Workaround for v2 memory leak --- README.md | 22 +++++++++++++++++++ .../audioservice/AudioServicePlugin.java | 2 +- example/android/app/build.gradle | 1 + .../audioserviceexample/MainActivity.java | 16 ++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eb0fe488..463bc862 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,28 @@ android { } ``` +3. (Optional) Versions of Flutter since 1.12 have a memory leak that affects this plugin. It will be fixed in an upcoming Flutter release but until then you can work around it by overriding the following method in your `MainActivity` class: + +``` +public class MainActivity extends FlutterActivity { + /** This is a temporary workaround to avoid a memory leak in the Flutter framework */ + @Override + public FlutterEngine provideFlutterEngine(Context context) { + // Instantiate a FlutterEngine. + FlutterEngine flutterEngine = new FlutterEngine(context.getApplicationContext()); + + // Start executing Dart code to pre-warm the FlutterEngine. + flutterEngine.getDartExecutor().executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ); + + return flutterEngine; + } +} +``` + +Alternatively, if you use a cached flutter engine (as per [these instructions](https://flutter.dev/docs/development/add-to-app/android/add-flutter-screen#step-3-optional-use-a-cached-flutterengine)), you will need to change the instantiation code from `new FlutterEngine(this)` to `new FlutterEngine(getApplicationContext())`. + ## iOS setup Insert this in your `Info.plist` file: diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java index 73588c87..d62c515f 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java @@ -531,7 +531,7 @@ public void onPrepareFromMediaId(String mediaId) { public void onPlay() { if (backgroundFlutterEngine == null) { Context context = AudioService.instance; - backgroundFlutterEngine = new FlutterEngine(context); + backgroundFlutterEngine = new FlutterEngine(context.getApplicationContext()); FlutterCallbackInformation cb = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); if (cb == null || appBundlePath == null) { sendStartResult(false); diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index edf99ef5..e18ba001 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -58,4 +58,5 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1' } diff --git a/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainActivity.java b/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainActivity.java index 4a9c79d5..b3b6ead5 100644 --- a/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainActivity.java +++ b/example/android/app/src/main/java/com/ryanheise/audioserviceexample/MainActivity.java @@ -1,6 +1,22 @@ package com.ryanheise.audioserviceexample; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.dart.DartExecutor; +import android.content.Context; import io.flutter.embedding.android.FlutterActivity; public class MainActivity extends FlutterActivity { + /** This is a temporary workaround to avoid a memory leak in the Flutter framework */ + @Override + public FlutterEngine provideFlutterEngine(Context context) { + // Instantiate a FlutterEngine. + FlutterEngine flutterEngine = new FlutterEngine(context.getApplicationContext()); + + // Start executing Dart code to pre-warm the FlutterEngine. + flutterEngine.getDartExecutor().executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ); + + return flutterEngine; + } } From 99877c2b287e2a385d0f1381634db528635d2c4b Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 30 Jan 2020 20:37:43 +1100 Subject: [PATCH 24/30] Version 0.6.0 --- CHANGELOG.md | 4 ++++ README.md | 2 +- pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d3bd1f..880129cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0 + +* Migrated to V2 embedding API (Flutter 1.12). + ## 0.5.7 * Destroy isolates after use. diff --git a/README.md b/README.md index 463bc862..ec16c180 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ android { 3. (Optional) Versions of Flutter since 1.12 have a memory leak that affects this plugin. It will be fixed in an upcoming Flutter release but until then you can work around it by overriding the following method in your `MainActivity` class: -``` +```java public class MainActivity extends FlutterActivity { /** This is a temporary workaround to avoid a memory leak in the Flutter framework */ @Override diff --git a/pubspec.yaml b/pubspec.yaml index 90829211..7d7a49fa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: audio_service description: Flutter plugin to play audio in the background while the screen is off. -version: 0.5.7 +version: 0.6.0 homepage: https://github.com/ryanheise/audio_service environment: From eace414ddfe9bb76815ae800ff5449c39045df75 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Mon, 3 Feb 2020 21:35:16 +1100 Subject: [PATCH 25/30] Prevent lockup of main thread during art loading --- .../ryanheise/audioservice/AudioService.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioService.java b/android/src/main/java/com/ryanheise/audioservice/AudioService.java index bea21cd4..d1bdedba 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioService.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioService.java @@ -41,6 +41,8 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import android.os.Handler; +import android.os.Looper; import java.util.Set; public class AudioService extends MediaBrowserServiceCompat implements AudioManager.OnAudioFocusChangeListener { @@ -73,7 +75,7 @@ public class AudioService extends MediaBrowserServiceCompat implements AudioMana private static Set artUriBlacklist = new HashSet<>(); private static Map artBitmapCache = new HashMap<>(); // TODO: old bitmaps should expire FIFO - public static synchronized void init(Activity activity, boolean resumeOnClick, String androidNotificationChannelName, String androidNotificationChannelDescription, Integer notificationColor, String androidNotificationIcon, boolean androidNotificationClickStartsActivity, boolean androidNotificationOngoing, boolean shouldPreloadArtwork, boolean androidStopForegroundOnPause, ServiceListener listener) { + public static void init(Activity activity, boolean resumeOnClick, String androidNotificationChannelName, String androidNotificationChannelDescription, Integer notificationColor, String androidNotificationIcon, boolean androidNotificationClickStartsActivity, boolean androidNotificationOngoing, boolean shouldPreloadArtwork, boolean androidStopForegroundOnPause, ServiceListener listener) { if (running) throw new IllegalStateException("AudioService already running"); running = true; @@ -118,7 +120,7 @@ public void stop() { stopSelf(); } - public static synchronized boolean isRunning() { + public static boolean isRunning() { return running; } @@ -133,6 +135,7 @@ public static synchronized boolean isRunning() { private MediaMetadataCompat mediaMetadata; private Object audioFocusRequest; private String notificationChannelId; + private Handler handler = new Handler(Looper.getMainLooper()); int getResourceId(String resource) { String[] parts = resource.split("/"); @@ -394,7 +397,8 @@ public void run() { }.start(); } - synchronized void setMetadata(final MediaMetadataCompat mediaMetadata) { + // Call only on main thread + void setMetadata(final MediaMetadataCompat mediaMetadata) { this.mediaMetadata = mediaMetadata; mediaSession.setMetadata(mediaMetadata); updateNotification(); @@ -409,6 +413,7 @@ public void run() { } } + // Must not be called on the main thread synchronized void loadArtBitmap(MediaMetadataCompat mediaMetadata) { if (needToLoadArt(mediaMetadata)) { Uri artUri = mediaMetadata.getDescription().getIconUri(); @@ -436,14 +441,19 @@ synchronized void loadArtBitmap(MediaMetadataCompat mediaMetadata) { } } String mediaId = mediaMetadata.getDescription().getMediaId(); - mediaMetadata = new MediaMetadataCompat.Builder(mediaMetadata) + final MediaMetadataCompat updatedMediaMetadata = new MediaMetadataCompat.Builder(mediaMetadata) .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) .putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap) .build(); - mediaMetadataCache.put(mediaId, mediaMetadata); + mediaMetadataCache.put(mediaId, updatedMediaMetadata); // If this the current media item, update the notification if (this.mediaMetadata != null && mediaId.equals(this.mediaMetadata.getDescription().getMediaId())) { - setMetadata(mediaMetadata); + handler.post(new Runnable() { + @Override + public void run() { + setMetadata(updatedMediaMetadata); + } + }); } } } From f2e3c03d8f9007adc0037f1eb24c3e8354d41733 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Mon, 3 Feb 2020 22:31:37 +1100 Subject: [PATCH 26/30] Remove redundant comment --- .../src/main/java/com/ryanheise/audioservice/AudioService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioService.java b/android/src/main/java/com/ryanheise/audioservice/AudioService.java index d1bdedba..a844dbea 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioService.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioService.java @@ -379,9 +379,6 @@ void setQueue(List queue) { } void preloadArtwork(final List queue) { - // XXX: Although this happens in a thread, it seems to cause a block - // somewhere in the Flutter engine, temporarily preventing messages from - // being passed over platform channels. new Thread() { @Override public void run() { From 4a2ca57a5afab3491ddd84476d736fac35c9b6e7 Mon Sep 17 00:00:00 2001 From: Esmond Wong <22164439+Camerash@users.noreply.github.com> Date: Mon, 3 Feb 2020 20:15:45 +0800 Subject: [PATCH 27/30] Added androidStopOnRemoveTask flag (#165) --- .../java/com/ryanheise/audioservice/AudioService.java | 8 +++++--- .../com/ryanheise/audioservice/AudioServicePlugin.java | 3 ++- lib/audio_service.dart | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioService.java b/android/src/main/java/com/ryanheise/audioservice/AudioService.java index a844dbea..a406080c 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioService.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioService.java @@ -69,13 +69,14 @@ public class AudioService extends MediaBrowserServiceCompat implements AudioMana static boolean androidNotificationOngoing; static boolean shouldPreloadArtwork; static boolean androidStopForegroundOnPause; + static boolean androidStopOnRemoveTask; private static List queue = new ArrayList(); private static int queueIndex = -1; private static Map mediaMetadataCache = new HashMap<>(); private static Set artUriBlacklist = new HashSet<>(); private static Map artBitmapCache = new HashMap<>(); // TODO: old bitmaps should expire FIFO - public static void init(Activity activity, boolean resumeOnClick, String androidNotificationChannelName, String androidNotificationChannelDescription, Integer notificationColor, String androidNotificationIcon, boolean androidNotificationClickStartsActivity, boolean androidNotificationOngoing, boolean shouldPreloadArtwork, boolean androidStopForegroundOnPause, ServiceListener listener) { + public static void init(Activity activity, boolean resumeOnClick, String androidNotificationChannelName, String androidNotificationChannelDescription, Integer notificationColor, String androidNotificationIcon, boolean androidNotificationClickStartsActivity, boolean androidNotificationOngoing, boolean shouldPreloadArtwork, boolean androidStopForegroundOnPause, boolean androidStopOnRemoveTask, ServiceListener listener) { if (running) throw new IllegalStateException("AudioService already running"); running = true; @@ -93,6 +94,7 @@ public static void init(Activity activity, boolean resumeOnClick, String android AudioService.androidNotificationOngoing = androidNotificationOngoing; AudioService.shouldPreloadArtwork = shouldPreloadArtwork; AudioService.androidStopForegroundOnPause = androidStopForegroundOnPause; + AudioService.androidStopOnRemoveTask = androidStopOnRemoveTask; } public void stop() { @@ -491,8 +493,8 @@ public void onDestroy() { @Override public void onTaskRemoved(Intent rootIntent) { MediaControllerCompat controller = mediaSession.getController(); - if (androidStopForegroundOnPause && controller.getPlaybackState().getState() == PlaybackStateCompat.STATE_PAUSED) { - stopSelf(); + if (androidStopOnRemoveTask || (androidStopForegroundOnPause && controller.getPlaybackState().getState() == PlaybackStateCompat.STATE_PAUSED)) { + listener.onStop(); } super.onTaskRemoved(rootIntent); } diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java index d62c515f..8270f109 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java @@ -248,10 +248,11 @@ public void onMethodCall(MethodCall call, final Result result) { boolean shouldPreloadArtwork = (Boolean)arguments.get("shouldPreloadArtwork"); final boolean enableQueue = (Boolean)arguments.get("enableQueue"); final boolean androidStopForegroundOnPause = (Boolean)arguments.get("androidStopForegroundOnPause"); + final boolean androidStopOnRemoveTask = (Boolean)arguments.get("androidStopOnRemoveTask"); final String appBundlePath = FlutterMain.findAppBundlePath(context.getApplicationContext()); backgroundHandler = new BackgroundHandler(callbackHandle, appBundlePath, enableQueue); - AudioService.init(activity, resumeOnClick, androidNotificationChannelName, androidNotificationChannelDescription, notificationColor, androidNotificationIcon, androidNotificationClickStartsActivity, androidNotificationOngoing, shouldPreloadArtwork, androidStopForegroundOnPause, backgroundHandler); + AudioService.init(activity, resumeOnClick, androidNotificationChannelName, androidNotificationChannelDescription, notificationColor, androidNotificationIcon, androidNotificationClickStartsActivity, androidNotificationOngoing, shouldPreloadArtwork, androidStopForegroundOnPause, androidStopOnRemoveTask, backgroundHandler); synchronized (connectionCallback) { if (mediaController != null) diff --git a/lib/audio_service.dart b/lib/audio_service.dart index dd8ceac3..c82d49aa 100644 --- a/lib/audio_service.dart +++ b/lib/audio_service.dart @@ -512,6 +512,7 @@ class AudioService { bool shouldPreloadArtwork = false, bool androidStopForegroundOnPause = false, bool enableQueue = false, + bool androidStopOnRemoveTask = false, }) async { final ui.CallbackHandle handle = ui.PluginUtilities.getCallbackHandle(backgroundTaskEntrypoint); @@ -546,6 +547,7 @@ class AudioService { 'shouldPreloadArtwork': shouldPreloadArtwork, 'androidStopForegroundOnPause': androidStopForegroundOnPause, 'enableQueue': enableQueue, + 'androidStopOnRemoveTask': androidStopOnRemoveTask, }); } From 9d48c0c4bd1d3bf3fc1f161e174a8d597d3b88d3 Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Wed, 5 Feb 2020 23:54:35 +1100 Subject: [PATCH 28/30] Handle focus request failure more gracefully --- .../main/java/com/ryanheise/audioservice/AudioService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/ryanheise/audioservice/AudioService.java b/android/src/main/java/com/ryanheise/audioservice/AudioService.java index a406080c..6eaa6510 100644 --- a/android/src/main/java/com/ryanheise/audioservice/AudioService.java +++ b/android/src/main/java/com/ryanheise/audioservice/AudioService.java @@ -559,8 +559,8 @@ public void run() { private void play(Runnable runner) { int result = requestAudioFocus(); if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - // TODO: Handle this more gracefully - throw new RuntimeException("Failed to gain audio focus"); + // Don't play audio + return; } startService(new Intent(AudioService.this, AudioService.class)); From c046e6efd8f34ce406329ac0efcf60d7c137d0cb Mon Sep 17 00:00:00 2001 From: Ryan Heise Date: Thu, 6 Feb 2020 00:12:22 +1100 Subject: [PATCH 29/30] Version 6.0.1 --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 880129cd..e19f5ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.1 + +* Option to stop service on closing task (Android). + ## 0.6.0 * Migrated to V2 embedding API (Flutter 1.12). diff --git a/pubspec.yaml b/pubspec.yaml index 7d7a49fa..b61c8964 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: audio_service description: Flutter plugin to play audio in the background while the screen is off. -version: 0.6.0 +version: 0.6.1 homepage: https://github.com/ryanheise/audio_service environment: From a9a86173d4e89f4c7bb118834889d29241abfa0c Mon Sep 17 00:00:00 2001 From: Eiichiro Adachi Date: Fri, 7 Feb 2020 20:44:42 +0900 Subject: [PATCH 30/30] Fix background seek on iOS. (#172) --- ios/Classes/AudioServicePlugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Classes/AudioServicePlugin.m b/ios/Classes/AudioServicePlugin.m index c026fb81..c5bc3b30 100644 --- a/ios/Classes/AudioServicePlugin.m +++ b/ios/Classes/AudioServicePlugin.m @@ -335,7 +335,7 @@ - (MPRemoteCommandHandlerStatus) previousTrack: (MPRemoteCommandEvent *) event { - (MPRemoteCommandHandlerStatus) changePlaybackPosition: (MPChangePlaybackPositionCommandEvent *) event { NSLog(@"changePlaybackPosition"); - [backgroundChannel invokeMethod:@"onSeekTo" arguments: @[@(event.positionTime)]]; + [backgroundChannel invokeMethod:@"onSeekTo" arguments: @[@((long long) (event.positionTime * 1000))]]; return MPRemoteCommandHandlerStatusSuccess; }