diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java
new file mode 100644
index 000000000..d5e6d0dec
--- /dev/null
+++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfig.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.remoteconfig;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.core.ApiFuture;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.ImplFirebaseTrampolines;
+import com.google.firebase.internal.CallableOperation;
+import com.google.firebase.internal.FirebaseService;
+
+/**
+ * This class is the entry point for all server-side Firebase Remote Config actions.
+ *
+ *
You can get an instance of {@link FirebaseRemoteConfig} via {@link #getInstance(FirebaseApp)},
+ * and then use it to manage Remote Config templates.
+ */
+public final class FirebaseRemoteConfig {
+
+ private static final String SERVICE_ID = FirebaseRemoteConfig.class.getName();
+ private final FirebaseApp app;
+ private final FirebaseRemoteConfigClient remoteConfigClient;
+
+ @VisibleForTesting
+ FirebaseRemoteConfig(FirebaseApp app, FirebaseRemoteConfigClient client) {
+ this.app = checkNotNull(app);
+ this.remoteConfigClient = checkNotNull(client);
+ }
+
+ private FirebaseRemoteConfig(FirebaseApp app) {
+ this(app, FirebaseRemoteConfigClientImpl.fromApp(app));
+ }
+
+ /**
+ * Gets the {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}.
+ *
+ * @return The {@link FirebaseRemoteConfig} instance for the default {@link FirebaseApp}.
+ */
+ public static FirebaseRemoteConfig getInstance() {
+ return getInstance(FirebaseApp.getInstance());
+ }
+
+ /**
+ * Gets the {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}.
+ *
+ * @return The {@link FirebaseRemoteConfig} instance for the specified {@link FirebaseApp}.
+ */
+ public static synchronized FirebaseRemoteConfig getInstance(FirebaseApp app) {
+ FirebaseRemoteConfigService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID,
+ FirebaseRemoteConfigService.class);
+ if (service == null) {
+ service = ImplFirebaseTrampolines.addService(app, new FirebaseRemoteConfigService(app));
+ }
+ return service.getInstance();
+ }
+
+ /**
+ * Gets the current active version of the Remote Config template.
+ *
+ * @return A {@link RemoteConfigTemplate}.
+ * @throws FirebaseRemoteConfigException If an error occurs while getting the template.
+ */
+ public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException {
+ return getTemplateOp().call();
+ }
+
+ /**
+ * Similar to {@link #getTemplate()} but performs the operation asynchronously.
+ *
+ * @return An {@code ApiFuture} that completes with a {@link RemoteConfigTemplate} when
+ * the template is available.
+ */
+ public ApiFuture getTemplateAsync() {
+ return getTemplateOp().callAsync(app);
+ }
+
+ private CallableOperation getTemplateOp() {
+ final FirebaseRemoteConfigClient remoteConfigClient = getRemoteConfigClient();
+ return new CallableOperation() {
+ @Override
+ protected RemoteConfigTemplate execute() throws FirebaseRemoteConfigException {
+ return remoteConfigClient.getTemplate();
+ }
+ };
+ }
+
+ @VisibleForTesting
+ FirebaseRemoteConfigClient getRemoteConfigClient() {
+ return remoteConfigClient;
+ }
+
+ private static class FirebaseRemoteConfigService extends FirebaseService {
+
+ FirebaseRemoteConfigService(FirebaseApp app) {
+ super(SERVICE_ID, new FirebaseRemoteConfig(app));
+ }
+
+ @Override
+ public void destroy() {
+ // NOTE: We don't explicitly tear down anything here, but public methods of
+ // FirebaseRemoteConfig 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/remoteconfig/FirebaseRemoteConfigClientImpl.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java
index 9a7ef31da..20b5e3a61 100644
--- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java
+++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigClientImpl.java
@@ -95,8 +95,7 @@ public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException {
.addAllHeaders(COMMON_HEADERS);
IncomingHttpResponse response = httpClient.send(request);
RemoteConfigTemplate parsed = httpClient.parse(response, RemoteConfigTemplate.class);
- parsed.setETag(getETag(response));
- return parsed;
+ return parsed.setETag(getETag(response));
}
private String getETag(IncomingHttpResponse response) {
diff --git a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java
index 66b066f38..061593105 100644
--- a/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java
+++ b/src/main/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigException.java
@@ -16,6 +16,7 @@
package com.google.firebase.remoteconfig;
+import com.google.common.annotations.VisibleForTesting;
import com.google.firebase.ErrorCode;
import com.google.firebase.FirebaseException;
import com.google.firebase.IncomingHttpResponse;
@@ -30,6 +31,11 @@ public final class FirebaseRemoteConfigException extends FirebaseException {
private final RemoteConfigErrorCode errorCode;
+ @VisibleForTesting
+ FirebaseRemoteConfigException(@NonNull ErrorCode code, @NonNull String message) {
+ this(code, message, null, null, null);
+ }
+
public FirebaseRemoteConfigException(
@NonNull ErrorCode errorCode,
@NonNull String message,
diff --git a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java
index e8028b000..632fb7777 100644
--- a/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java
+++ b/src/main/java/com/google/firebase/remoteconfig/RemoteConfigTemplate.java
@@ -27,7 +27,8 @@ public String getETag() {
return this.etag;
}
- void setETag(String etag) {
+ RemoteConfigTemplate setETag(String etag) {
this.etag = etag;
+ return this;
}
}
diff --git a/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java
new file mode 100644
index 000000000..9e5bd97f3
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/FirebaseRemoteConfigTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.remoteconfig;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.firebase.ErrorCode;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.TestOnlyImplFirebaseTrampolines;
+import com.google.firebase.auth.MockGoogleCredentials;
+import java.util.concurrent.ExecutionException;
+import org.junit.After;
+import org.junit.Test;
+
+public class FirebaseRemoteConfigTest {
+
+ private static final FirebaseOptions TEST_OPTIONS = FirebaseOptions.builder()
+ .setCredentials(new MockGoogleCredentials("test-token"))
+ .setProjectId("test-project")
+ .build();
+ private static final FirebaseRemoteConfigException TEST_EXCEPTION =
+ new FirebaseRemoteConfigException(ErrorCode.INTERNAL, "Test error message");
+
+ @After
+ public void tearDown() {
+ TestOnlyImplFirebaseTrampolines.clearInstancesForTest();
+ }
+
+ @Test
+ public void testGetInstance() {
+ FirebaseApp.initializeApp(TEST_OPTIONS);
+
+ FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance();
+
+ assertSame(remoteConfig, FirebaseRemoteConfig.getInstance());
+ }
+
+ @Test
+ public void testGetInstanceByApp() {
+ FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app");
+
+ FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app);
+
+ assertSame(remoteConfig, FirebaseRemoteConfig.getInstance(app));
+ }
+
+ @Test
+ public void testDefaultRemoteConfigClient() {
+ FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app");
+ FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app);
+
+ FirebaseRemoteConfigClient client = remoteConfig.getRemoteConfigClient();
+
+ assertTrue(client instanceof FirebaseRemoteConfigClientImpl);
+ assertSame(client, remoteConfig.getRemoteConfigClient());
+ String expectedUrl = "https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig";
+ assertEquals(expectedUrl, ((FirebaseRemoteConfigClientImpl) client).getRemoteConfigUrl());
+ }
+
+ @Test
+ public void testAppDelete() {
+ FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS, "custom-app");
+ FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.getInstance(app);
+ assertNotNull(remoteConfig);
+
+ app.delete();
+
+ try {
+ FirebaseRemoteConfig.getInstance(app);
+ fail("No error thrown when getting remote config instance after deleting app");
+ } catch (IllegalStateException expected) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testRemoteConfigClientWithoutProjectId() {
+ FirebaseOptions options = FirebaseOptions.builder()
+ .setCredentials(new MockGoogleCredentials("test-token"))
+ .build();
+ FirebaseApp.initializeApp(options);
+
+ try {
+ FirebaseRemoteConfig.getInstance();
+ fail("No error thrown for missing project ID");
+ } catch (IllegalArgumentException expected) {
+ String message = "Project ID is required to access Remote Config service. Use a service "
+ + "account credential or set the project ID explicitly via FirebaseOptions. "
+ + "Alternatively you can also set the project ID via the GOOGLE_CLOUD_PROJECT "
+ + "environment variable.";
+ assertEquals(message, expected.getMessage());
+ }
+ }
+
+ private static final String TEST_ETAG = "etag-123456789012-1";
+
+ @Test
+ public void testGetTemplate() throws FirebaseRemoteConfigException {
+ MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
+ new RemoteConfigTemplate().setETag(TEST_ETAG));
+ FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
+
+ RemoteConfigTemplate template = remoteConfig.getTemplate();
+
+ assertEquals(TEST_ETAG, template.getETag());
+ }
+
+ @Test
+ public void testGetTemplateFailure() {
+ MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION);
+ FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
+
+ try {
+ remoteConfig.getTemplate();
+ } catch (FirebaseRemoteConfigException e) {
+ assertSame(TEST_EXCEPTION, e);
+ }
+ }
+
+ @Test
+ public void testGetTemplateAsync() throws Exception {
+ MockRemoteConfigClient client = MockRemoteConfigClient.fromTemplate(
+ new RemoteConfigTemplate().setETag(TEST_ETAG));
+ FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
+
+ RemoteConfigTemplate template = remoteConfig.getTemplateAsync().get();
+
+ assertEquals(TEST_ETAG, template.getETag());
+ }
+
+ @Test
+ public void testGetTemplateAsyncFailure() throws InterruptedException {
+ MockRemoteConfigClient client = MockRemoteConfigClient.fromException(TEST_EXCEPTION);
+ FirebaseRemoteConfig remoteConfig = getRemoteConfig(client);
+
+ try {
+ remoteConfig.getTemplateAsync().get();
+ } catch (ExecutionException e) {
+ assertSame(TEST_EXCEPTION, e.getCause());
+ }
+ }
+
+ private FirebaseRemoteConfig getRemoteConfig(FirebaseRemoteConfigClient client) {
+ FirebaseApp app = FirebaseApp.initializeApp(TEST_OPTIONS);
+ return new FirebaseRemoteConfig(app, client);
+ }
+}
diff --git a/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java
new file mode 100644
index 000000000..47b298250
--- /dev/null
+++ b/src/test/java/com/google/firebase/remoteconfig/MockRemoteConfigClient.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.remoteconfig;
+
+public class MockRemoteConfigClient implements FirebaseRemoteConfigClient{
+
+ private RemoteConfigTemplate resultTemplate;
+ private FirebaseRemoteConfigException exception;
+
+ private MockRemoteConfigClient(RemoteConfigTemplate resultTemplate,
+ FirebaseRemoteConfigException exception) {
+ this.resultTemplate = resultTemplate;
+ this.exception = exception;
+ }
+
+ static MockRemoteConfigClient fromTemplate(RemoteConfigTemplate resultTemplate) {
+ return new MockRemoteConfigClient(resultTemplate, null);
+ }
+
+ static MockRemoteConfigClient fromException(FirebaseRemoteConfigException exception) {
+ return new MockRemoteConfigClient(null, exception);
+ }
+
+ @Override
+ public RemoteConfigTemplate getTemplate() throws FirebaseRemoteConfigException {
+ if (exception != null) {
+ throw exception;
+ }
+ return resultTemplate;
+ }
+}