Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update #1

Merged
merged 32 commits into from
Feb 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
00a8ac8
Add connected boolean
hacker1024 Dec 18, 2019
f815de3
Add MediaItem.copyWith
ryanheise Dec 18, 2019
26f56e1
Add systemActions option to setState
ryanheise Dec 18, 2019
ffeb691
Merge pull request #130 from hacker1024/patch-1
ryanheise Dec 18, 2019
fc3b464
Upgrade sdk and rxdart dependencies
ryanheise Dec 18, 2019
a46fa9c
Combine state streams.
ryanheise Dec 18, 2019
cfbca03
Notify all streams on connect
ryanheise Dec 18, 2019
dbb5ba8
Format
ryanheise Dec 18, 2019
c6e5dba
Version 0.5.5
ryanheise Dec 25, 2019
a04ef2a
Support Flutter 1.12
ryanheise Dec 25, 2019
15b04fc
Update bug template
ryanheise Dec 27, 2019
aa88e07
Version 0.5.6
ryanheise Dec 28, 2019
910e21e
Destroy FlutterNativeView
ryanheise Dec 28, 2019
1f699e5
Version 0.5.7
ryanheise Dec 28, 2019
cf385e6
Update gradle version
ryanheise Dec 29, 2019
a3e0623
Update sdk version
ryanheise Dec 29, 2019
40ded02
Update example to use just_audio
ryanheise Jan 2, 2020
665e018
Update Copyright dates
ryanheise Jan 2, 2020
c3c52c5
Remove try-with-resource
ryanheise Jan 2, 2020
7578d77
Update dependencies
ryanheise Jan 2, 2020
6d582b2
enable queue for audio player task
ryanheise Jan 25, 2020
19547f4
Support Flutter 1.12's new v2 embedding model
ryanheise Jan 26, 2020
8e949d9
iOS: prevent double start
ryanheise Jan 28, 2020
31c85d5
Workaround for v2 memory leak
ryanheise Jan 30, 2020
2f6ed7b
Merge branch 'master' of github.com:ryanheise/audio_service
ryanheise Jan 30, 2020
99877c2
Version 0.6.0
ryanheise Jan 30, 2020
eace414
Prevent lockup of main thread during art loading
ryanheise Feb 3, 2020
f2e3c03
Remove redundant comment
ryanheise Feb 3, 2020
4a2ca57
Added androidStopOnRemoveTask flag (#165)
Camerash Feb 3, 2020
9d48c0c
Handle focus request failure more gracefully
ryanheise Feb 5, 2020
c046e6e
Version 6.0.1
ryanheise Feb 5, 2020
a9a8617
Fix background seek on iOS. (#172)
addie9000 Feb 7, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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**
```
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
## 0.6.1

* Option to stop service on closing task (Android).

## 0.6.0

* Migrated to V2 embedding API (Flutter 1.12).

## 0.5.7

* Destroy isolates after use.

## 0.5.6

* Support Flutter 1.12.

## 0.5.5

* Bump sdk version to 2.6.0.

## 0.5.4

* Fix Android memory leak.
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
101 changes: 45 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 | ✅ | ✅ |
Expand Down Expand Up @@ -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:
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).

```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);
}

@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 `<service>` and `<receiver>`:
1. Edit your project's `AndroidManifest.xml` file to declare the permission to create a wake lock, and add component entries for the `<service>` and `<receiver>`:

```xml
<manifest ...>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

<application
android:name=".MainApplication"
...>
<application ...>

...

Expand All @@ -148,7 +121,7 @@ public class MainApplication extends FlutterApplication implements PluginRegistr
</manifest>
```

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
Expand All @@ -160,7 +133,44 @@ drawable-xxxhdpi

You can use [Android Asset Studio](https://romannurik.github.io/AndroidAssetStudio/) to generate these different subdirectories for any standard material design icon.

*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.*
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
}
}
}
```

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
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

Expand All @@ -174,24 +184,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);`
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.android.tools.build:gradle:3.5.0'
}
}

Expand All @@ -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'
}
3 changes: 3 additions & 0 deletions android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true
50 changes: 34 additions & 16 deletions android/src/main/java/com/ryanheise/audioservice/AudioService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -67,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<MediaSessionCompat.QueueItem> queue = new ArrayList<MediaSessionCompat.QueueItem>();
private static int queueIndex = -1;
private static Map<String,MediaMetadataCompat> mediaMetadataCache = new HashMap<>();
private static Set<String> artUriBlacklist = new HashSet<>();
private static Map<String,Bitmap> 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, boolean androidStopOnRemoveTask, ServiceListener listener) {
if (running)
throw new IllegalStateException("AudioService already running");
running = true;
Expand All @@ -91,6 +94,7 @@ public static synchronized void init(Activity activity, boolean resumeOnClick, S
AudioService.androidNotificationOngoing = androidNotificationOngoing;
AudioService.shouldPreloadArtwork = shouldPreloadArtwork;
AudioService.androidStopForegroundOnPause = androidStopForegroundOnPause;
AudioService.androidStopOnRemoveTask = androidStopOnRemoveTask;
}

public void stop() {
Expand Down Expand Up @@ -118,7 +122,7 @@ public void stop() {
stopSelf();
}

public static synchronized boolean isRunning() {
public static boolean isRunning() {
return running;
}

Expand All @@ -133,6 +137,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("/");
Expand Down Expand Up @@ -376,9 +381,6 @@ void setQueue(List<MediaSessionCompat.QueueItem> queue) {
}

void preloadArtwork(final List<MediaSessionCompat.QueueItem> 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() {
Expand All @@ -394,7 +396,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();
Expand All @@ -409,32 +412,47 @@ public void run() {
}
}

// Must not be called on the main thread
synchronized void loadArtBitmap(MediaMetadataCompat mediaMetadata) {
if (needToLoadArt(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();
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);
}
});
}
}
}
Expand Down Expand Up @@ -475,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);
}
Expand Down Expand Up @@ -541,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));
Expand Down
Loading