Repository: firebase/firebase-admin-java
Pull request #476 (2020-09-09 20:40:15)

fix: Cleaning up FirebaseApp state management
We have a mechanism in place to tear down any SDK state and managed resources upon calling `FirebaseApp.delete()`. However the way this is used and enforced across services is inconsistent at the moment:

* `FirebaseAuth` uses some complex and repetitive logic to throw errors from all methods after `delete()` has been called.
* Most other services (e.g. `FirebaseMessaging`) do not bother with this.

**In order to simplify the service implementations and make them consistent I'm proposing the following changes:**

1. `FirebaseAuth` doesn't have any state that requires explicit tear down. Therefore remove the existing repetitive logic for throwing errors after `delete()` is called. 
2. Make the `FirebaseService.destroy()` method non-abstract so that child classes are not forced to provide a no-op implementation. Child classes that do allocate managed resources (e.g. `FirebaseDatabase`) can still override it to tear down those resources.
3. Clarify the semantics of `FirebaseApp.delete()` in the API reference docs.

**This results in the following concrete behaviors after `FirebaseApp.delete()`:**

* Calling any of the `FirebaseXXXXX.getInstance()` methods will throw.
* Services with managed resources (`FirebaseDatabase` and `FirestoreClient`) will get cleaned up and become unusable.
* Some methods in services without managed resources will throw (notably all the async methods will throw since they rely on a thread pool attached to `FirebaseApp`).
* Some methods in services without managed resources will continue to work (this behavior exists today in some classes as mentioned above), but this is not a valid use of the SDK and developers should refrain from doing it.

**Potential future work:** If necessary we can implement a mechanism to block all rpc calls after `FirebaseApp.delete()` has been called. This is easy enough to enforce via the existing `FirebaseRequestInitializer`. But methods that do not make rpc calls (e.g. `FirebaseAuth.createCustomToken()`) will continue to work.


Patch:
diff --git a/src/main/java/com/google/firebase/FirebaseApp.java b/src/main/java/com/google/firebase/FirebaseApp.java
index 3848254..06188cd 100644
--- a/src/main/java/com/google/firebase/FirebaseApp.java
+++ b/src/main/java/com/google/firebase/FirebaseApp.java
@@ -277,6 +277,8 @@ public class FirebaseApp {
    */
   @Nullable
   String getProjectId() {
+    checkNotDeleted();
+
     // Try to get project ID from user-specified options.
     String projectId = options.getProjectId();
 
@@ -314,8 +316,10 @@ public class FirebaseApp {
   }
 
   /**
-   * Deletes the {@link FirebaseApp} and all its data. All calls to this {@link FirebaseApp}
-   * instance will throw once it has been called.
+   * Deletes this {@link FirebaseApp} object, and releases any local state and managed resources
+   * associated with it. All calls to this {@link FirebaseApp} instance will throw once this method
+   * has been called. This also releases any managed resources allocated by other services
+   * attached to this object instance (e.g. {@code FirebaseAuth}).
    *
    * <p>A no-op if delete was called before.
    */
diff --git a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java
index 7a50e0b..3a7f649 100644
--- a/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java
+++ b/src/main/java/com/google/firebase/ImplFirebaseTrampolines.java
@@ -26,7 +26,6 @@ import com.google.firebase.internal.FirebaseService;
 import com.google.firebase.internal.NonNull;
 
 import java.util.concurrent.Callable;
-import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.ThreadFactory;
 
diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
index 8004548..fc9c8df 100644
--- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java
@@ -18,7 +18,6 @@ package com.google.firebase.auth;
 
 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 com.google.api.client.json.JsonFactory;
 import com.google.api.client.util.Clock;
@@ -164,7 +163,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<String, FirebaseAuthException> createCustomTokenOp(
       final String uid, final Map<String, Object> developerClaims) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
     final FirebaseTokenFactory tokenFactory = this.tokenFactory.get();
     return new CallableOperation<String, FirebaseAuthException>() {
@@ -208,7 +206,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<String, FirebaseAuthException> createSessionCookieOp(
       final String idToken, final SessionCookieOptions options) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(idToken), "idToken must not be null or empty");
     checkNotNull(options, "options must not be null");
     final FirebaseUserManager userManager = getUserManager();
@@ -299,7 +296,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<FirebaseToken, FirebaseAuthException> verifyIdTokenOp(
       final String idToken, final boolean checkRevoked) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(idToken), "ID token must not be null or empty");
     final FirebaseTokenVerifier verifier = getIdTokenVerifier(checkRevoked);
     return new CallableOperation<FirebaseToken, FirebaseAuthException>() {
@@ -380,7 +376,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<FirebaseToken, FirebaseAuthException> verifySessionCookieOp(
       final String cookie, final boolean checkRevoked) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(cookie), "Session cookie must not be null or empty");
     final FirebaseTokenVerifier sessionCookieVerifier = getSessionCookieVerifier(checkRevoked);
     return new CallableOperation<FirebaseToken, FirebaseAuthException>() {
@@ -434,7 +429,6 @@ public abstract class AbstractFirebaseAuth {
   }
 
   private CallableOperation<Void, FirebaseAuthException> revokeRefreshTokensOp(final String uid) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<Void, FirebaseAuthException>() {
@@ -475,7 +469,6 @@ public abstract class AbstractFirebaseAuth {
   }
 
   private CallableOperation<UserRecord, FirebaseAuthException> getUserOp(final String uid) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
@@ -513,7 +506,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<UserRecord, FirebaseAuthException> getUserByEmailOp(
       final String email) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty");
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
@@ -551,7 +543,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<UserRecord, FirebaseAuthException> getUserByPhoneNumberOp(
       final String phoneNumber) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(phoneNumber), "phone number must not be null or empty");
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
@@ -620,7 +611,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<ListUsersPage, FirebaseAuthException> listUsersOp(
       @Nullable final String pageToken, final int maxResults) {
-    checkNotDestroyed();
     final FirebaseUserManager userManager = getUserManager();
     final DefaultUserSource source = new DefaultUserSource(userManager, jsonFactory);
     final ListUsersPage.Factory factory = new ListUsersPage.Factory(source, maxResults, pageToken);
@@ -661,7 +651,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<UserRecord, FirebaseAuthException> createUserOp(
       final UserRecord.CreateRequest request) {
-    checkNotDestroyed();
     checkNotNull(request, "create request must not be null");
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
@@ -701,7 +690,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<UserRecord, FirebaseAuthException> updateUserOp(
       final UserRecord.UpdateRequest request) {
-    checkNotDestroyed();
     checkNotNull(request, "update request must not be null");
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserRecord, FirebaseAuthException>() {
@@ -754,7 +742,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<Void, FirebaseAuthException> setCustomUserClaimsOp(
       final String uid, final Map<String, Object> claims) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<Void, FirebaseAuthException>() {
@@ -793,7 +780,6 @@ public abstract class AbstractFirebaseAuth {
   }
 
   private CallableOperation<Void, FirebaseAuthException> deleteUserOp(final String uid) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(uid), "uid must not be null or empty");
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<Void, FirebaseAuthException>() {
@@ -876,7 +862,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<UserImportResult, FirebaseAuthException> importUsersOp(
       final List<ImportUserRecord> users, final UserImportOptions options) {
-    checkNotDestroyed();
     final UserImportRequest request = new UserImportRequest(users, options, jsonFactory);
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<UserImportResult, FirebaseAuthException>() {
@@ -931,7 +916,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<GetUsersResult, FirebaseAuthException> getUsersOp(
       @NonNull final Collection<UserIdentifier> identifiers) {
-    checkNotDestroyed();
     checkNotNull(identifiers, "identifiers must not be null");
     checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE,
         "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE
@@ -1004,7 +988,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<DeleteUsersResult, FirebaseAuthException> deleteUsersOp(
       final List<String> uids) {
-    checkNotDestroyed();
     checkNotNull(uids, "uids must not be null");
     for (String uid : uids) {
       UserRecord.checkUid(uid);
@@ -1178,7 +1161,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<String, FirebaseAuthException> generateEmailActionLinkOp(
       final EmailLinkType type, final String email, final ActionCodeSettings settings) {
-    checkNotDestroyed();
     checkArgument(!Strings.isNullOrEmpty(email), "email must not be null or empty");
     if (type == EmailLinkType.EMAIL_SIGNIN) {
       checkNotNull(settings, "ActionCodeSettings must not be null when generating sign-in links");
@@ -1227,7 +1209,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<OidcProviderConfig, FirebaseAuthException>
       createOidcProviderConfigOp(final OidcProviderConfig.CreateRequest request) {
-    checkNotDestroyed();
     checkNotNull(request, "Create request must not be null.");
     OidcProviderConfig.checkOidcProviderId(request.getProviderId());
     final FirebaseUserManager userManager = getUserManager();
@@ -1271,7 +1252,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<OidcProviderConfig, FirebaseAuthException> updateOidcProviderConfigOp(
       final OidcProviderConfig.UpdateRequest request) {
-    checkNotDestroyed();
     checkNotNull(request, "Update request must not be null.");
     checkArgument(!request.getProperties().isEmpty(),
         "Update request must have at least one property set.");
@@ -1316,7 +1296,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<OidcProviderConfig, FirebaseAuthException>
       getOidcProviderConfigOp(final String providerId) {
-    checkNotDestroyed();
     OidcProviderConfig.checkOidcProviderId(providerId);
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<OidcProviderConfig, FirebaseAuthException>() {
@@ -1400,7 +1379,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<ListProviderConfigsPage<OidcProviderConfig>, FirebaseAuthException>
       listOidcProviderConfigsOp(@Nullable final String pageToken, final int maxResults) {
-    checkNotDestroyed();
     final FirebaseUserManager userManager = getUserManager();
     final DefaultOidcProviderConfigSource source = new DefaultOidcProviderConfigSource(userManager);
     final ListProviderConfigsPage.Factory<OidcProviderConfig> factory =
@@ -1443,7 +1421,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<Void, FirebaseAuthException> deleteOidcProviderConfigOp(
       final String providerId) {
-    checkNotDestroyed();
     OidcProviderConfig.checkOidcProviderId(providerId);
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<Void, FirebaseAuthException>() {
@@ -1490,7 +1467,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<SamlProviderConfig, FirebaseAuthException>
       createSamlProviderConfigOp(final SamlProviderConfig.CreateRequest request) {
-    checkNotDestroyed();
     checkNotNull(request, "Create request must not be null.");
     SamlProviderConfig.checkSamlProviderId(request.getProviderId());
     final FirebaseUserManager userManager = getUserManager();
@@ -1534,7 +1510,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<SamlProviderConfig, FirebaseAuthException> updateSamlProviderConfigOp(
       final SamlProviderConfig.UpdateRequest request) {
-    checkNotDestroyed();
     checkNotNull(request, "Update request must not be null.");
     checkArgument(!request.getProperties().isEmpty(),
         "Update request must have at least one property set.");
@@ -1579,7 +1554,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<SamlProviderConfig, FirebaseAuthException>
       getSamlProviderConfigOp(final String providerId) {
-    checkNotDestroyed();
     SamlProviderConfig.checkSamlProviderId(providerId);
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<SamlProviderConfig, FirebaseAuthException>() {
@@ -1663,7 +1637,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<ListProviderConfigsPage<SamlProviderConfig>, FirebaseAuthException>
       listSamlProviderConfigsOp(@Nullable final String pageToken, final int maxResults) {
-    checkNotDestroyed();
     final FirebaseUserManager userManager = getUserManager();
     final DefaultSamlProviderConfigSource source = new DefaultSamlProviderConfigSource(userManager);
     final ListProviderConfigsPage.Factory<SamlProviderConfig> factory =
@@ -1706,7 +1679,6 @@ public abstract class AbstractFirebaseAuth {
 
   private CallableOperation<Void, FirebaseAuthException> deleteSamlProviderConfigOp(
       final String providerId) {
-    checkNotDestroyed();
     SamlProviderConfig.checkSamlProviderId(providerId);
     final FirebaseUserManager userManager = getUserManager();
     return new CallableOperation<Void, FirebaseAuthException>() {
@@ -1729,32 +1701,12 @@ public abstract class AbstractFirebaseAuth {
           public T get() {
             checkNotNull(supplier);
             synchronized (lock) {
-              checkNotDestroyed();
               return supplier.get();
             }
           }
         });
   }
 
-  private void checkNotDestroyed() {
-    synchronized (lock) {
-      checkState(
-          !destroyed.get(),
-          "FirebaseAuth instance is no longer alive. This happens when "
-              + "the parent FirebaseApp instance has been deleted.");
-    }
-  }
-
-  final void destroy() {
-    synchronized (lock) {
-      doDestroy();
-      destroyed.set(true);
-    }
-  }
-
-  /** Performs any additional required clean up. */
-  protected abstract void doDestroy();
-
   protected abstract static class Builder<T extends  Builder<T>> {
 
     private FirebaseApp firebaseApp;
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
index 417ea64..27e7996 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java
@@ -69,9 +69,6 @@ public final class FirebaseAuth extends AbstractFirebaseAuth {
     return service.getInstance();
   }
 
-  @Override
-  protected void doDestroy() { }
-
   private static FirebaseAuth fromApp(final FirebaseApp app) {
     return populateBuilderFromApp(builder(), app, null)
         .setTenantManager(new Supplier<TenantManager>() {
@@ -88,11 +85,6 @@ public final class FirebaseAuth extends AbstractFirebaseAuth {
     FirebaseAuthService(FirebaseApp app) {
       super(SERVICE_ID, FirebaseAuth.fromApp(app));
     }
-
-    @Override
-    public void destroy() {
-      instance.destroy();
-    }
   }
 
   static Builder builder() {
diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java
index 95ef169..bc374c0 100644
--- a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java
+++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java
@@ -69,11 +69,6 @@ public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth {
     }, MoreExecutors.directExecutor());
   }
 
-  @Override
-  protected void doDestroy() {
-    // Nothing extra needs to be destroyed.
-  }
-
   static TenantAwareFirebaseAuth fromApp(FirebaseApp app, String tenantId) {
     return populateBuilderFromApp(builder(), app, tenantId)
         .setTenantId(tenantId)
diff --git a/src/main/java/com/google/firebase/cloud/StorageClient.java b/src/main/java/com/google/firebase/cloud/StorageClient.java
index d6a1567..955abd7 100644
--- a/src/main/java/com/google/firebase/cloud/StorageClient.java
+++ b/src/main/java/com/google/firebase/cloud/StorageClient.java
@@ -106,13 +106,5 @@ public class StorageClient {
     StorageClientService(StorageClient client) {
       super(SERVICE_ID, client);
     }
-
-    @Override
-    public void destroy() {
-      // NOTE: We don't explicitly tear down anything here, but public methods of StorageClient
-      // will now fail because calls to getOptions() and getToken() will hit FirebaseApp,
-      // which will throw once the app is deleted.
-    }
   }
-
 }
diff --git a/src/main/java/com/google/firebase/database/FirebaseDatabase.java b/src/main/java/com/google/firebase/database/FirebaseDatabase.java
index 0a79932..306ce92 100644
--- a/src/main/java/com/google/firebase/database/FirebaseDatabase.java
+++ b/src/main/java/com/google/firebase/database/FirebaseDatabase.java
@@ -173,7 +173,7 @@ public class FirebaseDatabase {
     return db;
   }
 
-  /** 
+  /**
    * @return The version for this build of the Firebase Database client
    */
   public static String getSdkVersion() {
@@ -358,6 +358,10 @@ public class FirebaseDatabase {
     return this.config;
   }
 
+  /**
+   * Tears down the WebSocket connections and background threads started by this {@code
+   * FirebaseDatabase} instance thus disconnecting from the remote database.
+   */
   void destroy() {
     synchronized (lock) {
       if (destroyed.get()) {
diff --git a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java
index 8226544..47026e6 100644
--- a/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java
+++ b/src/main/java/com/google/firebase/iid/FirebaseInstanceId.java
@@ -200,12 +200,5 @@ public class FirebaseInstanceId {
     FirebaseInstanceIdService(FirebaseApp app) {
       super(SERVICE_ID, new FirebaseInstanceId(app));
     }
-
-    @Override
-    public void destroy() {
-      // NOTE: We don't explicitly tear down anything here, but public methods of StorageClient
-      // will now fail because calls to getOptions() and getToken() will hit FirebaseApp,
-      // which will throw once the app is deleted.
-    }
   }
 }
diff --git a/src/main/java/com/google/firebase/internal/FirebaseService.java b/src/main/java/com/google/firebase/internal/FirebaseService.java
index d3e8755..2b802ae 100644
--- a/src/main/java/com/google/firebase/internal/FirebaseService.java
+++ b/src/main/java/com/google/firebase/internal/FirebaseService.java
@@ -28,7 +28,7 @@ import com.google.common.base.Strings;
  *
  * @param <T> Type of the service
  */
-public abstract class FirebaseService<T> {
+public class FirebaseService<T> {
 
   private final String id;
   protected final T instance;
@@ -62,5 +62,7 @@ public abstract class FirebaseService<T> {
    * Tear down this FirebaseService instance and the service object wrapped in it, cleaning up
    * any allocated resources in the process.
    */
-  public abstract void destroy();
+  public void destroy() {
+    // Child classes can override this method to implement any service-specific cleanup logic.
+  }
 }
diff --git a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java
index e1b4f79..a08e8cf 100644
--- a/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java
+++ b/src/main/java/com/google/firebase/messaging/FirebaseMessaging.java
@@ -403,13 +403,6 @@ public class FirebaseMessaging {
     FirebaseMessagingService(FirebaseApp app) {
       super(SERVICE_ID, FirebaseMessaging.fromApp(app));
     }
-
-    @Override
-    public void destroy() {
-      // NOTE: We don't explicitly tear down anything here, but public methods of FirebaseMessaging
-      // will now fail because calls to getOptions() and getToken() will hit FirebaseApp,
-      // which will throw once the app is deleted.
-    }
   }
 
   private static FirebaseMessaging fromApp(final FirebaseApp app) {
diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagement.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagement.java
index fe5472e..9e294e0 100644
--- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagement.java
+++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagement.java
@@ -286,10 +286,5 @@ public class FirebaseProjectManagement {
       serviceInstance.setAndroidAppService(serviceImpl);
       serviceInstance.setIosAppService(serviceImpl);
     }
-
-    @Override
-    public void destroy() {
-      serviceImpl.destroy();
-    }
   }
 }
diff --git a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java
index 0f9eb3b..2bf9270 100644
--- a/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java
+++ b/src/main/java/com/google/firebase/projectmanagement/FirebaseProjectManagementServiceImpl.java
@@ -87,14 +87,6 @@ class FirebaseProjectManagementServiceImpl implements AndroidAppService, IosAppS
     httpHelper.setInterceptor(interceptor);
   }
 
-  void destroy() {
-    // NOTE: We don't explicitly tear down anything here. Any instance of IosApp, AndroidApp, or
-    // FirebaseProjectManagement that depends on this instance will no longer be able to make RPC
-    // calls. All polling or waiting iOS or Android App creations will be interrupted, even though
-    // the initial creation RPC (if made successfully) is still processed normally (asynchronously)
-    // by the server.
-  }
-
   /* getAndroidApp */
 
   @Override


Test patch:
diff --git a/src/test/java/com/google/firebase/FirebaseAppTest.java b/src/test/java/com/google/firebase/FirebaseAppTest.java
index 26ca934..56e025d 100644
--- a/src/test/java/com/google/firebase/FirebaseAppTest.java
+++ b/src/test/java/com/google/firebase/FirebaseAppTest.java
@@ -41,6 +41,7 @@ import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.firebase.FirebaseApp.TokenRefresher;
 import com.google.firebase.database.FirebaseDatabase;
+import com.google.firebase.internal.FirebaseService;
 import com.google.firebase.testing.FirebaseAppRule;
 import com.google.firebase.testing.ServiceAccount;
 import com.google.firebase.testing.TestUtils;
@@ -213,6 +214,33 @@ public class FirebaseAppTest {
     }
   }
 
+  @Test
+  public void testUseAfterDelete() {
+    FirebaseApp firebaseApp = FirebaseApp.initializeApp(OPTIONS);
+    firebaseApp.delete();
+
+    try {
+      firebaseApp.getOptions();
+      fail();
+    } catch (IllegalStateException expected) {
+      // ignore
+    }
+
+    try {
+      firebaseApp.getProjectId();
+      fail();
+    } catch (IllegalStateException expected) {
+      // ignore
+    }
+
+    try {
+      ImplFirebaseTrampolines.addService(firebaseApp, new MockFirebaseService());
+      fail();
+    } catch (IllegalStateException expected) {
+      // ignore
+    }
+  }
+
   @Test
   public void testGetApps() {
     FirebaseApp app1 = FirebaseApp.initializeApp(OPTIONS, "app1");
@@ -684,4 +712,10 @@ public class FirebaseAppTest {
       return new AccessToken(UUID.randomUUID().toString(), expiry);
     }
   }
+
+  private static class MockFirebaseService extends FirebaseService<Object> {
+    MockFirebaseService() {
+      super("MockFirebaseService", new Object());
+    }
+  }
 }
diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
index e34fede..bd7aa0f 100644
--- a/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
+++ b/src/test/java/com/google/firebase/auth/FirebaseAuthTest.java
@@ -28,7 +28,6 @@ import static org.junit.Assert.fail;
 import com.google.api.client.testing.http.MockHttpTransport;
 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
 import com.google.api.core.ApiFuture;
-import com.google.common.base.Defaults;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableMap;
@@ -40,11 +39,6 @@ import com.google.firebase.TestOnlyImplFirebaseTrampolines;
 import com.google.firebase.testing.ServiceAccount;
 import com.google.firebase.testing.TestResponseInterceptor;
 import com.google.firebase.testing.TestUtils;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -105,26 +99,33 @@ public class FirebaseAuthTest {
     assertNotNull(auth);
     app.delete();
 
-    for (Method method : auth.getClass().getDeclaredMethods()) {
-      int modifiers = method.getModifiers();
-      if (!Modifier.isPublic(modifiers) || Modifier.isStatic(modifiers)) {
-        continue;
-      }
-
-      List<Object> parameters = new ArrayList<>(method.getParameterTypes().length);
-      for (Class<?> parameterType : method.getParameterTypes()) {
-        parameters.add(Defaults.defaultValue(parameterType));
-      }
-      try {
-        method.invoke(auth, parameters.toArray());
-        fail("No error thrown when invoking auth after deleting app; method: " + method.getName());
-      } catch (InvocationTargetException expected) {
-        String message = "FirebaseAuth instance is no longer alive. This happens when "
-            + "the parent FirebaseApp instance has been deleted.";
-        Throwable cause = expected.getCause();
-        assertTrue(cause instanceof IllegalStateException);
-        assertEquals(message, cause.getMessage());
-      }
+    String message = "FirebaseApp 'testInvokeAfterAppDelete' was deleted";
+    try {
+      FirebaseAuth.getInstance(app);
+      fail("No error thrown when invoking auth after deleting app");
+    } catch (IllegalStateException ex) {
+      assertEquals(message, ex.getMessage());
+    }
+
+    try {
+      auth.createCustomToken("uid");
+      fail("No error thrown when invoking auth after deleting app");
+    } catch (IllegalStateException ex) {
+      assertEquals(message, ex.getMessage());
+    }
+
+    try {
+      auth.verifyIdToken("idToken");
+      fail("No error thrown when invoking auth after deleting app");
+    } catch (IllegalStateException ex) {
+      assertEquals(message, ex.getMessage());
+    }
+
+    try {
+      auth.getUser("uid");
+      fail("No error thrown when invoking auth after deleting app");
+    } catch (IllegalStateException ex) {
+      assertEquals(message, ex.getMessage());
     }
   }
 
@@ -461,10 +462,6 @@ public class FirebaseAuthTest {
     }
   }
 
-  private FirebaseToken getFirebaseToken(String subject) {
-    return new FirebaseToken(ImmutableMap.<String, Object>of("sub", subject));
-  }
-
   private FirebaseToken getFirebaseToken(long issuedAt) {
     return new FirebaseToken(ImmutableMap.<String, Object>of("sub", TEST_USER, "iat", issuedAt));
   }
diff --git a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java
index f15861a..27864d9 100644
--- a/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java
+++ b/src/test/java/com/google/firebase/iid/FirebaseInstanceIdTest.java
@@ -50,6 +50,11 @@ import org.junit.Test;
 
 public class FirebaseInstanceIdTest {
 
+  private static final FirebaseOptions APP_OPTIONS = FirebaseOptions.builder()
+      .setCredentials(new MockGoogleCredentials("test-token"))
+      .setProjectId("test-project")
+      .build();
+
   private static final Map<Integer, String> ERROR_MESSAGES = ImmutableMap.of(
       404, "Instance ID \"test-iid\": Failed to find the instance ID.",
       409, "Instance ID \"test-iid\": Already deleted.",
@@ -88,13 +93,25 @@ public class FirebaseInstanceIdTest {
     }
   }
 
+  @Test
+  public void testInvokeAfterAppDelete() {
+    FirebaseApp app = FirebaseApp.initializeApp(APP_OPTIONS, "testInvokeAfterAppDelete");
+    FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance(app);
+    assertNotNull(instanceId);
+    app.delete();
+
+    try {
+      FirebaseInstanceId.getInstance(app);
+      fail("No error thrown when invoking instanceId after deleting app");
+    } catch (IllegalStateException ex) {
+      String message = "FirebaseApp 'testInvokeAfterAppDelete' was deleted";
+      assertEquals(message, ex.getMessage());
+    }
+  }
+
   @Test
   public void testInvalidInstanceId() {
-    FirebaseOptions options = FirebaseOptions.builder()
-        .setCredentials(new MockGoogleCredentials("test-token"))
-        .setProjectId("test-project")
-        .build();
-    FirebaseApp.initializeApp(options);
+    FirebaseApp.initializeApp(APP_OPTIONS);
 
     FirebaseInstanceId instanceId = FirebaseInstanceId.getInstance();
     TestResponseInterceptor interceptor = new TestResponseInterceptor();
@@ -122,9 +139,7 @@ public class FirebaseInstanceIdTest {
     MockHttpTransport transport = new MockHttpTransport.Builder()
         .setLowLevelHttpResponse(response)
         .build();
-    FirebaseOptions options = FirebaseOptions.builder()
-        .setCredentials(new MockGoogleCredentials("test-token"))
-        .setProjectId("test-project")
+    FirebaseOptions options = APP_OPTIONS.toBuilder()
         .setHttpTransport(transport)
         .build();
     FirebaseApp app = FirebaseApp.initializeApp(options);
@@ -168,9 +183,7 @@ public class FirebaseInstanceIdTest {
     MockHttpTransport transport = new MockHttpTransport.Builder()
         .setLowLevelHttpResponse(response)
         .build();
-    FirebaseOptions options = FirebaseOptions.builder()
-        .setCredentials(new MockGoogleCredentials("test-token"))
-        .setProjectId("test-project")
+    FirebaseOptions options = APP_OPTIONS.toBuilder()
         .setHttpTransport(transport)
         .build();
     FirebaseApp app = FirebaseApp.initializeApp(options);