Search code examples
androidkotlingoogle-play-servicesgoogle-play-games

Saving Game with Google Play Game Services gives error saying Cannot use snapshots without enabling the 'Saved Game' feature in the Play console


I have integrated Google Play Game services in my android app, the login works fine and now I am trying to save my game but when I try to do so I get error saying java.lang.IllegalStateException: Cannot use snapshots without enabling the 'Saved Game' feature in the Play console, I have enabled 'Saved Game' in Google play console, refer to below screenshot

enter image description here

enter image description here

When I was going through the doc, I noticed that it requires GoogleSignInClient as well before using saved games, now given that GoogleSignInClient is deprecated I am using credential manager, please do not confuse this with Google play game services sign in which I already do as soon as the app is opened by following this doc

So I decided to use credential manager with google drive access, here is the sample code for it

private fun signInSilently() {

        val credentialManager = CredentialManager.create(this)

        val rawNonce = UUID.randomUUID().toString()
        val bytes = rawNonce.toByteArray()
        val md = MessageDigest.getInstance("SHA-256")
        val digest = md.digest(bytes)
        val hashedNonce = digest.fold("") { str, it -> str + "%02x".format(it) }

        val googleIdOption = GetGoogleIdOption.Builder()
            .setFilterByAuthorizedAccounts(false) //IMP Part
            .setServerClientId("MY_WEB_CLIENT_ID")
            .setAutoSelectEnabled(true)
            .setNonce(hashedNonce)
            .build()


        val request: GetCredentialRequest = GetCredentialRequest.Builder()
            .addCredentialOption(googleIdOption)
            .build()

        lifecycleScope.launch {
            try {
                val result = credentialManager.getCredential(
                    request = request,
                    context = this@MainActivity,
                )

                handleSignIn(result)
            } catch (e: GetCredentialException) {
                Log.e("Erroris", "Error getting credential", e)
            }
        }

    }

    private fun handleSignIn(result: GetCredentialResponse) {
        // Handle the successfully returned credential.
        when (val credential = result.credential) {

            // Passkey credential
            is PublicKeyCredential -> {
                // Share responseJson such as a GetCredentialResponse on your server to
                // validate and authenticate
                val responseJson = credential.authenticationResponseJson
            }

            // Password credential
            is PasswordCredential -> {
                // Send ID and password to your server to validate and authenticate.
                val username = credential.id
                val password = credential.password
            }

            // GoogleIdToken credential
            is CustomCredential -> {
                if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
                    try {
                        // Use googleIdTokenCredential and extract id to validate and
                        // authenticate on your server.
                        val googleIdTokenCredential = GoogleIdTokenCredential
                            .createFrom(credential.data)
                        val googleIdToken = googleIdTokenCredential.idToken
                        Log.i("answer", googleIdToken)
                        val personId = googleIdTokenCredential.id
                        Log.i("answer", personId) //email
                        val displayName = googleIdTokenCredential.displayName
                        Log.i("answer", displayName.toString())
                        val personPhoto = googleIdTokenCredential.profilePictureUri
                        Log.i("answer", personPhoto.toString())

                        val requestedScopes = listOf(Scope(DriveScopes.DRIVE_APPDATA))
                        val authorizationRequest =
                            AuthorizationRequest.builder()
                                .setRequestedScopes(requestedScopes)
                                .build()

                        Identity.getAuthorizationClient(this@MainActivity)
                            .authorize(authorizationRequest)
                            .addOnSuccessListener {
                                if (it.hasResolution()) {
                                    val pendingIntent = it.pendingIntent
                                    val intentSenderRequest = pendingIntent?.intentSender?.let { it1 ->
                                        IntentSenderRequest.Builder(
                                            it1
                                        ).build()
                                    }
                                    intentSenderRequest?.let { it1 -> authorizationLauncher.launch(it1) }
                                } else {

                                    Toast.makeText(
                                        this@MainActivity,
                                        "Access already granted ",
                                        Toast.LENGTH_LONG
                                    ).show()
                                }
                            }.addOnFailureListener {
                                Toast.makeText(this@MainActivity, "Failure ${it.message}", Toast.LENGTH_LONG)
                                    .show()
                            }

                    } catch (e: GoogleIdTokenParsingException) {
                        Log.e("Erroris", "Received an invalid google id token response", e)
                    }
                } else {
                    // Catch any unrecognized custom credential type here.
                    Log.e("Erroris", "Unexpected type of credential")
                }
            }

            else -> {
                // Catch any unrecognized credential type here.
                Log.e("Erroris", "Unexpected type of credential")
            }
        }
    }

Once this is successful then I call the below code to save my game

 val snapshotsClient =
                PlayGames.getSnapshotsClient(this)
            val maxNumberOfSavedGamesToShow = 5

            val intentTask = snapshotsClient.getSelectSnapshotIntent(
                "See My Saves", true, true, maxNumberOfSavedGamesToShow
            )
            intentTask.addOnCompleteListener {
                if (intentTask.isSuccessful) {
                    savedGameLauncher.launch(intentTask.result)
                } else {
                    Log.i("here123","${it.exception}")
                    Toast.makeText(this, "Fail to get snapshot", Toast.LENGTH_LONG).show()
                }
            }


private val savedGameLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        val intent = result.data

        if(intent != null){

            val snapshotsClient =
                PlayGames.getSnapshotsClient(this)

            if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA)) {
                // Load a snapshot.
                //TODO display progress bar
                val snapshotMetadata = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    intent.getParcelableExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA, SnapshotMetadata::class.java)
                } else {
                    intent.getParcelableExtra(SnapshotsClient.EXTRA_SNAPSHOT_METADATA)
                }

                val mCurrentSaveName = snapshotMetadata!!.uniqueName
                val conflictResolutionPolicy =
                    SnapshotsClient.RESOLUTION_POLICY_MOST_RECENTLY_MODIFIED


                snapshotsClient.open(mCurrentSaveName,true,conflictResolutionPolicy)
                    .addOnFailureListener {
                        Toast.makeText(this, "Save Game Fail", Toast.LENGTH_LONG).show()
                    }
                    .continueWith {
                        val snapshot = it.result.data
                        if(snapshot != null){
                            val byteArray = snapshot.snapshotContents.readFully()
                            val stringData = String(byteArray)
                            Log.i("datais",stringData)
                            Toast.makeText(this, stringData, Toast.LENGTH_LONG).show()
                        }
                    }
                    .addOnCompleteListener{
                   //TODO dismiss progress bar
                }



            } else if (intent.hasExtra(SnapshotsClient.EXTRA_SNAPSHOT_NEW)) {
                // Create a new snapshot named with a unique string
                val unique: String = BigInteger(281, Random()).toString(13)
                val mCurrentSaveName = "snapshotTemp-$unique"

                snapshotsClient.open(mCurrentSaveName,true).addOnCompleteListener{
                        if(it.isSuccessful){
                            val snapshot = it.result.data
                            val stringData = "Level 100"

                            if(snapshot != null){
                                snapshot.snapshotContents.writeBytes(stringData.toByteArray())

                                val metadataChange = SnapshotMetadataChange.Builder().setDescription("$stringData reached").build()

                                snapshotsClient.commitAndClose(snapshot, metadataChange).addOnCompleteListener{ finalResult ->
                                    if(finalResult.isSuccessful){
                                        Toast.makeText(this, "Save Game Success", Toast.LENGTH_LONG).show()
                                    }
                                }
                            } else {
                                Toast.makeText(this, "Snapshot is null", Toast.LENGTH_LONG).show()
                            }


                        } else {
                            Toast.makeText(this, "Save Game Fail", Toast.LENGTH_LONG).show()
                        }
                    }

            }
        }



    }

but I keep seeing error saying java.lang.IllegalStateException: Cannot use snapshots without enabling the 'Saved Game' feature in the Play console, I even waited for a day thinking it might take some time to propagate the changes on Google server side but unfortunately waiting does not work


Solution

  • Not sure if this resolved your specific issue, but I was running into some similar error messages with my own code, while trying to do pretty much the same as what you've described above. A specific difference between our cases is that my code was working before, and the problem only started up when I tried to replace the deprecated GoogleSigninClient with Credentials.

    What worked for me was to do the following:

    override fun onCreate(savedInstanceState: Bundle?) {
        //...
        PlayGamesSdk.initialize(this)
    }
    

    The initialize basically takes care of the Play Games signin flow. When saving the game, I then do the following:

    fun saveSnapshot(snapshotName: String) {
        currentSnapshot = snapshotName
        // Check for permissions
        val requestedScopes = listOf(Scope(DriveScopes.DRIVE_APPDATA))
        val authorizationRequest = AuthorizationRequest.builder().setRequestedScopes(requestedScopes).build()
        Identity.getAuthorizationClient(this).authorize(authorizationRequest).addOnSuccessListener { authorizationResult ->
            if (authorizationResult.hasResolution()) {
                val pendingIntent = authorizationResult.getPendingIntent()
                try {
                    startIntentSenderForResult(pendingIntent?.getIntentSender(), RC_AUTHORIZE_SAVE_SNAPSHOT, null, 0, 0, 0)
                } catch (e: SendIntentException) {
                    App.log.e("Couldn't start Authorization UI: " + e.getLocalizedMessage())
                }
            } else {
                // Access already granted, continue with user action
                saveSnapshotToCloud(currentSnapshot)
            }
        }.addOnFailureListener { e ->
            App.log.e("Failed to authorize", e)
            App.messages.show("Could not save to Google Drive. You need to give permission for cloud saves to work.", MSG_LONG)
        }
    }
    

    saveSnapshotToCloud is the function that does the actual saving of data to the SnapShotsClient. In the onActivityResult, I check the requestCode:

        when (requestCode) {
            RC_AUTHORIZE_SAVE_SNAPSHOT -> {
                val authorizationResult = Identity.getAuthorizationClient(this).getAuthorizationResultFromIntent(intent)
                if (authorizationResult.grantedScopes.contains(DriveScopes.DRIVE_APPDATA)) {
                    saveSnapshotToCloud(currentSnapshot)
                } else {
                    App.log.e("Failed to authorize. Scopes: ${authorizationResult.grantedScopes}")
                }
            }
        }
    

    My initial attempt to use the Credentials UI, like you do above, had all sorts of issues (my theory being that I wasn't getting the right permissions set up, even though they looked right. Limiting this to just using the Authorization UI + the PlayGamesSdk worked for me.

    Hope this helps (if not for your specific case, then hopefully others looking for a similar solution).