package com.thebluealliance.androidclient.datafeed.gce; import com.google.android.gms.auth.GoogleAuthException; import com.google.android.gms.auth.GoogleAuthUtil; import com.thebluealliance.androidclient.TbaLogger; import com.thebluealliance.androidclient.accounts.AccountController; import android.content.Context; import android.os.SystemClock; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import java.io.IOException; import javax.inject.Inject; /** * Class that handles getting oauth tokens to interact with Cloud Endpoints * Mainly uses the {@link GoogleAuthUtil} helper class * Resulting oauth tokens are to be used with the GCE Retrofit Services */ public class GceAuthController { private static final int MAX_BACKOFF_TRIES = 3; private static final String AUTH_HEADER_FORMAT = "Bearer %1$s"; private final Context mContext; private final AccountController mAccountController; /* Time to wait during exponential backoff (ms) */ private long mBackoffTime; /* Number of tries we've made while getting token */ private int mBackoffCount; @Inject public GceAuthController( Context context, AccountController accountController) { mContext = context; mAccountController = accountController; resetBackoff(); } private void resetBackoff() { mBackoffTime = 1000; mBackoffCount = 0; } /** * Negotiates a Google oauth2 token to use with Cloud Endpoints + Retrofit * This <b>MUST</b> be run from a background thread, performs much network operations and sleep * Based on https://developers.google.com/api-client-library/java/google-api-java-client/reference/1.19.1/com/google/api/client/googleapis/extensions/android/gms/auth/GoogleAccountCredential * Specifically #getToken() * @return An auth token to be used with Cloud Endpoints * @throws GoogleAuthException Trouble authenticating to Google */ @WorkerThread @VisibleForTesting @Nullable private String getAuthTokenWithBackoff() throws GoogleAuthException { String scope = getAudience(); String account = mAccountController.getSelectedAccount(); if (account == null || account.isEmpty()) { TbaLogger.e("No system account found, can't get auth token"); return null; } resetBackoff(); while (mBackoffCount < MAX_BACKOFF_TRIES) { try { return getGoogleAuthToken(account, scope); } catch (IOException e) { TbaLogger.i("Unable to get token, sleeping " + mBackoffTime + " ms"); e.printStackTrace(); SystemClock.sleep(mBackoffTime); mBackoffTime *= 2; mBackoffCount++; } } return null; } /** * Builds the correct Authorization header to be used with GCE requests * This method <b>MUST</b> be called from a background thread; it uses {@link #getAuthTokenWithBackoff()} * @return Authorization header, or null if {@link GoogleAuthException} or other error happened */ @WorkerThread public @Nullable String getAuthHeader() { try { String token = getAuthTokenWithBackoff(); if (token == null) { return null; } return String.format(AUTH_HEADER_FORMAT, token); } catch (GoogleAuthException e) { TbaLogger.w("Auth exception while fetching google token"); return null; } } @WorkerThread @VisibleForTesting String getGoogleAuthToken(String account, String scope) throws IOException, GoogleAuthException { if (account == null || account.isEmpty()) return null; return GoogleAuthUtil.getToken(mContext, account, scope); } private String getAudience() { String webClientId = mAccountController.getWebClientId(); return "audience:server:client_id:" + webClientId; } }