18 changed files with 872 additions and 360 deletions
@ -0,0 +1,30 @@ |
|||
import 'package:json_annotation/json_annotation.dart'; |
|||
|
|||
part 'appconfig_model.g.dart'; // 生成的文件名 |
|||
|
|||
@JsonSerializable() |
|||
class UserGetAppConfigResp { |
|||
final Map<String, String> env; |
|||
final Map<String, DBMCPServer> mcps; |
|||
UserGetAppConfigResp({required this.env, required this.mcps}); |
|||
|
|||
factory UserGetAppConfigResp.fromJson(Map<String, dynamic> json) => |
|||
_$UserGetAppConfigRespFromJson(json); |
|||
Map<String, dynamic> toJson() => _$UserGetAppConfigRespToJson(this); |
|||
} |
|||
|
|||
@JsonSerializable() |
|||
class DBMCPServer { |
|||
final String servername; |
|||
final String url; |
|||
final List<String>? tools; |
|||
DBMCPServer({ |
|||
required this.servername, |
|||
required this.url, |
|||
required this.tools, |
|||
}); |
|||
|
|||
factory DBMCPServer.fromJson(Map<String, dynamic> json) => |
|||
_$DBMCPServerFromJson(json); |
|||
Map<String, dynamic> toJson() => _$DBMCPServerToJson(this); |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
// GENERATED CODE - DO NOT MODIFY BY HAND |
|||
|
|||
part of 'appconfig_model.dart'; |
|||
|
|||
// ************************************************************************** |
|||
// JsonSerializableGenerator |
|||
// ************************************************************************** |
|||
|
|||
UserGetAppConfigResp _$UserGetAppConfigRespFromJson( |
|||
Map<String, dynamic> json) => |
|||
UserGetAppConfigResp( |
|||
env: Map<String, String>.from(json['env'] as Map), |
|||
mcps: (json['mcps'] as Map<String, dynamic>).map( |
|||
(k, e) => MapEntry(k, DBMCPServer.fromJson(e as Map<String, dynamic>)), |
|||
), |
|||
); |
|||
|
|||
Map<String, dynamic> _$UserGetAppConfigRespToJson( |
|||
UserGetAppConfigResp instance) => |
|||
<String, dynamic>{ |
|||
'env': instance.env, |
|||
'mcps': instance.mcps, |
|||
}; |
|||
|
|||
DBMCPServer _$DBMCPServerFromJson(Map<String, dynamic> json) => DBMCPServer( |
|||
servername: json['servername'] as String, |
|||
url: json['url'] as String, |
|||
tools: |
|||
(json['tools'] as List<dynamic>?)?.map((e) => e as String).toList(), |
|||
); |
|||
|
|||
Map<String, dynamic> _$DBMCPServerToJson(DBMCPServer instance) => |
|||
<String, dynamic>{ |
|||
'servername': instance.servername, |
|||
'url': instance.url, |
|||
'tools': instance.tools, |
|||
}; |
|||
@ -0,0 +1,311 @@ |
|||
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 |
|||
import com.spotify.android.appremote.api.PlayerApi |
|||
import com.spotify.android.appremote.api.SpotifyAppRemote |
|||
import com.spotify.android.appremote.api.error.AuthenticationFailedException |
|||
import com.spotify.android.appremote.api.error.CouldNotFindSpotifyApp |
|||
import com.spotify.android.appremote.api.error.NotLoggedInException |
|||
import com.spotify.android.appremote.api.error.OfflineModeException |
|||
import com.spotify.android.appremote.api.error.SpotifyConnectionTerminatedException |
|||
import com.spotify.android.appremote.api.error.SpotifyDisconnectedException |
|||
import com.spotify.android.appremote.api.error.SpotifyRemoteServiceException |
|||
import com.spotify.android.appremote.api.error.UnsupportedFeatureVersionException |
|||
import com.spotify.android.appremote.api.error.UserNotAuthorizedException |
|||
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.CapabilitiesChannel |
|||
import de.minimalme.spotify_sdk.subscriptions.ConnectionStatusChannel |
|||
import de.minimalme.spotify_sdk.subscriptions.PlayerContextChannel |
|||
import de.minimalme.spotify_sdk.subscriptions.PlayerStateChannel |
|||
import de.minimalme.spotify_sdk.subscriptions.UserStatusChannel |
|||
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.PluginRegistry |
|||
import kotlinx.event.SetEvent |
|||
import kotlinx.event.event |
|||
class SpotifySdkService(private val context: Context? = null) { |
|||
|
|||
// 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 val requestCodeAuthentication = 1337 |
|||
|
|||
private var spotifyAppRemote: SpotifyAppRemote? = null |
|||
private var playerApi: PlayerApi? = null |
|||
|
|||
//-- Method implementations |
|||
private fun connectToSpotify(clientId: String?, redirectUrl: String?, result: MethodChannel.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 : Connector.ConnectionListener { |
|||
override fun onConnected(spotifyAppRemoteValue: SpotifyAppRemote) { |
|||
spotifyAppRemote = spotifyAppRemoteValue |
|||
playerApi = spotifyAppRemote?.playerApi |
|||
|
|||
initiallyConnected = true |
|||
Log.i(loggingTag, "App Remote successfully connected") |
|||
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: MethodChannel.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: MethodChannel.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") |
|||
} |
|||
} |
|||
|
|||
private 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() |
|||
// } |
|||
if (playerApi != null && !spotifyUri.isNullOrBlank()) { |
|||
playerApi!!.play(spotifyUri) |
|||
} else if (spotifyUri.isNullOrBlank()) { |
|||
|
|||
} else { |
|||
|
|||
} |
|||
} |
|||
private fun pause(){ |
|||
// if (playerApi != null) { |
|||
// playerApi.pause() |
|||
// .setResultCallback { result.success(true) } |
|||
// .setErrorCallback { throwable -> result.error(errorPause, "error when pausing", throwable.toString()) } |
|||
// } else { |
|||
// spotifyRemoteAppNotSetError() |
|||
// } |
|||
if (playerApi != null) { |
|||
playerApi!!.pause() |
|||
.setResultCallback {} |
|||
.setErrorCallback { } |
|||
} else { |
|||
|
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
Loading…
Reference in new issue