Compare commits

...

No commits in common. 'master' and 'master' have entirely different histories.

276 changed files with 34099 additions and 6308 deletions
Split View
  1. +0
    -1
      .github/FUNDING.yml
  2. +0
    -80
      .github/ISSUE_TEMPLATE/bug_report.md
  3. +0
    -8
      .github/ISSUE_TEMPLATE/config.yml
  4. +0
    -43
      .github/ISSUE_TEMPLATE/documentation-request.md
  5. +0
    -49
      .github/ISSUE_TEMPLATE/feature_request.md
  6. +0
    -19
      .github/ISSUE_TEMPLATE/frequently-asked-questions.md
  7. +0
    -12
      .github/workflows/auto-close.yml
  8. +82
    -19
      .gitignore
  9. +0
    -2
      .idea/.gitignore
  10. +8
    -0
      .idea/artifacts/lib_js_0_1_0.xml
  11. +8
    -0
      .idea/artifacts/lib_jvm_0_1_0.xml
  12. +8
    -0
      .idea/artifacts/platypus_js_0_1_0.xml
  13. +8
    -0
      .idea/artifacts/platypus_jvm_0_1_0.xml
  14. +8
    -0
      .idea/artifacts/unfreeze_js_0_1.xml
  15. +8
    -0
      .idea/artifacts/unfreeze_js_0_1_0.xml
  16. +8
    -0
      .idea/artifacts/unfreeze_js_1_0_SNAPSHOT.xml
  17. +8
    -0
      .idea/artifacts/unfreeze_jslegacy_0_1.xml
  18. +8
    -0
      .idea/artifacts/unfreeze_jvm_0_1.xml
  19. +8
    -0
      .idea/artifacts/unfreeze_jvm_0_1_0.xml
  20. +8
    -0
      .idea/artifacts/unfreeze_jvm_1_0_SNAPSHOT.xml
  21. +0
    -116
      .idea/codeStyles/Project.xml
  22. +6
    -0
      .idea/inspectionProfiles/Project_Default.xml
  23. +26
    -0
      .idea/kotlinScripting.xml
  24. +0
    -19
      .idea/libraries/Dart_SDK.xml
  25. +0
    -15
      .idea/libraries/Flutter_Plugins.xml
  26. +0
    -9
      .idea/libraries/Flutter_for_Android.xml
  27. +5
    -0
      .idea/misc.xml
  28. +0
    -9
      .idea/modules.xml
  29. +10
    -0
      .idea/runConfigurations.xml
  30. +0
    -6
      .idea/runConfigurations/example_lib_main_dart.xml
  31. +124
    -0
      .idea/uiDesigner.xml
  32. +0
    -67
      .idea/workspace.xml
  33. +5
    -0
      .vscode/settings.json
  34. +0
    -260
      CHANGELOG.md
  35. +0
    -21
      LICENSE
  36. +47
    -250
      README.md
  37. +0
    -8
      android/.gitignore
  38. +0
    -39
      android/build.gradle
  39. +0
    -4
      android/gradle.properties
  40. BIN
      android/gradle/wrapper/gradle-wrapper.jar
  41. +0
    -1
      android/settings.gradle
  42. +0
    -3
      android/src/main/AndroidManifest.xml
  43. +0
    -8
      android/src/main/java/com/ryanheise/audioservice/AudioInterruption.java
  44. +0
    -16
      android/src/main/java/com/ryanheise/audioservice/AudioProcessingState.java
  45. +0
    -852
      android/src/main/java/com/ryanheise/audioservice/AudioService.java
  46. +0
    -1069
      android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java
  47. +0
    -19
      android/src/main/java/com/ryanheise/audioservice/MediaButtonReceiver.java
  48. +0
    -7
      android/src/main/java/com/ryanheise/audioservice/MediaControl.java
  49. +0
    -11
      android/src/main/java/com/ryanheise/audioservice/Size.java
  50. BIN
      android/src/main/res/drawable-hdpi/audio_service_fast_forward.png
  51. BIN
      android/src/main/res/drawable-hdpi/audio_service_fast_rewind.png
  52. BIN
      android/src/main/res/drawable-hdpi/audio_service_pause.png
  53. BIN
      android/src/main/res/drawable-hdpi/audio_service_play_arrow.png
  54. BIN
      android/src/main/res/drawable-hdpi/audio_service_skip_next.png
  55. BIN
      android/src/main/res/drawable-hdpi/audio_service_skip_previous.png
  56. BIN
      android/src/main/res/drawable-hdpi/audio_service_stop.png
  57. BIN
      android/src/main/res/drawable-hdpi/ic_favorite.png
  58. BIN
      android/src/main/res/drawable-mdpi/audio_service_fast_forward.png
  59. BIN
      android/src/main/res/drawable-mdpi/audio_service_fast_rewind.png
  60. BIN
      android/src/main/res/drawable-mdpi/audio_service_pause.png
  61. BIN
      android/src/main/res/drawable-mdpi/audio_service_play_arrow.png
  62. BIN
      android/src/main/res/drawable-mdpi/audio_service_skip_next.png
  63. BIN
      android/src/main/res/drawable-mdpi/audio_service_skip_previous.png
  64. BIN
      android/src/main/res/drawable-mdpi/audio_service_stop.png
  65. BIN
      android/src/main/res/drawable-mdpi/ic_favorite.png
  66. BIN
      android/src/main/res/drawable-xhdpi/audio_service_fast_forward.png
  67. BIN
      android/src/main/res/drawable-xhdpi/audio_service_fast_rewind.png
  68. BIN
      android/src/main/res/drawable-xhdpi/audio_service_pause.png
  69. BIN
      android/src/main/res/drawable-xhdpi/audio_service_play_arrow.png
  70. BIN
      android/src/main/res/drawable-xhdpi/audio_service_skip_next.png
  71. BIN
      android/src/main/res/drawable-xhdpi/audio_service_skip_previous.png
  72. BIN
      android/src/main/res/drawable-xhdpi/audio_service_stop.png
  73. BIN
      android/src/main/res/drawable-xhdpi/ic_favorite.png
  74. BIN
      android/src/main/res/drawable-xxhdpi/audio_service_fast_forward.png
  75. BIN
      android/src/main/res/drawable-xxhdpi/audio_service_fast_rewind.png
  76. BIN
      android/src/main/res/drawable-xxhdpi/audio_service_pause.png
  77. BIN
      android/src/main/res/drawable-xxhdpi/audio_service_play_arrow.png
  78. BIN
      android/src/main/res/drawable-xxhdpi/audio_service_skip_next.png
  79. BIN
      android/src/main/res/drawable-xxhdpi/audio_service_skip_previous.png
  80. BIN
      android/src/main/res/drawable-xxhdpi/audio_service_stop.png
  81. BIN
      android/src/main/res/drawable-xxhdpi/ic_favorite.png
  82. BIN
      android/src/main/res/drawable-xxxhdpi/audio_service_fast_forward.png
  83. BIN
      android/src/main/res/drawable-xxxhdpi/audio_service_fast_rewind.png
  84. BIN
      android/src/main/res/drawable-xxxhdpi/audio_service_pause.png
  85. BIN
      android/src/main/res/drawable-xxxhdpi/audio_service_play_arrow.png
  86. BIN
      android/src/main/res/drawable-xxxhdpi/audio_service_skip_next.png
  87. BIN
      android/src/main/res/drawable-xxxhdpi/audio_service_skip_previous.png
  88. BIN
      android/src/main/res/drawable-xxxhdpi/audio_service_stop.png
  89. BIN
      android/src/main/res/drawable-xxxhdpi/ic_favorite.png
  90. +0
    -19
      audio_service.iml
  91. +0
    -30
      audio_service_android.iml
  92. +9
    -0
      build.gradle.kts
  93. +0
    -617
      darwin/Classes/AudioServicePlugin.m
  94. +20
    -0
      gradle.properties
  95. BIN
      gradle/wrapper/gradle-wrapper.jar
  96. +1
    -2
      gradle/wrapper/gradle-wrapper.properties
  97. +34
    -21
      gradlew
  98. +89
    -84
      gradlew.bat
  99. +0
    -37
      ios/.gitignore
  100. +0
    -0
      ios/Assets/.gitkeep

+ 0
- 1
.github/FUNDING.yml View File

@ -1 +0,0 @@
github: ryanheise

+ 0
- 80
.github/ISSUE_TEMPLATE/bug_report.md View File

@ -1,80 +0,0 @@
---
name: Bug report
about: Follow the instructions carefully on the next page.
title: ''
labels: 1 backlog, bug
assignees: ryanheise
---
<!--
Note: Issues that don't follow these instructions will be closed,
therefore please read them carefully.
1. A bug report must demonstrate a bug in the plugin, and not merely a
bug in your app. Understand that this plugin WILL throw exceptions
or otherwise misbehave if not used in accordance with the
documentation. In order to verify that you have indeed found a bug,
you will need to make a reference to the documentation in order to
explain how the actual behaviour you experienced is different from
the behaviour that was documented. If the behaviour you want is
undocumented, please submit either a documentation request or a
feature request instead, whichever is more appropriate.
2. You must supply a link to a minimal reproduction project and explain
what steps I need to perform (as a user) in the app to reproduce the
bug. A minimal reproduction project can be created by forking this
project and making the minimal number of changes required to the
example to reproduce the bug. Do not post code directly into the bug
report, it must be a link to a git repo that I can clone and then
immediately run.
3. Leave all markdown formatting in this template intact. Do not modify
the section headings in any way, and insert your answers below each
section heading. Use code markdown (3 backticks) when inserting
errors and logs, not only for readability, but also to avoid issue
reference spamming using the # symbol.
THANK YOU :-D
-->
**Which API doesn't behave as documented, and how does it misbehave?**
Name here the specific methods or fields that are not behaving as documented, and explain clearly what is happening.
**Minimal reproduction project**
Provide a link here using one of two options:
1. Fork this repository and modify the example to reproduce the bug, then provide a link here.
2. If the unmodified official example already reproduces the bug, just write "The example".
**To Reproduce (i.e. user steps, not code)**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Error messages**
```
If applicable, copy & paste error message here, within the triple quotes to preserve formatting.
```
**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 if relevant):**
- Device: [e.g. Samsung Galaxy Note 8]
- OS: [e.g. Android 8.0.0]
**Flutter SDK version**
```
insert output of "flutter doctor" here
```
**Additional context**
Add any other context about the problem here.

+ 0
- 8
.github/ISSUE_TEMPLATE/config.yml View File

@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Stack Overflow
url: https://stackoverflow.com/search?q=audio_service
about: Ask here if it's not a bug report, documentation request or feature request.
- name: Gitter
url: https://gitter.im/flutter/flutter
about: Ask here if you want to have a live chat with other Flutter developers.

+ 0
- 43
.github/ISSUE_TEMPLATE/documentation-request.md View File

@ -1,43 +0,0 @@
---
name: Documentation request
about: Follow the instructions carefully on the next page.
title: ''
labels: 1 backlog, documentation
assignees: ryanheise
---
<!--
Note: Issues that don't follow these instructions will be closed,
therefore please read them carefully.
1. This form is not intended for asking questions or asking for
support. For that, you are advised to ask your question on
StackOverflow or Gitter. Instead, this form is intended for people
who wish to help improve this plugin's documentation in a concrete
way.
2. To that end, it is required that you link to the specific
page/section, and quote the words that are unclear (unless you are
proposing an entirely new section), and describe how you would like
it to be improved.
THANK YOU :-D
-->
**To which pages does your suggestion apply?**
- Direct URL 1
- Direct URL 2
- ...
**Quote the sentences(s) from the documentation to be improved (if any)**
> Insert here. (Skip if you are proposing an entirely new section.)
**Describe your suggestion**
...

+ 0
- 49
.github/ISSUE_TEMPLATE/feature_request.md View File

@ -1,49 +0,0 @@
---
name: Feature request
about: Follow the instructions carefully on the next page.
title: ''
labels: 1 backlog, enhancement
assignees: ryanheise
---
<!--
Note: Issues that don't follow these instructions will be closed,
therefore please read them carefully.
1. A prerequisite before requesting a feature is that you familiarise
yourself with the existing features by reading the API
documentation.
2. If it is unclear from the documentation whether an existing feature
is the one you want, this is a shortcoming of the documentation. In
this case, please submit a documentation request instead.
3. Do not use this form for asking questions. My goal is to provide
good documentation that answers your questions, and if the
documentation isn't doing its job, please submit a documentation
request to help me to improve it. Remember that the purpose of this
GitHub issues page is for plugin development and not for support
(community support is available via StackOverflow and Gitter).
4. You must complete at least the first 3 sections below. Leave the
section headings intact, and insert your answers below each heading.
THANK YOU :-D
-->
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

+ 0
- 19
.github/ISSUE_TEMPLATE/frequently-asked-questions.md View File

@ -1,19 +0,0 @@
---
name: Frequently Asked Questions
about: Suggest a new question for the Wiki FAQ
title: ''
labels: 1 backlog, question
assignees: ryanheise
---
## Checklist
<!-- Replace [ ] with [x] to confirm an item in the checklist -->
- [ ] The question is not already in the FAQ.
- [ ] The question is not too narrow or specific to a particular application.
## Suggested Question
Write the question here.

+ 0
- 12
.github/workflows/auto-close.yml View File

@ -1,12 +0,0 @@
name: Autocloser
on: [issues]
jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose issues that did not follow issue template
uses: roots/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-close-message: "This issue was automatically closed because it did not follow the issue template."
issue-pattern: "Which API(.|[\\r\\n])*Minimal reproduction project(.|[\\r\\n])*To Reproduce|To which pages(.|[\\r\\n])*Describe your suggestion|Is your feature request(.|[\\r\\n])*Describe the solution you'd like"

+ 82
- 19
.gitignore View File

@ -1,22 +1,85 @@
.DS_Store
.dart_tool/
.packages
.pub/
pubspec.lock
.idea/shelf
/confluence/target
/dependencies/repo
/android.tests.dependencies
/dependencies/android.tests.dependencies
/dist
/local
/gh-pages
/ideaSDK
/clionSDK
/android-studio/sdk
out/
/tmp
kotlin-ide/
workspace.xml
*.versionsBackup
/idea/testData/debugger/tinyApp/classes*
/jps-plugin/testData/kannotator
/js/js.translator/testData/out/
/js/js.translator/testData/out-min/
/js/js.translator/testData/out-pir/
.gradle/
build/
!**/src/**/build
!**/test/**/build
!**/testData/**/*.iml
.idea/libraries/Gradle*.xml
.idea/libraries/Maven*.xml
.idea/artifacts/PILL_*.xml
.idea/artifacts/KotlinPlugin.xml
.idea/modules
.idea/runConfigurations/JPS_*.xml
.idea/runConfigurations/PILL_*.xml
.idea/runConfigurations/_FP_*.xml
.idea/runConfigurations/_MT_*.xml
.idea/libraries
.idea/modules.xml
.idea/gradle.xml
.idea/compiler.xml
.idea/inspectionProfiles/profiles_settings.xml
.idea/.name
.idea/artifacts/dist_auto_*
.idea/artifacts/dist.xml
.idea/artifacts/ideaPlugin.xml
.idea/artifacts/kotlinc.xml
.idea/artifacts/kotlin_compiler_jar.xml
.idea/artifacts/kotlin_plugin_jar.xml
.idea/artifacts/kotlin_jps_plugin_jar.xml
.idea/artifacts/kotlin_daemon_client_jar.xml
.idea/artifacts/kotlin_imports_dumper_compiler_plugin_jar.xml
.idea/artifacts/kotlin_main_kts_jar.xml
.idea/artifacts/kotlin_compiler_client_embeddable_jar.xml
.idea/artifacts/kotlin_reflect_jar.xml
.idea/artifacts/kotlin_stdlib_js_ir_*
.idea/artifacts/kotlin_test_js_ir_*
.idea/artifacts/kotlin_stdlib_wasm_*
.idea/jarRepositories.xml
.idea/csv-plugin.xml
.idea/libraries-with-intellij-classes.xml
node_modules/
.rpt2_cache/
libraries/tools/kotlin-test-js-runner/lib/
local.properties
buildSrcTmp/
distTmp/
outTmp/
/test.output
/kotlin-native/dist
**/.cxx
doc/
**/ios/Flutter/flutter_export_environment.sh
android/.project
example/android/.project
android/.classpath
android/.settings/org.eclipse.buildship.core.prefs
example/android/.settings/org.eclipse.buildship.core.prefs
example/android/app/.classpath
example/android/app/.project
example/android/app/.settings/org.eclipse.buildship.core.prefs
.flutter-plugins
.flutter-plugins-dependencies
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
.idea/jarRepositories.xml
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml

+ 0
- 2
.idea/.gitignore View File

@ -1,2 +0,0 @@
# Project exclude paths
/.

+ 8
- 0
.idea/artifacts/lib_js_0_1_0.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="lib-js-0.1.0">
<output-path>$PROJECT_DIR$/lib/build/libs</output-path>
<root id="archive" name="lib-js-0.1.0.jar">
<element id="module-output" name="unfreeze.lib.jsMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/lib_jvm_0_1_0.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="lib-jvm-0.1.0">
<output-path>$PROJECT_DIR$/lib/build/libs</output-path>
<root id="archive" name="lib-jvm-0.1.0.jar">
<element id="module-output" name="unfreeze.lib.jvmMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/platypus_js_0_1_0.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="platypus-js-0.1.0">
<output-path>$PROJECT_DIR$/platypus/build/libs</output-path>
<root id="archive" name="platypus-js-0.1.0.jar">
<element id="module-output" name="unfreeze.platypus.jsMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/platypus_jvm_0_1_0.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="platypus-jvm-0.1.0">
<output-path>$PROJECT_DIR$/platypus/build/libs</output-path>
<root id="archive" name="platypus-jvm-0.1.0.jar">
<element id="module-output" name="unfreeze.platypus.jvmMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/unfreeze_js_0_1.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="unfreeze-js-0.1">
<output-path>$PROJECT_DIR$/unfreeze/build/libs</output-path>
<root id="archive" name="unfreeze-js-0.1.jar">
<element id="module-output" name="unfreeze.unfreeze.jsMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/unfreeze_js_0_1_0.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="unfreeze-js-0.1.0">
<output-path>$PROJECT_DIR$/unfreeze/build/libs</output-path>
<root id="archive" name="unfreeze-js-0.1.0.jar">
<element id="module-output" name="unfreeze.unfreeze.jsMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/unfreeze_js_1_0_SNAPSHOT.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="unfreeze-js-1.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/unfreeze/build/libs</output-path>
<root id="archive" name="unfreeze-js-1.0-SNAPSHOT.jar">
<element id="module-output" name="unfreeze.unfreeze.jsMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/unfreeze_jslegacy_0_1.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="unfreeze-jslegacy-0.1">
<output-path>$PROJECT_DIR$/unfreeze/build/libs</output-path>
<root id="archive" name="unfreeze-jslegacy-0.1.jar">
<element id="module-output" name="unfreeze.unfreeze.jsMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/unfreeze_jvm_0_1.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="unfreeze-jvm-0.1">
<output-path>$PROJECT_DIR$/unfreeze/build/libs</output-path>
<root id="archive" name="unfreeze-jvm-0.1.jar">
<element id="module-output" name="unfreeze.unfreeze.jvmMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/unfreeze_jvm_0_1_0.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="unfreeze-jvm-0.1.0">
<output-path>$PROJECT_DIR$/unfreeze/build/libs</output-path>
<root id="archive" name="unfreeze-jvm-0.1.0.jar">
<element id="module-output" name="unfreeze.unfreeze.jvmMain" />
</root>
</artifact>
</component>

+ 8
- 0
.idea/artifacts/unfreeze_jvm_1_0_SNAPSHOT.xml View File

@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="unfreeze-jvm-1.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/unfreeze/build/libs</output-path>
<root id="archive" name="unfreeze-jvm-1.0-SNAPSHOT.jar">
<element id="module-output" name="unfreeze.unfreeze.jvmMain" />
</root>
</artifact>
</component>

+ 0
- 116
.idea/codeStyles/Project.xml View File

@ -1,116 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

+ 6
- 0
.idea/inspectionProfiles/Project_Default.xml View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Reformat" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>

+ 26
- 0
.idea/kotlinScripting.xml View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinScriptingSettings">
<scriptDefinition className="org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate" definitionName="KotlinBuildScript">
<order>2</order>
</scriptDefinition>
<scriptDefinition className="org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate" definitionName="KotlinInitScript">
<order>0</order>
</scriptDefinition>
<scriptDefinition className="org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate" definitionName="KotlinSettingsScript">
<order>1</order>
</scriptDefinition>
<scriptDefinition className="circlet.pipelines.config.dsl.scriptdefinition.ProjectScriptDefinition" definitionName="ProjectScriptDefinition">
<order>3</order>
</scriptDefinition>
<scriptDefinition className="org.jetbrains.kotlin.mainKts.MainKtsScript" definitionName="MainKtsScript">
<order>4</order>
</scriptDefinition>
<scriptDefinition className="kotlin.script.templates.standard.ScriptTemplateWithBindings" definitionName="Script definition for extension scripts and IDE console">
<order>5</order>
</scriptDefinition>
<scriptDefinition className="org.jetbrains.kotlin.idea.core.script.StandardIdeScriptDefinition" definitionName="Kotlin Script">
<order>6</order>
</scriptDefinition>
</component>
</project>

+ 0
- 19
.idea/libraries/Dart_SDK.xml View File

@ -1,19 +0,0 @@
<component name="libraryTable">
<library name="Dart SDK">
<CLASSES>
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/async" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/collection" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/convert" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/core" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/developer" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/html" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/io" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/isolate" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/math" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/mirrors" />
<root url="file:///home/ryan/opt/flutter/bin/cache/dart-sdk/lib/typed_data" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

+ 0
- 15
.idea/libraries/Flutter_Plugins.xml View File

@ -1,15 +0,0 @@
<component name="libraryTable">
<library name="Flutter Plugins" type="FlutterPluginsLibraryType">
<CLASSES>
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_isolate-1.0.0+14" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.1+1" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/audio_session-0.0.7" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4+4" />
<root url="file://$PROJECT_DIR$" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.14" />
<root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_linux-0.0.1+2" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

+ 0
- 9
.idea/libraries/Flutter_for_Android.xml View File

@ -1,9 +0,0 @@
<component name="libraryTable">
<library name="Flutter for Android">
<CLASSES>
<root url="jar:///home/ryan/opt/flutter/bin/cache/artifacts/engine/android-arm/flutter.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

+ 5
- 0
.idea/misc.xml View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_15_PREVIEW" project-jdk-name="azul-15" project-jdk-type="JavaSDK" />
</project>

+ 0
- 9
.idea/modules.xml View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/audio_service.iml" filepath="$PROJECT_DIR$/audio_service.iml" />
<module fileurl="file://$PROJECT_DIR$/audio_service_android.iml" filepath="$PROJECT_DIR$/audio_service_android.iml" />
</modules>
</component>
</project>

+ 10
- 0
.idea/runConfigurations.xml View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

+ 0
- 6
.idea/runConfigurations/example_lib_main_dart.xml View File

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="example/lib/main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
<option name="filePath" value="$PROJECT_DIR$/example/lib/main.dart" />
<method />
</configuration>
</component>

+ 124
- 0
.idea/uiDesigner.xml View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

+ 0
- 67
.idea/workspace.xml View File

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="10fe4e03-808b-4cca-b552-b754ebc9fc8e" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/lib/audio_service.dart" beforeDir="false" afterPath="$PROJECT_DIR$/lib/audio_service.dart" afterDir="false" />
</list>
<option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="Android10" />
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="IgnoredFileRootStore">
<option name="generatedRoots">
<set>
<option value="$PROJECT_DIR$/.idea" />
</set>
</option>
</component>
<component name="ProjectId" id="1heOT4v7Yxgr9Nb2PRjB67yYpUz" />
<component name="PropertiesComponent">
<property name="dart.analysis.tool.window.force.activate" value="true" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="show.migrate.to.gradle.popup" value="false" />
</component>
<component name="RunDashboard">
<option name="ruleStates">
<list>
<RuleState>
<option name="name" value="ConfigurationTypeDashboardGroupingRule" />
</RuleState>
<RuleState>
<option name="name" value="StatusDashboardGroupingRule" />
</RuleState>
</list>
</option>
</component>
<component name="SvnConfiguration">
<configuration />
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="10fe4e03-808b-4cca-b552-b754ebc9fc8e" name="Default Changelist" comment="" />
<created>1600368096000</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1600368096000</updated>
</task>
<servers />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
</project>

+ 5
- 0
.vscode/settings.json View File

@ -0,0 +1,5 @@
{
"cSpell.words": [
"Deezer"
]
}

+ 0
- 260
CHANGELOG.md View File

@ -1,260 +0,0 @@
## 0.15.1
* Fix loading of file:// artUri values.
* Allow booleans/doubles in MediaItems.
* Silently ignore duplicate onStop requests.
## 0.15.0
* Web support (@keaganhilliard)
* macOS support (@hacker1024)
* Route next/previous buttons to onClick on Android (@stonega)
* Correctly scale skip intervals for control center (@subhash279)
* Handle repeated stop/start calls more robustly.
* Fix Android 11 bugs.
## 0.14.1
* audio_session dependency now supports minSdkVersion 16 on Android.
## 0.14.0
* audio session management now handled by audio_session (see [Migration Guide](https://github.com/ryanheise/audio_service/wiki/Migration-Guide#0140)).
* Exceptions in background audio task are logged and forwarded to client.
## 0.13.0
* All BackgroundAudioTask callbacks are now async.
* Add default implementation of onSkipToNext/onSkipToPrevious.
* Bug fixes.
## 0.12.0
* Add setRepeatMode/setShuffleMode.
* Enable iOS Control Center buttons based on setState.
* Support seek forward/backward in iOS Control Center.
* Add default behaviour to BackgroundAudioTask.
* Bug fixes.
* Simplify example.
## 0.11.2
* Fix bug with album metadata on Android.
## 0.11.1
* Allow setting the iOS audio session category and options.
* Allow AudioServiceWidget to recognise swipe gesture on iOS.
* Check for null title and album on Android.
## 0.11.0
* Breaking change: onStop must await super.onStop to shutdown task.
* Fix Android memory leak.
## 0.10.0
* Replace androidStopOnRemoveTask with onTaskRemoved callback.
* Add onClose callback.
* Breaking change: new MediaButtonReceiver in AndroidManifest.xml.
## 0.9.0
* New state model: split into playing + processingState.
* androidStopForegroundOnPause ties foreground state to playing state.
* Add MediaItem.toJson/fromJson.
* Add AudioService.notificationClickEventStream (Android).
* Add AudioService.updateMediaItem.
* Add AudioService.setSpeed.
* Add PlaybackState.bufferedPosition.
* Add custom AudioService.start parameters.
* Rename replaceQueue -> updateQueue.
* Rename Android-specific start parameters with android- prefix.
* Use Duration type for all time values.
* Pass fastForward/rewind intervals through to background task.
* Allow connections from background contexts (e.g. android_alarm_manager).
* Unify iOS/Android focus APIs.
* Bug fixes and dependency updates.
## 0.8.0
* Allow UI to await the result of custom actions.
* Allow background to broadcast custom events to UI.
* Improve memory management for art bitmaps on Android.
* Convenience methods: replaceQueue, playMediaItem, addQueueItems.
* Bug fixes and dependency updates.
## 0.7.2
* Shutdown background task if task killed by IO (Android).
* Bug fixes and dependency updates.
## 0.7.1
* Add AudioServiceWidget to auto-manage connections.
* Allow file URIs for artUri.
## 0.7.0
* Support skip forward/backward in command center (iOS).
* Add 'extras' field to MediaItem.
* Artwork caching and preloading supported on Android+iOS.
* Bug fixes.
## 0.6.2
* Bug fixes.
## 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.
## 0.5.3
* Support Queue, album art and other missing features on iOS.
## 0.5.2
* Update documentation and example.
## 0.5.1
* Playback state broadcast on connect (iOS).
## 0.5.0
* Partial iOS support.
## 0.4.2
* Option to call stopForeground on pause.
## 0.4.1
* Fix queue support bug
## 0.4.0
* Breaking change: AudioServiceBackground.run takes a single parameter.
## 0.3.1
* Update example to disconnect when pressing back button.
## 0.3.0
* Breaking change: updateTime now measured since epoch instead of boot time.
## 0.2.1
* Streams use RxDart BehaviorSubject.
## 0.2.0
* Migrate to AndroidX.
## 0.1.1
* Bump targetSdkVersion to 28
* Clear client-side metadata and state on stop.
## 0.1.0
* onClick is now always called for media button clicks.
* Option to set notifications as ongoing.
## 0.0.15
* Option to set subText in notification.
* Support media item ratings
## 0.0.14
* Can update existing media items.
* Can specify order of Android notification compact actions.
* Bug fix with connect.
## 0.0.13
* Option to preload artwork.
* Allow client to browse media items.
## 0.0.12
* More options to customise the notification content.
## 0.0.11
* Breaking API changes.
* Connection callbacks replaced by a streams API.
* AudioService properties for playbackState, currentMediaItem, queue.
* Option to set Android notification channel description.
* AudioService.customAction awaits completion of the action.
## 0.0.10
* Bug fixes with queue management.
* AudioService.start completes when the background task is ready.
## 0.0.9
* Support queue management.
## 0.0.8
* Bug fix.
## 0.0.7
* onMediaChanged takes MediaItem parameter.
* Support playFromMediaId, fastForward, rewind.
## 0.0.6
* All APIs address media items by String mediaId.
## 0.0.5
* Show media art in notification and lock screen.
## 0.0.4
* Support and example for playing TextToSpeech.
* Click notification to launch UI.
* More properties added to MediaItem.
* Minor API changes.
## 0.0.3
* Pause now keeps background isolate running
* Notification channel id is generated from package name
* Updated example to use audioplayer plugin
* Fixed media button handling
## 0.0.2
* Better connection handling.
## 0.0.1
* Initial release.

+ 0
- 21
LICENSE View File

@ -1,21 +0,0 @@
MIT License
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 47
- 250
README.md View File

@ -1,269 +1,66 @@
# audio_service
# UnFreeze
This plugin wraps around your existing audio code to allow it to run in the background or with the screen turned off, and allows your app to interact with headset buttons, the Android lock screen and notification, iOS control center, wearables and Android Auto. It is suitable for:
This is a lib that serves as UNified FREEZEr backend
* Music players
* Text-to-speech readers
* Podcast players
* Navigators
* More!
Its goal is to replace server stack in FreezerPC and java code from Freezer
## How does this plugin work?
This lib primary platforms are:
- Node (FreezerPC)
- Android (Freezer)
You encapsulate your audio code in a background task which runs in a special isolate that continues to run when your UI is absent. Your background task implements callbacks to respond to playback requests coming from your Flutter UI, headset buttons, the lock screen, notification, iOS control center, car displays and smart watches:
but there is also low effort to support other platforms including (but not limited to):
+ JVM
- Web
- Electron
- ...
![audio_service_callbacks](https://user-images.githubusercontent.com/19899190/84386442-b305cc80-ac34-11ea-8c2f-1b4cb126a98d.png)
And maybe someday FreezerPC will be liberated from Chromium/Electron/Node
You can implement these callbacks to play any sort of audio that is appropriate for your app, such as music files or streams, audio assets, text to speech, synthesised audio, or combinations of these.
## Importing
| Feature | Android | iOS | macOS | Web |
| ------- | :-------: | :-----: | :-----: | :-----: |
| background audio | ✅ | ✅ | ✅ | ✅ |
| headset clicks | ✅ | ✅ | ✅ | ✅ |
| start/stop/play/pause/seek/rate | ✅ | ✅ | ✅ | ✅ |
| fast forward/rewind | ✅ | ✅ | ✅ | ✅ |
| repeat/shuffle mode | ✅ | ✅ | ✅ | ✅ |
| queue manipulation, skip next/prev | ✅ | ✅ | ✅ | ✅ |
| custom actions | ✅ | ✅ | ✅ | ✅ |
| custom events | ✅ | ✅ | ✅ | ✅ |
| notifications/control center | ✅ | ✅ | ✅ | ✅ |
| lock screen controls | ✅ | ✅ | | ✅ |
| album art | ✅ | ✅ | ✅ | ✅ |
| Android Auto, Apple CarPlay | (untested) | ✅ | | |
### NODE
To use this lib in node you must compile it to node package with: ` ./gradlew packJsNpmPublication`.
Result is `unfreeze/build/publications/npm/unfreeze-[version].tgz` so to push it to npm git branch for release use: `./npm.sh [version]`
If you'd like to help with any missing features, please join us on the [GitHub issues page](https://github.com/ryanheise/audio_service/issues).
Use git npm branch:
`"unfreeze": "git+https://git.freezer.life/p24/unfreeze.git#npm"`
## Migrating to 0.14.0
or for using local build:
`"unfreeze": "file:../../unfreeze/build/js/packages/unfreeze-unfreeze"`
Audio focus, interruptions (e.g. phone calls), mixing, ducking and the configuration of your app's audio category and attributes, are now handled by the [audio_session](https://pub.dev/packages/audio_session) package. Read the [Migration Guide](https://github.com/ryanheise/audio_service/wiki/Migration-Guide#0140) for details.
## Can I make use of other plugins within the background audio task?
Yes! `audio_service` is designed to let you implement the audio logic however you want, using whatever plugins you want. You can 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, within your background audio task. There are also plugins like [just_audio_service](https://github.com/yringler/just_audio_service) that provide default implementations of `BackgroundAudioTask` to make your job easier.
Note that this plugin will not work with other audio plugins that overlap in responsibility with this plugin (i.e. background audio, iOS control center, Android notifications, lock screen, headset buttons, etc.)
## Example
### Background code
Your audio code will run in a special background isolate, separate and detachable from your app's UI. To achieve this, define a subclass of `BackgroundAudioTask` that overrides a set of callbacks to respond to client requests:
```dart
class MyBackgroundTask extends BackgroundAudioTask {
// Initialise your audio task.
onStart(Map<String, dynamic> params) {}
// Handle a request to stop audio and finish the task.
onStop() async {}
// Handle a request to play audio.
onPlay() {}
// Handle a request to pause audio.
onPause() {}
// Handle a headset button click (play/pause, skip next/prev).
onClick(MediaButton button) {}
// Handle a request to skip to the next queue item.
onSkipToNext() {}
// Handle a request to skip to the previous queue item.
onSkipToPrevious() {}
// Handle a request to seek to a position.
onSeekTo(Duration position) {}
### Android
Add this to `settings.gradle.kts`:
```kotlin
sourceControl {
gitRepository(java.net.URI.create("https://git.freezer.life/p24/unfreeze.git")) {
producesModule("f.f.unfreeze:unfreeze")
}
}
```
You can implement these (and other) callbacks to play any type of audio depending on the requirements of your app. For example, if you are building a podcast player, you may have code such as the following:
```dart
import 'package:just_audio/just_audio.dart';
class PodcastBackgroundTask extends BackgroundAudioTask {
AudioPlayer _player = AudioPlayer();
onPlay() async {
_player.play();
// Show the media notification, and let all clients know what
// playback state and media item to display.
await AudioServiceBackground.setState(playing: true, ...);
await AudioServiceBackground.setMediaItem(MediaItem(title: "Hey Jude", ...))
}
```
If you are instead building a text-to-speech reader, you may have code such as the following:
```dart
import 'package:flutter_tts/flutter_tts.dart';
class ReaderBackgroundTask extends BackgroundAudioTask {
FlutterTts _tts = FlutterTts();
String article;
onPlay() async {
_tts.speak(article);
// Show the media notification, and let all clients know what
// playback state and media item to display.
await AudioServiceBackground.setState(playing: true, ...);
await AudioServiceBackground.setMediaItem(MediaItem(album: "Business Insider", ...))
}
}
```
There are several methods in the `AudioServiceBackground` class that are made available to your background audio task to allow it to communicate to clients outside the isolate, such as your Flutter UI (if present), the iOS control center, the Android notification and lock screen. These are:
* `AudioServiceBackground.setState` broadcasts the current playback state to all clients. This includes whether or not audio is playing, but also whether audio is buffering, the current playback position and buffer position, the current playback speed, and the set of audio controls that should be made available. When you broadcast this information to all clients, it allows them to update their user interfaces to show the appropriate set of buttons, and show the correct audio position on seek bars, for example. It is important for you to call this method whenever any of these pieces of state changes. You will typically want to call this method from your `onStart`, `onPlay`, `onPause`, `onSkipToNext`, `onSkipToPrevious` and `onStop` callbacks.
* `AudioServiceBackground.setMediaItem` broadcasts the currently playing media item to all clients. This includes the track title, artist, genre, duration, any artwork to display, and other information. When you broadcast this information to all clients, it allows them to update their user interface accordingly so that it is displayed on the lock screen, the notification, and in your Flutter UI (if present). You will typically want to call this method from your `onStart`, `onSkipToNext` and `onSkipToPrevious` callbacks.
* `AudioServiceBackground.setQueue` broadcasts the current queue to all clients. Some clients like Android Auto may display this information in their user interfaces. You will typically want to call this method from your `onStart` callback. Other callbacks exist where it may be appropriate to call this method such as `onAddQueueItem` and `onRemoveQueueItem`.
### UI code
Connecting to `AudioService`:
```dart
// Wrap your "/" route's widget tree in an AudioServiceWidget:
return MaterialApp(
home: AudioServiceWidget(MainScreen()),
);
```
Starting your background audio task:
```dart
await AudioService.start(
backgroundTaskEntrypoint: _myEntrypoint,
androidNotificationIcon: 'mipmap/ic_launcher',
// An example of passing custom parameters.
// These will be passed through to your `onStart` callback.
params: {'url': 'https://somewhere.com/sometrack.mp3'},
);
// this must be a top-level function
void _myEntrypoint() => AudioServiceBackground.run(() => MyBackgroundTask());
```
Sending messages to it:
* `AudioService.play()`
* `AudioService.pause()`
* `AudioService.click()`
* `AudioService.skipToNext()`
* `AudioService.skipToPrevious()`
* `AudioService.seekTo(Duration(seconds: 53))`
Shutting it down:
```dart
// This will pass through to your `onStop` callback.
AudioService.stop();
```
Reacting to state changes:
* `AudioService.playbackStateStream` (e.g. playing/paused, buffering/ready)
* `AudioService.currentMediaItemStream` (metadata about the currently playing media item)
* `AudioService.queueStream` (the current queue/playlist)
Keep in mind that your UI and background task run in separate isolates and do not share memory. The only way they communicate is via message passing. Your Flutter UI will only use the `AudioService` API to communicate with the background task, while your background task will only use the `AudioServiceBackground` API to interact with the clients, which include the Flutter UI.
### Connecting to `AudioService` from the background
You can also send messages to your background audio task from another background callback (e.g. android_alarm_manager) by manually connecting to it:
```dart
await AudioService.connect(); // Note: the "await" is necessary!
AudioService.play();
```
## Configuring the audio session
If your app uses audio, you should tell the operating system what kind of usage scenario your app has and how your app will interact with other audio apps on the device. Different audio apps often have unique requirements. For example, when a navigator app speaks driving instructions, a music player should duck its audio while a podcast player should pause its audio. Depending on which one of these three apps you are building, you will need to configure your app's audio settings and callbacks to appropriately handle these interactions.
Use the [audio_session](https://pub.dev/packages/audio_session) package to change the default audio session configuration for your app. E.g. for a podcast player, you may use:
```dart
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration.speech());
```
Each time you invoke an audio plugin to play audio, that plugin will activate your app's shared audio session to inform the operating system that your app is actively playing audio. Depending on the configuration set above, this will also inform other audio apps to either stop playing audio, or possibly continue playing at a lower volume (i.e. ducking). You normally do not need to activate the audio session yourself, however if the audio plugin you use does not activate the audio session, you can activate it yourself:
```dart
// Activate the audio session before playing audio.
if (await session.setActive(true)) {
// Now play audio.
} else {
// The request was denied and the app should not play audio
}
```
When another app activates its audio session, it similarly may ask your app to pause or duck its audio. Once again, the particular audio plugin you use may automatically pause or duck audio when requested. However, if it does not, you can respond to these events yourself by listening to `session.interruptionEventStream`. Similarly, if the audio plugin doesn't handle unplugged headphone events, you can respond to these yourself by listening to `session.becomingNoisyEventStream`. For more information, consult the documentation for [audio_session](https://pub.dev/packages/audio_session).
Note: If your app uses a number of different audio plugins, e.g. for audio recording, or text to speech, or background audio, it is possible that those plugins may internally override each other's audio session settings since there is only a single audio session shared by your app. Therefore, it is recommended that you apply your own preferred configuration using audio_session after all other audio plugins have loaded. You may consider asking the developer of each audio plugin you use to provide an option to not overwrite these global settings and allow them be managed externally.
## Android setup
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 update your project to follow the [new project template](https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects).
Additionally:
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 ...>
...
<service android:name="com.ryanheise.audioservice.AudioService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver android:name="com.ryanheise.audioservice.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</application>
</manifest>
```
2. Starting from Flutter 1.12, you will need to disable the `shrinkResources` setting in your `android/app/build.gradle` file, otherwise the icon resources used in the Android notification will be removed during the build:
```
android {
compileSdkVersion 28
...
buildTypes {
release {
signingConfig ...
shrinkResources false // ADD THIS LINE
}
and this to `build.gradle.kts`:
```kotlin
implementation('f.f.unfreeze:unfreeze') {
version {
branch = 'master'
}
}
```
## iOS setup
Insert this in your `Info.plist` file:
```
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
```
The example project may be consulted for context.
## On Freezer
Check out [freezer](https://git.freezer.life/p24/freezer/src/branch/master) and [freezerpc](https://git.freezer.life/p24/freezerpc/src/branch/uni)
## macOS setup
The minimum supported macOS version is 10.12.2 (though this could be changed with some work in the future).
Modify the platform line in `macos/Podfile` to look like the following:
## Structure (TODO)
```
platform :osx, '10.12.2'
Unfreeze
├── Platypus (Platform wrapper that makes it self comfortable on all platforms and situations like Perry the Platypus)
│ ├── Decryptor
│ ├── flac metadata (TODO)
│ ├── mp3 metadata (TODO)
│ └── axios (TODO)
└── api
├── Deezer API (TODO)
├── Definitions (TODO)
├── Settings (TODO - not decided)
├── Importer (TODO)
└── Downloader (TODO)
```
# Where can I find more information?
* [Tutorial](https://github.com/ryanheise/audio_service/wiki/Tutorial): walks you through building a simple audio player while explaining the basic concepts.
* [Full example](https://github.com/ryanheise/audio_service/blob/master/example/lib/main.dart): The `example` subdirectory on GitHub demonstrates both music and text-to-speech use cases.
* [Frequently Asked Questions](https://github.com/ryanheise/audio_service/wiki/FAQ)
* [API documentation](https://pub.dev/documentation/audio_service/latest/audio_service/audio_service-library.html)

+ 0
- 8
android/.gitignore View File

@ -1,8 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures

+ 0
- 39
android/build.gradle View File

@ -1,39 +0,0 @@
group 'com.ryanheise.audioservice'
version '1.0-SNAPSHOT'
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
rootProject.allprojects {
repositories {
google()
jcenter()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 16
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
}
}
dependencies {
implementation 'androidx.core:core:1.1.0'
implementation 'androidx.media:media:1.1.0'
}

+ 0
- 4
android/gradle.properties View File

@ -1,4 +0,0 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

BIN
android/gradle/wrapper/gradle-wrapper.jar View File


+ 0
- 1
android/settings.gradle View File

@ -1 +0,0 @@
rootProject.name = 'audio_service'

+ 0
- 3
android/src/main/AndroidManifest.xml View File

@ -1,3 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ryanheise.audioservice">
</manifest>

+ 0
- 8
android/src/main/java/com/ryanheise/audioservice/AudioInterruption.java View File

@ -1,8 +0,0 @@
package com.ryanheise.audioservice;
public enum AudioInterruption {
pause,
temporaryPause,
temporaryDuck,
unknownPause,
}

+ 0
- 16
android/src/main/java/com/ryanheise/audioservice/AudioProcessingState.java View File

@ -1,16 +0,0 @@
package com.ryanheise.audioservice;
public enum AudioProcessingState {
none,
connecting,
ready,
buffering,
fastForwarding,
rewinding,
skippingToPrevious,
skippingToNext,
skippingToQueueItem,
completed,
stopped,
error,
}

+ 0
- 852
android/src/main/java/com/ryanheise/audioservice/AudioService.java View File

@ -1,852 +0,0 @@
package com.ryanheise.audioservice;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.LruCache;
import android.view.KeyEvent;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.media.MediaBrowserServiceCompat;
import androidx.media.app.NotificationCompat.MediaStyle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class AudioService extends MediaBrowserServiceCompat {
private static final int NOTIFICATION_ID = 1124;
private static final int REQUEST_CONTENT_INTENT = 1000;
private static final String MEDIA_ROOT_ID = "root";
// See the comment in onMediaButtonEvent to understand how the BYPASS keycodes work.
// We hijack KEYCODE_MUTE and KEYCODE_MEDIA_RECORD since the media session subsystem
// considers these keycodes relevant to media playback and will pass them on to us.
public static final int KEYCODE_BYPASS_PLAY = KeyEvent.KEYCODE_MUTE;
public static final int KEYCODE_BYPASS_PAUSE = KeyEvent.KEYCODE_MEDIA_RECORD;
public static final int MAX_COMPACT_ACTIONS = 3;
private static volatile boolean running;
static AudioService instance;
private static PendingIntent contentIntent;
private static boolean resumeOnClick;
private static ServiceListener listener;
static String androidNotificationChannelName;
static String androidNotificationChannelDescription;
static Integer notificationColor;
static String androidNotificationIcon;
static boolean androidNotificationClickStartsActivity;
static boolean androidNotificationOngoing;
static boolean androidStopForegroundOnPause;
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 LruCache<String, Bitmap> artBitmapCache;
private static Size artDownscaleSize;
private static boolean playing = false;
private static AudioProcessingState processingState = AudioProcessingState.none;
private static int repeatMode;
private static int shuffleMode;
private static boolean notificationCreated;
public static void init(Activity activity, boolean resumeOnClick, String androidNotificationChannelName, String androidNotificationChannelDescription, String action, Integer notificationColor, String androidNotificationIcon, boolean androidNotificationClickStartsActivity, boolean androidNotificationOngoing, boolean androidStopForegroundOnPause, Size artDownscaleSize, ServiceListener listener) {
if (running)
throw new IllegalStateException("AudioService already running");
running = true;
Context context = activity.getApplicationContext();
Intent intent = new Intent(context, activity.getClass());
intent.setAction(action);
contentIntent = PendingIntent.getActivity(context, REQUEST_CONTENT_INTENT, intent, PendingIntent.FLAG_UPDATE_CURRENT);
AudioService.listener = listener;
AudioService.resumeOnClick = resumeOnClick;
AudioService.androidNotificationChannelName = androidNotificationChannelName;
AudioService.androidNotificationChannelDescription = androidNotificationChannelDescription;
AudioService.notificationColor = notificationColor;
AudioService.androidNotificationIcon = androidNotificationIcon;
AudioService.androidNotificationClickStartsActivity = androidNotificationClickStartsActivity;
AudioService.androidNotificationOngoing = androidNotificationOngoing;
AudioService.androidStopForegroundOnPause = androidStopForegroundOnPause;
AudioService.artDownscaleSize = artDownscaleSize;
notificationCreated = false;
playing = false;
processingState = AudioProcessingState.none;
repeatMode = 0;
shuffleMode = 0;
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
artBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
}
public static AudioProcessingState getProcessingState() {
return processingState;
}
public static boolean isPlaying() {
return playing;
}
public static int getRepeatMode() {
return repeatMode;
}
public static int getShuffleMode() {
return shuffleMode;
}
public void stop() {
running = false;
mediaMetadata = null;
resumeOnClick = false;
listener = null;
androidNotificationChannelName = null;
androidNotificationChannelDescription = null;
notificationColor = null;
androidNotificationIcon = null;
artDownscaleSize = null;
queue.clear();
queueIndex = -1;
mediaMetadataCache.clear();
actions.clear();
artBitmapCache.evictAll();
compactActionIndices = null;
mediaSession.setQueue(queue);
mediaSession.setActive(false);
releaseWakeLock();
stopForeground(true);
stopSelf();
// This still does not solve the Android 11 problem.
// if (notificationCreated) {
// NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
// notificationManager.cancel(NOTIFICATION_ID);
// }
notificationCreated = false;
}
public static boolean isRunning() {
return running;
}
private PowerManager.WakeLock wakeLock;
private MediaSessionCompat mediaSession;
private MediaSessionCallback mediaSessionCallback;
private MediaMetadataCompat preparedMedia;
private List<NotificationCompat.Action> actions = new ArrayList<NotificationCompat.Action>();
private int[] compactActionIndices;
private MediaMetadataCompat mediaMetadata;
private Object audioFocusRequest;
private String notificationChannelId;
private Handler handler = new Handler(Looper.getMainLooper());
int getResourceId(String resource) {
String[] parts = resource.split("/");
String resourceType = parts[0];
String resourceName = parts[1];
return getResources().getIdentifier(resourceName, resourceType, getApplicationContext().getPackageName());
}
NotificationCompat.Action action(String resource, String label, long actionCode) {
int iconId = getResourceId(resource);
return new NotificationCompat.Action(iconId, label,
buildMediaButtonPendingIntent(actionCode));
}
PendingIntent buildMediaButtonPendingIntent(long action) {
int keyCode = toKeyCode(action);
if (keyCode == KeyEvent.KEYCODE_UNKNOWN)
return null;
Intent intent = new Intent(this, MediaButtonReceiver.class);
intent.setAction(Intent.ACTION_MEDIA_BUTTON);
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
return PendingIntent.getBroadcast(this, keyCode, intent, 0);
}
PendingIntent buildDeletePendingIntent() {
Intent intent = new Intent(this, MediaButtonReceiver.class);
intent.setAction(MediaButtonReceiver.ACTION_NOTIFICATION_DELETE);
return PendingIntent.getBroadcast(this, 0, intent, 0);
}
public static int toKeyCode(long action) {
if (action == PlaybackStateCompat.ACTION_PLAY) {
return KEYCODE_BYPASS_PLAY;
} else if (action == PlaybackStateCompat.ACTION_PAUSE) {
return KEYCODE_BYPASS_PAUSE;
} else {
return PlaybackStateCompat.toKeyCode(action);
}
}
void setState(List<NotificationCompat.Action> actions, int actionBits, int[] compactActionIndices, AudioProcessingState processingState, boolean playing, long position, long bufferedPosition, float speed, long updateTime, int repeatMode, int shuffleMode) {
this.actions = actions;
this.compactActionIndices = compactActionIndices;
boolean wasPlaying = AudioService.playing;
AudioService.processingState = processingState;
AudioService.playing = playing;
AudioService.repeatMode = repeatMode;
AudioService.shuffleMode = shuffleMode;
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | actionBits)
.setState(getPlaybackState(), position, speed, updateTime)
.setBufferedPosition(bufferedPosition);
mediaSession.setPlaybackState(stateBuilder.build());
if (!running) return;
if (!wasPlaying && playing) {
enterPlayingState();
} else if (wasPlaying && !playing) {
exitPlayingState();
}
updateNotification();
}
public int getPlaybackState() {
switch (processingState) {
case none: return PlaybackStateCompat.STATE_NONE;
case connecting: return PlaybackStateCompat.STATE_CONNECTING;
case ready: return playing ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
case buffering: return PlaybackStateCompat.STATE_BUFFERING;
case fastForwarding: return PlaybackStateCompat.STATE_FAST_FORWARDING;
case rewinding: return PlaybackStateCompat.STATE_REWINDING;
case skippingToPrevious: return PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS;
case skippingToNext: return PlaybackStateCompat.STATE_SKIPPING_TO_NEXT;
case skippingToQueueItem: return PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM;
case completed: return playing ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
case stopped: return PlaybackStateCompat.STATE_STOPPED;
case error: return PlaybackStateCompat.STATE_ERROR;
default: return PlaybackStateCompat.STATE_NONE;
}
}
private Notification buildNotification() {
int[] compactActionIndices = this.compactActionIndices;
if (compactActionIndices == null) {
compactActionIndices = new int[Math.min(MAX_COMPACT_ACTIONS, actions.size())];
for (int i = 0; i < compactActionIndices.length; i++) compactActionIndices[i] = i;
}
NotificationCompat.Builder builder = getNotificationBuilder();
if (mediaMetadata != null) {
MediaDescriptionCompat description = mediaMetadata.getDescription();
if (description.getTitle() != null)
builder.setContentTitle(description.getTitle());
if (description.getSubtitle() != null)
builder.setContentText(description.getSubtitle());
if (description.getDescription() != null)
builder.setSubText(description.getDescription());
if (description.getIconBitmap() != null)
builder.setLargeIcon(description.getIconBitmap());
}
if (androidNotificationClickStartsActivity)
builder.setContentIntent(mediaSession.getController().getSessionActivity());
if (notificationColor != null)
builder.setColor(notificationColor);
for (NotificationCompat.Action action : actions) {
builder.addAction(action);
}
builder.setStyle(new MediaStyle()
.setMediaSession(mediaSession.getSessionToken())
.setShowActionsInCompactView(compactActionIndices)
.setShowCancelButton(true)
.setCancelButtonIntent(buildMediaButtonPendingIntent(PlaybackStateCompat.ACTION_STOP))
);
if (androidNotificationOngoing)
builder.setOngoing(true);
Notification notification = builder.build();
return notification;
}
private NotificationCompat.Builder getNotificationBuilder() {
NotificationCompat.Builder notificationBuilder = null;
if (notificationBuilder == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
createChannel();
int iconId = getResourceId(androidNotificationIcon);
notificationBuilder = new NotificationCompat.Builder(this, notificationChannelId)
.setSmallIcon(iconId)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setShowWhen(false)
.setDeleteIntent(buildDeletePendingIntent())
;
}
return notificationBuilder;
}
public void handleDeleteNotification() {
if (listener == null) return;
listener.onClose();
}
@RequiresApi(Build.VERSION_CODES.O)
private void createChannel() {
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = notificationManager.getNotificationChannel(notificationChannelId);
if (channel == null) {
channel = new NotificationChannel(notificationChannelId, androidNotificationChannelName, NotificationManager.IMPORTANCE_LOW);
if (androidNotificationChannelDescription != null)
channel.setDescription(androidNotificationChannelDescription);
notificationManager.createNotificationChannel(channel);
}
}
private void updateNotification() {
if (!notificationCreated) return;
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, buildNotification());
}
private boolean enterPlayingState() {
startService(new Intent(AudioService.this, AudioService.class));
if (!mediaSession.isActive())
mediaSession.setActive(true);
acquireWakeLock();
mediaSession.setSessionActivity(contentIntent);
internalStartForeground();
return true;
}
private void exitPlayingState() {
if (androidStopForegroundOnPause) {
exitForegroundState();
}
}
private void exitForegroundState() {
stopForeground(false);
releaseWakeLock();
}
private void internalStartForeground() {
startForeground(NOTIFICATION_ID, buildNotification());
notificationCreated = true;
}
private void acquireWakeLock() {
if (!wakeLock.isHeld())
wakeLock.acquire();
}
private void releaseWakeLock() {
if (wakeLock.isHeld())
wakeLock.release();
}
static MediaMetadataCompat createMediaMetadata(String mediaId, String album, String title, String artist, String genre, Long duration, String artUri, Boolean playable, String displayTitle, String displaySubtitle, String displayDescription, RatingCompat rating, Map<?, ?> extras) {
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
if (artist != null)
builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist);
if (genre != null)
builder.putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre);
if (duration != null)
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
if (artUri != null) {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, artUri);
String artCacheFilePath = null;
if (extras != null) {
artCacheFilePath = (String)extras.get("artCacheFile");
}
if (artCacheFilePath != null) {
Bitmap bitmap = loadArtBitmapFromFile(artCacheFilePath);
if (bitmap != null) {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap);
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap);
}
}
}
if (playable != null)
builder.putLong("playable_long", playable ? 1 : 0);
if (displayTitle != null)
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle);
if (displaySubtitle != null)
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displaySubtitle);
if (displayDescription != null)
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayDescription);
if (rating != null) {
builder.putRating(MediaMetadataCompat.METADATA_KEY_RATING, rating);
}
if (extras != null) {
for (Object o : extras.keySet()) {
String key = (String)o;
Object value = extras.get(key);
if (value instanceof Long) {
builder.putLong("extra_long_" + key, (Long)value);
} else if (value instanceof Integer) {
builder.putLong("extra_long_" + key, (Integer)value);
} else if (value instanceof String) {
builder.putString("extra_string_" + key, (String)value);
} else if (value instanceof Boolean) {
builder.putLong("extra_boolean_" + key, (Boolean)value ? 1 : 0);
} else if (value instanceof Double) {
builder.putString("extra_double_" + key, value.toString());
}
}
}
MediaMetadataCompat mediaMetadata = builder.build();
mediaMetadataCache.put(mediaId, mediaMetadata);
return mediaMetadata;
}
static MediaMetadataCompat getMediaMetadata(String mediaId) {
return mediaMetadataCache.get(mediaId);
}
@Override
public void onCreate() {
super.onCreate();
instance = this;
notificationChannelId = getApplication().getPackageName() + ".channel";
mediaSession = new MediaSessionCompat(this, "media-session");
mediaSession.setMediaButtonReceiver(null); // TODO: Make this configurable
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
.setActions(PlaybackStateCompat.ACTION_PLAY);
mediaSession.setPlaybackState(stateBuilder.build());
mediaSession.setCallback(mediaSessionCallback = new MediaSessionCallback());
setSessionToken(mediaSession.getSessionToken());
mediaSession.setQueue(queue);
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, AudioService.class.getName());
}
void enableQueue() {
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
}
void setQueue(List<MediaSessionCompat.QueueItem> queue) {
this.queue = queue;
mediaSession.setQueue(queue);
}
void playMediaItem(MediaDescriptionCompat description) {
mediaSessionCallback.onPlayMediaItem(description);
}
void setMetadata(final MediaMetadataCompat mediaMetadata) {
this.mediaMetadata = mediaMetadata;
mediaSession.setMetadata(mediaMetadata);
updateNotification();
}
static Bitmap loadArtBitmapFromFile(String path) {
Bitmap bitmap = artBitmapCache.get(path);
if (bitmap != null) return bitmap;
try {
if (artDownscaleSize != null) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
options.inSampleSize = calculateInSampleSize(options, artDownscaleSize.width, artDownscaleSize.height);
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeFile(path, options);
} else {
bitmap = BitmapFactory.decodeFile(path);
}
artBitmapCache.put(path, bitmap);
return bitmap;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
@Override
public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
return new BrowserRoot(MEDIA_ROOT_ID, null);
}
//Create fake media item for android auto
private MediaBrowserCompat.MediaItem androidAutoPreloadMediaItem(String id, String title) {
MediaDescriptionCompat description = new MediaDescriptionCompat.Builder()
.setMediaId(id)
.setTitle(title)
.build();
return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE);
}
@Override
public void onLoadChildren(final String parentMediaId, final Result<List<MediaBrowserCompat.MediaItem>> result) {
if (listener == null) {
//Freezer not started actions
if (parentMediaId != null && parentMediaId.startsWith("__")) {
Intent intent = getPackageManager().getLaunchIntentForPackage("f.f.freezer");
//Start Freezer
if (parentMediaId.equals("__start")) {
startActivity(intent);
}
//Start with Flow
if (parentMediaId.equals("__flow")) {
intent.putExtra("preload", "flow");
startActivity(intent);
}
//Start with favorites
if (parentMediaId.equals("__favorites")) {
intent.putExtra("preload", "favorites");
startActivity(intent);
}
result.sendResult(new ArrayList<MediaBrowserCompat.MediaItem>());
return;
}
ArrayList<MediaBrowserCompat.MediaItem> out = new ArrayList<>();
//Start Freezer MediaItem
out.add(androidAutoPreloadMediaItem("__start", "Start Freezer"));
out.add(androidAutoPreloadMediaItem("__flow", "Start Flow"));
out.add(androidAutoPreloadMediaItem("__favorites", "Start Favorites"));
result.sendResult(out);
return;
}
listener.onLoadChildren(parentMediaId, result);
}
@Override
public int onStartCommand(final Intent intent, int flags, int startId) {
MediaButtonReceiver.handleIntent(mediaSession, intent);
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
if (listener != null) {
listener.onDestroy();
}
mediaSession.release();
instance = null;
}
@Override
public void onTaskRemoved(Intent rootIntent) {
if (listener != null) {
listener.onTaskRemoved();
}
super.onTaskRemoved(rootIntent);
}
public class MediaSessionCallback extends MediaSessionCompat.Callback {
@Override
public void onAddQueueItem(MediaDescriptionCompat description) {
if (listener == null) return;
listener.onAddQueueItem(getMediaMetadata(description.getMediaId()));
}
@Override
public void onAddQueueItem(MediaDescriptionCompat description, int index) {
if (listener == null) return;
listener.onAddQueueItemAt(getMediaMetadata(description.getMediaId()), index);
}
@Override
public void onRemoveQueueItem(MediaDescriptionCompat description) {
if (listener == null) return;
listener.onRemoveQueueItem(getMediaMetadata(description.getMediaId()));
}
@Override
public void onPrepare() {
if (listener == null) return;
if (!mediaSession.isActive())
mediaSession.setActive(true);
listener.onPrepare();
}
@Override
public void onPlay() {
if (listener == null) return;
listener.onPlay();
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
if (listener == null) return;
if (!mediaSession.isActive())
mediaSession.setActive(true);
listener.onPrepareFromMediaId(mediaId);
}
@Override
public void onPlayFromMediaId(final String mediaId, final Bundle extras) {
if (listener == null) return;
listener.onPlayFromMediaId(mediaId);
}
@Override
public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
if (listener == null) return false;
final KeyEvent event = (KeyEvent)mediaButtonEvent.getExtras().get(Intent.EXTRA_KEY_EVENT);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (event.getKeyCode()) {
case KEYCODE_BYPASS_PLAY:
onPlay();
break;
case KEYCODE_BYPASS_PAUSE:
onPause();
break;
case KeyEvent.KEYCODE_MEDIA_STOP:
onStop();
break;
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
onFastForward();
break;
case KeyEvent.KEYCODE_MEDIA_REWIND:
onRewind();
break;
// Android unfortunately reroutes media button clicks to
// KEYCODE_MEDIA_PLAY/PAUSE instead of the expected KEYCODE_HEADSETHOOK
// or KEYCODE_MEDIA_PLAY_PAUSE. As a result, we can't genuinely tell if
// onMediaButtonEvent was called because a media button was actually
// pressed or because a PLAY/PAUSE action was pressed instead! To get
// around this, we make PLAY and PAUSE actions use different keycodes:
// KEYCODE_BYPASS_PLAY/PAUSE. Now if we get KEYCODE_MEDIA_PLAY/PUASE
// we know it is actually a media button press.
case KeyEvent.KEYCODE_MEDIA_NEXT:
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
case KeyEvent.KEYCODE_MEDIA_PLAY:
case KeyEvent.KEYCODE_MEDIA_PAUSE:
// These are the "genuine" media button click events
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
MediaControllerCompat controller = mediaSession.getController();
listener.onClick(mediaControl(event));
break;
}
}
return true;
}
private MediaControl mediaControl(KeyEvent event) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
return MediaControl.media;
case KeyEvent.KEYCODE_MEDIA_NEXT:
return MediaControl.next;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
return MediaControl.previous;
default:
return MediaControl.media;
}
}
@Override
public void onPause() {
if (listener == null) return;
listener.onPause();
}
@Override
public void onStop() {
if (listener == null) return;
listener.onStop();
}
@Override
public void onSkipToNext() {
if (listener == null) return;
listener.onSkipToNext();
}
@Override
public void onSkipToPrevious() {
if (listener == null) return;
listener.onSkipToPrevious();
}
@Override
public void onFastForward() {
if (listener == null) return;
listener.onFastForward();
}
@Override
public void onRewind() {
if (listener == null) return;
listener.onRewind();
}
@Override
public void onSkipToQueueItem(long id) {
if (listener == null) return;
listener.onSkipToQueueItem(id);
}
@Override
public void onSeekTo(long pos) {
if (listener == null) return;
listener.onSeekTo(pos);
}
@Override
public void onSetRating(RatingCompat rating) {
if (listener == null) return;
listener.onSetRating(rating);
}
@Override
public void onSetRepeatMode(int repeatMode) {
if (listener == null) return;
listener.onSetRepeatMode(repeatMode);
}
@Override
public void onSetShuffleMode(int shuffleMode) {
if (listener == null) return;
listener.onSetShuffleMode(shuffleMode);
}
@Override
public void onSetRating(RatingCompat rating, Bundle extras) {
if (listener == null) return;
listener.onSetRating(rating, extras);
}
//
// NON-STANDARD METHODS
//
public void onPlayMediaItem(final MediaDescriptionCompat description) {
if (listener == null) return;
listener.onPlayMediaItem(getMediaMetadata(description.getMediaId()));
}
}
public static interface ServiceListener {
void onLoadChildren(String parentMediaId, Result<List<MediaBrowserCompat.MediaItem>> result);
void onClick(MediaControl mediaControl);
void onPrepare();
void onPrepareFromMediaId(String mediaId);
//void onPrepareFromSearch(String query);
//void onPrepareFromUri(String uri);
void onPlay();
void onPlayFromMediaId(String mediaId);
//void onPlayFromSearch(String query, Map<?,?> extras);
//void onPlayFromUri(String uri, Map<?,?> extras);
void onSkipToQueueItem(long id);
void onPause();
void onSkipToNext();
void onSkipToPrevious();
void onFastForward();
void onRewind();
void onStop();
void onDestroy();
void onSeekTo(long pos);
void onSetRating(RatingCompat rating);
void onSetRating(RatingCompat rating, Bundle extras);
void onSetRepeatMode(int repeatMode);
//void onSetShuffleModeEnabled(boolean enabled);
void onSetShuffleMode(int shuffleMode);
//void onCustomAction(String action, Bundle extras);
void onAddQueueItem(MediaMetadataCompat metadata);
void onAddQueueItemAt(MediaMetadataCompat metadata, int index);
void onRemoveQueueItem(MediaMetadataCompat metadata);
//
// NON-STANDARD METHODS
//
void onPlayMediaItem(MediaMetadataCompat metadata);
void onTaskRemoved();
void onClose();
}
}

+ 0
- 1069
android/src/main/java/com/ryanheise/audioservice/AudioServicePlugin.java
File diff suppressed because it is too large
View File


+ 0
- 19
android/src/main/java/com/ryanheise/audioservice/MediaButtonReceiver.java View File

@ -1,19 +0,0 @@
package com.ryanheise.audioservice;
import android.content.Context;
import android.content.Intent;
public class MediaButtonReceiver extends androidx.media.session.MediaButtonReceiver {
public static final String ACTION_NOTIFICATION_DELETE = "com.ryanheise.audioservice.intent.action.ACTION_NOTIFICATION_DELETE";
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null
&& ACTION_NOTIFICATION_DELETE.equals(intent.getAction())
&& AudioService.instance != null) {
AudioService.instance.handleDeleteNotification();
return;
}
super.onReceive(context, intent);
}
}

+ 0
- 7
android/src/main/java/com/ryanheise/audioservice/MediaControl.java View File

@ -1,7 +0,0 @@
package com.ryanheise.audioservice;
public enum MediaControl {
media,
next,
previous
}

+ 0
- 11
android/src/main/java/com/ryanheise/audioservice/Size.java View File

@ -1,11 +0,0 @@
package com.ryanheise.audioservice;
public class Size {
public int width;
public int height;
public Size(int width, int height) {
this.width = width;
this.height = height;
}
}

BIN
android/src/main/res/drawable-hdpi/audio_service_fast_forward.png View File

Before After
Width: 36  |  Height: 36  |  Size: 561 B

BIN
android/src/main/res/drawable-hdpi/audio_service_fast_rewind.png View File

Before After
Width: 36  |  Height: 36  |  Size: 584 B

BIN
android/src/main/res/drawable-hdpi/audio_service_pause.png View File

Before After
Width: 36  |  Height: 36  |  Size: 157 B

BIN
android/src/main/res/drawable-hdpi/audio_service_play_arrow.png View File

Before After
Width: 36  |  Height: 36  |  Size: 352 B

BIN
android/src/main/res/drawable-hdpi/audio_service_skip_next.png View File

Before After
Width: 36  |  Height: 36  |  Size: 379 B

BIN
android/src/main/res/drawable-hdpi/audio_service_skip_previous.png View File

Before After
Width: 36  |  Height: 36  |  Size: 410 B

BIN
android/src/main/res/drawable-hdpi/audio_service_stop.png View File

Before After
Width: 36  |  Height: 36  |  Size: 121 B

BIN
android/src/main/res/drawable-hdpi/ic_favorite.png View File

Before After
Width: 63  |  Height: 63  |  Size: 803 B

BIN
android/src/main/res/drawable-mdpi/audio_service_fast_forward.png View File

Before After
Width: 24  |  Height: 24  |  Size: 241 B

BIN
android/src/main/res/drawable-mdpi/audio_service_fast_rewind.png View File

Before After
Width: 24  |  Height: 24  |  Size: 267 B

BIN
android/src/main/res/drawable-mdpi/audio_service_pause.png View File

Before After
Width: 24  |  Height: 24  |  Size: 170 B

BIN
android/src/main/res/drawable-mdpi/audio_service_play_arrow.png View File

Before After
Width: 24  |  Height: 24  |  Size: 285 B

BIN
android/src/main/res/drawable-mdpi/audio_service_skip_next.png View File

Before After
Width: 24  |  Height: 24  |  Size: 344 B

BIN
android/src/main/res/drawable-mdpi/audio_service_skip_previous.png View File

Before After
Width: 24  |  Height: 24  |  Size: 354 B

BIN
android/src/main/res/drawable-mdpi/audio_service_stop.png View File

Before After
Width: 24  |  Height: 24  |  Size: 114 B

BIN
android/src/main/res/drawable-mdpi/ic_favorite.png View File

Before After
Width: 42  |  Height: 42  |  Size: 597 B

BIN
android/src/main/res/drawable-xhdpi/audio_service_fast_forward.png View File

Before After
Width: 48  |  Height: 48  |  Size: 389 B

BIN
android/src/main/res/drawable-xhdpi/audio_service_fast_rewind.png View File

Before After
Width: 48  |  Height: 48  |  Size: 460 B

BIN
android/src/main/res/drawable-xhdpi/audio_service_pause.png View File

Before After
Width: 48  |  Height: 48  |  Size: 188 B

BIN
android/src/main/res/drawable-xhdpi/audio_service_play_arrow.png View File

Before After
Width: 48  |  Height: 48  |  Size: 538 B

BIN
android/src/main/res/drawable-xhdpi/audio_service_skip_next.png View File

Before After
Width: 48  |  Height: 48  |  Size: 602 B

BIN
android/src/main/res/drawable-xhdpi/audio_service_skip_previous.png View File

Before After
Width: 48  |  Height: 48  |  Size: 639 B

BIN
android/src/main/res/drawable-xhdpi/audio_service_stop.png View File

Before After
Width: 48  |  Height: 48  |  Size: 152 B

BIN
android/src/main/res/drawable-xhdpi/ic_favorite.png View File

Before After
Width: 84  |  Height: 84  |  Size: 1.2 KiB

BIN
android/src/main/res/drawable-xxhdpi/audio_service_fast_forward.png View File

Before After
Width: 72  |  Height: 72  |  Size: 684 B

BIN
android/src/main/res/drawable-xxhdpi/audio_service_fast_rewind.png View File

Before After
Width: 72  |  Height: 72  |  Size: 770 B

BIN
android/src/main/res/drawable-xxhdpi/audio_service_pause.png View File

Before After
Width: 72  |  Height: 72  |  Size: 315 B

BIN
android/src/main/res/drawable-xxhdpi/audio_service_play_arrow.png View File

Before After
Width: 72  |  Height: 72  |  Size: 720 B

BIN
android/src/main/res/drawable-xxhdpi/audio_service_skip_next.png View File

Before After
Width: 72  |  Height: 72  |  Size: 832 B

BIN
android/src/main/res/drawable-xxhdpi/audio_service_skip_previous.png View File

Before After
Width: 72  |  Height: 72  |  Size: 857 B

BIN
android/src/main/res/drawable-xxhdpi/audio_service_stop.png View File

Before After
Width: 72  |  Height: 72  |  Size: 252 B

BIN
android/src/main/res/drawable-xxhdpi/ic_favorite.png View File

Before After
Width: 126  |  Height: 126  |  Size: 1.5 KiB

BIN
android/src/main/res/drawable-xxxhdpi/audio_service_fast_forward.png View File

Before After
Width: 96  |  Height: 96  |  Size: 838 B

BIN
android/src/main/res/drawable-xxxhdpi/audio_service_fast_rewind.png View File

Before After
Width: 96  |  Height: 96  |  Size: 952 B

BIN
android/src/main/res/drawable-xxxhdpi/audio_service_pause.png View File

Before After
Width: 96  |  Height: 96  |  Size: 461 B

BIN
android/src/main/res/drawable-xxxhdpi/audio_service_play_arrow.png View File

Before After
Width: 96  |  Height: 96  |  Size: 1.1 KiB

BIN
android/src/main/res/drawable-xxxhdpi/audio_service_skip_next.png View File

Before After
Width: 96  |  Height: 96  |  Size: 1.2 KiB

BIN
android/src/main/res/drawable-xxxhdpi/audio_service_skip_previous.png View File

Before After
Width: 96  |  Height: 96  |  Size: 1.2 KiB

BIN
android/src/main/res/drawable-xxxhdpi/audio_service_stop.png View File

Before After
Width: 96  |  Height: 96  |  Size: 316 B

BIN
android/src/main/res/drawable-xxxhdpi/ic_favorite.png View File

Before After
Width: 168  |  Height: 168  |  Size: 2.4 KiB

+ 0
- 19
audio_service.iml View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart Packages" level="project" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
</component>
</module>

+ 0
- 30
audio_service_android.iml View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android" name="Android">
<configuration>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="GEN_FOLDER_RELATIVE_PATH_APT" value="/android/gen" />
<option name="GEN_FOLDER_RELATIVE_PATH_AIDL" value="/android/gen" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/android/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/android/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/android/assets" />
<option name="LIBS_FOLDER_RELATIVE_PATH" value="/android/libs" />
<option name="PROGUARD_LOGS_FOLDER_RELATIVE_PATH" value="/android/proguard_logs" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/android">
<sourceFolder url="file://$MODULE_DIR$/android/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/android/gen" isTestSource="false" generated="true" />
</content>
<content url="file://$MODULE_DIR$/example/android">
<sourceFolder url="file://$MODULE_DIR$/example/android/app/src/main/java" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Flutter for Android" level="project" />
</component>
</module>

+ 9
- 0
build.gradle.kts View File

@ -0,0 +1,9 @@
allprojects {
repositories {
google()
mavenCentral()
maven {
url = uri("https://jitpack.io")
}
}
}

+ 0
- 617
darwin/Classes/AudioServicePlugin.m View File

@ -1,617 +0,0 @@
#import "AudioServicePlugin.h"
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>
// If you'd like to help, please see the TODO comments below, then open a
// GitHub issue to announce your intention to work on a particular feature, and
// submit a pull request. We have an open discussion over at issue #10 about
// all things iOS if you'd like to discuss approaches or ask for input. Thank
// you for your support!
@implementation AudioServicePlugin
static FlutterMethodChannel *channel = nil;
static FlutterMethodChannel *backgroundChannel = nil;
static BOOL _running = NO;
static FlutterResult startResult = nil;
static MPRemoteCommandCenter *commandCenter = nil;
static NSArray *queue = nil;
static NSMutableDictionary *mediaItem = nil;
static long actionBits;
static NSArray *commands;
static BOOL _controlsUpdated = NO;
static enum AudioProcessingState processingState = none;
static BOOL playing = NO;
static NSNumber *position = nil;
static NSNumber *bufferedPosition = nil;
static NSNumber *updateTime = nil;
static NSNumber *speed = nil;
static NSNumber *repeatMode = nil;
static NSNumber *shuffleMode = nil;
static NSNumber *fastForwardInterval = nil;
static NSNumber *rewindInterval = nil;
static NSMutableDictionary *params = nil;
static MPMediaItemArtwork* artwork = nil;
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
@synchronized(self) {
// TODO: Need a reliable way to detect whether this is the client
// or background.
// TODO: Handle multiple clients.
// As no separate isolate is used on macOS, add both handlers to the one registrar.
#if TARGET_OS_IPHONE
if (channel == nil) {
#endif
AudioServicePlugin *instance = [[AudioServicePlugin alloc] init:registrar];
channel = [FlutterMethodChannel
methodChannelWithName:@"ryanheise.com/audioService"
binaryMessenger:[registrar messenger]];
[registrar addMethodCallDelegate:instance channel:channel];
#if TARGET_OS_IPHONE
} else {
AudioServicePlugin *instance = [[AudioServicePlugin alloc] init:registrar];
#endif
backgroundChannel = [FlutterMethodChannel
methodChannelWithName:@"ryanheise.com/audioServiceBackground"
binaryMessenger:[registrar messenger]];
[registrar addMethodCallDelegate:instance channel:backgroundChannel];
#if TARGET_OS_IPHONE
}
#endif
}
}
- (instancetype)init:(NSObject<FlutterPluginRegistrar> *)registrar {
self = [super init];
NSAssert(self, @"super init cannot be nil");
return self;
}
- (void)broadcastPlaybackState {
[channel invokeMethod:@"onPlaybackStateChanged" arguments:@[
// processingState
@(processingState),
// playing
@(playing),
// actions
@(actionBits),
// position
position,
// bufferedPosition
bufferedPosition,
// playback speed
speed,
// update time since epoch
updateTime,
// repeat mode
repeatMode,
// shuffle mode
shuffleMode,
]];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
// TODO:
// - Restructure this so that we have a separate method call delegate
// for the client instance and the background instance so that methods
// can't be called on the wrong instance.
if ([@"connect" isEqualToString:call.method]) {
long long msSinceEpoch = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
if (position == nil) {
position = @(0);
bufferedPosition = @(0);
updateTime = [NSNumber numberWithLongLong: msSinceEpoch];
speed = [NSNumber numberWithDouble: 1.0];
repeatMode = @(0);
shuffleMode = @(0);
}
// Notify client of state on subscribing.
[self broadcastPlaybackState];
[channel invokeMethod:@"onMediaChanged" arguments:@[mediaItem ? mediaItem : [NSNull null]]];
[channel invokeMethod:@"onQueueChanged" arguments:@[queue ? queue : [NSNull null]]];
result(nil);
} else if ([@"disconnect" isEqualToString:call.method]) {
result(nil);
} else if ([@"start" isEqualToString:call.method]) {
if (_running) {
result(@NO);
return;
}
_running = YES;
// The result will be sent after the background task actually starts.
// See the "ready" case below.
startResult = result;
#if TARGET_OS_IPHONE
[AVAudioSession sharedInstance];
#endif
// Set callbacks on MPRemoteCommandCenter
fastForwardInterval = [call.arguments objectForKey:@"fastForwardInterval"];
rewindInterval = [call.arguments objectForKey:@"rewindInterval"];
commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
commands = @[
commandCenter.stopCommand,
commandCenter.pauseCommand,
commandCenter.playCommand,
commandCenter.skipBackwardCommand,
commandCenter.previousTrackCommand,
commandCenter.nextTrackCommand,
commandCenter.skipForwardCommand,
[NSNull null],
commandCenter.changePlaybackPositionCommand,
commandCenter.togglePlayPauseCommand,
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
[NSNull null],
commandCenter.changeRepeatModeCommand,
[NSNull null],
[NSNull null],
commandCenter.changeShuffleModeCommand,
commandCenter.seekBackwardCommand,
commandCenter.seekForwardCommand,
];
[commandCenter.changePlaybackRateCommand setEnabled:YES];
[commandCenter.togglePlayPauseCommand setEnabled:YES];
[commandCenter.togglePlayPauseCommand addTarget:self action:@selector(togglePlayPause:)];
// TODO: enable more commands
// Language options
if (@available(iOS 9.0, macOS 10.12.2, *)) {
[commandCenter.enableLanguageOptionCommand setEnabled:NO];
[commandCenter.disableLanguageOptionCommand setEnabled:NO];
}
// Rating
[commandCenter.ratingCommand setEnabled:NO];
// Feedback
[commandCenter.likeCommand setEnabled:NO];
[commandCenter.dislikeCommand setEnabled:NO];
[commandCenter.bookmarkCommand setEnabled:NO];
[self updateControls];
// Params
params = [call.arguments objectForKey:@"params"];
#if TARGET_OS_OSX
// No isolate can be used for macOS until https://github.com/flutter/flutter/issues/65222 is resolved.
// We send a result here, and then the Dart code continues in the main isolate.
result(@YES);
#endif
} else if ([@"ready" isEqualToString:call.method]) {
NSMutableDictionary *startParams = [NSMutableDictionary new];
startParams[@"fastForwardInterval"] = fastForwardInterval;
startParams[@"rewindInterval"] = rewindInterval;
startParams[@"params"] = params;
result(startParams);
} else if ([@"started" isEqualToString:call.method]) {
#if TARGET_OS_IPHONE
if (startResult) {
startResult(@YES);
startResult = nil;
}
#endif
result(@YES);
} else if ([@"stopped" isEqualToString:call.method]) {
_running = NO;
[channel invokeMethod:@"onStopped" arguments:nil];
[commandCenter.changePlaybackRateCommand setEnabled:NO];
[commandCenter.togglePlayPauseCommand setEnabled:NO];
[commandCenter.togglePlayPauseCommand removeTarget:nil];
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nil;
processingState = none;
playing = NO;
position = nil;
bufferedPosition = nil;
updateTime = nil;
speed = nil;
artwork = nil;
mediaItem = nil;
repeatMode = @(0);
shuffleMode = @(0);
actionBits = 0;
[self updateControls];
_controlsUpdated = NO;
queue = nil;
startResult = nil;
fastForwardInterval = nil;
rewindInterval = nil;
params = nil;
commandCenter = nil;
result(@YES);
} else if ([@"isRunning" isEqualToString:call.method]) {
if (_running) {
result(@YES);
} else {
result(@NO);
}
} else if ([@"setBrowseMediaParent" isEqualToString:call.method]) {
result(@YES);
} else if ([@"addQueueItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onAddQueueItem" arguments:@[call.arguments] result: result];
} else if ([@"addQueueItemAt" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onAddQueueItemAt" arguments:call.arguments result: result];
} else if ([@"removeQueueItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onRemoveQueueItem" arguments:@[call.arguments] result: result];
} else if ([@"updateQueue" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onUpdateQueue" arguments:@[call.arguments] result: result];
} else if ([@"updateMediaItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onUpdateMediaItem" arguments:@[call.arguments] result: result];
} else if ([@"click" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onClick" arguments:@[call.arguments] result: result];
} else if ([@"prepare" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPrepare" arguments:nil result: result];
} else if ([@"prepareFromMediaId" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPrepareFromMediaId" arguments:@[call.arguments] result: result];
} else if ([@"play" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPlay" arguments:nil result: result];
} else if ([@"playFromMediaId" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPlayFromMediaId" arguments:@[call.arguments] result: result];
} else if ([@"playMediaItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPlayMediaItem" arguments:@[call.arguments] result: result];
} else if ([@"skipToQueueItem" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSkipToQueueItem" arguments:@[call.arguments] result: result];
} else if ([@"pause" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onPause" arguments:nil result: result];
} else if ([@"stop" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onStop" arguments:nil result: result];
} else if ([@"seekTo" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSeekTo" arguments:@[call.arguments] result: result];
} else if ([@"skipToNext" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSkipToNext" arguments:nil result: result];
} else if ([@"skipToPrevious" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSkipToPrevious" arguments:nil result: result];
} else if ([@"fastForward" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onFastForward" arguments:nil result: result];
} else if ([@"rewind" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onRewind" arguments:nil result: result];
} else if ([@"setRepeatMode" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSetRepeatMode" arguments:@[call.arguments] result: result];
} else if ([@"setShuffleMode" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSetShuffleMode" arguments:@[call.arguments] result: result];
} else if ([@"setRating" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSetRating" arguments:@[call.arguments[@"rating"], call.arguments[@"extras"]] result: result];
} else if ([@"setSpeed" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSetSpeed" arguments:@[call.arguments] result: result];
} else if ([@"seekForward" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSeekForward" arguments:@[call.arguments] result: result];
} else if ([@"seekBackward" isEqualToString:call.method]) {
[backgroundChannel invokeMethod:@"onSeekBackward" arguments:@[call.arguments] result: result];
} else if ([@"setState" isEqualToString:call.method]) {
long long msSinceEpoch;
if (call.arguments[7] != [NSNull null]) {
msSinceEpoch = [call.arguments[7] longLongValue];
} else {
msSinceEpoch = (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
}
actionBits = 0;
NSArray *controlsArray = call.arguments[0];
for (int i = 0; i < controlsArray.count; i++) {
NSDictionary *control = (NSDictionary *)controlsArray[i];
NSNumber *actionIndex = (NSNumber *)control[@"action"];
int actionCode = 1 << [actionIndex intValue];
actionBits |= actionCode;
}
NSArray *systemActionsArray = call.arguments[1];
for (int i = 0; i < systemActionsArray.count; i++) {
NSNumber *actionIndex = (NSNumber *)systemActionsArray[i];
int actionCode = 1 << [actionIndex intValue];
actionBits |= actionCode;
}
processingState = [call.arguments[2] intValue];
playing = [call.arguments[3] boolValue];
position = call.arguments[4];
bufferedPosition = call.arguments[5];
speed = call.arguments[6];
repeatMode = call.arguments[9];
shuffleMode = call.arguments[10];
updateTime = [NSNumber numberWithLongLong: msSinceEpoch];
[self broadcastPlaybackState];
[self updateControls];
[self updateNowPlayingInfo];
result(@(YES));
} else if ([@"setQueue" isEqualToString:call.method]) {
queue = call.arguments;
[channel invokeMethod:@"onQueueChanged" arguments:@[queue]];
result(@YES);
} else if ([@"setMediaItem" isEqualToString:call.method]) {
mediaItem = call.arguments;
NSString* artUri = mediaItem[@"artUri"];
artwork = nil;
if (![artUri isEqual: [NSNull null]]) {
NSString* artCacheFilePath = [NSNull null];
NSDictionary* extras = mediaItem[@"extras"];
if (![extras isEqual: [NSNull null]]) {
artCacheFilePath = extras[@"artCacheFile"];
}
if (![artCacheFilePath isEqual: [NSNull null]]) {
#if TARGET_OS_IPHONE
UIImage* artImage = [UIImage imageWithContentsOfFile:artCacheFilePath];
#else
NSImage* artImage = [[NSImage alloc] initWithContentsOfFile:artCacheFilePath];
#endif
if (artImage != nil) {
#if TARGET_OS_IPHONE
artwork = [[MPMediaItemArtwork alloc] initWithImage: artImage];
#else
artwork = [[MPMediaItemArtwork alloc] initWithBoundsSize:artImage.size requestHandler:^NSImage* _Nonnull(CGSize aSize) {
return artImage;
}];
#endif
}
}
}
[self updateNowPlayingInfo];
[channel invokeMethod:@"onMediaChanged" arguments:@[call.arguments]];
result(@(YES));
} else if ([@"notifyChildrenChanged" isEqualToString:call.method]) {
result(@YES);
} else if ([@"androidForceEnableMediaButtons" isEqualToString:call.method]) {
result(@YES);
} else {
// TODO: Check if this implementation is correct.
// Can I just pass on the result as the last argument?
[backgroundChannel invokeMethod:call.method arguments:call.arguments result: result];
}
}
- (MPRemoteCommandHandlerStatus) play: (MPRemoteCommandEvent *) event {
NSLog(@"play");
[backgroundChannel invokeMethod:@"onPlay" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) pause: (MPRemoteCommandEvent *) event {
NSLog(@"pause");
[backgroundChannel invokeMethod:@"onPause" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (void) updateNowPlayingInfo {
NSMutableDictionary *nowPlayingInfo = [NSMutableDictionary new];
if (mediaItem) {
nowPlayingInfo[MPMediaItemPropertyTitle] = mediaItem[@"title"];
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = mediaItem[@"album"];
if (mediaItem[@"artist"] != [NSNull null]) {
nowPlayingInfo[MPMediaItemPropertyArtist] = mediaItem[@"artist"];
}
if (mediaItem[@"duration"] != [NSNull null]) {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = [NSNumber numberWithLongLong: ([mediaItem[@"duration"] longLongValue] / 1000)];
}
if (@available(iOS 3.0, macOS 10.13.2, *)) {
if (artwork) {
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork;
}
}
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = [NSNumber numberWithInt:([position intValue] / 1000)];
}
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = [NSNumber numberWithDouble: playing ? 1.0 : 0.0];
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo;
}
- (void) updateControls {
for (enum MediaAction action = AStop; action <= ASeekForward; action++) {
[self updateControl:action];
}
_controlsUpdated = YES;
}
- (void) updateControl:(enum MediaAction)action {
MPRemoteCommand *command = commands[action];
if (command == [NSNull null]) return;
// Shift the actionBits right until the least significant bit is the tested action bit, and AND that with a 1 at the same position.
// All bytes become 0, other than the tested action bit, which will be 0 or 1 according to its status in the actionBits long.
BOOL enable = ((actionBits >> action) & 1);
if (_controlsUpdated && enable == command.enabled) return;
[command setEnabled:enable];
switch (action) {
case AStop:
if (enable) {
[commandCenter.stopCommand addTarget:self action:@selector(stop:)];
} else {
[commandCenter.stopCommand removeTarget:nil];
}
break;
case APause:
if (enable) {
[commandCenter.pauseCommand addTarget:self action:@selector(pause:)];
} else {
[commandCenter.pauseCommand removeTarget:nil];
}
break;
case APlay:
if (enable) {
[commandCenter.playCommand addTarget:self action:@selector(play:)];
} else {
[commandCenter.playCommand removeTarget:nil];
}
break;
case ARewind:
if (rewindInterval.integerValue > 0) {
if (enable) {
[commandCenter.skipBackwardCommand addTarget: self action:@selector(skipBackward:)];
int rewindIntervalInSeconds = [rewindInterval intValue]/1000;
NSNumber *rewindIntervalInSec = [NSNumber numberWithInt: rewindIntervalInSeconds];
commandCenter.skipBackwardCommand.preferredIntervals = @[rewindIntervalInSec];
} else {
[commandCenter.skipBackwardCommand removeTarget:nil];
}
}
break;
case ASkipToPrevious:
if (enable) {
[commandCenter.previousTrackCommand addTarget:self action:@selector(previousTrack:)];
} else {
[commandCenter.previousTrackCommand removeTarget:nil];
}
break;
case ASkipToNext:
if (enable) {
[commandCenter.nextTrackCommand addTarget:self action:@selector(nextTrack:)];
} else {
[commandCenter.nextTrackCommand removeTarget:nil];
}
break;
case AFastForward:
if (fastForwardInterval.integerValue > 0) {
if (enable) {
[commandCenter.skipForwardCommand addTarget: self action:@selector(skipForward:)];
int fastForwardIntervalInSeconds = [fastForwardInterval intValue]/1000;
NSNumber *fastForwardIntervalInSec = [NSNumber numberWithInt: fastForwardIntervalInSeconds];
commandCenter.skipForwardCommand.preferredIntervals = @[fastForwardIntervalInSec];
} else {
[commandCenter.skipForwardCommand removeTarget:nil];
}
}
break;
case ASetRating:
// TODO:
// commandCenter.ratingCommand
// commandCenter.dislikeCommand
// commandCenter.bookmarkCommand
break;
case ASeekTo:
if (@available(iOS 9.1, macOS 10.12.2, *)) {
if (enable) {
[commandCenter.changePlaybackPositionCommand addTarget:self action:@selector(changePlaybackPosition:)];
} else {
[commandCenter.changePlaybackPositionCommand removeTarget:nil];
}
}
case APlayPause:
// Automatically enabled.
break;
case ASetRepeatMode:
if (enable) {
[commandCenter.changeRepeatModeCommand addTarget:self action:@selector(changeRepeatMode:)];
} else {
[commandCenter.changeRepeatModeCommand removeTarget:nil];
}
break;
case ASetShuffleMode:
if (enable) {
[commandCenter.changeShuffleModeCommand addTarget:self action:@selector(changeShuffleMode:)];
} else {
[commandCenter.changeShuffleModeCommand removeTarget:nil];
}
break;
case ASeekBackward:
if (enable) {
[commandCenter.seekBackwardCommand addTarget:self action:@selector(seekBackward:)];
} else {
[commandCenter.seekBackwardCommand removeTarget:nil];
}
break;
case ASeekForward:
if (enable) {
[commandCenter.seekForwardCommand addTarget:self action:@selector(seekForward:)];
} else {
[commandCenter.seekForwardCommand removeTarget:nil];
}
break;
}
}
- (MPRemoteCommandHandlerStatus) togglePlayPause: (MPRemoteCommandEvent *) event {
NSLog(@"togglePlayPause");
[backgroundChannel invokeMethod:@"onClick" arguments:@[@(0)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) stop: (MPRemoteCommandEvent *) event {
NSLog(@"stop");
[backgroundChannel invokeMethod:@"onStop" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) nextTrack: (MPRemoteCommandEvent *) event {
NSLog(@"nextTrack");
[backgroundChannel invokeMethod:@"onSkipToNext" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) previousTrack: (MPRemoteCommandEvent *) event {
NSLog(@"previousTrack");
[backgroundChannel invokeMethod:@"onSkipToPrevious" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) changePlaybackPosition: (MPChangePlaybackPositionCommandEvent *) event {
NSLog(@"changePlaybackPosition");
[backgroundChannel invokeMethod:@"onSeekTo" arguments: @[@((long long) (event.positionTime * 1000))]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) skipForward: (MPRemoteCommandEvent *) event {
NSLog(@"skipForward");
[backgroundChannel invokeMethod:@"onFastForward" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) skipBackward: (MPRemoteCommandEvent *) event {
NSLog(@"skipBackward");
[backgroundChannel invokeMethod:@"onRewind" arguments:nil];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) seekForward: (MPSeekCommandEvent *) event {
NSLog(@"seekForward");
BOOL begin = event.type == MPSeekCommandEventTypeBeginSeeking;
[backgroundChannel invokeMethod:@"onSeekForward" arguments:@[@(begin)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) seekBackward: (MPSeekCommandEvent *) event {
NSLog(@"seekBackward");
BOOL begin = event.type == MPSeekCommandEventTypeBeginSeeking;
[backgroundChannel invokeMethod:@"onSeekBackward" arguments:@[@(begin)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) changeRepeatMode: (MPChangeRepeatModeCommandEvent *) event {
NSLog(@"changeRepeatMode");
int modeIndex;
switch (event.repeatType) {
case MPRepeatTypeOff:
modeIndex = 0;
break;
case MPRepeatTypeOne:
modeIndex = 1;
break;
// MPRepeatTypeAll
default:
modeIndex = 2;
break;
}
[backgroundChannel invokeMethod:@"onSetRepeatMode" arguments:@[@(modeIndex)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (MPRemoteCommandHandlerStatus) changeShuffleMode: (MPChangeShuffleModeCommandEvent *) event {
NSLog(@"changeShuffleMode");
int modeIndex;
switch (event.shuffleType) {
case MPShuffleTypeOff:
modeIndex = 0;
break;
case MPShuffleTypeItems:
modeIndex = 1;
break;
// MPShuffleTypeCollections
default:
modeIndex = 2;
break;
}
[backgroundChannel invokeMethod:@"onSetShuffleMode" arguments:@[@(modeIndex)]];
return MPRemoteCommandHandlerStatusSuccess;
}
- (void) dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
@end

+ 20
- 0
gradle.properties View File

@ -0,0 +1,20 @@
kotlin.mpp.enableGranularSourceSetsMetadata=true
#Kotlin
kotlin.code.style=official
#Android
android.useAndroidX=true
#Common versions
version.unfreeze=0.1.0
version.okio=3.0.0-alpha.9
#version.kotlinx.serialization=1.2.1
# SYS versions
systemProp.kotlin=1.5.21
systemProp.androidGradlePlugin=4.1.3
systemProp.npmPublish=2.0.4
#Shut up
kotlin.mpp.stability.nowarn=true

BIN
gradle/wrapper/gradle-wrapper.jar View File


android/gradle/wrapper/gradle-wrapper.properties → gradle/wrapper/gradle-wrapper.properties View File


android/gradlew → gradlew View File


android/gradlew.bat → gradlew.bat View File


+ 0
- 37
ios/.gitignore View File

@ -1,37 +0,0 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/flutter_export_environment.sh

+ 0
- 0
ios/Assets/.gitkeep View File


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save