/* * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.github.andlyticsproject.adsense; import java.io.IOException; import java.util.Collection; import android.accounts.Account; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.os.Bundle; import com.google.android.gms.auth.GoogleAuthException; import com.google.android.gms.auth.GoogleAuthUtil; import com.google.android.gms.auth.GooglePlayServicesAvailabilityException; import com.google.android.gms.auth.UserRecoverableAuthException; import com.google.android.gms.common.AccountPicker; import com.google.api.client.googleapis.extensions.android.accounts.GoogleAccountManager; import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAuthIOException; import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException; import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException; import com.google.api.client.http.HttpExecuteInterceptor; import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpUnsuccessfulResponseHandler; import com.google.api.client.util.BackOff; import com.google.api.client.util.BackOffUtils; import com.google.api.client.util.Beta; import com.google.api.client.util.ExponentialBackOff; import com.google.api.client.util.Joiner; import com.google.api.client.util.Preconditions; import com.google.api.client.util.Sleeper; // Based on GoogleAccountCredential from google-api-java-client, // original JavaDoc below /** * {@link Beta} <br/> * Manages authorization and account selection for Google accounts. * * <p> * When fetching a token, any thrown {@link GoogleAuthException} would be wrapped: * <ul> * <li>{@link GooglePlayServicesAvailabilityException} would be wrapped inside of * {@link GooglePlayServicesAvailabilityIOException}</li> * <li>{@link UserRecoverableAuthException} would be wrapped inside of * {@link UserRecoverableAuthIOException}</li> * <li>{@link GoogleAuthException} when be wrapped inside of {@link GoogleAuthIOException}</li> * </ul> * </p> * * <p> * Upgrade warning: in prior version 1.14 exponential back-off was enabled by default when I/O * exception was thrown inside {@link #getToken}, but starting with version 1.15 you need to call * {@link #setBackOff} with {@link ExponentialBackOff} to enable it. * </p> * * @since 1.12 * @author Yaniv Inbar */ @SuppressLint("NewApi") public class BackgroundGoogleAccountCredential implements HttpRequestInitializer { /** Context. */ final Context context; /** Scope to use on {@link GoogleAuthUtil#getToken}. */ final String scope; /** Google account manager. */ private final GoogleAccountManager accountManager; /** * Selected Google account name (e-mail address), for example {@code "johndoe@gmail.com"}, or * {@code null} for none. */ private String accountName; /** Selected Google account or {@code null} for none. */ private Account selectedAccount; /** Sleeper. */ private Sleeper sleeper = Sleeper.DEFAULT; /** * Back-off policy which is used when an I/O exception is thrown inside {@link #getToken} or * {@code null} for none. */ private BackOff backOff; private Bundle extras; private String authority; private Bundle syncBundle; /** * @param context * context * @param scope * scope to use on {@link GoogleAuthUtil#getToken} */ public BackgroundGoogleAccountCredential(Context context, String scope, Bundle extras, String authority, Bundle syncBundle) { accountManager = new GoogleAccountManager(context); this.context = context; this.scope = scope; this.extras = extras; this.authority = authority; this.syncBundle = syncBundle; } /** * {@link Beta} <br/> * Constructs a new instance using OAuth 2.0 scopes. * * @param context * context * @param scope * first OAuth 2.0 scope * @param extraScopes * any additional OAuth 2.0 scopes * @return new instance * @deprecated (scheduled to be removed in 1.16) Use {@link #usingOAuth2(Context, Collection)} * instead. */ // @Deprecated // public static GoogleAccountCredential usingOAuth2(Context context, String scope, // String... extraScopes) { // StringBuilder scopeBuilder = new StringBuilder("oauth2:").append(scope); // for (String extraScope : extraScopes) { // scopeBuilder.append(' ').append(extraScope); // } // return new GoogleAccountCredential(context, scopeBuilder.toString()); // } /** * Constructs a new instance using OAuth 2.0 scopes. * * @param context * context * @param scopes * non empty OAuth 2.0 scope list * @return new instance * * @since 1.15 */ public static BackgroundGoogleAccountCredential usingOAuth2(Context context, Collection<String> scopes, Bundle extras, String authority, Bundle syncBundle) { Preconditions.checkArgument(scopes != null && scopes.iterator().hasNext()); String scopesStr = "oauth2: " + Joiner.on(' ').join(scopes); return new BackgroundGoogleAccountCredential(context, scopesStr, extras, authority, syncBundle); } /** * Sets the audience scope to use with Google Cloud Endpoints. * * @param context * context * @param audience * audience * @return new instance */ public static BackgroundGoogleAccountCredential usingAudience(Context context, String audience, Bundle extras, String authority, Bundle syncBundle) { Preconditions.checkArgument(audience.length() != 0); return new BackgroundGoogleAccountCredential(context, "audience:" + audience, extras, authority, syncBundle); } /** * Sets the selected Google account name (e-mail address) -- for example * {@code "johndoe@gmail.com"} -- or {@code null} for none. */ public final BackgroundGoogleAccountCredential setSelectedAccountName(String accountName) { selectedAccount = accountManager.getAccountByName(accountName); // check if account has been deleted this.accountName = selectedAccount == null ? null : accountName; return this; } public void initialize(HttpRequest request) { RequestHandler handler = new RequestHandler(); request.setInterceptor(handler); request.setUnsuccessfulResponseHandler(handler); } /** Returns the context. */ public final Context getContext() { return context; } /** Returns the scope to use on {@link GoogleAuthUtil#getToken}. */ public final String getScope() { return scope; } /** Returns the Google account manager. */ public final GoogleAccountManager getGoogleAccountManager() { return accountManager; } /** Returns all Google accounts or {@code null} for none. */ public final Account[] getAllAccounts() { return accountManager.getAccounts(); } /** Returns the selected Google account or {@code null} for none. */ public final Account getSelectedAccount() { return selectedAccount; } /** * Returns the back-off policy which is used when an I/O exception is thrown inside * {@link #getToken} or {@code null} for none. * * @since 1.15 */ public BackOff getBackOff() { return backOff; } /** * Sets the back-off policy which is used when an I/O exception is thrown inside * {@link #getToken} or {@code null} for none. * * @since 1.15 */ public BackgroundGoogleAccountCredential setBackOff(BackOff backOff) { this.backOff = backOff; return this; } /** * Returns the sleeper. * * @since 1.15 */ public final Sleeper getSleeper() { return sleeper; } /** * Sets the sleeper. The default value is {@link Sleeper#DEFAULT}. * * @since 1.15 */ public final BackgroundGoogleAccountCredential setSleeper(Sleeper sleeper) { this.sleeper = Preconditions.checkNotNull(sleeper); return this; } /** * Returns the selected Google account name (e-mail address), for example * {@code "johndoe@gmail.com"}, or {@code null} for none. */ public final String getSelectedAccountName() { return accountName; } /** * Returns an intent to show the user to select a Google account, or create a new one if there * are * none on the device yet. * * <p> * Must be run from the main UI thread. * </p> */ public final Intent newChooseAccountIntent() { return AccountPicker.newChooseAccountIntent(selectedAccount, null, new String[] { GoogleAccountManager.ACCOUNT_TYPE }, true, null, null, null, null); } /** * Returns an OAuth 2.0 access token. * * <p> * Must be run from a background thread, not the main UI thread. * </p> */ public String getToken() throws IOException, GoogleAuthException { if (backOff != null) { backOff.reset(); } while (true) { try { // return GoogleAuthUtil.getToken(context, accountName, scope); return GoogleAuthUtil.getTokenWithNotification(context, accountName, scope, extras, authority, syncBundle); } catch (IOException e) { // network or server error, so retry using back-off policy try { if (backOff == null || !BackOffUtils.next(sleeper, backOff)) { throw e; } } catch (InterruptedException e2) { // ignore } } } } class RequestHandler implements HttpExecuteInterceptor, HttpUnsuccessfulResponseHandler { /** Whether we've received a 401 error code indicating the token is invalid. */ boolean received401; String token; public void intercept(HttpRequest request) throws IOException { try { token = getToken(); request.getHeaders().setAuthorization("Bearer " + token); } catch (GooglePlayServicesAvailabilityException e) { // throw new GooglePlayServicesAvailabilityIOException(e); throw new IOException(e); } catch (UserRecoverableAuthException e) { // throw new UserRecoverableAuthIOException(e); throw new IOException(e); } catch (GoogleAuthException e) { // throw new GoogleAuthIOException(e); throw new IOException(e); } } public boolean handleResponse(HttpRequest request, HttpResponse response, boolean supportsRetry) { if (response.getStatusCode() == 401 && !received401) { received401 = true; GoogleAuthUtil.invalidateToken(context, token); return true; } return false; } } }