+* finished android native implementation for
+ * authorization with token response
+ * play
+ * pause
+ * queue
+ * toggleShuffle
+ * toggleRepeat
+ * addToLibrary
diff --git a/local_plugins/spotify_sdk/CODE_OF_CONDUCT.md b/local_plugins/spotify_sdk/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..bb829fedf
--- /dev/null
+++ b/local_plugins/spotify_sdk/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at tobi@minimalme.de. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/local_plugins/spotify_sdk/CONTRIBUTING.md b/local_plugins/spotify_sdk/CONTRIBUTING.md
new file mode 100644
index 000000000..db7ce8670
--- /dev/null
+++ b/local_plugins/spotify_sdk/CONTRIBUTING.md
@@ -0,0 +1,48 @@
+# How you can contribute to spotify_sdk
+
+Hi nice to see you here. 🙌🎉
+
+Thank you for taking the time to contribute to this package ! 👍
+
+This document should be a set of guidelines that can help you to create a meaningful
+pull request or issue. Don't see this as any kind of rules, this is a living document,
+so if you have an idea how to optimize this propose changes in a pull request.
+
+## How do I propose a change?
+
+Changes to the public API should be done via an issue. So we can discuss the proposed
+changes before you put any work into them.
+
+If you are fixing a bug, you can just submit a pull request. We do recommend filing an issue
+as well to get an overview what it is that you are fixing.
+This is helpful in case we don’t accept that specific fix but want to keep
+track of the issue.
+
+## What do I do before creating a pull request
+
+1. Fork the repository and branch out of `master`, prefixing your branch's name with `feature/`, `bug/` or `task/` to indicate the scope of the PR.
+1. Install all dependencies (`flutter packages get` or `pub get`)
+1. Ensure you have a meaningful PR name using the [imperative mood](https://chris.beams.io/posts/git-commit/#imperative) as all your commits will be squashed upon merge and the PR's name will be used as the merge commit's message.
+1. If the PR can be broken down into multiple meaningful PRs please do so so that it is easier to review.
+1. If you’ve fixed a bug or added code that should be tested, add tests!
+1. If you've changed the public API, make sure to update/add documentation (for now that is the [Readme](README.md))
+1. If you've made breaking changes give us a heads up in the pull request. Try to provide a compatibility path for the deprecated APIs and if necessary provide migration instructions in the [Readme](README.md).
+1. Format your code (`dartfmt -w .`)
+1. Analyze your code (`flutter analyze`)
+1. Create the Pull Request
+1. Verify that all status checks are passing
+
+While the prerequisites above must be satisfied prior to having your
+pull request reviewed, the reviewer(s) may ask you to complete additional
+design work, tests, or other changes before your pull request can be ultimately
+accepted.
+
+## Getting in Touch
+
+If you want to just ask a question or get feedback on an idea you can post it
+on [Slack](https://join.slack.com/t/spotifysdk/shared_invite/zt-gibgpkf9-o2ZEJBMPqXNTvqqPONYUQA).
+
+## License
+
+By contributing to spotify_sdk, you agree that your contributions will be licensed
+under its [MIT license](LICENSE).
diff --git a/local_plugins/spotify_sdk/LICENSE b/local_plugins/spotify_sdk/LICENSE
new file mode 100644
index 000000000..261eeb9e9
--- /dev/null
+++ b/local_plugins/spotify_sdk/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/local_plugins/spotify_sdk/README.md b/local_plugins/spotify_sdk/README.md
new file mode 100644
index 000000000..d9a48385e
--- /dev/null
+++ b/local_plugins/spotify_sdk/README.md
@@ -0,0 +1,257 @@
+# spotify_sdk
+
+
+
+
+
+
+
+---
+
+## Description
+
+This is a flutter package that wraps the native [iOS](https://github.com/spotify/ios-sdk) and [Android](https://github.com/spotify/android-sdk) Spotify "remote" SDKs as well as the [Spotify Web Playback SDK](https://developer.spotify.com/documentation/web-playback-sdk/) for web. Since it wraps the native SDKs it has the same features and limitations.
+
+## Setup
+
+### Android
+
+This package is using both the spotify-app-remote sdk and spotify-auth library. The auth library is needed to get the access token to work with the web api.
+
+From the [Spotify Android SDK Quick Start](https://developer.spotify.com/documentation/android/quick-start/). You need two things:
+
+1. Register your app in the [spotify developer portal](https://developer.spotify.com/dashboard/). You also need to create a sha-1 fingerprint and add this and your package name to the app settings on the dashboard as well as a redirect url.
+2. Follow the steps below for either [Option A: Auto setup](#option-a-auto-setup) or [Option B: Manual setup](#option-b-manual-setup-instructions-for-android-studio-42).
+
+#### Option A: Auto setup
+
+Use the provided setup script to automatically download the latest version of the spotify-app-remote sdk from GitHub and setup the gradle files inside your android project. Run the following command in the root folder of your flutter project.
+
+```bash
+dart run spotify_sdk:android_setup
+```
+
+Use the `--help` flag to see all available options.
+
+#### Option B: Manual setup, instructions for Android Studio 4.2+
+
+Download the current [Spotify Android SDK](https://github.com/spotify/android-sdk/releases). Here you need the `spotify-app-remote-\*.aar``.
+
+After you are all setup you need to add the SDKs `\*.aar`` file to your Android Project as Module. See the [Spotify Android SDK Quick Start](https://developer.spotify.com/documentation/android/quick-start/) for detailed information.
+
+Since Android Studio 4.2 you need to manually perform these steps in order to add .jar/.aar files:
+
+1. Open the android folder of your flutter project as an Android Studio project
+2. In the android root folder create a single folder for `spotify-app-remote`, place the corresponding aar file and create an empty build.gradle file, like on the screenshot below:
+ 
+
+3. Content of the `spotify-app-remote/build.gradle` file:
+
+```groovy
+configurations.maybeCreate("default")
+artifacts.add("default", file('spotify-app-remote-release-x.x.x.aar'))
+```
+
+4. In the android root folder find `settings.gradle` file, open it and add the following line at the top of the file:
+
+```groovy
+include ':spotify-app-remote'
+```
+
+5. In the app/build.gradle add the following to the default config
+
+```groovy
+defaultConfig {
+ manifestPlaceholders = [redirectSchemeName: "spotify-sdk", redirectHostName: "auth"]
+ ...
+ }
+```
+
+### iOS
+
+Register your app in the [spotify developer portal](https://developer.spotify.com/dashboard/). You also need to register your Bundle ID as well as a Redirect URI.
+
+Follow the instructions in the section `Setup the iOS SDK` of [Spotify iOS SDK Quick Start](https://developer.spotify.com/documentation/ios/quick-start/).
+
+### Web
+
+1. Register your app in the [spotify developer portal](https://developer.spotify.com/dashboard/). You need to provide a redirect URL which points to a dedicated page on a website you own.
+
+2. Paste the following onto the webpage, which you linked to in your redirect URL.
+```html
+
+
+
+ Authenticating Spotify
+
+
+ Please wait while we authenticate Spotify...
+
+
+
+```
+
+3. Optionally add this to your Flutter app web/index.html to avoid a Javascript `TypeError: r.__extends is not a function` error in development mode.
+
+```html
+
+
+```
+
+[You need Spotify Premium to access the Web SDK.](https://developer.spotify.com/documentation/web-playback-sdk/quick-start/)
+
+## Usage
+
+To start using this package first import it in your Dart file.
+
+```dart
+import 'package:spotify_sdk/spotify_sdk.dart';
+```
+
+To connect to the Spotify app you can call connectToSpotifyRemote(...) or getAccessToken(...). In both of these methods you need the client id, which you will find in the Spotify developer dashboard and the redirect url you set there for that specific client.
+
+```dart
+await SpotifySdk.connectToSpotifyRemote(clientId: "", redirectUrl: "")
+```
+
+Subscribe to `PlayerState` or `PlayerContext` streams only after connecting successfully
+
+```dart
+SpotifySdk.subscribePlayerState();
+SpotifySdk.subscribePlayerContext();
+```
+
+If you want to use the web api as well you have to use this method to get the access token.
+You can specify multiple scopes by separating them with a comma "," as shown below. For more information on scopes you can refer to [Spotify Authorization Scopes Guide](https://developer.spotify.com/documentation/general/guides/authorization/scopes/)
+
+```dart
+final accessToken = await SpotifySdk.getAccessToken(clientId: "", redirectUrl: "", scope: "app-remote-control,user-modify-playback-state,playlist-read-private");
+```
+
+On Web you can use the token that you get from `getAccessToken(...)` and then pass it to `connectToSpotifyRemote(...)`. This will avoid having to send user through two Spotify OAuth prompts. You should not persist this token, nor supply a different token, because the refresh token is only set interally by `getAccessToken` or `connectToSpotifyRemote`.
+
+On iOS you can store the token that you get from `getAccessToken(...)` and then pass it to `connectToSpotifyRemote(...)` during the next session. This will avoid having to switch to the Spotify app for establishing the connection. This library does not handle storing the token. It is up to you to persist it wherever you see fit. Keep in mind that this feature is currently quite buggy in the native iOS SDK and has many side effects like random disconnections. Proceed with caution.
+
+On iOS Spotify starts playing music when attempting connection. This is a default behavior and there is no official way to prevent this with the currently supported authentication flows. You have the option to pass a Spotify URI upon connection or set it to a blank string to play the last played song. There is an undocumented workaround if you don't want music to start playing which is to pass an invalid Spotify URI instead. This is not officially supported by the Spotify SDK or this library and it can fail or stop working at any time!
+
+Have a look [in the example](example/lib/main.dart) for detailed insights on how you can use this package.
+
+### Token Swap
+
+You can optionally specify "token swap" URLs to manage tokens with a backend service that protects your OAuth client secret. For more information refer to the [Spotify Token Swap and Refresh Guide](https://developer.spotify.com/documentation/ios/guides/token-swap-and-refresh/)
+
+```dart
+SpotifySdkPlugin.tokenSwapURL = 'https://example.com/api/spotify/token';
+SpotifySdkPlugin.tokenRefreshURL = 'https://example.com/api/spotify/refresh';
+````
+
+On web, this package will perform an Authorization Code (without PKCE) flow, then exchange the code and refresh the token with a backend service you run at the URLs provided.
+
+Token Swap is for now "web only". While the iOS SDK also supports the "token swap", this flow is not yet supported.
+
+### Api
+
+#### Connecting/Authenticating
+
+| Function | Description| Android | iOS | Web |
+|---|---|---|---|---|
+| connectToSpotifyRemote | Connects the App to Spotify | ✔ | ✔ | ✔ |
+| getAccessToken | Gets the Access Token that you can use to work with the [Web Api](https://developer.spotify.com/documentation/web-api/) | ✔ | ✔ | ✔ |
+| disconnect | Disconnects the app connection | ✔ | ✔ | ✔ |
+| subscribeConnectionStatus | Subscribes to the current player state. | ✔ | ✔ | 🚧 |
+
+#### Player Api
+
+The playerApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/PlayerApi.html).
+
+| Function | Description | Android | iOS | Web |
+|-------------------------|---|--|---|---|
+| getCrossfadeState | Gets the current crossfade state | ✔ | ✔ | ❌ |
+| getPlayerState | Gets the current player state |✔ | ✔ | ✔ |
+| pause | Pauses the current track |✔ | ✔ | ✔ |
+| play | Plays the given spotifyUri |✔ | ✔ | ✔ |
+| playWithStreamType | Play the given Spotify uri with specific behaviour for that streamtype | 🚧 | 🚧 | 🚧 |
+| queue | Queues given spotifyUri |✔ | ✔ | ✔ |
+| resume | Resumes the current track |✔ | ✔ | ✔ |
+| seekTo | Seeks the current track to the given position in milliseconds | ✔ | ✔ | 🚧 |
+| seekToRelativePosition | Adds to the current position of the track the given milliseconds | ✔ | ❌ | 🚧 |
+| setPodcastPlaybackSpeed | Set playback speed for Podcast | ✔ | 🚧 | 🚧 |
+| setRepeatMode | Set the repeat mode | ✔ | ✔ | ✔ |
+| setShuffle | Set the shuffle mode | ✔ | ✔ | ✔ |
+| skipNext | Skips to next track | ✔ | ✔ | ✔ |
+| skipPrevious | Skips to previous track |✔ | ✔ | ✔ |
+| skipToIndex | Skips to track at specified index in album or playlist |✔ | ✔ | 🚧 |
+| subscribePlayerContext | Subscribes to the current player context | ✔ | ✔ | ✔ |
+| subscribePlayerState | Subscribes to the current player state | ✔ | ✔ | ✔ |
+| toggleRepeat | Cycles through the repeat modes | ✔ | ✔ | ❌ |
+| toggleShuffle | Cycles through the shuffle modes | ✔ | ❌ | ❌ |
+
+On Web, an automatic call to play may not work due to media activation policies which send an error: "Authentication Error: Browser prevented autoplay due to lack of interaction". This error is ignored by the SDK so you can still present a button for the user to click to `play` or `resume` to start playback. See the [Web SDK Troubleshooting guide](https://developer.spotify.com/documentation/web-playback-sdk/reference/#troubleshooting) for more details.
+
+#### Images Api
+
+The imagesApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/ImagesApi.html).
+
+| Function | Description| Android | iOS | Web |
+|---|---|---|---|---|
+| getImage | Get the image from the given spotifyUri | ✔ | ✔ | 🚧 |
+
+#### User Api
+
+The userApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/UserApi.html).
+
+| Function | Description| Android | iOS | Web |
+|---|---|---|---|---|
+| addToLibrary | Adds the given spotifyUri to the users library | ✔ | ✔ | 🚧 |
+| getCapabilities | Gets the current users capabilities | ✔ | ✔ | 🚧 |
+| getLibraryState | Gets the current library state | ✔ | ✔ | 🚧 |
+| removeFromLibrary | Removes the given spotifyUri to the users library | ✔ | ✔ | 🚧 |
+| subscribeCapabilities | Subscribes to the current users capabilities | ✔ | 🚧 | 🚧 |
+| subscribeUserStatus | Subscribes to the current users status | ✔ | 🚧 | 🚧 |
+
+#### Connect Api
+
+The connectApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/ConnectApi.html).
+
+| Function | Description | Android | iOS | Web |
+|----------------------------|--------------------------------------------|---|---|---|
+| connectDecreaseVolume | Decrease volume by a step size determined | 🚧 | 🚧 | 🚧 |
+| connectIncreaseVolume | Increase volume by a step size determined | 🚧 | 🚧 | 🚧 |
+| connectSetVolume | Set a volume on the currently active device | 🚧 | 🚧 | 🚧 |
+| connectSwitchToLocalDevice | Switch to play music on this (local) device | ✔ | 🚧 | 🚧 |
+| subscribeToVolumeState | Subscribe to volume state | 🚧 | 🚧 | 🚧 |
+
+#### Content Api
+
+The contentApi as described [here](https://spotify.github.io/android-sdk/app-remote-lib/docs/com/spotify/android/appremote/api/ContentApi.html).
+
+| Function | Description| Android | iOS | Web |
+|---|---|---|---|---|
+| getChildrenOfItem | tbd | 🚧 | 🚧 | 🚧 |
+| getRecommendedContentItems | tbd | 🚧 | 🚧 | 🚧 |
+| playContentItem | tbd | 🚧 | 🚧 | 🚧 |
+
+## Migration
+
+### spotify-auth: moving from locally sourced aar to maven central
+`spotify-auth` SDK is now retrieved via Maven Central instead of being sourced from an AAR file
+* Steps to remove the locally sourced `spotify-auth` SDK:
+ * android/settings.gradle -> remove `':spotify-auth'`
+ * android/spotify-auth/build.gradle -> remove file
+ * android/spotify-auth/spotify-auth-release-x.x.x.aar -> remove file
+
+## Official Spotify Docs
+
+- [Auth](https://spotify.github.io/android-sdk/auth-lib/docs/index.html)
+- [App Remote](https://spotify.github.io/android-sdk/app-remote-lib/docs/index.html)
+- [Web Playback SDK](https://developer.spotify.com/documentation/web-playback-sdk/)
+
+
diff --git a/local_plugins/spotify_sdk/analysis_options.yaml b/local_plugins/spotify_sdk/analysis_options.yaml
new file mode 100644
index 000000000..1cec1d72f
--- /dev/null
+++ b/local_plugins/spotify_sdk/analysis_options.yaml
@@ -0,0 +1,12 @@
+include: package:flutter_lints/flutter.yaml
+
+analyzer:
+ exclude:
+ # Ignore generated files
+ - "**/*.g.dart"
+ - "lib/generated/**"
+ errors:
+ close_sinks: ignore
+linter:
+ rules:
+ unsafe_html: false
diff --git a/local_plugins/spotify_sdk/android/.gitignore b/local_plugins/spotify_sdk/android/.gitignore
new file mode 100644
index 000000000..c6cbe562a
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/.gitignore
@@ -0,0 +1,8 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
diff --git a/local_plugins/spotify_sdk/android/build.gradle b/local_plugins/spotify_sdk/android/build.gradle
new file mode 100644
index 000000000..c11324b59
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/build.gradle
@@ -0,0 +1,74 @@
+group 'de.minimalme.spotify_sdk'
+version '1.0-SNAPSHOT'
+
+buildscript {
+ ext.kotlin_version = '1.8.22'
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "https://jitpack.io" }
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:8.1.4'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+rootProject.allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "https://jitpack.io" }
+ }
+
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ if (project.android.hasProperty("namespace")) {
+ namespace 'de.minimalme.spotify_sdk'
+ }
+
+ compileSdk 34
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ minSdkVersion 21
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ // -- spotify
+ implementation "com.spotify.android:auth:2.1.0"
+ implementation(name: 'spotify-app-remote-release-0.8.0', ext: 'aar')
+ implementation 'com.google.code.gson:gson:2.10.1'
+ // -- events
+ implementation "com.github.stuhlmeier:kotlin-events:v2.0"
+}
+
+repositories {
+ flatDir {
+ dirs 'libs'
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/gradle.properties b/local_plugins/spotify_sdk/android/gradle.properties
new file mode 100644
index 000000000..94adc3a3f
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/local_plugins/spotify_sdk/android/gradle/wrapper/gradle-wrapper.properties b/local_plugins/spotify_sdk/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..dedd5d1e6
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/local_plugins/spotify_sdk/android/libs/spotify-app-remote-release-0.8.0.aar b/local_plugins/spotify_sdk/android/libs/spotify-app-remote-release-0.8.0.aar
new file mode 100644
index 000000000..ec49aad33
Binary files /dev/null and b/local_plugins/spotify_sdk/android/libs/spotify-app-remote-release-0.8.0.aar differ
diff --git a/local_plugins/spotify_sdk/android/settings.gradle b/local_plugins/spotify_sdk/android/settings.gradle
new file mode 100644
index 000000000..eabe3661d
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'spotify_sdk'
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/src/main/AndroidManifest.xml b/local_plugins/spotify_sdk/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..156eeeae3
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/BaseSpotifyApi.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/BaseSpotifyApi.kt
new file mode 100644
index 000000000..0c5624d9b
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/BaseSpotifyApi.kt
@@ -0,0 +1,13 @@
+package de.minimalme.spotify_sdk
+
+import com.spotify.android.appremote.api.SpotifyAppRemote
+import io.flutter.plugin.common.MethodChannel
+
+open class BaseSpotifyApi(internal val spotifyAppRemote: SpotifyAppRemote?, internal val result: MethodChannel.Result) {
+
+ private val errorAppRemoteNull = "spotifyAppRemoteNull"
+
+ internal fun spotifyRemoteAppNotSetError() {
+ result.error(errorAppRemoteNull, "spotifyAppRemote is null", "")
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyConnectApi.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyConnectApi.kt
new file mode 100644
index 000000000..615f890c5
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyConnectApi.kt
@@ -0,0 +1,25 @@
+package de.minimalme.spotify_sdk
+
+import com.spotify.android.appremote.api.SpotifyAppRemote
+import com.spotify.protocol.types.Image.Dimension
+import com.spotify.protocol.types.ImageUri
+import io.flutter.plugin.common.MethodChannel
+import java.io.ByteArrayOutputStream
+import android.graphics.Bitmap
+
+class SpotifyConnectApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChannel.Result) : BaseSpotifyApi(spotifyAppRemote, result) {
+
+ private val errorConnectSwitchToLocalDevice = "errorConnectSwitchToLocalDevice"
+
+ private val connectApi = spotifyAppRemote?.connectApi
+
+ fun switchToLocalDevice() {
+ if (connectApi != null) {
+ connectApi.connectSwitchToLocalDevice()
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorConnectSwitchToLocalDevice, "error when switching to local device", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyImagesApi.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyImagesApi.kt
new file mode 100644
index 000000000..e7439758f
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyImagesApi.kt
@@ -0,0 +1,35 @@
+package de.minimalme.spotify_sdk
+
+import com.spotify.android.appremote.api.SpotifyAppRemote
+import com.spotify.protocol.types.Image.Dimension
+import com.spotify.protocol.types.ImageUri
+import io.flutter.plugin.common.MethodChannel
+import java.io.ByteArrayOutputStream
+import android.graphics.Bitmap
+
+class SpotifyImagesApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChannel.Result) : BaseSpotifyApi(spotifyAppRemote, result) {
+
+ private val errorGetImage = "errorGetImage"
+ private val errorImageUri = "errorImageUri"
+ private val errorImageDimension = "errorImageDimensionUri"
+
+ private val imagesApi = spotifyAppRemote?.imagesApi
+
+ fun getImage(imageUri: String?, dimension: Int?) {
+ if (imagesApi != null && imageUri != null && dimension != null) {
+ imagesApi.getImage(ImageUri(imageUri), Dimension.values().first{it.value == dimension})
+ .setResultCallback { bitmap ->
+ val stream = ByteArrayOutputStream()
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
+ result.success(stream.toByteArray())
+ }
+ .setErrorCallback { throwable -> result.error(errorGetImage, "error when getting the image", throwable.toString()) }
+ } else if (imageUri == null) {
+ result.error(errorImageUri, "imageUri has invalid format or is not set", "")
+ } else if (dimension == null) {
+ result.error(errorImageDimension, "imageDimension has invalid format or is not set", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyPlayerApi.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyPlayerApi.kt
new file mode 100644
index 000000000..f3378b153
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyPlayerApi.kt
@@ -0,0 +1,210 @@
+package de.minimalme.spotify_sdk
+
+import com.google.gson.Gson
+import com.spotify.android.appremote.api.SpotifyAppRemote
+import com.spotify.protocol.types.PlaybackSpeed
+import io.flutter.plugin.common.MethodChannel
+
+class SpotifyPlayerApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChannel.Result) : BaseSpotifyApi(spotifyAppRemote, result) {
+
+
+ private val errorCrossfadeState = "crossfadeStateError"
+ private val errorPlayerState = "PlayerStateError"
+ private val errorQue = "queueError"
+ private val errorPlay = "playError"
+ private val errorPause = "pauseError"
+ private val errorResume = "resumeError"
+ private val errorSkipNext = "skipNextError"
+ private val errorSkipPrevious = "skipPreviousError"
+ private val errorSeekTo = "seekToError"
+ private val errorSkipToIndex = "skipToIndexError"
+ private val errorPodcastPlaybackSpeed = "podcastPlaybackSpeedError"
+ private val errorToggleShuffle = "toggleShuffleError"
+ private val errorToggleRepeat = "toggleRepeatError"
+
+ private val playerApi = spotifyAppRemote?.playerApi
+
+ internal fun getCrossfadeState() {
+ if (playerApi != null) {
+ playerApi.crossfadeState
+ .setResultCallback { crossfadeState ->
+ result.success(Gson().toJson(crossfadeState))
+ }
+ .setErrorCallback { throwable -> result.error(errorCrossfadeState, "error when getting the current state of crossfade setting", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun getPlayerState() {
+ if (playerApi != null) {
+ playerApi.playerState
+ .setResultCallback { playerState ->
+ result.success(Gson().toJson(playerState))
+
+ }
+ .setErrorCallback { throwable -> result.error(errorPlayerState, "error when getting the current state of the player", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun queue(spotifyUri: String?) {
+ if (playerApi != null && !spotifyUri.isNullOrBlank()) {
+ playerApi.queue(spotifyUri)
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorQue, "error when adding uri: $spotifyUri to queue", throwable.toString()) }
+ } else if (spotifyUri.isNullOrBlank()) {
+ result.error(errorQue, "spotifyUri has invalid format or is not set", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun play(spotifyUri: String?) {
+ if (playerApi != null && !spotifyUri.isNullOrBlank()) {
+ playerApi.play(spotifyUri)
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorPlay, "error when playing uri: $spotifyUri", throwable.toString()) }
+ } else if (spotifyUri.isNullOrBlank()) {
+ result.error(errorPlay, "spotifyUri has invalid format or is not set", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun pause() {
+ if (playerApi != null) {
+ playerApi.pause()
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorPause, "error when pausing", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun resume() {
+ if (playerApi != null) {
+ playerApi.resume()
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorResume, "error when resuming", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun seekTo(milliseconds: Int?) {
+ val castedMilliseconds = milliseconds?.toLong()
+ if (playerApi != null && castedMilliseconds != null) {
+ playerApi.seekTo(castedMilliseconds)
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorResume, "error when seeking to: $castedMilliseconds", throwable.toString()) }
+ } else if (castedMilliseconds == null) {
+ result.error(errorSeekTo, "positionMS is not set", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun seekToRelativePosition(milliseconds: Int?) {
+ val castedMilliseconds = milliseconds?.toLong()
+ if (playerApi != null && castedMilliseconds != null) {
+ playerApi.seekToRelativePosition(castedMilliseconds)
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorResume, "error when seeking relative to: $castedMilliseconds", throwable.toString()) }
+ } else if (castedMilliseconds == null) {
+ result.error(errorSeekTo, "milliseconds is not set", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun setPodcastPlaybackSpeed(podcastPlaybackSpeedValue: Int?) {
+ if (playerApi != null && podcastPlaybackSpeedValue != null) {
+
+ val podcastPlaybackSpeed = PlaybackSpeed.PodcastPlaybackSpeed.values().firstOrNull{ it.value == podcastPlaybackSpeedValue }
+
+ playerApi.setPodcastPlaybackSpeed(podcastPlaybackSpeed)
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorPodcastPlaybackSpeed, "error when setting the podcastPlaybackSpeed to: $podcastPlaybackSpeed", throwable.toString()) }
+ } else if (podcastPlaybackSpeedValue == null) {
+ result.error(errorPodcastPlaybackSpeed, "podcastPlaybackSpeedValue is not set", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun skipNext() {
+ if (playerApi != null) {
+ playerApi.skipNext()
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorSkipNext, "error when skipping next", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun skipPrevious() {
+ if (playerApi != null) {
+ playerApi.skipPrevious()
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorSkipPrevious, "error when skipping previous", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun skipToIndex(uri: String?, index: Int?) {
+ if (playerApi != null && !uri.isNullOrBlank() && index != null) {
+ playerApi.skipToIndex(uri, index)
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorSkipToIndex, "error when skipping to index", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun toggleShuffle() {
+ if (playerApi != null) {
+ playerApi.toggleShuffle()
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorToggleShuffle, "error when toggle shuffle", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun setShuffle(shuffle: Boolean?) {
+ if (playerApi != null && shuffle != null) {
+ playerApi.setShuffle(shuffle)
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorToggleRepeat, "error when toggle shuffle", throwable.toString()) }
+ } else if (shuffle == null) {
+ result.error(errorQue, "shuffle has invalid format or is not set", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun toggleRepeat() {
+ if (playerApi != null) {
+ playerApi.toggleRepeat()
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorToggleRepeat, "error when toggle repeat", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ internal fun setRepeatMode(repeatMode: Int?) {
+ if (playerApi != null && repeatMode != null) {
+ playerApi.setRepeat(repeatMode)
+ .setResultCallback { result.success(true) }
+ .setErrorCallback { throwable -> result.error(errorToggleRepeat, "error when toggle repeat", throwable.toString()) }
+ } else if (repeatMode == null) {
+ result.error(errorQue, "repeatMode has invalid format or is not set", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+}
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkPlugin.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkPlugin.kt
new file mode 100644
index 000000000..262837b33
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifySdkPlugin.kt
@@ -0,0 +1,381 @@
+package de.minimalme.spotify_sdk
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import com.spotify.android.appremote.api.ConnectionParams
+import com.spotify.android.appremote.api.Connector.ConnectionListener
+import com.spotify.android.appremote.api.SpotifyAppRemote
+import com.spotify.android.appremote.api.error.*
+import com.spotify.sdk.android.auth.AuthorizationClient
+import com.spotify.sdk.android.auth.AuthorizationRequest
+import com.spotify.sdk.android.auth.AuthorizationResponse
+import de.minimalme.spotify_sdk.subscriptions.*
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.embedding.engine.plugins.activity.ActivityAware
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
+import io.flutter.plugin.common.EventChannel
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+import io.flutter.plugin.common.MethodChannel.Result
+import io.flutter.plugin.common.PluginRegistry
+import kotlinx.event.SetEvent
+import kotlinx.event.event
+
+class SpotifySdkPlugin : MethodCallHandler, FlutterPlugin, ActivityAware, PluginRegistry.ActivityResultListener {
+
+ // application context
+ private var applicationContext : Context? = null
+ private var applicationActivity : Activity? = null
+
+ // method channel
+ private lateinit var methodChannel : MethodChannel
+ private val channelName = "spotify_sdk"
+ private val loggingTag = "spotify_sdk"
+
+ // event channels
+ private var playerContextChannel : EventChannel? = null
+ private var playerStateChannel : EventChannel? = null
+ private var capabilitiesChannel : EventChannel? = null
+ private var userStatusChannel : EventChannel? = null
+ private var connectionStatusChannel : EventChannel? = null
+
+ private val playerContextSubscription = "player_context_subscription"
+ private val playerStateSubscription = "player_state_subscription"
+ private val capabilitiesSubscription = "capabilities_subscription"
+ private val userStatusSubscription = "user_status_subscription"
+ private val connectionStatusSubscription = "connection_status_subscription"
+
+ //connecting
+ private val methodConnectToSpotify = "connectToSpotify"
+ private val methodGetAccessToken = "getAccessToken"
+ private val methodDisconnectFromSpotify = "disconnectFromSpotify"
+
+ // connectApi
+ private val methodSwitchToLocalDevice = "switchToLocalDevice"
+
+ //playerApi
+ private val methodGetCrossfadeState = "getCrossfadeState"
+ private val methodGetPlayerState = "getPlayerState"
+ private val methodPlay = "play"
+ private val methodPause = "pause"
+ private val methodQueueTrack = "queueTrack"
+ private val methodResume = "resume"
+ private val methodSeekToRelativePosition = "seekToRelativePosition"
+ private val methodSetPodcastPlaybackSpeed = "setPodcastPlaybackSpeed"
+ private val methodSkipNext = "skipNext"
+ private val methodSkipPrevious = "skipPrevious"
+ private val methodSkipToIndex = "skipToIndex"
+ private val methodSeekTo = "seekTo"
+ private val methodToggleRepeat = "toggleRepeat"
+ private val methodToggleShuffle = "toggleShuffle"
+ private val methodSetShuffle = "setShuffle"
+ private val methodSetRepeatMode = "setRepeatMode"
+
+ //userApi
+ private val methodAddToLibrary = "addToLibrary"
+ private val methodRemoveFromLibrary = "removeFromLibrary"
+ private val methodGetCapabilities = "getCapabilities"
+ private val methodGetLibraryState = "getLibraryState"
+
+ //imagesApi
+ private val methodGetImage = "getImage"
+
+ private val paramClientId = "clientId"
+ private val paramRedirectUrl = "redirectUrl"
+ private val paramScope = "scope"
+ private val paramSpotifyUri = "spotifyUri"
+ private val paramImageUri = "imageUri"
+ private val paramImageDimension = "imageDimension"
+ private val paramPositionedMilliseconds = "positionedMilliseconds"
+ private val paramRelativeMilliseconds = "relativeMilliseconds"
+ private val paramPodcastPlaybackSpeed = "podcastPlaybackSpeed"
+ private val paramTrackIndex = "trackIndex"
+ private val paramRepeatMode = "repeatMode"
+ private val paramShuffle = "shuffle"
+
+ private val errorConnecting = "errorConnecting"
+ private val errorDisconnecting = "errorDisconnecting"
+ private val errorConnection = "errorConnection"
+ private val errorAuthenticationToken = "authenticationTokenError"
+
+ private var connStatusEventChannel: SetEvent = event()
+
+ private val requestCodeAuthentication = 1337
+
+ private var pendingOperation: PendingOperation? = null
+ private var spotifyAppRemote: SpotifyAppRemote? = null
+ private var spotifyPlayerApi: SpotifyPlayerApi? = null
+ private var spotifyConnectApi: SpotifyConnectApi? = null
+ private var spotifyUserApi: SpotifyUserApi? = null
+ private var spotifyImagesApi: SpotifyImagesApi? = null
+
+ override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+ this.applicationContext = binding.applicationContext
+
+ methodChannel = MethodChannel(binding.binaryMessenger, channelName)
+ methodChannel.setMethodCallHandler(this)
+
+ playerContextChannel = EventChannel(binding.binaryMessenger, playerContextSubscription)
+ playerStateChannel = EventChannel(binding.binaryMessenger, playerStateSubscription)
+ capabilitiesChannel = EventChannel(binding.binaryMessenger, capabilitiesSubscription)
+ userStatusChannel = EventChannel(binding.binaryMessenger, userStatusSubscription)
+ connectionStatusChannel = EventChannel(binding.binaryMessenger, connectionStatusSubscription)
+
+ connectionStatusChannel?.setStreamHandler(ConnectionStatusChannel(connStatusEventChannel))
+ }
+
+ override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+
+ applicationContext = null
+
+ methodChannel.setMethodCallHandler(null)
+
+ playerContextChannel?.setStreamHandler(null)
+ playerStateChannel?.setStreamHandler(null)
+ capabilitiesChannel?.setStreamHandler(null)
+ userStatusChannel?.setStreamHandler(null)
+ connectionStatusChannel?.setStreamHandler(null)
+
+ playerContextChannel = null
+ playerStateChannel = null
+ capabilitiesChannel = null
+ userStatusChannel = null
+ connectionStatusChannel = null
+ }
+
+ override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+ binding.addActivityResultListener(this)
+ applicationActivity = binding.activity
+ }
+
+ override fun onDetachedFromActivityForConfigChanges() {
+ applicationActivity = null
+ }
+
+ override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
+ applicationActivity = binding.activity
+ }
+
+ override fun onDetachedFromActivity() {
+ applicationActivity = null
+ }
+
+ override fun onMethodCall(call: MethodCall, result: Result) {
+
+ if (spotifyAppRemote != null) {
+ spotifyPlayerApi = SpotifyPlayerApi(spotifyAppRemote, result)
+ spotifyUserApi = SpotifyUserApi(spotifyAppRemote, result)
+ spotifyImagesApi = SpotifyImagesApi(spotifyAppRemote, result)
+ spotifyConnectApi = SpotifyConnectApi(spotifyAppRemote, result)
+ }
+
+ when (call.method) {
+ //connecting to spotify
+ methodConnectToSpotify -> connectToSpotify(call.argument(paramClientId), call.argument(paramRedirectUrl), result)
+ methodGetAccessToken -> getAccessToken(call.argument(paramClientId), call.argument(paramRedirectUrl), call.argument(paramScope), result)
+ methodDisconnectFromSpotify -> disconnectFromSpotify(result)
+ //connectApi calls
+ methodSwitchToLocalDevice -> spotifyConnectApi?.switchToLocalDevice()
+ //playerApi calls
+ methodGetCrossfadeState -> spotifyPlayerApi?.getCrossfadeState()
+ methodGetPlayerState -> spotifyPlayerApi?.getPlayerState()
+ methodPlay -> spotifyPlayerApi?.play(call.argument(paramSpotifyUri))
+ methodPause -> spotifyPlayerApi?.pause()
+ methodQueueTrack -> spotifyPlayerApi?.queue(call.argument(paramSpotifyUri))
+ methodResume -> spotifyPlayerApi?.resume()
+ methodSeekTo -> spotifyPlayerApi?.seekTo(call.argument(paramPositionedMilliseconds))
+ methodSeekToRelativePosition -> spotifyPlayerApi?.seekToRelativePosition(call.argument(paramRelativeMilliseconds))
+ methodSetPodcastPlaybackSpeed -> spotifyPlayerApi?.setPodcastPlaybackSpeed(call.argument(paramPodcastPlaybackSpeed))
+ methodSkipNext -> spotifyPlayerApi?.skipNext()
+ methodSkipPrevious -> spotifyPlayerApi?.skipPrevious()
+ methodSkipToIndex -> spotifyPlayerApi?.skipToIndex(call.argument(paramSpotifyUri), call.argument(paramTrackIndex))
+ methodToggleShuffle -> spotifyPlayerApi?.toggleShuffle()
+ methodSetShuffle -> spotifyPlayerApi?.setShuffle(call.argument(paramShuffle))
+ methodToggleRepeat -> spotifyPlayerApi?.toggleRepeat()
+ methodSetRepeatMode -> spotifyPlayerApi?.setRepeatMode(call.argument(paramRepeatMode))
+ //userApi calls
+ methodAddToLibrary -> spotifyUserApi?.addToUserLibrary(call.argument(paramSpotifyUri))
+ methodRemoveFromLibrary -> spotifyUserApi?.removeFromUserLibrary(call.argument(paramSpotifyUri))
+ methodGetCapabilities -> spotifyUserApi?.getCapabilities()
+ methodGetLibraryState -> spotifyUserApi?.getLibraryState(call.argument(paramSpotifyUri))
+ //imageApi calls
+ methodGetImage -> spotifyImagesApi?.getImage(call.argument(paramImageUri), call.argument(paramImageDimension))
+ // method call is not implemented yet
+ else -> result.notImplemented()
+ }
+ }
+
+ //-- Method implementations
+ private fun connectToSpotify(clientId: String?, redirectUrl: String?, result: Result) {
+
+ if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) {
+ result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "")
+ } else {
+ val connectionParams = ConnectionParams.Builder(clientId)
+ .setRedirectUri(redirectUrl)
+ .showAuthView(true)
+ .build()
+ SpotifyAppRemote.disconnect(spotifyAppRemote)
+ var initiallyConnected = false
+ SpotifyAppRemote.connect(applicationContext, connectionParams,
+ object : ConnectionListener {
+ override fun onConnected(spotifyAppRemoteValue: SpotifyAppRemote) {
+ spotifyAppRemote = spotifyAppRemoteValue
+
+ playerContextChannel?.setStreamHandler(PlayerContextChannel(spotifyAppRemote!!.playerApi))
+ Log.i(loggingTag, "Set stream handler for PlayerContextChannel")
+ playerStateChannel?.setStreamHandler(PlayerStateChannel(spotifyAppRemote!!.playerApi))
+ Log.i(loggingTag, "Set stream handler for PlayerStateChannel")
+ capabilitiesChannel?.setStreamHandler(CapabilitiesChannel(spotifyAppRemote!!.userApi))
+ Log.i(loggingTag, "Set stream handler for CapabilitiesChannel")
+ userStatusChannel?.setStreamHandler(UserStatusChannel(spotifyAppRemote!!.userApi))
+ Log.i(loggingTag, "Set stream handler for UserStatusChannel")
+ initiallyConnected = true
+
+ Log.i(loggingTag, "App Remote successfully connected")
+ // emit connection established event
+ connStatusEventChannel(ConnectionStatusChannel.ConnectionEvent(true, "Successfully connected to Spotify.", null, null))
+ // method success
+ result.success(true)
+ }
+
+ override fun onFailure(throwable: Throwable) {
+ val errorDetails = throwable.toString()
+ // determine the error
+ val errorMessage: String
+ val errorCode: String
+ var connected = false
+ when (throwable) {
+ is SpotifyDisconnectedException, is SpotifyConnectionTerminatedException -> {
+ // The Spotify app was/is disconnected by the Spotify app.
+ // This indicates typically that the Spotify app was closed by the user or for other reasons.
+ // You need to reconnect to continue using Spotify App Remote.
+ errorMessage = "The Spotify app was/is disconnected by the Spotify app.Reconnect necessary"
+ errorCode = "SpotifyDisconnectedException"
+ }
+ is CouldNotFindSpotifyApp -> {
+ errorMessage = "The Spotify app is not installed on the device"
+ errorCode = "CouldNotFindSpotifyApp"
+ }
+ is AuthenticationFailedException -> {
+ errorMessage = "Partner app failed to authenticate with Spotify. Check client credentials and make sure your app is registered correctly at developer.spotify.com"
+ errorCode = "AuthenticationFailedException"
+ }
+ is UserNotAuthorizedException -> {
+ errorMessage = "Indicates the user did not authorize this client of App Remote to use Spotify on the users behalf."
+ errorCode = "UserNotAuthorizedException"
+ }
+ is UnsupportedFeatureVersionException -> {
+ errorMessage = "Spotify app can't support requested features. User should update Spotify app."
+ errorCode = "UnsupportedFeatureVersionException"
+ connected = true
+ }
+ is OfflineModeException -> {
+ errorMessage = "Spotify user has set their Spotify app to be in offline mode"
+ errorCode = "OfflineModeException"
+ connected = true
+ }
+ is NotLoggedInException -> {
+ errorMessage = "User has logged out from Spotify."
+ errorCode = "NotLoggedInException"
+ }
+ is SpotifyRemoteServiceException -> {
+ errorMessage = "Encapsulates possible SecurityException and IllegalStateException errors."
+ errorCode = "SpotifyRemoteServiceException"
+ }
+ else -> {
+ errorMessage = "Something went wrong connecting spotify remote"
+ errorCode = errorConnection
+ }
+ }
+ Log.e(loggingTag, errorMessage)
+ // notify plugin
+ if (initiallyConnected) {
+ // emit connection error event
+ connStatusEventChannel(ConnectionStatusChannel.ConnectionEvent(connected, errorMessage, errorCode, errorDetails))
+ } else {
+ // throw exception as the connect method
+ result.error(errorCode, errorMessage, errorDetails)
+ }
+ }
+ })
+ }
+ }
+
+ private fun getAccessToken(clientId: String?, redirectUrl: String?, scope: String?, result: Result) {
+ if (applicationActivity == null) {
+ throw IllegalStateException("getAccessToken needs a foreground activity")
+ }
+
+ if (clientId.isNullOrBlank() || redirectUrl.isNullOrBlank()) {
+ result.error(errorConnecting, "client id or redirectUrl are not set or have invalid format", "")
+ } else {
+ //Convert String? scope to Array. Delimiter set as comma ","
+ val scopeArray = scope?.split(",")?.toTypedArray()
+ methodConnectToSpotify.checkAndSetPendingOperation(result)
+
+ val builder = AuthorizationRequest.Builder(clientId, AuthorizationResponse.Type.TOKEN, redirectUrl)
+ builder.setScopes(scopeArray)
+ val request = builder.build()
+
+ AuthorizationClient.openLoginActivity(applicationActivity, requestCodeAuthentication, request)
+ }
+ }
+
+ private fun disconnectFromSpotify(result: Result) {
+ if (spotifyAppRemote != null && spotifyAppRemote!!.isConnected) {
+ SpotifyAppRemote.disconnect(spotifyAppRemote)
+
+ // emit connection terminated event
+ connStatusEventChannel(ConnectionStatusChannel.ConnectionEvent(false, "Successfully disconnected from Spotify.", null, null))
+ // method success
+ result.success(true)
+ } else if (!spotifyAppRemote!!.isConnected) {
+ result.error(errorDisconnecting, "could not disconnect spotify remote", "you are not connected, no need to disconnect")
+ } else {
+ result.error(errorDisconnecting, "could not disconnect spotify remote", "spotifyAppRemote is not set")
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
+ if (pendingOperation == null) {
+ return false
+ }
+ return when (requestCode) {
+ requestCodeAuthentication -> {
+ authFlow(resultCode, data)
+ return true
+ }
+ else -> false
+ }
+ }
+
+ private fun authFlow(resultCode: Int, data: Intent?) {
+
+ val response: AuthorizationResponse = AuthorizationClient.getResponse(resultCode, data)
+ val result = pendingOperation!!.result
+ pendingOperation = null
+
+ when (response.type) {
+ AuthorizationResponse.Type.TOKEN -> {
+ result.success(response.accessToken)
+ }
+ AuthorizationResponse.Type.ERROR -> result.error(errorAuthenticationToken, "Authentication went wrong", response.error)
+ else -> result.notImplemented()
+ }
+ }
+
+ private fun String.checkAndSetPendingOperation(result: Result) {
+
+ check(pendingOperation == null)
+ {
+ "Concurrent operations detected: " + pendingOperation?.method.toString() + ", " + this
+ }
+ pendingOperation = PendingOperation(this, result)
+ }
+}
+
+private class PendingOperation internal constructor(val method: String, val result: Result)
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyUserApi.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyUserApi.kt
new file mode 100644
index 000000000..4ccb4cf51
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/SpotifyUserApi.kt
@@ -0,0 +1,59 @@
+package de.minimalme.spotify_sdk
+
+import com.google.gson.Gson
+import com.spotify.android.appremote.api.SpotifyAppRemote
+import io.flutter.plugin.common.MethodChannel
+
+class SpotifyUserApi(spotifyAppRemote: SpotifyAppRemote?, result: MethodChannel.Result) : BaseSpotifyApi(spotifyAppRemote, result) {
+
+ private val errorAddToLibrary = "addToLibraryError"
+ private val errorRemoveFromLibrary = "removeFromLibraryError"
+ private val errorGettingCapabilities = "getCapabilitiesError"
+ private val errorGettingLibraryState = "getLibraryStateError"
+
+ private val userApi = spotifyAppRemote?.userApi
+
+ fun addToUserLibrary(spotifyUri: String?) {
+ if (userApi != null && !spotifyUri.isNullOrBlank()) {
+ userApi.addToLibrary(spotifyUri)
+ .setResultCallback {result.success(true)}
+ .setErrorCallback { throwable -> result.error(errorAddToLibrary, "error when adding uri to user library", throwable.toString()) }
+ } else if (spotifyUri.isNullOrBlank()) {
+ result.error(errorAddToLibrary, "spotifyUri has invalid format", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ fun removeFromUserLibrary(spotifyUri: String?){
+ if (userApi != null && !spotifyUri.isNullOrBlank()) {
+ userApi.removeFromLibrary(spotifyUri)
+ .setResultCallback {result.success(true)}
+ .setErrorCallback { throwable -> result.error(errorRemoveFromLibrary, "error when removing uri from user library", throwable.toString()) }
+ } else if (spotifyUri.isNullOrBlank()) {
+ result.error(errorRemoveFromLibrary, "spotifyUri has invalid format", "")
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ fun getCapabilities(){
+ if (userApi != null) {
+ userApi.capabilities
+ .setResultCallback {capabilities -> result.success(Gson().toJson(capabilities))}
+ .setErrorCallback { throwable -> result.error(errorGettingCapabilities, "error when getting capabilities", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+
+ fun getLibraryState(spotifyUri: String?){
+ if (userApi != null && !spotifyUri.isNullOrBlank()) {
+ userApi.getLibraryState(spotifyUri)
+ .setResultCallback {libraryState -> result.success(Gson().toJson(libraryState))}
+ .setErrorCallback { throwable -> result.error(errorGettingLibraryState, "error when getting the library state", throwable.toString()) }
+ } else {
+ spotifyRemoteAppNotSetError()
+ }
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/CapabilitiesChannel.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/CapabilitiesChannel.kt
new file mode 100644
index 000000000..df15a164d
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/CapabilitiesChannel.kt
@@ -0,0 +1,23 @@
+package de.minimalme.spotify_sdk.subscriptions
+
+import com.google.gson.Gson
+import com.spotify.android.appremote.api.PlayerApi
+import com.spotify.android.appremote.api.UserApi
+import io.flutter.plugin.common.EventChannel
+
+class CapabilitiesChannel(private val userApi: UserApi) : EventChannel.StreamHandler {
+
+ private val errorSubscribeCapabilities = "subscribeCapabilitiesError"
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
+ userApi.subscribeToCapabilities()
+ .setEventCallback {
+ capabilities -> events?.success(Gson().toJson(capabilities))
+ }
+ .setErrorCallback {
+ throwable -> events?.error(errorSubscribeCapabilities, "error when subscribing to the users capabilities", throwable.toString())
+ }
+ }
+
+ override fun onCancel(arguments: Any?) {
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/ConnectionStatusChannel.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/ConnectionStatusChannel.kt
new file mode 100644
index 000000000..4a24d46e2
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/ConnectionStatusChannel.kt
@@ -0,0 +1,27 @@
+package de.minimalme.spotify_sdk.subscriptions
+
+import android.util.Log
+import com.google.gson.Gson
+import com.google.gson.annotations.SerializedName
+import io.flutter.plugin.common.EventChannel
+import kotlinx.event.SetEvent
+
+class ConnectionStatusChannel(private val connStatusStream: SetEvent) : EventChannel.StreamHandler {
+
+ data class ConnectionEvent(
+ @SerializedName("connected") val connected: Boolean,
+ @SerializedName("message" )val message: String,
+ @SerializedName("errorCode") val errorCode: String?,
+ @SerializedName("errorDetails") val errorDetails: Any?
+ )
+
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
+ connStatusStream += { event ->
+ // not error
+ events?.success(Gson().toJson(event))
+ }
+ }
+
+ override fun onCancel(arguments: Any?) {
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerContextChannel.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerContextChannel.kt
new file mode 100644
index 000000000..cac23f48c
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerContextChannel.kt
@@ -0,0 +1,22 @@
+package de.minimalme.spotify_sdk.subscriptions
+
+import com.google.gson.Gson
+import com.spotify.android.appremote.api.PlayerApi
+import io.flutter.plugin.common.EventChannel
+
+class PlayerContextChannel(private val playerApi: PlayerApi) : EventChannel.StreamHandler {
+
+ private val errorSubscribePlayerContext = "subscribePlayerContextError"
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
+ playerApi.subscribeToPlayerContext()
+ .setEventCallback {
+ playerContext -> events?.success(Gson().toJson(playerContext))
+ }
+ .setErrorCallback {
+ throwable -> events?.error(errorSubscribePlayerContext, "error when subscribing to the player context", throwable.toString())
+ }
+ }
+
+ override fun onCancel(arguments: Any?) {
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerStateChannel.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerStateChannel.kt
new file mode 100644
index 000000000..1edb6823a
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/PlayerStateChannel.kt
@@ -0,0 +1,22 @@
+package de.minimalme.spotify_sdk.subscriptions
+
+import com.google.gson.Gson
+import com.spotify.android.appremote.api.PlayerApi
+import io.flutter.plugin.common.EventChannel
+
+class PlayerStateChannel(private val playerApi: PlayerApi) : EventChannel.StreamHandler {
+
+ private val errorSubscribePlayerState = "subscribePlayerStateError"
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
+ playerApi.subscribeToPlayerState()
+ .setEventCallback {
+ playerState -> events?.success(Gson().toJson(playerState))
+ }
+ .setErrorCallback {
+ throwable -> events?.error(errorSubscribePlayerState, "error when subscribing to the player state", throwable.toString())
+ }
+ }
+
+ override fun onCancel(arguments: Any?) {
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/UserStatusChannel.kt b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/UserStatusChannel.kt
new file mode 100644
index 000000000..a034b4982
--- /dev/null
+++ b/local_plugins/spotify_sdk/android/src/main/kotlin/de/minimalme/spotify_sdk/subscriptions/UserStatusChannel.kt
@@ -0,0 +1,23 @@
+package de.minimalme.spotify_sdk.subscriptions
+
+import com.google.gson.Gson
+import com.spotify.android.appremote.api.PlayerApi
+import com.spotify.android.appremote.api.UserApi
+import io.flutter.plugin.common.EventChannel
+
+class UserStatusChannel(private val userApi: UserApi) : EventChannel.StreamHandler {
+
+ private val errorSubscribeUserStatus = "subscribeUserStatusError"
+ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
+ userApi.subscribeToUserStatus()
+ .setEventCallback {
+ userStatus -> events?.success(Gson().toJson(userStatus))
+ }
+ .setErrorCallback {
+ throwable -> events?.error(errorSubscribeUserStatus, "error when subscribing to the users status", throwable.toString())
+ }
+ }
+
+ override fun onCancel(arguments: Any?) {
+ }
+}
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/bin/android_cleanup.dart b/local_plugins/spotify_sdk/bin/android_cleanup.dart
new file mode 100644
index 000000000..aca4d637d
--- /dev/null
+++ b/local_plugins/spotify_sdk/bin/android_cleanup.dart
@@ -0,0 +1,39 @@
+import 'dart:io';
+
+import 'android_setup.dart';
+
+/// Removes all changes made by the [android_setup] script.
+Future main(List args) async {
+ logger.i('running android_cleanup script');
+
+ // remove the module directory if it exists
+ final moduleDir = Directory('android/$moduleName');
+ if (moduleDir.existsSync()) {
+ await moduleDir.delete(recursive: true);
+ logger.t('deleted directory ${moduleDir.path}');
+ }
+
+ // remove the include statement from settings.gradle
+ final settingsFile = await File('android/settings.gradle').readAsString();
+ const includeStatements = [
+ "include ':$moduleName'",
+ 'include ":$moduleName"'
+ ];
+ if (includeStatements.any((element) => settingsFile.contains(element))) {
+ final newSettingsFile = includeStatements.fold(settingsFile,
+ (previousValue, element) => previousValue.replaceAll(element, ''));
+ await File('android/settings.gradle').writeAsString(newSettingsFile);
+ logger.t('removed include statement from settings.gradle');
+ }
+
+ // remove the manifestPlaceholder from app/build.gradle
+ final appBuildFile = await File('android/app/build.gradle').readAsString();
+ if (appBuildFile.contains('manifestPlaceholders')) {
+ final newAppBuildFile = appBuildFile
+ .split('\n')
+ .where((element) => !element.contains('manifestPlaceholders'))
+ .join('\n');
+ await File('android/app/build.gradle').writeAsString(newAppBuildFile);
+ logger.t('removed manifestPlaceholders from app/build.gradle');
+ }
+}
diff --git a/local_plugins/spotify_sdk/bin/android_module_creator.dart b/local_plugins/spotify_sdk/bin/android_module_creator.dart
new file mode 100644
index 000000000..2074999ea
--- /dev/null
+++ b/local_plugins/spotify_sdk/bin/android_module_creator.dart
@@ -0,0 +1,96 @@
+import 'dart:io';
+
+import 'package:logger/logger.dart';
+
+import 'android_setup.dart';
+
+/// Responsible for creating a new android module in the /android directory
+/// of the flutter project.
+class AndroidModuleCreator {
+ String moduleName;
+ String aarFileName;
+
+ AndroidModuleCreator(this.moduleName, this.aarFileName);
+
+ String _gradleFileContent(String aarFileName) => '''
+configurations.maybeCreate("default")
+artifacts.add("default", file('$aarFileName'))
+''';
+
+ String _readmeContent(String moduleName, String aarFileName) => '''
+# Spotify Android SDK for Flutter
+This module (`:$moduleName`) was generated by running the `android_setup` script of
+the [spotify_sdk](https://pub.dev/packages/spotify_sdk) plugin.
+
+If you want to update the `aar`-library, please run the `android_setup` script using
+one of the following commands:
+```
+dart run spotify_sdk:android_setup --cleanup
+flutter pub run spotify_sdk:android_setup --cleanup
+```
+''';
+
+ Future createModuleDirectory() async {
+ // create directory in /android
+ final aarDir = await Directory('android/$moduleName').create();
+ logger.t('created new directory ${aarDir.path}');
+
+ // create build.gradle file
+ final gradleFile = await File('${aarDir.path}/build.gradle').create();
+ logger.t('created new file ${gradleFile.path}');
+ await gradleFile.writeAsString(_gradleFileContent(aarFileName));
+
+ _settingsGradle(logger);
+
+ _appBuildGradle(logger);
+
+ // create README.md file
+ final readmeFile = await File('${aarDir.path}/README.md').create();
+ logger.t('created new file ${readmeFile.path}');
+ await readmeFile.writeAsString(_readmeContent(moduleName, aarFileName));
+ }
+
+ void _settingsGradle(Logger logger) async {
+ // edit settings.gradle file
+ final settingsFile = await File('android/settings.gradle').readAsString();
+ final includeStatement = "include ':$moduleName'";
+ if (settingsFile.contains(includeStatement)) {
+ logger.t('settings.gradle already contains $includeStatement');
+ } else {
+ final String newSettingsFile;
+ if (settingsFile.contains("include ':app'")) {
+ newSettingsFile = settingsFile.replaceFirst(
+ 'include \':app\'', 'include \':app\'\ninclude \':$moduleName\'');
+ } else {
+ newSettingsFile = settingsFile.replaceFirst(
+ 'include ":app"', 'include ":app"\ninclude ":$moduleName"');
+ }
+ await File('android/settings.gradle').writeAsString(newSettingsFile);
+ logger.t('added "$includeStatement" to android/settings.gradle');
+ }
+ }
+
+ void _appBuildGradle(Logger logger) async {
+ // edit app/build.gradle file
+ final appBuildFile = await File('android/app/build.gradle').readAsString();
+ if (!appBuildFile.contains('defaultConfig')) {
+ logger.e(
+ 'Error: The file "android/app/build.gradle" does not contain a defaultConfig block. '
+ 'Your android project might be misconfigured.');
+ return;
+ }
+ if (appBuildFile.contains('manifestPlaceholders')) {
+ logger.w(
+ 'android/app/build.gradle already contains manifestPlaceholders. '
+ 'Make sure to update the redirect scheme and host name manually if necessary.');
+ return;
+ }
+
+ final String newAppBuildFile = appBuildFile.replaceFirst(
+ 'defaultConfig {',
+ 'defaultConfig {\n'
+ ' manifestPlaceholders = [redirectSchemeName: "spotify-sdk", redirectHostName: "auth"]');
+ await File('android/app/build.gradle').writeAsString(newAppBuildFile);
+ logger.t('added manifestPlaceholders to android/app/build.gradle');
+ }
+}
diff --git a/local_plugins/spotify_sdk/bin/android_setup.dart b/local_plugins/spotify_sdk/bin/android_setup.dart
new file mode 100644
index 000000000..0854d64b6
--- /dev/null
+++ b/local_plugins/spotify_sdk/bin/android_setup.dart
@@ -0,0 +1,91 @@
+import 'dart:io';
+
+import 'package:logger/logger.dart';
+
+import 'android_module_creator.dart';
+import 'github_api.dart';
+import 'precondition_checker.dart';
+
+import 'android_cleanup.dart' as cleanup;
+
+const String scriptName = 'android_setup';
+const String moduleName = 'spotify-app-remote';
+
+final logger = Logger(filter: ScriptLogFilter(), printer: SimplePrinter());
+Level loglevel = Level.info;
+
+Future main(List args) async {
+ if (args.contains('--help')) {
+ logger.i('''
+ Usage: $scriptName [options]
+ Options:
+ --help: show this help message
+ --verbose: show all logs
+ --cleanup: runs the cleanup script before executing the setup script to remove all previously created changes
+ --sdk-version: the version of the Spotify Android SDK, default is the latest release on GitHub (eg. --sdk-version=0.8.0)
+ ''');
+ return;
+ }
+
+ if (args.contains('--verbose')) {
+ loglevel = Level.trace;
+ logger.t('verbose logging enabled');
+ }
+
+ if (args.contains('--cleanup')) {
+ logger.t('running cleanup script');
+ await cleanup.main(args);
+ }
+
+ if (!PreconditionChecker.setupConditionsMet()) {
+ logger.e('$scriptName can not be executed, '
+ 'please make sure to meet all requirements and try again.');
+ } else {
+ logger.i('running $scriptName script');
+ String? sdkVersion = args.cast().firstWhere(
+ (element) => element?.startsWith('--sdk-version=') ?? false,
+ orElse: () => null);
+ _runSetup(sdkVersion: sdkVersion);
+ }
+}
+
+/// Runs the setup process.
+void _runSetup({String? sdkVersion}) async {
+ Uri url;
+ String name;
+ try {
+ if (sdkVersion == null) {
+ (name, url) =
+ await GitHubApi.fetchLatestAppRemoteReleaseAssetDownloadUrl();
+ } else {
+ (name, url) =
+ await GitHubApi.fetchVersionedAppRemoteReleaseAssetDownloadUrl(
+ sdkVersion);
+ }
+ } catch (e) {
+ logger.e('Failed to fetch the Spotify Android SDK asset, terminating.');
+ return;
+ }
+
+ // create the new module directory
+ final destination = File('android/$moduleName/$name')
+ ..createSync(recursive: true);
+ logger.t('created new file ${destination.path}');
+
+ // download the aar file to the new destination module
+ final client = HttpClient();
+ final request = await client.getUrl(url);
+ final response = await request.close();
+ await response.pipe(destination.openWrite());
+ client.close();
+ logger.t('downloaded $name to ${destination.path}');
+
+ // create the new module
+ await AndroidModuleCreator(moduleName, name).createModuleDirectory();
+}
+
+/// Log filter to show all logs when running the script in release mode.
+class ScriptLogFilter extends LogFilter {
+ @override
+ bool shouldLog(LogEvent event) => event.level.index >= loglevel.index;
+}
diff --git a/local_plugins/spotify_sdk/bin/github_api.dart b/local_plugins/spotify_sdk/bin/github_api.dart
new file mode 100644
index 000000000..0b8606152
--- /dev/null
+++ b/local_plugins/spotify_sdk/bin/github_api.dart
@@ -0,0 +1,100 @@
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+
+import 'android_setup.dart';
+
+/// [fetchLatestAppRemoteReleaseAssetDownloadUrl] fetches the latest release of the
+/// Spotify Android SDK from GitHub API and returns the name and download url of
+/// the spotify-app-remote-release-*.aar asset.
+/// Throws an exception if the request fails.
+class GitHubApi {
+ static const String apiUrl = 'https://api.github.com/repos';
+ static const String spotifyAndroidSdkRepo = '/spotify/android-sdk';
+ static const String latestRelease = '/releases/latest';
+ static const String allReleases = '/releases';
+
+ static Future<(String, Uri)>
+ fetchLatestAppRemoteReleaseAssetDownloadUrl() async {
+ // fetch the github api to get the latest release
+ Uri uri = Uri.parse(apiUrl + spotifyAndroidSdkRepo + latestRelease);
+ try {
+ final response = await http.get(uri);
+ if (response.statusCode == 200) {
+ final data = jsonDecode(response.body);
+ return _findAsset(data);
+ } else {
+ logger.e('Failed to fetch data from the API.');
+ }
+ } catch (e) {
+ logger.e('An error occurred: $e');
+ }
+ throw Exception();
+ }
+
+ static Future<(String, Uri)> fetchVersionedAppRemoteReleaseAssetDownloadUrl(
+ String sdkVersion) async {
+ if (sdkVersion.startsWith('--sdk-version=')) {
+ sdkVersion = sdkVersion.substring(14);
+ }
+ if (sdkVersion.startsWith('v')) {
+ sdkVersion = sdkVersion.substring(1);
+ }
+
+ // fetch the github api to get all releases
+ Uri uri = Uri.parse(apiUrl + spotifyAndroidSdkRepo + allReleases);
+ try {
+ final response = await http.get(uri);
+ if (response.statusCode == 200) {
+ final data = jsonDecode(response.body) as List;
+
+ logger.t(
+ 'Found ${data.length} releases of the Spotify Android SDK on GitHub.');
+ final releaseData = data.firstWhere(
+ (element) {
+ final tagName = element['tag_name'] as String;
+ return tagName.contains('v$sdkVersion');
+ },
+ orElse: () {
+ logger.e('Failed to find a release with version "$sdkVersion".');
+ throw Exception();
+ },
+ );
+
+ return _findAsset(releaseData);
+ }
+ } catch (e) {
+ logger.e('An error occurred: $e');
+ }
+ throw Exception();
+ }
+
+ static (String, Uri) _findAsset(Map data) {
+ final assets = data['assets'] as List;
+
+ final assetMap = {};
+ for (var asset in assets) {
+ final id = asset['id'] as int;
+ final name = asset['name'] as String;
+ final url = asset['browser_download_url'] as String;
+ assetMap[id] = (name, url);
+ }
+
+ // find the spotify-app-remote-release-*.aar asset
+ final assetId = assetMap.keys.firstWhere(
+ (id) =>
+ assetMap[id]?.$1.startsWith('spotify-app-remote-release-') ?? false,
+ orElse: () => -1,
+ );
+
+ if (assetId == -1) {
+ logger.e('Failed to find the Spotify Android SDK asset.');
+ throw Exception();
+ }
+
+ // return the download url of the spotify-app-remote-release-*.aar asset
+ logger.i('Found the Spotify Android SDK asset: '
+ '${assetMap[assetId]!.$1}');
+ return (assetMap[assetId]!.$1, Uri.parse(assetMap[assetId]!.$2));
+ }
+}
diff --git a/local_plugins/spotify_sdk/bin/precondition_checker.dart b/local_plugins/spotify_sdk/bin/precondition_checker.dart
new file mode 100644
index 000000000..dbe49a412
--- /dev/null
+++ b/local_plugins/spotify_sdk/bin/precondition_checker.dart
@@ -0,0 +1,52 @@
+import 'dart:io';
+
+import 'android_setup.dart';
+
+/// Checks if all preconditions are met to execute the script.
+/// Returns true if all preconditions are met, false otherwise.
+class PreconditionChecker {
+ static bool setupConditionsMet() {
+ // check if the script is executed on a supported platform
+ if (!Platform.isMacOS && !Platform.isLinux) {
+ logger.w(
+ 'Warning: This script has not been tested on your platform (${Platform.operatingSystem}).');
+ }
+
+ // check if flutter is installed
+ if (!(Platform.environment['PATH']?.contains("flutter") ?? false) &&
+ Platform.environment['FLUTTER_ROOT'] == null) {
+ logger.e('Error: Flutter is not installed or not in your PATH.');
+ return false;
+ }
+
+ // check if the script is executed from inside a flutter project
+ if (!File('pubspec.yaml').existsSync()) {
+ logger.e(
+ 'Error: The script must be executed from inside a flutter project.');
+ return false;
+ }
+
+ // check if the necessary android files exist
+ if (!File('android/app/build.gradle').existsSync()) {
+ logger.e('Error: The file "android/app/build.gradle" does not exist.');
+ return false;
+ }
+
+ // check if the setup may have already been executed and recommend to run the cleanup script
+ bool prevRun = Directory('android/$moduleName').existsSync() ||
+ File('android/$moduleName/build.gradle').existsSync();
+ if (!prevRun && File('android/settings.gradle').existsSync()) {
+ final settingsFile = File('android/settings.gradle').readAsStringSync();
+ prevRun |= settingsFile.contains("include ':$moduleName'");
+ prevRun |= settingsFile.contains('include ":$moduleName"');
+ }
+
+ if (prevRun) {
+ logger.w('Warning: The setup may have already been executed. '
+ 'Please run dart run spotify_sdk:android_setup --cleanup before running this script again.');
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/local_plugins/spotify_sdk/ios/.gitignore b/local_plugins/spotify_sdk/ios/.gitignore
new file mode 100644
index 000000000..aa479fd3c
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/.gitignore
@@ -0,0 +1,37 @@
+.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
\ No newline at end of file
diff --git a/local_plugins/spotify_sdk/ios/Assets/.gitkeep b/local_plugins/spotify_sdk/ios/Assets/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/local_plugins/spotify_sdk/ios/Classes/ConnectionStatusHandler.swift b/local_plugins/spotify_sdk/ios/Classes/ConnectionStatusHandler.swift
new file mode 100644
index 000000000..ce2ea0493
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/ConnectionStatusHandler.swift
@@ -0,0 +1,45 @@
+import SpotifyiOS
+
+class ConnectionStatusHandler: StatusHandler, SPTAppRemoteDelegate {
+
+ var tokenResult: FlutterResult?
+ var connectionResult: FlutterResult?
+
+ func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
+ connectionResult?(true)
+ tokenResult?(appRemote.connectionParameters.accessToken)
+ eventSink?("{\"connected\": true}")
+
+ connectionResult = nil
+ tokenResult = nil
+ }
+
+ func appRemote(_ appRemote: SPTAppRemote, didFailConnectionAttemptWithError error: Error?) {
+ defer {
+ connectionResult = nil
+ tokenResult = nil
+ }
+
+ if error != nil {
+ // report spotify remote error to plugin
+ eventSink?("{\"connected\": false, \"errorCode\": \"\(error!._code)\", \"errorDetails\": \"\(error!.localizedDescription)\"}")
+ connectionResult?(FlutterError(code: String(error!._code), message: error!.localizedDescription, details: nil))
+ tokenResult?(FlutterError(code: String(error!._code), message: error!.localizedDescription, details: nil))
+ } else {
+ // report disconnection to plugin
+ eventSink?("{\"connected\": false}")
+ connectionResult?(FlutterError(code: "errorConnection", message: "Failed Connection Attempt", details: nil))
+ tokenResult?(FlutterError(code: "errorConnection", message: "Failed Connection Attempt", details: nil))
+ }
+ }
+
+ func appRemote(_ appRemote: SPTAppRemote, didDisconnectWithError error: Error?) {
+ if error != nil {
+ // report spotify remote error to plugin
+ eventSink?("{\"connected\": false, \"errorCode\": \"\(error!._code)\", \"errorDetails\": \"\(error!.localizedDescription)\"}")
+ } else {
+ // report disconnection to plugin
+ eventSink?("{\"connected\": false}")
+ }
+ }
+}
diff --git a/local_plugins/spotify_sdk/ios/Classes/Extension/Dictionary.swift b/local_plugins/spotify_sdk/ios/Classes/Extension/Dictionary.swift
new file mode 100644
index 000000000..831068ed8
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/Extension/Dictionary.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+extension Dictionary {
+ var json: String {
+ let invalidJson = "Invalid JSON"
+ do {
+ let jsonData = try JSONSerialization.data(withJSONObject: self, options: .prettyPrinted)
+ return String(bytes: jsonData, encoding: String.Encoding.utf8) ?? invalidJson
+ } catch {
+ return invalidJson
+ }
+ }
+}
diff --git a/local_plugins/spotify_sdk/ios/Classes/PlayerDelegate.swift b/local_plugins/spotify_sdk/ios/Classes/PlayerDelegate.swift
new file mode 100644
index 000000000..f99391052
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/PlayerDelegate.swift
@@ -0,0 +1,14 @@
+import SpotifyiOS
+import Flutter
+
+class PlayerDelegate: NSObject, SPTAppRemotePlayerStateDelegate {
+
+ var playerStateSink: FlutterEventSink?
+ var playerContextSink: FlutterEventSink?
+
+ func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
+ print("playerStateDidChange")
+ playerStateSink?(State.playerStateDictionary(playerState).json)
+ playerContextSink?(State.playerContextDictionary(playerState).json)
+ }
+}
diff --git a/local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerContextHandler.swift b/local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerContextHandler.swift
new file mode 100644
index 000000000..32b333f13
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerContextHandler.swift
@@ -0,0 +1,18 @@
+import SpotifyiOS
+
+class PlayerContextHandler: StatusHandler {
+ private let appRemote: SPTAppRemote
+ private let playerDelegate: PlayerDelegate
+
+ init (appRemote: SPTAppRemote, playerDelegate: PlayerDelegate) {
+ self.appRemote = appRemote
+ self.playerDelegate = playerDelegate
+ super.init()
+ }
+
+ override func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
+ _ = super.onListen(withArguments: arguments, eventSink: events)
+ playerDelegate.playerContextSink = events
+ return nil
+ }
+}
diff --git a/local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerState.swift b/local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerState.swift
new file mode 100644
index 000000000..067d99b84
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerState.swift
@@ -0,0 +1,85 @@
+import SpotifyiOS
+
+struct State {
+ static func playerStateDictionary(_ playerState: SPTAppRemotePlayerState) -> [String : Any] {
+ let artist = [
+ "name" : playerState.track.artist.name,
+ "uri" : playerState.track.artist.uri
+ ]
+
+ let album = [
+ "name" : playerState.track.album.name,
+ "uri" : playerState.track.album.uri
+ ]
+
+ let imageIdentifier = [
+ "raw" : playerState.track.imageIdentifier
+
+ ]
+
+ let track: [String : Any] = [
+ "album" : album,
+ "artist" : artist,
+ "artists" : [artist],
+ "duration_ms" : playerState.track.duration,
+ "image_id" : imageIdentifier,
+ "is_episode" : playerState.track.isEpisode,
+ "is_podcast" : playerState.track.isPodcast,
+ "name" : playerState.track.name,
+ "uri" : playerState.track.uri
+ ]
+
+ let playbackOptions: [String : Any] = [
+ "shuffle" : playerState.playbackOptions.isShuffling,
+ "repeat" : playerState.playbackOptions.repeatMode.rawValue
+ ]
+
+ let playbackRestrictions = [
+ "can_skip_next" : playerState.playbackRestrictions.canSkipNext,
+ "can_skip_prev" : playerState.playbackRestrictions.canSkipPrevious,
+ "can_repeat_track" : playerState.playbackRestrictions.canRepeatTrack,
+ "can_repeat_context" : playerState.playbackRestrictions.canRepeatContext,
+ "can_toggle_shuffle" : playerState.playbackRestrictions.canToggleShuffle,
+ "can_seek" : playerState.playbackRestrictions.canSeek
+ ]
+
+ return [
+ "track" : track,
+ "is_paused" : playerState.isPaused,
+ "playback_speed" : playerState.playbackSpeed,
+ "playback_position" : playerState.playbackPosition,
+ "playback_options" : playbackOptions,
+ "playback_restrictions" : playbackRestrictions
+ ]
+ }
+
+ static func playerContextDictionary(_ playerState: SPTAppRemotePlayerState) -> [String : Any] {
+ return [
+ "title" : playerState.contextTitle,
+ "subtitle" : playerState.contextTitle,
+ "type" : playerState.contextURI.absoluteString.components(separatedBy: ":")[1],
+ "uri" : playerState.contextURI.absoluteString
+ ]
+ }
+
+ static func crossfadeStateDictionary(_ crossfadeState: SPTAppRemoteCrossfadeState) -> [String : Any] {
+ return [
+ "duration": crossfadeState.duration,
+ "isEnabled": crossfadeState.isEnabled,
+ ]
+ }
+
+ static func userCapabilitiesDictionary(_ userCapabilities: SPTAppRemoteUserCapabilities) -> [String : Any] {
+ return [
+ "can_play_on_demand": userCapabilities.canPlayOnDemand
+ ]
+ }
+
+ static func libraryStateDictionary(_ libraryState: SPTAppRemoteLibraryState) -> [String : Any] {
+ return [
+ "uri": libraryState.uri,
+ "saved": libraryState.isAdded,
+ "can_save": libraryState.canAdd,
+ ]
+ }
+}
diff --git a/local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerStateHandler.swift b/local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerStateHandler.swift
new file mode 100644
index 000000000..fd5f7e814
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/PlayerState/PlayerStateHandler.swift
@@ -0,0 +1,22 @@
+import SpotifyiOS
+
+class PlayerStateHandler: StatusHandler {
+ private let appRemote: SPTAppRemote
+ private let playerDelegate: PlayerDelegate
+
+ init (appRemote: SPTAppRemote, playerDelegate: PlayerDelegate) {
+ self.appRemote = appRemote
+ self.playerDelegate = playerDelegate
+ super.init()
+ }
+
+ override func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
+ _ = super.onListen(withArguments: arguments, eventSink: events)
+ playerDelegate.playerStateSink = events
+ appRemote.playerAPI?.delegate = playerDelegate
+ appRemote.playerAPI?.subscribe { (_, error) -> Void in
+ guard error == nil else { return }
+ }
+ return nil
+ }
+}
diff --git a/local_plugins/spotify_sdk/ios/Classes/SpotifyError.swift b/local_plugins/spotify_sdk/ios/Classes/SpotifyError.swift
new file mode 100644
index 000000000..062bedfa2
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/SpotifyError.swift
@@ -0,0 +1,4 @@
+enum SpotifyError: Error {
+ case spotifyNotInstalledError
+ case redirectURLInvalid
+}
diff --git a/local_plugins/spotify_sdk/ios/Classes/SpotifySdkConstants.swift b/local_plugins/spotify_sdk/ios/Classes/SpotifySdkConstants.swift
new file mode 100644
index 000000000..ec5550cd5
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/SpotifySdkConstants.swift
@@ -0,0 +1,46 @@
+import Foundation
+
+public class SpotifySdkConstants
+{
+ //connecting
+ public static let methodConnectToSpotify = "connectToSpotify"
+ public static let methodGetAccessToken = "getAccessToken"
+ public static let methodDisconnectFromSpotify = "disconnectFromSpotify"
+
+ //player api
+ public static let methodQueueTrack = "queueTrack"
+ public static let methodPlay = "play"
+ public static let methodPause = "pause"
+ public static let methodResume = "resume"
+ public static let methodSkipNext = "skipNext"
+ public static let methodSkipPrevious = "skipPrevious"
+ public static let methodSkipToIndex = "skipToIndex"
+ public static let methodSeekTo = "seekTo"
+ public static let methodGetPlayerState = "getPlayerState"
+ public static let methodGetCrossfadeState = "getCrossfadeState"
+ public static let methodSetShuffle = "setShuffle"
+ public static let methodSetRepeatMode = "setRepeatMode"
+
+ //user api
+ public static let methodAddToLibrary = "addToLibrary"
+ public static let methodRemoveFromLibrary = "removeFromLibrary"
+ public static let methodGetCapabilities = "getCapabilities"
+
+ //images api
+ public static let methodGetImage = "getImage"
+
+ public static let paramClientId = "clientId"
+ public static let paramRedirectUrl = "redirectUrl"
+ public static let paramSpotifyUri = "spotifyUri"
+ public static let paramAsRadio = "asRadio"
+ public static let paramImageUri = "imageUri"
+ public static let paramImageDimension = "imageDimension"
+ public static let paramPositionedMilliseconds = "positionedMilliseconds"
+ public static let paramRelativeMilliseconds = "relativeMilliseconds"
+ public static let paramAccessToken = "accessToken"
+ public static let paramShuffle = "shuffle"
+ public static let paramRepeatMode = "repeatMode"
+ public static let paramTrackIndex = "trackIndex"
+ public static let scope = "scope"
+ public static let getLibraryState = "getLibraryState"
+}
diff --git a/local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.h b/local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.h
new file mode 100644
index 000000000..b6a0c0a91
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.h
@@ -0,0 +1,4 @@
+#import
+
+@interface SpotifySdkPlugin : NSObject
+@end
diff --git a/local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.m b/local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.m
new file mode 100644
index 000000000..427325650
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/SpotifySdkPlugin.m
@@ -0,0 +1,18 @@
+#import "SpotifySdkPlugin.h"
+#if __has_include()
+#import
+#else
+// Support project import fallback if the generated compatibility header
+// is not copied when this plugin is created as a library.
+// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
+#import "spotify_sdk-Swift.h"
+#endif
+
+#import
+
+@implementation SpotifySdkPlugin
++ (void)registerWithRegistrar:(NSObject*)registrar {
+ [SwiftSpotifySdkPlugin registerWithRegistrar:registrar];
+
+}
+@end
diff --git a/local_plugins/spotify_sdk/ios/Classes/StatusHandler.swift b/local_plugins/spotify_sdk/ios/Classes/StatusHandler.swift
new file mode 100644
index 000000000..62ffaf840
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/StatusHandler.swift
@@ -0,0 +1,17 @@
+import Flutter
+import UIKit
+import SpotifyiOS
+
+class StatusHandler: NSObject, FlutterStreamHandler {
+ var eventSink: FlutterEventSink?
+
+ func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
+ eventSink = events
+ return nil
+ }
+
+ func onCancel(withArguments arguments: Any?) -> FlutterError? {
+ eventSink = nil
+ return nil
+ }
+}
diff --git a/local_plugins/spotify_sdk/ios/Classes/SwiftSpotifySdkPlugin.swift b/local_plugins/spotify_sdk/ios/Classes/SwiftSpotifySdkPlugin.swift
new file mode 100644
index 000000000..45e136b91
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/Classes/SwiftSpotifySdkPlugin.swift
@@ -0,0 +1,416 @@
+import Flutter
+import SpotifyiOS
+
+public class SwiftSpotifySdkPlugin: NSObject, FlutterPlugin {
+ private static var instance = SwiftSpotifySdkPlugin()
+ private var appRemote: SPTAppRemote?
+ private var connectionStatusHandler: ConnectionStatusHandler?
+ private var playerStateHandler: PlayerStateHandler?
+ private var playerContextHandler: PlayerContextHandler?
+ private static var playerStateChannel: FlutterEventChannel?
+ private static var playerContextChannel: FlutterEventChannel?
+
+ public static func register(with registrar: FlutterPluginRegistrar) {
+ guard playerStateChannel == nil else {
+ // Avoid multiple plugin registations
+ return
+ }
+ let spotifySDKChannel = FlutterMethodChannel(name: "spotify_sdk", binaryMessenger: registrar.messenger())
+ let connectionStatusChannel = FlutterEventChannel(name: "connection_status_subscription", binaryMessenger: registrar.messenger())
+ playerStateChannel = FlutterEventChannel(name: "player_state_subscription", binaryMessenger: registrar.messenger())
+ playerContextChannel = FlutterEventChannel(name: "player_context_subscription", binaryMessenger: registrar.messenger())
+ registrar.addApplicationDelegate(instance)
+ registrar.addMethodCallDelegate(instance, channel: spotifySDKChannel)
+ instance.connectionStatusHandler = ConnectionStatusHandler()
+ connectionStatusChannel.setStreamHandler(instance.connectionStatusHandler)
+ }
+
+ public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ var defaultPlayAPICallback: SPTAppRemoteCallback {
+ get {
+ return {_, error in
+ if let error = error {
+ result(FlutterError(code: "PlayerAPI Error", message: error.localizedDescription, details: nil))
+ } else {
+ result(true)
+ }
+ }
+ }
+ }
+
+ switch call.method {
+ case SpotifySdkConstants.methodConnectToSpotify:
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let clientID = swiftArguments[SpotifySdkConstants.paramClientId] as? String,
+ !clientID.isEmpty else {
+ result(FlutterError(code: "Argument Error", message: "Client ID is not set", details: nil))
+ return
+ }
+
+ guard let url = swiftArguments[SpotifySdkConstants.paramRedirectUrl] as? String,
+ !url.isEmpty else {
+ result(FlutterError(code: "Argument Error", message: "Redirect URL is not set", details: nil))
+ return
+ }
+
+ connectionStatusHandler?.connectionResult = result
+
+
+ let accessToken: String? = swiftArguments[SpotifySdkConstants.paramAccessToken] as? String
+ let spotifyUri: String = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String ?? ""
+
+ do {
+ try connectToSpotify(clientId: clientID, redirectURL: url, accessToken: accessToken, spotifyUri: spotifyUri, asRadio: swiftArguments[SpotifySdkConstants.paramAsRadio] as? Bool, additionalScopes: swiftArguments[SpotifySdkConstants.scope] as? String)
+ }
+ catch SpotifyError.redirectURLInvalid {
+ result(FlutterError(code: "errorConnecting", message: "Redirect URL is not set or has invalid format", details: nil))
+ }
+ catch {
+ result(FlutterError(code: "CouldNotFindSpotifyApp", message: "The Spotify app is not installed on the device", details: nil))
+ return
+ }
+
+ case SpotifySdkConstants.methodGetAccessToken:
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let clientID = swiftArguments[SpotifySdkConstants.paramClientId] as? String,
+ let url = swiftArguments[SpotifySdkConstants.paramRedirectUrl] as? String else {
+ result(FlutterError(code: "Arguments Error", message: "One or more arguments are missing", details: nil))
+ return
+ }
+ connectionStatusHandler?.tokenResult = result
+ let spotifyUri: String = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String ?? ""
+
+ do {
+ try connectToSpotify(clientId: clientID, redirectURL: url, spotifyUri: spotifyUri, asRadio: swiftArguments[SpotifySdkConstants.paramAsRadio] as? Bool, additionalScopes: swiftArguments[SpotifySdkConstants.scope] as? String)
+ }
+ catch SpotifyError.redirectURLInvalid {
+ result(FlutterError(code: "errorConnecting", message: "Redirect URL is not set or has invalid format", details: nil))
+ }
+ catch {
+ result(FlutterError(code: "CouldNotFindSpotifyApp", message: "The Spotify app is not installed on the device", details: nil))
+ return
+ }
+ case SpotifySdkConstants.methodGetImage:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let paramImageUri = swiftArguments[SpotifySdkConstants.paramImageUri] as? String,
+ let paramImageDimension = swiftArguments[SpotifySdkConstants.paramImageDimension] as? Int else {
+ result(FlutterError(code: "Arguments Error", message: "One or more arguments are missing", details: nil))
+ return
+ }
+
+ class ImageObject: NSObject, SPTAppRemoteImageRepresentable {
+ var imageIdentifier: String = ""
+ }
+
+ let imageObject = ImageObject()
+ imageObject.imageIdentifier = paramImageUri
+ appRemote.imageAPI?.fetchImage(forItem: imageObject, with: CGSize(width: paramImageDimension, height: paramImageDimension), callback: { (image, error) in
+ guard error == nil else {
+ result(FlutterError(code: "ImageAPI Error", message: error?.localizedDescription, details: nil))
+ return
+ }
+ guard let imageData = (image as? UIImage)?.pngData() else {
+ result(FlutterError(code: "ImageAPI Error", message: "Image is empty", details: nil))
+ return
+ }
+ result(imageData)
+ })
+ case SpotifySdkConstants.methodGetPlayerState:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+
+ appRemote.playerAPI?.getPlayerState({ (playerState, error) in
+ guard error == nil else {
+ result(FlutterError(code: "PlayerAPI Error", message: error?.localizedDescription, details: nil))
+ return
+ }
+ guard let playerState = playerState as? SPTAppRemotePlayerState else {
+ result(FlutterError(code: "PlayerAPI Error", message: "PlayerState is empty", details: nil))
+ return
+ }
+ result(State.playerStateDictionary(playerState).json)
+ })
+ case SpotifySdkConstants.methodDisconnectFromSpotify:
+ appRemote?.disconnect()
+// appRemote?.connectionParameters.accessToken = nil
+ result(true)
+ case SpotifySdkConstants.methodPlay:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
+ result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
+ return
+ }
+ let asRadio: Bool = (swiftArguments[SpotifySdkConstants.paramAsRadio] as? Bool) ?? false
+ appRemote.playerAPI?.play(uri, asRadio: asRadio, callback: defaultPlayAPICallback)
+ case SpotifySdkConstants.methodPause:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ appRemote.playerAPI?.pause(defaultPlayAPICallback)
+ case SpotifySdkConstants.methodResume:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ appRemote.playerAPI?.resume(defaultPlayAPICallback)
+ case SpotifySdkConstants.methodSkipNext:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ appRemote.playerAPI?.skip(toNext: defaultPlayAPICallback)
+ case SpotifySdkConstants.methodSkipPrevious:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ appRemote.playerAPI?.skip(toPrevious: { (spotifyResult, error) in
+ if let error = error {
+ result(FlutterError(code: "PlayerAPI Error", message: error.localizedDescription, details: nil))
+ return
+ }
+ result(true)
+ })
+ case SpotifySdkConstants.methodSkipToIndex:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
+ result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
+ return
+ }
+ let index = (swiftArguments[SpotifySdkConstants.paramTrackIndex] as? Int) ?? 0
+
+ appRemote.contentAPI?.fetchContentItem(forURI: uri, callback: { (contentItemResult, error) in
+ guard error == nil else {
+ result(FlutterError(code: "PlayerAPI Error", message: error?.localizedDescription, details: nil))
+ return
+ }
+ guard let contentItem = contentItemResult as? SPTAppRemoteContentItem else {
+ result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
+ return
+ }
+ appRemote.playerAPI?.play(contentItem, skipToTrackIndex: index, callback: defaultPlayAPICallback)
+ })
+
+ case SpotifySdkConstants.methodAddToLibrary:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
+ result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
+ return
+ }
+ appRemote.userAPI?.addItemToLibrary(withURI: uri, callback: defaultPlayAPICallback)
+ case SpotifySdkConstants.methodRemoveFromLibrary:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
+ result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
+ return
+ }
+ appRemote.userAPI?.removeItemFromLibrary(withURI: uri, callback: defaultPlayAPICallback)
+ case SpotifySdkConstants.methodGetCapabilities:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ appRemote.userAPI?.fetchCapabilities(callback: { (capabilitiesResult, error) in
+ guard error == nil else {
+ result(FlutterError(code: "getCapabilitiesError", message: error?.localizedDescription, details: nil))
+ return
+ }
+ guard let userCapabilities = capabilitiesResult as? SPTAppRemoteUserCapabilities else {
+ result(FlutterError(code: "getCapabilitiesError", message: error?.localizedDescription, details: nil))
+ return
+ }
+
+ result(State.userCapabilitiesDictionary(userCapabilities).json)
+ })
+ case SpotifySdkConstants.methodQueueTrack:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
+ result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
+ return
+ }
+ appRemote.playerAPI?.enqueueTrackUri(uri, callback: defaultPlayAPICallback)
+ case SpotifySdkConstants.methodSeekTo:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let position = swiftArguments[SpotifySdkConstants.paramPositionedMilliseconds] as? Int else {
+ result(FlutterError(code: "Position error", message: "No position was specified", details: nil))
+ return
+ }
+ appRemote.playerAPI?.seek(toPosition: position, callback: defaultPlayAPICallback)
+ case SpotifySdkConstants.methodGetCrossfadeState:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ appRemote.playerAPI?.getCrossfadeState({ (crossfadeState, error) in
+ guard error == nil else {
+ result(FlutterError(code: "PlayerAPI Error", message: error?.localizedDescription, details: nil))
+ return
+ }
+ guard let crossfadeState = crossfadeState as? SPTAppRemoteCrossfadeState else {
+ result(FlutterError(code: "PlayerAPI Error", message: "PlayerState is empty", details: nil))
+ return
+ }
+ result(State.crossfadeStateDictionary(crossfadeState).json)
+ })
+ case SpotifySdkConstants.methodSetShuffle:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let shuffle = swiftArguments[SpotifySdkConstants.paramShuffle] as? Bool else {
+ result(FlutterError(code: "Shuffle mode error", message: "No ShuffleMode was specified", details: nil))
+ return
+ }
+ appRemote.playerAPI?.setShuffle(shuffle, callback: defaultPlayAPICallback)
+ case SpotifySdkConstants.methodSetRepeatMode:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let repeatModeIndex = swiftArguments[SpotifySdkConstants.paramRepeatMode] as? UInt,
+ let repeatMode = SPTAppRemotePlaybackOptionsRepeatMode(rawValue: repeatModeIndex) else {
+ result(FlutterError(code: "Repeat mode error", message: "No RepeatMode was specified", details: nil))
+ return
+ }
+ appRemote.playerAPI?.setRepeatMode(repeatMode, callback: defaultPlayAPICallback)
+ case SpotifySdkConstants.getLibraryState:
+ guard let appRemote = appRemote else {
+ result(FlutterError(code: "Connection Error", message: "AppRemote is null", details: nil))
+ return
+ }
+ guard let swiftArguments = call.arguments as? [String:Any],
+ let uri = swiftArguments[SpotifySdkConstants.paramSpotifyUri] as? String else {
+ result(FlutterError(code: "URI Error", message: "No URI was specified", details: nil))
+ return
+ }
+ appRemote.userAPI?.fetchLibraryState(forURI: uri, callback: {libraryStateResult, error in
+ guard error == nil else {
+ result(FlutterError(code: "fetchLibraryStateError", message: error?.localizedDescription, details: nil))
+ return
+ }
+ guard let libraryState = libraryStateResult as? SPTAppRemoteLibraryState else {
+ result(FlutterError(code: "fetchLibraryStateError", message: error?.localizedDescription, details: nil))
+ return
+ }
+
+ result(State.libraryStateDictionary(libraryState).json)
+ })
+ default:
+ result(FlutterMethodNotImplemented)
+ }
+ }
+
+ private func connectToSpotify(clientId: String, redirectURL: String, accessToken: String? = nil, spotifyUri: String = "", asRadio: Bool?, additionalScopes: String? = nil) throws {
+ func configureAppRemote(clientID: String, redirectURL: String, accessToken: String? = nil) throws {
+ guard let redirectURL = URL(string: redirectURL) else {
+ throw SpotifyError.redirectURLInvalid
+ }
+ let configuration = SPTConfiguration(clientID: clientID, redirectURL: redirectURL)
+ let appRemote = SPTAppRemote(configuration: configuration, logLevel: .none)
+ appRemote.delegate = connectionStatusHandler
+ let playerDelegate = PlayerDelegate()
+ playerStateHandler = PlayerStateHandler(appRemote: appRemote, playerDelegate: playerDelegate)
+ SwiftSpotifySdkPlugin.playerStateChannel?.setStreamHandler(playerStateHandler)
+
+ playerContextHandler = PlayerContextHandler(appRemote: appRemote, playerDelegate: playerDelegate)
+ SwiftSpotifySdkPlugin.playerContextChannel?.setStreamHandler(playerContextHandler)
+
+ appRemote.connectionParameters.accessToken = accessToken
+ self.appRemote = appRemote
+ }
+
+ try configureAppRemote(clientID: clientId, redirectURL: redirectURL, accessToken: accessToken)
+
+ var scopes: [String]?
+ if let additionalScopes = additionalScopes {
+ scopes = additionalScopes.components(separatedBy: ",")
+ }
+
+ if accessToken != nil {
+ appRemote?.connect()
+ } else {
+ // Note: A blank string will play the user's last song or pick a random one.
+ self.appRemote?.authorizeAndPlayURI(spotifyUri, asRadio: asRadio ?? false, additionalScopes: scopes) { success in
+ if (!success) {
+ self.connectionStatusHandler?.connectionResult?(FlutterError(code: "spotifyNotInstalled", message: "Spotify app is not installed", details: nil))
+ }
+ }
+ }
+ }
+}
+
+extension SwiftSpotifySdkPlugin {
+ public func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
+ setAccessTokenFromURL(url: url)
+ return true
+ }
+
+ public func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]) -> Void) -> Bool {
+ guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
+ let url = userActivity.webpageURL
+ else {
+ connectionStatusHandler?.connectionResult?(FlutterError(code: "errorConnecting", message: "client id or redirectUrl is invalid", details: nil))
+ connectionStatusHandler?.tokenResult?(FlutterError(code: "errorConnecting", message: "client id or redirectUrl is invalid", details: nil))
+ connectionStatusHandler?.connectionResult = nil
+ connectionStatusHandler?.tokenResult = nil
+ return false
+ }
+
+ setAccessTokenFromURL(url: url)
+ return false
+ }
+
+ private func setAccessTokenFromURL(url: URL) {
+ guard let appRemote = appRemote else {
+ connectionStatusHandler?.connectionResult?(FlutterError(code: "errorConnection", message: "AppRemote is null", details: nil))
+ connectionStatusHandler?.tokenResult?(FlutterError(code: "errorConnection", message: "AppRemote is null", details: nil))
+ connectionStatusHandler?.connectionResult = nil
+ connectionStatusHandler?.tokenResult = nil
+ return
+ }
+
+ guard let token = appRemote.authorizationParameters(from: url)?[SPTAppRemoteAccessTokenKey] else {
+ connectionStatusHandler?.connectionResult?(FlutterError(code: "authenticationTokenError", message: appRemote.authorizationParameters(from: url)?[SPTAppRemoteErrorDescriptionKey], details: nil))
+ connectionStatusHandler?.tokenResult?(FlutterError(code: "authenticationTokenError", message: appRemote.authorizationParameters(from: url)?[SPTAppRemoteErrorDescriptionKey], details: nil))
+ connectionStatusHandler?.connectionResult = nil
+ connectionStatusHandler?.tokenResult = nil
+ return
+ }
+
+ appRemote.connectionParameters.accessToken = token
+ appRemote.connect()
+ }
+}
diff --git a/local_plugins/spotify_sdk/ios/prepare-iOS-SDK.sh b/local_plugins/spotify_sdk/ios/prepare-iOS-SDK.sh
new file mode 100755
index 000000000..7f66afd4f
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/prepare-iOS-SDK.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+REPO_NAME="ios-sdk"
+FRAMEWORK_NAME="SpotifyiOS.xcframework"
+
+#!/bin/sh
+rm -fR ${REPO_NAME}
+mkdir ${REPO_NAME}
+git clone https://github.com/spotify/${REPO_NAME}
+git -C ${REPO_NAME} checkout tags/v3.0.0
+find ./${REPO_NAME} -mindepth 1 -maxdepth 1 -not -name ${FRAMEWORK_NAME} -exec rm -rf '{}' \; # Keep on only the xcframework folder
diff --git a/local_plugins/spotify_sdk/ios/spotify_sdk.podspec b/local_plugins/spotify_sdk/ios/spotify_sdk.podspec
new file mode 100644
index 000000000..7be779eb9
--- /dev/null
+++ b/local_plugins/spotify_sdk/ios/spotify_sdk.podspec
@@ -0,0 +1,26 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint spotify_sdk.podspec' to validate before publishing.
+#
+Pod::Spec.new do |s|
+ s.name = 'spotify_sdk'
+ s.version = '0.0.1'
+ s.summary = 'Unofficial Spotify Flutter SDK.'
+ s.description = <<-DESC
+Unofficial Spotify Flutter SDK.
+ DESC
+ s.homepage = 'https://github.com/brim-borium/spotify_sdk'
+ s.license = { :file => '../LICENSE' }
+ s.author = { 'Your Company' => 'fdimanidis@gmail.com' }
+ s.source = { :path => '.' }
+ s.source_files = 'Classes/**/*'
+ s.dependency 'Flutter'
+ s.platform = :ios, '12.0'
+ s.ios.deployment_target = '12.0'
+ # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
+ s.swift_version = '5.0'
+ s.prepare_command = './prepare-iOS-SDK.sh'
+ s.vendored_frameworks = 'ios-sdk/SpotifyiOS.xcframework'
+ s.preserve_paths = 'ios-sdk/SpotifyiOS.xcframework'
+ end
diff --git a/local_plugins/spotify_sdk/lib/enums/image_dimension_enum.dart b/local_plugins/spotify_sdk/lib/enums/image_dimension_enum.dart
new file mode 100644
index 000000000..1e4105405
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/enums/image_dimension_enum.dart
@@ -0,0 +1,17 @@
+/// Holds the values from the spotify api for supported Image dimensions
+enum ImageDimension {
+ /// large image
+ large,
+
+ ///medium image
+ medium,
+
+ /// small image
+ small,
+
+ ///xsmall image
+ xSmall,
+
+ /// thumbnail image
+ thumbnail
+}
diff --git a/local_plugins/spotify_sdk/lib/enums/podcast_playback_speed.dart b/local_plugins/spotify_sdk/lib/enums/podcast_playback_speed.dart
new file mode 100644
index 000000000..01f27bc11
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/enums/podcast_playback_speed.dart
@@ -0,0 +1,23 @@
+/// Holds the values from the spotify api for supported Podcast playback speeds
+enum PodcastPlaybackSpeed {
+ /// 0.5 x playback speed
+ playbackSpeed_50,
+
+ /// 0.8 x playback speed
+ playbackSpeed_80,
+
+ /// 1 x playback speed
+ playbackSpeed_100,
+
+ /// 1.2 x playback speed
+ playbackSpeed_120,
+
+ /// 1.5 x playback speed
+ playbackSpeed_150,
+
+ /// 2 x playback speed
+ playbackSpeed_200,
+
+ /// 3 x playback speed
+ playbackSpeed_300,
+}
diff --git a/local_plugins/spotify_sdk/lib/enums/repeat_mode_enum.dart b/local_plugins/spotify_sdk/lib/enums/repeat_mode_enum.dart
new file mode 100644
index 000000000..94cd0437f
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/enums/repeat_mode_enum.dart
@@ -0,0 +1,11 @@
+/// Holds the values from the spotify api for RepeatModes
+enum RepeatMode {
+ /// repeat is off
+ off,
+
+ /// repeats the current track
+ track,
+
+ /// repeats the current context
+ context,
+}
diff --git a/local_plugins/spotify_sdk/lib/extensions/image_dimension_extension.dart b/local_plugins/spotify_sdk/lib/extensions/image_dimension_extension.dart
new file mode 100644
index 000000000..3ced43dc5
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/extensions/image_dimension_extension.dart
@@ -0,0 +1,19 @@
+import 'package:spotify_sdk/enums/image_dimension_enum.dart';
+
+///Extension for formatting the ImageDimension enum to value
+///@nodoc
+extension ImageDimensionExtension on ImageDimension {
+ ///maps the value to the specified enum
+ ///@nodoc
+ static const values = {
+ ImageDimension.large: 720,
+ ImageDimension.medium: 480,
+ ImageDimension.small: 360,
+ ImageDimension.xSmall: 240,
+ ImageDimension.thumbnail: 144,
+ };
+
+ /// returns the value
+ ///@nodoc
+ int get value => values[this]!;
+}
diff --git a/local_plugins/spotify_sdk/lib/extensions/podcast_playback_speed_extension.dart b/local_plugins/spotify_sdk/lib/extensions/podcast_playback_speed_extension.dart
new file mode 100644
index 000000000..346cca9e5
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/extensions/podcast_playback_speed_extension.dart
@@ -0,0 +1,21 @@
+import 'package:spotify_sdk/enums/podcast_playback_speed.dart';
+
+///Extension for formatting the PodcastPlaybackSpeed enum to value
+///@nodoc
+extension PodcastPlaybackSpeedExtension on PodcastPlaybackSpeed {
+ ///maps the value to the specified enum
+ ///@nodoc
+ static const values = {
+ PodcastPlaybackSpeed.playbackSpeed_50: 50,
+ PodcastPlaybackSpeed.playbackSpeed_80: 80,
+ PodcastPlaybackSpeed.playbackSpeed_100: 100,
+ PodcastPlaybackSpeed.playbackSpeed_120: 120,
+ PodcastPlaybackSpeed.playbackSpeed_150: 150,
+ PodcastPlaybackSpeed.playbackSpeed_200: 200,
+ PodcastPlaybackSpeed.playbackSpeed_300: 300,
+ };
+
+ /// returns the value
+ ///@nodoc
+ int get value => values[this]!;
+}
diff --git a/local_plugins/spotify_sdk/lib/logging/custom_log_filter.dart b/local_plugins/spotify_sdk/lib/logging/custom_log_filter.dart
new file mode 100644
index 000000000..e8e15a912
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/logging/custom_log_filter.dart
@@ -0,0 +1,8 @@
+import 'package:logger/logger.dart';
+
+class CustomLogFilter extends LogFilter {
+ @override
+ bool shouldLog(LogEvent event) {
+ return true;
+ }
+}
diff --git a/local_plugins/spotify_sdk/lib/models/album.dart b/local_plugins/spotify_sdk/lib/models/album.dart
new file mode 100644
index 000000000..e1c8e244e
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/album.dart
@@ -0,0 +1,15 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'album.g.dart';
+
+@JsonSerializable()
+class Album {
+ Album(this.name, this.uri);
+
+ final String? name;
+ final String? uri;
+
+ factory Album.fromJson(Map json) => _$AlbumFromJson(json);
+
+ Map toJson() => _$AlbumToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/album.g.dart b/local_plugins/spotify_sdk/lib/models/album.g.dart
new file mode 100644
index 000000000..cdbbed1ae
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/album.g.dart
@@ -0,0 +1,17 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'album.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+Album _$AlbumFromJson(Map json) => Album(
+ json['name'] as String?,
+ json['uri'] as String?,
+ );
+
+Map _$AlbumToJson(Album instance) => {
+ 'name': instance.name,
+ 'uri': instance.uri,
+ };
diff --git a/local_plugins/spotify_sdk/lib/models/artist.dart b/local_plugins/spotify_sdk/lib/models/artist.dart
new file mode 100644
index 000000000..f70e97f19
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/artist.dart
@@ -0,0 +1,15 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'artist.g.dart';
+
+@JsonSerializable()
+class Artist {
+ Artist(this.name, this.uri);
+
+ final String? name;
+ final String? uri;
+
+ factory Artist.fromJson(Map json) => _$ArtistFromJson(json);
+
+ Map toJson() => _$ArtistToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/artist.g.dart b/local_plugins/spotify_sdk/lib/models/artist.g.dart
new file mode 100644
index 000000000..003364e55
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/artist.g.dart
@@ -0,0 +1,17 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'artist.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+Artist _$ArtistFromJson(Map json) => Artist(
+ json['name'] as String?,
+ json['uri'] as String?,
+ );
+
+Map _$ArtistToJson(Artist instance) => {
+ 'name': instance.name,
+ 'uri': instance.uri,
+ };
diff --git a/local_plugins/spotify_sdk/lib/models/capabilities.dart b/local_plugins/spotify_sdk/lib/models/capabilities.dart
new file mode 100644
index 000000000..4652528fb
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/capabilities.dart
@@ -0,0 +1,18 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'capabilities.g.dart';
+
+@JsonSerializable()
+class Capabilities {
+ Capabilities({
+ required this.canPlayOnDemand,
+ });
+
+ @JsonKey(name: 'can_play_on_demand')
+ final bool canPlayOnDemand;
+
+ factory Capabilities.fromJson(Map json) =>
+ _$CapabilitiesFromJson(json);
+
+ Map toJson() => _$CapabilitiesToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/capabilities.g.dart b/local_plugins/spotify_sdk/lib/models/capabilities.g.dart
new file mode 100644
index 000000000..b35d53cb6
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/capabilities.g.dart
@@ -0,0 +1,16 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'capabilities.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+Capabilities _$CapabilitiesFromJson(Map json) => Capabilities(
+ canPlayOnDemand: json['can_play_on_demand'] as bool,
+ );
+
+Map _$CapabilitiesToJson(Capabilities instance) =>
+ {
+ 'can_play_on_demand': instance.canPlayOnDemand,
+ };
diff --git a/local_plugins/spotify_sdk/lib/models/connection_status.dart b/local_plugins/spotify_sdk/lib/models/connection_status.dart
new file mode 100644
index 000000000..2b913b416
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/connection_status.dart
@@ -0,0 +1,31 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'connection_status.g.dart';
+
+@JsonSerializable()
+class ConnectionStatus {
+ ConnectionStatus(
+ this.message,
+ this.errorCode,
+ this.errorDetails, {
+ required this.connected,
+ });
+
+ @JsonKey(name: 'connected')
+ final bool connected;
+ @JsonKey(name: 'message')
+ final String? message;
+ @JsonKey(name: 'errorCode')
+ final String? errorCode;
+ @JsonKey(name: 'errorDetails')
+ final String? errorDetails;
+
+ bool hasError() {
+ return errorCode?.isNotEmpty == true;
+ }
+
+ factory ConnectionStatus.fromJson(Map json) =>
+ _$ConnectionStatusFromJson(json);
+
+ Map toJson() => _$ConnectionStatusToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/connection_status.g.dart b/local_plugins/spotify_sdk/lib/models/connection_status.g.dart
new file mode 100644
index 000000000..0d2f13320
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/connection_status.g.dart
@@ -0,0 +1,23 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'connection_status.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ConnectionStatus _$ConnectionStatusFromJson(Map json) =>
+ ConnectionStatus(
+ json['message'] as String?,
+ json['errorCode'] as String?,
+ json['errorDetails'] as String?,
+ connected: json['connected'] as bool,
+ );
+
+Map _$ConnectionStatusToJson(ConnectionStatus instance) =>
+ {
+ 'connected': instance.connected,
+ 'message': instance.message,
+ 'errorCode': instance.errorCode,
+ 'errorDetails': instance.errorDetails,
+ };
diff --git a/local_plugins/spotify_sdk/lib/models/crossfade_state.dart b/local_plugins/spotify_sdk/lib/models/crossfade_state.dart
new file mode 100644
index 000000000..a0ba32e5f
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/crossfade_state.dart
@@ -0,0 +1,19 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'crossfade_state.g.dart';
+
+@JsonSerializable()
+class CrossfadeState {
+ CrossfadeState(
+ this.duration, {
+ required this.isEnabled,
+ });
+
+ final bool isEnabled;
+ final int duration;
+
+ factory CrossfadeState.fromJson(Map json) =>
+ _$CrossfadeStateFromJson(json);
+
+ Map toJson() => _$CrossfadeStateToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/crossfade_state.g.dart b/local_plugins/spotify_sdk/lib/models/crossfade_state.g.dart
new file mode 100644
index 000000000..74ad4c6f4
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/crossfade_state.g.dart
@@ -0,0 +1,19 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'crossfade_state.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+CrossfadeState _$CrossfadeStateFromJson(Map json) =>
+ CrossfadeState(
+ json['duration'] as int,
+ isEnabled: json['isEnabled'] as bool,
+ );
+
+Map _$CrossfadeStateToJson(CrossfadeState instance) =>
+ {
+ 'isEnabled': instance.isEnabled,
+ 'duration': instance.duration,
+ };
diff --git a/local_plugins/spotify_sdk/lib/models/image_uri.dart b/local_plugins/spotify_sdk/lib/models/image_uri.dart
new file mode 100644
index 000000000..4523a25ed
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/image_uri.dart
@@ -0,0 +1,15 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'image_uri.g.dart';
+
+@JsonSerializable()
+class ImageUri {
+ ImageUri(this.raw);
+
+ final String raw;
+
+ factory ImageUri.fromJson(Map json) =>
+ _$ImageUriFromJson(json);
+
+ Map toJson() => _$ImageUriToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/image_uri.g.dart b/local_plugins/spotify_sdk/lib/models/image_uri.g.dart
new file mode 100644
index 000000000..6082a9f6b
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/image_uri.g.dart
@@ -0,0 +1,15 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'image_uri.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ImageUri _$ImageUriFromJson(Map json) => ImageUri(
+ json['raw'] as String,
+ );
+
+Map _$ImageUriToJson(ImageUri instance) => {
+ 'raw': instance.raw,
+ };
diff --git a/local_plugins/spotify_sdk/lib/models/library_state.dart b/local_plugins/spotify_sdk/lib/models/library_state.dart
new file mode 100644
index 000000000..ff5501534
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/library_state.dart
@@ -0,0 +1,24 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'library_state.g.dart';
+
+@JsonSerializable()
+class LibraryState {
+ LibraryState(
+ this.uri, {
+ required this.isSaved,
+ required this.canSave,
+ });
+
+ @JsonKey(name: 'uri')
+ final String uri;
+ @JsonKey(name: 'saved')
+ final bool isSaved;
+ @JsonKey(name: 'can_save')
+ final bool canSave;
+
+ factory LibraryState.fromJson(Map json) =>
+ _$LibraryStateFromJson(json);
+
+ Map toJson() => _$LibraryStateToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/library_state.g.dart b/local_plugins/spotify_sdk/lib/models/library_state.g.dart
new file mode 100644
index 000000000..ec1288ab1
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/library_state.g.dart
@@ -0,0 +1,20 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'library_state.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+LibraryState _$LibraryStateFromJson(Map json) => LibraryState(
+ json['uri'] as String,
+ isSaved: json['saved'] as bool,
+ canSave: json['can_save'] as bool,
+ );
+
+Map _$LibraryStateToJson(LibraryState instance) =>
+ {
+ 'uri': instance.uri,
+ 'saved': instance.isSaved,
+ 'can_save': instance.canSave,
+ };
diff --git a/local_plugins/spotify_sdk/lib/models/player_context.dart b/local_plugins/spotify_sdk/lib/models/player_context.dart
new file mode 100644
index 000000000..5da6c89a3
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/player_context.dart
@@ -0,0 +1,18 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'player_context.g.dart';
+
+@JsonSerializable()
+class PlayerContext {
+ PlayerContext(this.title, this.subtitle, this.type, this.uri);
+
+ final String title;
+ final String subtitle;
+ final String type;
+ final String uri;
+
+ factory PlayerContext.fromJson(Map json) =>
+ _$PlayerContextFromJson(json);
+
+ Map toJson() => _$PlayerContextToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/player_context.g.dart b/local_plugins/spotify_sdk/lib/models/player_context.g.dart
new file mode 100644
index 000000000..5e9f989d0
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/player_context.g.dart
@@ -0,0 +1,23 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'player_context.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+PlayerContext _$PlayerContextFromJson(Map json) =>
+ PlayerContext(
+ json['title'] as String,
+ json['subtitle'] as String,
+ json['type'] as String,
+ json['uri'] as String,
+ );
+
+Map _$PlayerContextToJson(PlayerContext instance) =>
+ {
+ 'title': instance.title,
+ 'subtitle': instance.subtitle,
+ 'type': instance.type,
+ 'uri': instance.uri,
+ };
diff --git a/local_plugins/spotify_sdk/lib/models/player_options.dart b/local_plugins/spotify_sdk/lib/models/player_options.dart
new file mode 100644
index 000000000..f4766b2f5
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/player_options.dart
@@ -0,0 +1,30 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'player_options.g.dart';
+
+@JsonSerializable()
+class PlayerOptions {
+ PlayerOptions(
+ this.repeatMode, {
+ required this.isShuffling,
+ });
+
+ @JsonKey(name: 'shuffle')
+ final bool isShuffling;
+ @JsonKey(name: 'repeat')
+ final RepeatMode repeatMode;
+
+ factory PlayerOptions.fromJson(Map json) =>
+ _$PlayerOptionsFromJson(json);
+
+ Map toJson() => _$PlayerOptionsToJson(this);
+}
+
+enum RepeatMode {
+ @JsonValue(0)
+ off,
+ @JsonValue(1)
+ track,
+ @JsonValue(2)
+ context
+}
diff --git a/local_plugins/spotify_sdk/lib/models/player_options.g.dart b/local_plugins/spotify_sdk/lib/models/player_options.g.dart
new file mode 100644
index 000000000..64fcefb39
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/player_options.g.dart
@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'player_options.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+PlayerOptions _$PlayerOptionsFromJson(Map json) =>
+ PlayerOptions(
+ $enumDecode(_$RepeatModeEnumMap, json['repeat']),
+ isShuffling: json['shuffle'] as bool,
+ );
+
+Map _$PlayerOptionsToJson(PlayerOptions instance) =>
+ {
+ 'shuffle': instance.isShuffling,
+ 'repeat': _$RepeatModeEnumMap[instance.repeatMode]!,
+ };
+
+const _$RepeatModeEnumMap = {
+ RepeatMode.off: 0,
+ RepeatMode.track: 1,
+ RepeatMode.context: 2,
+};
diff --git a/local_plugins/spotify_sdk/lib/models/player_restrictions.dart b/local_plugins/spotify_sdk/lib/models/player_restrictions.dart
new file mode 100644
index 000000000..bc673ff52
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/player_restrictions.dart
@@ -0,0 +1,33 @@
+import 'package:json_annotation/json_annotation.dart';
+
+part 'player_restrictions.g.dart';
+
+@JsonSerializable()
+class PlayerRestrictions {
+ PlayerRestrictions({
+ required this.canSkipNext,
+ required this.canSkipPrevious,
+ required this.canRepeatTrack,
+ required this.canRepeatContext,
+ required this.canToggleShuffle,
+ required this.canSeek,
+ });
+
+ @JsonKey(name: 'can_skip_next')
+ final bool canSkipNext;
+ @JsonKey(name: 'can_skip_prev')
+ final bool canSkipPrevious;
+ @JsonKey(name: 'can_repeat_track')
+ final bool canRepeatTrack;
+ @JsonKey(name: 'can_repeat_context')
+ final bool canRepeatContext;
+ @JsonKey(name: 'can_toggle_shuffle')
+ final bool canToggleShuffle;
+ @JsonKey(name: 'can_seek')
+ final bool canSeek;
+
+ factory PlayerRestrictions.fromJson(Map json) =>
+ _$PlayerRestrictionsFromJson(json);
+
+ Map toJson() => _$PlayerRestrictionsToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/player_restrictions.g.dart b/local_plugins/spotify_sdk/lib/models/player_restrictions.g.dart
new file mode 100644
index 000000000..7780e122b
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/player_restrictions.g.dart
@@ -0,0 +1,27 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'player_restrictions.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+PlayerRestrictions _$PlayerRestrictionsFromJson(Map json) =>
+ PlayerRestrictions(
+ canSkipNext: json['can_skip_next'] as bool,
+ canSkipPrevious: json['can_skip_prev'] as bool,
+ canRepeatTrack: json['can_repeat_track'] as bool,
+ canRepeatContext: json['can_repeat_context'] as bool,
+ canToggleShuffle: json['can_toggle_shuffle'] as bool,
+ canSeek: json['can_seek'] as bool,
+ );
+
+Map _$PlayerRestrictionsToJson(PlayerRestrictions instance) =>
+ {
+ 'can_skip_next': instance.canSkipNext,
+ 'can_skip_prev': instance.canSkipPrevious,
+ 'can_repeat_track': instance.canRepeatTrack,
+ 'can_repeat_context': instance.canRepeatContext,
+ 'can_toggle_shuffle': instance.canToggleShuffle,
+ 'can_seek': instance.canSeek,
+ };
diff --git a/local_plugins/spotify_sdk/lib/models/player_state.dart b/local_plugins/spotify_sdk/lib/models/player_state.dart
new file mode 100644
index 000000000..547efea3c
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/player_state.dart
@@ -0,0 +1,36 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import 'player_options.dart';
+import 'player_restrictions.dart';
+import 'track.dart';
+
+part 'player_state.g.dart';
+
+@JsonSerializable()
+class PlayerState {
+ PlayerState(
+ this.track,
+ this.playbackSpeed,
+ this.playbackPosition,
+ this.playbackOptions,
+ this.playbackRestrictions, {
+ required this.isPaused,
+ });
+
+ final Track? track;
+ @JsonKey(name: 'is_paused')
+ final bool isPaused;
+ @JsonKey(name: 'playback_speed')
+ final double playbackSpeed;
+ @JsonKey(name: 'playback_position')
+ final int playbackPosition;
+ @JsonKey(name: 'playback_options')
+ final PlayerOptions playbackOptions;
+ @JsonKey(name: 'playback_restrictions')
+ final PlayerRestrictions playbackRestrictions;
+
+ factory PlayerState.fromJson(Map json) =>
+ _$PlayerStateFromJson(json);
+
+ Map toJson() => _$PlayerStateToJson(this);
+}
diff --git a/local_plugins/spotify_sdk/lib/models/player_state.g.dart b/local_plugins/spotify_sdk/lib/models/player_state.g.dart
new file mode 100644
index 000000000..d38451065
--- /dev/null
+++ b/local_plugins/spotify_sdk/lib/models/player_state.g.dart
@@ -0,0 +1,29 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'player_state.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+PlayerState _$PlayerStateFromJson(Map