* Copyright 2017 Google Inc.
 * 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,
 * See the License for the specific language governing permissions and
 * limitations under the License.

package com.google.firebase.database.core;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.OAuth2Credentials;
import com.google.auth.oauth2.OAuth2Credentials.CredentialsChangedListener;
import com.google.firebase.FirebaseApp;
import com.google.firebase.ImplFirebaseTrampolines;
import com.google.firebase.database.util.GAuthToken;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Executor;

public class JvmAuthTokenProvider implements AuthTokenProvider {

  private final GoogleCredentials credentials;
  private final Map<String, Object> authVariable;
  private final Executor executor;

  JvmAuthTokenProvider(FirebaseApp firebaseApp, Executor executor) {
    this(firebaseApp, executor, true);

  JvmAuthTokenProvider(FirebaseApp firebaseApp, Executor executor, boolean autoRefresh) {
    this(firebaseApp, executor, autoRefresh, ImplFirebaseTrampolines.getCredentials(firebaseApp));

  JvmAuthTokenProvider(FirebaseApp firebaseApp, Executor executor, boolean autoRefresh,
      GoogleCredentials customCredentials) {
    this.credentials = customCredentials;
    this.authVariable = firebaseApp.getOptions().getDatabaseAuthVariableOverride();
    this.executor = executor;
    if (autoRefresh) {

  public void getToken(boolean forceRefresh, final GetTokenCompletionListener listener) {
    try {
      if (forceRefresh) {

      // The typical way to use a GoogleCredentials instance is to call its getRequestMetadata(),
      // and include the metadata in your request. Since we are accessing the token directly via
      // getAccessToken(), we must first call getRequestMetadata() to ensure the token is available
      // (refreshed if necessary).

      AccessToken accessToken = credentials.getAccessToken();
      listener.onSuccess(wrapOAuthToken(accessToken, authVariable));
    } catch (Exception e) {

  public void addTokenChangeListener(TokenChangeListener listener) {
    CredentialsChangedListener listenerWrapper = new TokenChangeListenerWrapper(
        listener, executor, authVariable);

  private static String wrapOAuthToken(AccessToken result, Map<String, Object> authVariable) {
    if (result == null) {
      // This shouldn't happen in the actual production SDK, but can happen in tests.
      return null;

    GAuthToken googleAuthToken = new GAuthToken(result.getTokenValue(), authVariable);
    return googleAuthToken.serializeToString();

   * Wraps a TokenChangeListener instance inside a CredentialsChangedListener. Equality
   * comparisons are delegated to the TokenChangeListener so that listener addition and removal will
   * work as expected.
  private static class TokenChangeListenerWrapper implements CredentialsChangedListener {

    private final TokenChangeListener listener;
    private final Executor executor;
    private final Map<String, Object> authVariable;

        TokenChangeListener listener,
        Executor executor,
        Map<String, Object> authVariable) {
      this.listener = checkNotNull(listener, "Listener must not be null");
      this.executor = checkNotNull(executor, "Executor must not be null");
      this.authVariable = authVariable;

    public void onChanged(OAuth2Credentials credentials) throws IOException {
      // When this event fires, it is guaranteed that credentials.getAccessToken() will return a
      // valid OAuth2 token.
      final AccessToken accessToken = credentials.getAccessToken();

      // Notify the TokenChangeListener on database's thread pool to make sure that
      // all database work happens on database worker threads.
          new Runnable() {
            public void run() {
              listener.onTokenChange(wrapOAuthToken(accessToken, authVariable));

    public int hashCode() {
      return listener.hashCode();

    public boolean equals(Object obj) {
      return obj != null
          && obj instanceof TokenChangeListenerWrapper
          && ((TokenChangeListenerWrapper) obj).listener.equals(listener);