spotify / ios-sdk

Spotify SDK for iOS
https://developer.spotify.com/documentation/ios/
653 stars 185 forks source link

Issue: Calling `authorizeAndPlayURI` doesn't setup a `playerAPI` or call any of the `SPTAppRemoteDelegate` functions #362

Open simonmitchell opened 2 years ago

simonmitchell commented 2 years ago

SDK Version: Latest release Spotify Version: 8.7.64

Example code:

appRemote = SPTAppRemote(configuration: configuration, logLevel: .debug)
appRemote?.connectionParameters.accessToken = accessToken
appRemote?.delegate = self

guard appRemote?.isConnected == false else {
   callback(.success(true))
    return
}

// (1) This process is a bit convoluted unfortunately, due to way we connect/control
// spotify we first check if app is active, this will fail if the app isn't installed
// or running. Skip to (2.b)
SPTAppRemote.checkIfSpotifyAppIsActive { [weak self] active in

   guard let self = self else { return }

    // (2.a) If we're already active then store the closure and ask the remote to
    // connect (see (3))
    guard !active else {
        self.setupCallback = callback
        self.appRemote?.connect()
        return
    }

    // (2.b) If this method returns false then the spotify app isn't installed!
    let installed = self.appRemote?.authorizeAndPlayURI(self.track.uri ?? "") == true
    guard installed else {
        callback(.failure(SpotifyAudioManagerError.spotifyNotInstalled))
        return
    }

    self.setupCallback = callback
    // (2.c) Await delegate callbacks!
}

Delegate methods:

extension SpotifyAudioManager: SPTAppRemoteDelegate {

    func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
        // (3.a) If our `connect()` call succeeds, we can progress
        setupCallback?(.success(true))
        setupCallback = nil
    }

    func appRemote(_ appRemote: SPTAppRemote, didDisconnectWithError error: Error?) {

    }

    func appRemote(_ appRemote: SPTAppRemote, didFailConnectionAttemptWithError error: Error?) {

        // (3.b) If our `connect()` call fails, we'll have a last-ditched attempt wake the Spotify app
        let installed = appRemote.authorizeAndPlayURI(track.uri ?? "")
        guard installed else {
            setupCallback?(.failure(SpotifyAudioManagerError.spotifyNotInstalled))
            return
        }
        setupCallback?(.success(true))
        setupCallback = nil
    }
}

If the spotify app is running/playing music everything works great ✅

If the spotify app is force-quit/not running then the spotify app is launched, connects, starts playing, and my app is re-opened, but I don't get told if the process was successful, and playerAPI is never made non-nil on my SPTAppRemote instance.

I have found a workaround for this however, which is to at point 2.c in my comments add a foreground observer and re-call connect() once the app enters the foreground!

// (2.c) The current version of the spotify app doesn't seem to setup `playerAPI` even
// when the app re-launches after kicking out to spotify. To rectify this we listen
// to app foreground notifications and then retry if we need to!
self.foregroundObserver = NotificationCenter.default.addObserver(
    forName: UIApplication.willEnterForegroundNotification,
    object: nil,
    queue: .main,
    using: { [weak self] _ in
        // (3.c) Re-attempt connection
        guard let self = self else { return }
        self.setupCallback = callback
        self.appRemote?.connect()
        // We're done with this now, so we can remove it!
        guard let foregroundObserver = self.foregroundObserver else {
            return
        }
        NotificationCenter.default.removeObserver(foregroundObserver)
    }
)

This seems to work for now, but I do not trust it in the slightest, and I'm unsure what kind of state I'll end up in if something in the spotify app fails e.t.c (user isn't logged in, doesn't have premium e.t.c.)

Can anyone advise if:

  1. I am doing anything wrong? I'm missing some API or class that handles the "Spotify app isn't running" case correctly?
  2. Will my workaround be okay (will probably take a spotify engineer to work this out)
  3. Is there anything that can be done to fix this in the root SDK?

Thanks everyone! 😄

netshade commented 1 year ago

Thank you for the workaround snippet here, can confirm this allows for a connection in 8.7.90.459 as well. Trying to guess at the situation that would cause this sort of behavior - some sort of time window of availability for the now-backgrounded apps socket being open? Some sort of interplay of background modes and networking on the iOS SDK side?

arlomedia commented 1 year ago

FWIW I'm seeing this work about half the time in SDK version 1.2.3 and the Spotify app version 8.8.15.609. Here's my basic code:

AppDelegate:

func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
    if (url.absoluteString.contains("spotify/authorized")) {
        self.recordingPlayer?.authorizeSpotify(url: url)
        return true
    }
}

RecordingPlayer:

func authorizeSpotify(url: URL) {
    let parameters = self.spotifyRemote?.authorizationParameters(from: url)
    if let token = parameters?[SPTAppRemoteAccessTokenKey] {
        self.spotifyRemote?.connectionParameters.accessToken = token
        self.spotifyRemote?.connect()
        let test = self.spotifyRemote?.isConnected
        self.spotifyRemote?.playerAPI?.delegate = self
        self.spotifyRemote?.playerAPI?.subscribe()
        self.spotifyRemote?.delegate = self
    } else if let errorDescription = parameters?[SPTAppRemoteErrorDescriptionKey] {
        NSLog("Spotify authentication error: " + errorDescription)
    }
}

About half the time, the test variable is true and the subsequent lines work; the other half the time, the test variable is false and the delegate functions aren't called.

Since the problem only happens when the Spotify app was not already running, it seems like it's calling back to our apps too soon, rather than getting suspended before our apps can connect to it. Could this be fixed by the Spotify app waiting until it is ready to receive a connection before reopening our apps?