How to migrate from GoogleCredential to GoogleCredentials and still get access to People API?
Asked Answered
P

3

6

Background

For an app I'm working on, which uses People API using credentials (user login). Once the user gives the credentials, I can access various Google APIs, such as People API. An example is one to get a list of contacts:

https://developers.google.com/people/api/rest/v1/people.connections/list

I've noticed that the class com.google.api.client.googleapis.auth.oauth2.GoogleCredential has become deprecated:

https://googleapis.dev/java/google-api-client/latest/com/google/api/client/googleapis/auth/oauth2/GoogleCredential.html

The problem

The app has old code that is based on some old G+ code (here) to reach contacts via the Google account. Here's a snippet of the most important part of it, which causes me trouble of migrating away from it:

object GoogleOuthHelper {
    @WorkerThread
    fun setUp(context: Context, serverAuthCode: String?): Services {
        val httpTransport: HttpTransport = NetHttpTransport()
        val jsonFactory = JacksonFactory.getDefaultInstance()
        // Redirect URL for web based applications. Can be empty too.
        val redirectUrl = "urn:ietf:wg:oauth:2.0:oob"
        // Exchange auth code for access token
        val tokenResponse = GoogleAuthorizationCodeTokenRequest(
            httpTransport, jsonFactory, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
            serverAuthCode, redirectUrl)
            .execute()
        // Then, create a GoogleCredential object using the tokens from GoogleTokenResponse
        val credential = GoogleCredential.Builder()
            .setClientSecrets(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET)
            .setTransport(httpTransport)
            .setJsonFactory(jsonFactory)
            .build()
        val accessToken = tokenResponse.accessToken
        getDefaultSecuredSharedPreferences(context).edit()
            .putString(SecuredSharedPreferences.KEY__GOOGLE_ACCESS_TOKEN, accessToken).apply()
        credential.setFromTokenResponse(tokenResponse)
        val appPackageName = context.packageName
        val peopleServiceApi = PeopleService.Builder(httpTransport, jsonFactory, credential)
            .setApplicationName(appPackageName)
            .build()
        val peopleService = peopleServiceApi.people()
        val otherContactsService = peopleServiceApi.otherContacts()
        val contactGroups = peopleServiceApi.contactGroups()
        return Services(peopleService, otherContactsService, contactGroups)
    }

    class Services(
        /**https://developers.google.com/people/api/rest/v1/people*/
        val peopleService: PeopleService.People,
        /**https://developers.google.com/people/api/rest/v1/otherContacts*/
        val otherContactsService: OtherContacts,
        /**https://developers.google.com/people/api/rest/v1/contactGroups*/
        val contactGroups: ContactGroups)
}

The problem is even from the very beginning:

The class GoogleCredentials doesn't seem to accept anything I got above it for the GoogleCredential class.

To add more to it, this function takes "serverAuthCode" as parameter, which is from GoogleSignInAccount, but to get it, I need to use the deprecated GoogleApiClient class:

        fun prepareGoogleApiClient(someContext: Context): GoogleApiClient {
            val context = someContext.applicationContext ?: someContext
            val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestServerAuthCode(GOOGLE_CLIENT_ID)
                .requestEmail()
                .requestScopes(
                    Scope(PeopleServiceScopes.CONTACTS_READONLY),
                    Scope(PeopleServiceScopes.USERINFO_PROFILE),
                    Scope(PeopleServiceScopes.USER_EMAILS_READ),
                    Scope(PeopleServiceScopes.CONTACTS),
                    Scope(PeopleServiceScopes.CONTACTS_OTHER_READONLY)
                )
                .build()
            return GoogleApiClient.Builder(context)
                .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
                .build()
        }

And this is what I do with it:

val connectionResult = googleApiClient!!.blockingConnect()
if (!connectionResult.isSuccess)
    return
val operation = Auth.GoogleSignInApi.silentSignIn(googleApiClient)
val googleSignInResult: GoogleSignInResult = operation.await()
val googleSignInAccount = googleSignInResult.signInAccount
//use googleSignInAccount.serverAuthCode in setUp() function above

Gradle file has these dependencies:

// https://mvnrepository.com/artifact/com.google.auth/google-auth-library-oauth2-http
implementation 'com.google.auth:google-auth-library-oauth2-http:0.26.0'
// https://github.com/googleapis/google-api-java-client-services/tree/master/clients/google-api-services-people/v1#gradle   https://mvnrepository.com/artifact/com.google.apis/google-api-services-people
implementation 'com.google.apis:google-api-services-people:v1-rev20210515-1.31.0'

What I've tried

Other than looking at the docs (and failing to see a resemblance to what I have to handle), I tried to write about this here.

Sadly I can't find how to migrate away from the old code yet.

I tried to ask there how can I migrate (here and here) but didn't get an answer yet.

The questions

How can I migrate away from GoogleCredential to GoogleCredentials while still using the various APIs such as People API?

In other words: How can I avoid using any of those deprecated classes (GoogleCredential and GoogleApiClient), on Android, while still being able to use the various APIs?

Parous answered 12/5, 2021 at 9:3 Comment(9)
I find the documentation on Google credentials rather opaque. I am trying to understand what you are looking for. Are you trying to move to GoogleCredentials without making a user sign on again? Or are you looking for an example of how GoogleCredentials can be used with the various APIs? Or something else?Willumsen
The code I wrote is used to get access to Google People API, like getting the list of contacts on Google's contacts : developers.google.com/people/api/rest/v1/people.connections/… . It requires a login, and it makes sense. What I'm trying is to find how to migrate from deprecated code (I've shown at least 2 places here that are deprecated) to the new one. I will update the question.Parous
Can you post the build.gradle file for the app? I am interested in knowing where you are pulling PeopleService.Builder() from? I ask because I think there may have been an change to the arguments, but I'm not sure. I just want to confirm my suspicion.Willumsen
Maybe it's tied into the old G+ code, but do you even need to use GoogleCredentials? Take a look at the Google signin quickstart specifically this line of the REST API example. There,GoogleAccountCredential for the credential is used which is what I have been using. Maybe I am missing something.Willumsen
@Willumsen Updated question to hold the relevant dependencies. The old G+ code was only what it all started from. It changed over time. I've updated now the code to be of what it changed to. I don't understand how to use what you've sent me. Have you tried it? You say that the login procedure itself should also be changed? I see that it has Account as parameter, but what I have is GoogleSignInAccount (not extending it).Parous
I have not tried that code for the People API, but I have used code that is very, very similar to it for the Google Drive V3 API and it works for me. Since the code I linked to is a signin "quickstart", I believe that it should work. I think what I am suggesting is to look at the whole sign in flow and rework it instead of looking to just replace GoogleCredential. My Drive code and the signin quickstart code do not use the deprevated GoogleApiClient. Also, see Moving Past GoogleApiClient,Willumsen
Well, I had a chance to take a closer look and I was able to get the QuickStart signin code to work with some changes to the RestApiActivity code. You can see my changes here. I upgraded the version of People API to rev 528. (implementation 'com.google.apis:google-api-services-people:v1-rev528-1.25.0'). Also note that I added a "2" to the package name although that is not required. Based upon my (limited) experience, I think this is the way to go. Nothing in the code is deprecated (except startActivityForResult())Willumsen
Also wanted to mention this although I haven't tried it and you may have already seen the posting.Willumsen
It uses "fromStream" of some file. I don't think this is possible in my case.Parous
W
1

How can I migrate away from GoogleCredential to GoogleCredentials while still using the various APIs such as People API?

In other words: How can I avoid using any of those deprecated classes (GoogleCredential and GoogleApiClient), on Android, while still being able to use the various APIs?

Although you can make GoogleCredentials work directly, it will be better to use a class derived from GoogleCredentials such as UserCredentials which will accommodate token refresh as GoogleCredential does. GoogleCredentials is more of a foundational class.

The following code makes use of UserCredentials. This is mostly what you have presented, but I have changed some credential storing logic for the purpose of the demo. This code has no deprecated methods except startActivityForResult().

serverAuthCode is available from GoogleSignInAccount. Take a look at Moving Past GoogleApiClient on how to remove the dependency on GoogleApiClient. I have update my public gist of RestApiActivity from the Google signin quickstart which shows how to use GoogleOauthHelper as well as GoogleApi.

GoogleOauthHelper.kt

object GoogleOauthHelper {
    @WorkerThread
    fun setUp(context: Context, serverAuthCode: String?): Services {
        val httpTransport: HttpTransport = NetHttpTransport()
        val jsonFactory = GsonFactory.getDefaultInstance()
        // Redirect URL for web based applications. Can be empty too.
        val redirectUrl = "urn:ietf:wg:oauth:2.0:oob"

        // Patch for demo
        val GOOGLE_CLIENT_ID = context.getString(R.string.GOOGLE_CLIENT_ID)
        val GOOGLE_CLIENT_SECRET = context.getString(R.string.GOOGLE_CLIENT_SECRET)

        var accessToken: AccessToken? = null
        var refreshToken =
            getDefaultSecuredSharedPreferences(context).getString(
                SecuredSharedPreferences.KEY_GOOGLE_REFRESH_TOKEN,
                null
            )

        if (refreshToken == null) {
            /*  Did we lose the refresh token, or is this the first time? Refresh tokens are only
                returned the first time after the user grants us permission to use the API. So, if
                this is the first time doing this, we should get a refresh token. If it's not the
                first time, we will not get a refresh token, so we will proceed with the access
                token alone. If the access token expires (in about an hour), we will get an error.
                What we should do is to ask the user to reauthorize the app and go through the
                OAuth flow again to recover a refresh token.

                See https://developers.google.com/identity/protocols/oauth2#expiration regarding
                how a refresh token can become invalid.
            */
            val tokenResponse = GoogleAuthorizationCodeTokenRequest(
                httpTransport, jsonFactory, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
                serverAuthCode, redirectUrl
            ).execute()

            refreshToken = tokenResponse.refreshToken
            if (refreshToken != null) {
                getDefaultSecuredSharedPreferences(context).edit()
                    .putString(SecuredSharedPreferences.KEY_GOOGLE_REFRESH_TOKEN, refreshToken)
                    .apply()
            } else {
                Log.d("Applog", "No refresh token. Going with access token alone.")
                val expiresAtMilliseconds =
                    Clock.SYSTEM.currentTimeMillis() + tokenResponse.expiresInSeconds * 1000
                accessToken = AccessToken(tokenResponse.accessToken, Date(expiresAtMilliseconds))
            }
        }

        Log.d("Applog", "Refresh token: $refreshToken")
        // UserCredentials extends GoogleCredentials and permits token refreshing.
        val googleCredentials = UserCredentials.newBuilder().run {
            clientId = GOOGLE_CLIENT_ID
            clientSecret = GOOGLE_CLIENT_SECRET
            setRefreshToken(refreshToken)
            setAccessToken(accessToken)
            build()
        }

        // Save access token on change
        googleCredentials.addChangeListener { oAuth2Credentials ->
            saveAccessToken(oAuth2Credentials.accessToken)
        }

        val requestInitializer: HttpRequestInitializer = HttpCredentialsAdapter(googleCredentials)
        val appPackageName = context.packageName

        val peopleServiceApi = PeopleService.Builder(httpTransport, jsonFactory, requestInitializer)
            .setApplicationName(appPackageName)
            .build()

        return peopleServiceApi.run { Services(people(), otherContacts(), contactGroups()) }
    }

    private fun saveAccessToken(accessToken: AccessToken) {
        // Persist the token securely.
        Log.d("Applog", "Access token has changed: ${accessToken.tokenValue}")
    }

    // Warning insecure!: Patch for demo.
    private fun getDefaultSecuredSharedPreferences(context: Context): SharedPreferences {
        return PreferenceManager.getDefaultSharedPreferences(context)
    }

    // Warning insecure!: Patch for demo.
    object SecuredSharedPreferences {
        const val KEY_GOOGLE_REFRESH_TOKEN = "GOOGLE_REFRESH_TOKEN"
    }

    class Services(
        /**https://developers.google.com/people/api/rest/v1/people*/
        val peopleService: PeopleService.People,
        /**https://developers.google.com/people/api/rest/v1/otherContacts*/
        val otherContactsService: PeopleService.OtherContacts,
        /**https://developers.google.com/people/api/rest/v1/contactGroups*/
        val contactGroups: PeopleService.ContactGroups,
    )
}

I have posted a demo project on GitHub.

Willumsen answered 8/6, 2021 at 14:58 Comment(10)
That's amazing! Very nice! About saveAccessToken, shouldn't it be similar to the previous saving using KEY__GOOGLE_ACCESS_TOKEN , just that the value would be ` oAuth2Credentials.accessToken.tokenValue` ? As for login, using the new GoogleSignInClient, I can see it lacks various functions I had before: blockingConnect (doesn't seem to exist now), isConnected (same), clearDefaultAccountAndReconnect ,disconnect (maybe just need googleApiClient.signOut() ? but how can I start it?) . I hope I've found the alternatives. Can you please put your sample here too?Parous
I added a stub method to show how to capture the access token in case it needs to be saved for some reason. The same secure storing logic in the original code should work as you say. Regarding the blockingConnect() call, that is for GoogleApiClient. I don't see anything analogous for the newer way of signing in. For signing out, see the signOut method in RestApiActivity.java. I have also posted a demo project that I mention in the answer.Willumsen
Nice. Thank you. Still, could be nice to see it here too.Parous
There is also this tutorial that I've found, which I'm not sure if you've mentioned: developers.google.com/people/quickstart/javaParous
@androiddeveloper I saw that tutorial but decided to go with the signin quickstart instead. Nothing against that tutorial, though. I'll also mention that UserCredentials has a save function to create a JSON file for the credentials if you give it a secure place to store the file. You could then just load the credentials from the file.Willumsen
Is having the credentials JSON file inside the app a wise and safe thing to do? I wonder how it works without it : Is it saved anywhere inside the APK files ?Parous
@androiddeveloper I was thinking about JSON storage for the refresh and access tokens as an alternative. I don't know if I would change that if the app is working OK now. I only mentioned it as a possible convenience.Willumsen
You are talking about saving the tokens? That's not an issue. I was talking about the JSON credentials that sometimes I saw on the samples, that it reads from there.Parous
@androiddeveloper I was pointing out the save function of UserCredentials as a way to store the user's credentials. I assumed that the samples would read from such a file created by the app, although I am just guessing.Willumsen
Weird. Didn't know this function exists. What kind of UserCredentials does it save? Is it the JSON file that the samples use? Isn't it weird to save to a file something the libraries can reach anyway?Parous
B
1

There is an extremely simple solution, hidden inside Google's doc, com.google.auth.http.HttpCredentialsAdapter. It allows creating an com.google.api.client.http.HttpRequestInitializer from the new com.google.auth.oauth2.GoogleCredentials, and calling the .Builder() constructor with it.

For a simplified example:

import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.services.bigquery.Bigquery;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;

GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credentials);

Bigquery bq = new Bigquery.Builder(HTTP_TRANSPORT, JSON_FACTORY, requestInitializer)
    .setApplicationName(APPLICATION_NAME)
    .build();
Battles answered 16/6, 2022 at 8:37 Comment(0)
V
0

Here's how you access the genders and birthdays with the People API after executing the SignInIntent and receiving the GoogleSignInAccount:

private suspend fun handlePeopleApi(account: GoogleSignInAccount) {
    withContext(Dispatchers.IO) {
        val credential =
            GoogleAccountCredential.usingOAuth2(
                requireContext(),
                listOf(
                    PeopleServiceScopes.USER_GENDER_READ,
                    PeopleServiceScopes.USER_BIRTHDAY_READ
                )
            )
        credential.selectedAccountName = account.email

        val peopleService = PeopleService.Builder(NetHttpTransport(), GsonFactory(), credential)
            .setApplicationName(getString(R.string.app_name))
            .build()

        val person = peopleService.people()
            .get("people/${account.id}")
            .setPersonFields("birthdays,genders")
            .execute()

        Log.d("People API", "Person: ${person.birthdays}, ${person.genders}")
    }
}

I have these Grade dependencies:

implementation 'com.google.apis:google-api-services-people:v1-rev20220531-2.0.0'
implementation 'com.google.api-client:google-api-client-android:1.20.0'
Vanir answered 6/9, 2022 at 13:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.