/* * 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, * 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.google.firebase; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.firebase.FirebaseOptions.APPLICATION_DEFAULT_CREDENTIALS; import com.google.api.client.googleapis.util.Utils; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonParser; import com.google.api.core.ApiFuture; 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.auth.oauth2.ServiceAccountCredentials; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.firebase.internal.FirebaseAppStore; import com.google.firebase.internal.FirebaseScheduledExecutor; import com.google.firebase.internal.FirebaseService; import com.google.firebase.internal.ListenableFuture2ApiFuture; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The entry point of Firebase SDKs. It holds common configuration and state for Firebase APIs. Most * applications don't need to directly interact with FirebaseApp. * *
Firebase APIs use the default FirebaseApp by default, unless a different one is explicitly * passed to the API via FirebaseFoo.getInstance(firebaseApp). * *
{@link FirebaseApp#initializeApp(FirebaseOptions)} initializes the default app instance. This
* method should be invoked at startup.
*/
public class FirebaseApp {
private static final Logger logger = LoggerFactory.getLogger(FirebaseApp.class);
/** A map of (name, FirebaseApp) instances. */
private static final Map A no-op if delete was called before.
*/
public void delete() {
synchronized (lock) {
boolean valueChanged = deleted.compareAndSet(false /* expected */, true);
if (!valueChanged) {
return;
}
for (FirebaseService service : services.values()) {
service.destroy();
}
services.clear();
tokenRefresher.stop();
// Clean up and terminate the thread pools
threadManager.releaseFirebaseExecutors(this, executors);
if (scheduledExecutor != null) {
scheduledExecutor.shutdownNow();
scheduledExecutor = null;
}
}
synchronized (appsLock) {
instances.remove(name);
}
FirebaseAppStore appStore = FirebaseAppStore.getInstance();
if (appStore != null) {
appStore.removeApp(name);
}
}
private void checkNotDeleted() {
// Wrap the name argument in an array to ensure the invocation gets bound to the commonly
// available checkState(boolean, String, Object...) overload. Otherwise the invocation may
// get bound to the checkState(boolean, String, Object) overload, which is not present in older
// guava versions.
checkState(!deleted.get(), "FirebaseApp '%s' was deleted", new Object[]{name});
}
private ScheduledExecutorService ensureScheduledExecutorService() {
if (scheduledExecutor == null) {
synchronized (lock) {
checkNotDeleted();
if (scheduledExecutor == null) {
scheduledExecutor = new FirebaseScheduledExecutor(getThreadFactory(),
"firebase-scheduled-worker");
}
}
}
return scheduledExecutor;
}
ThreadFactory getThreadFactory() {
return threadManager.getThreadFactory();
}
ScheduledExecutorService getScheduledExecutorService() {
return ensureScheduledExecutorService();
}
This class is thread safe. It will handle only one token change event at a time. It also
* cancels any pending token refresh events, before scheduling a new one.
*/
static class TokenRefresher implements CredentialsChangedListener {
private final FirebaseApp firebaseApp;
private final GoogleCredentials credentials;
private final AtomicReference This operation is idempotent. Calling it multiple times, or calling it after the
* refresher has been stopped has no effect.
*/
final synchronized void start() {
// Allow starting only from the ready state.
if (!state.compareAndSet(State.READY, State.STARTED)) {
return;
}
logger.debug("Starting the proactive token refresher");
credentials.addChangeListener(this);
AccessToken accessToken = credentials.getAccessToken();
long refreshDelay;
if (accessToken != null) {
// If the token is about to expire (i.e. expires in less than 5 minutes), schedule a
// refresh event with 0 delay. Otherwise schedule a refresh event at the regular token
// expiry time, minus 5 minutes.
refreshDelay = Math.max(getRefreshDelay(accessToken), 0L);
} else {
// If there is no token fetched so far, fetch one immediately.
refreshDelay = 0L;
}
scheduleRefresh(refreshDelay);
}
final synchronized void stop() {
// Allow stopping from any state.
State previous = state.getAndSet(State.STOPPED);
if (previous == State.STARTED) {
cancelPrevious();
logger.debug("Stopped the proactive token refresher");
}
}
/**
* Schedule a forced token refresh to be executed after a specified duration.
*
* @param delayMillis Duration in milliseconds, after which the token should be forcibly
* refreshed.
*/
private void scheduleRefresh(final long delayMillis) {
cancelPrevious();
scheduleNext(new Callable'{', it is parsed as a JSON object. Otherwise it is
* treated as a file name and the JSON content is read from the corresponding file.
*
* @throws IllegalStateException if the default app has already been initialized.
* @throws IllegalArgumentException if an error occurs while loading options from the environment.
*/
public static FirebaseApp initializeApp() {
return initializeApp(DEFAULT_APP_NAME);
}
/**
* Initializes a named {@link FirebaseApp} instance using Google Application Default Credentials.
* Loads additional {@link FirebaseOptions} from the environment in the same way as the
* {@link #initializeApp()} method.
*
* @throws IllegalStateException if an app with the same name has already been initialized.
* @throws IllegalArgumentException if an error occurs while loading options from the environment.
*/
public static FirebaseApp initializeApp(String name) {
try {
return initializeApp(getOptionsFromEnvironment(), name);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to load settings from the system's environment variables", e);
}
}
/**
* Initializes the default {@link FirebaseApp} instance using the given options.
*
* @throws IllegalStateException if the default app has already been initialized.
*/
public static FirebaseApp initializeApp(FirebaseOptions options) {
return initializeApp(options, DEFAULT_APP_NAME);
}
/**
* Initializes a named {@link FirebaseApp} instance using the given options.
*
* @param options represents the global {@link FirebaseOptions}
* @param name unique name for the app. It is an error to initialize an app with an already
* existing name. Starting and ending whitespace characters in the name are ignored (trimmed).
* @return an instance of {@link FirebaseApp}
* @throws IllegalStateException if an app with the same name has already been initialized.
*/
public static FirebaseApp initializeApp(FirebaseOptions options, String name) {
return initializeApp(options, name, DEFAULT_TOKEN_REFRESHER_FACTORY);
}
static FirebaseApp initializeApp(FirebaseOptions options, String name,
TokenRefresher.Factory tokenRefresherFactory) {
FirebaseAppStore appStore = FirebaseAppStore.initialize();
String normalizedName = normalize(name);
final FirebaseApp firebaseApp;
synchronized (appsLock) {
checkState(
!instances.containsKey(normalizedName),
"FirebaseApp name " + normalizedName + " already exists!");
firebaseApp = new FirebaseApp(normalizedName, options, tokenRefresherFactory);
instances.put(normalizedName, firebaseApp);
}
appStore.persistApp(firebaseApp);
return firebaseApp;
}
@VisibleForTesting
static void clearInstancesForTest() {
synchronized (appsLock) {
// Copy the instances list before iterating, as delete() would attempt to remove from the
// original list.
for (FirebaseApp app : ImmutableList.copyOf(instances.values())) {
app.delete();
}
instances.clear();
}
}
private static List