diff --git a/firebase-common/src/androidTest/java/com/google/firebase/FirebaseAppTest.java b/firebase-common/src/androidTest/java/com/google/firebase/FirebaseAppTest.java index bf913d0254b..52c4b8ac623 100644 --- a/firebase-common/src/androidTest/java/com/google/firebase/FirebaseAppTest.java +++ b/firebase-common/src/androidTest/java/com/google/firebase/FirebaseAppTest.java @@ -45,6 +45,9 @@ import com.google.firebase.components.TestComponentOne; import com.google.firebase.components.TestComponentTwo; import com.google.firebase.components.TestUserAgentDependentComponent; +import com.google.firebase.emulators.EmulatedServiceSettings; +import com.google.firebase.emulators.EmulatorSettings; +import com.google.firebase.emulators.FirebaseEmulator; import com.google.firebase.platforminfo.UserAgentPublisher; import com.google.firebase.testing.FirebaseAppRule; import java.lang.reflect.InvocationTargetException; @@ -417,6 +420,43 @@ public void testDirectBoot_shouldPreserveDataCollectionAfterUnlock() { assertTrue(firebaseApp.isDataCollectionDefaultEnabled()); } + @Test + public void testEnableEmulators_shouldAllowDoubleSetBeforeAccess() { + Context mockContext = createForwardingMockContext(); + FirebaseApp firebaseApp = FirebaseApp.initializeApp(mockContext); + + // A developer would call FirebaseDatabase.EMULATOR but we can't introduce that + // dependency for this test. + FirebaseEmulator emulator = FirebaseEmulator.forName("database"); + + EmulatedServiceSettings databaseSettings = new EmulatedServiceSettings("10.0.2.2", 9000); + EmulatorSettings emulatorSettings = + new EmulatorSettings.Builder().addEmulatedService(emulator, databaseSettings).build(); + + // Set twice + firebaseApp.enableEmulators(emulatorSettings); + firebaseApp.enableEmulators(emulatorSettings); + } + + @Test + public void testEnableEmulators_shouldThrowIfSetAfterAccess() { + Context mockContext = createForwardingMockContext(); + FirebaseApp firebaseApp = FirebaseApp.initializeApp(mockContext); + + FirebaseEmulator emulator = FirebaseEmulator.forName("database"); + + EmulatedServiceSettings databaseSettings = new EmulatedServiceSettings("10.0.2.2", 9000); + EmulatorSettings emulatorSettings = + new EmulatorSettings.Builder().addEmulatedService(emulator, databaseSettings).build(); + firebaseApp.enableEmulators(emulatorSettings); + + // Access (as if from the Database SDK) + firebaseApp.getEmulatorSettings().getServiceSettings(emulator); + + // Try to set again + assertThrows(IllegalStateException.class, () -> firebaseApp.enableEmulators(emulatorSettings)); + } + /** Returns mock context that forwards calls to targetContext and localBroadcastManager. */ private Context createForwardingMockContext() { final UserManager spyUserManager = spy(targetContext.getSystemService(UserManager.class)); diff --git a/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java b/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java index 00c4bdb5b28..dee3e1d9e90 100644 --- a/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java +++ b/firebase-common/src/main/java/com/google/firebase/FirebaseApp.java @@ -44,6 +44,7 @@ import com.google.firebase.components.ComponentRegistrar; import com.google.firebase.components.ComponentRuntime; import com.google.firebase.components.Lazy; +import com.google.firebase.emulators.EmulatorSettings; import com.google.firebase.events.Publisher; import com.google.firebase.heartbeatinfo.DefaultHeartBeatInfo; import com.google.firebase.internal.DataCollectionConfigStorage; @@ -110,6 +111,9 @@ public class FirebaseApp { private final FirebaseOptions options; private final ComponentRuntime componentRuntime; + private final AtomicBoolean emulatorSettingsFrozen = new AtomicBoolean(false); + private EmulatorSettings emulatorSettings = EmulatorSettings.DEFAULT; + // Default disabled. We released Firebase publicly without this feature, so making it default // enabled is a backwards incompatible change. private final AtomicBoolean automaticResourceManagementEnabled = new AtomicBoolean(false); @@ -142,6 +146,20 @@ public FirebaseOptions getOptions() { return options; } + /** + * Returns the specified {@link EmulatorSettings} or a default. + * + *
TODO(samstern): Un-hide this once Firestore, Database, and Functions are implemented + * + * @hide + */ + @NonNull + public EmulatorSettings getEmulatorSettings() { + checkNotDeleted(); + emulatorSettingsFrozen.set(true); + return emulatorSettings; + } + @Override public boolean equals(Object o) { if (!(o instanceof FirebaseApp)) { @@ -305,6 +323,26 @@ public static FirebaseApp initializeApp( return firebaseApp; } + /** + * Specify which services should access local emulators for this FirebaseApp instance. + * + *
For example, if the {@link EmulatorSettings} contain {@link + * com.google.firebase.emulators.EmulatedServiceSettings} for {@link FirebaseDatabase#EMULATOR}, + * then calls to Cloud Firestore will communicate with the emulator rather than production. + * + *
TODO(samstern): Un-hide this once Firestore, Database, and Functions are implemented + * + * @param emulatorSettings the emulator settings for all services. + * @hide + */ + public void enableEmulators(@NonNull EmulatorSettings emulatorSettings) { + checkNotDeleted(); + Preconditions.checkState( + !this.emulatorSettingsFrozen.get(), + "Cannot enable emulators after Firebase SDKs have already been used."); + this.emulatorSettings = emulatorSettings; + } + /** * Deletes the {@link FirebaseApp} and all its data. All calls to this {@link FirebaseApp} * instance will throw once it has been called. diff --git a/firebase-common/src/main/java/com/google/firebase/emulators/EmulatedServiceSettings.java b/firebase-common/src/main/java/com/google/firebase/emulators/EmulatedServiceSettings.java new file mode 100644 index 00000000000..5acd04e9aa8 --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/emulators/EmulatedServiceSettings.java @@ -0,0 +1,44 @@ +// 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.emulators; + +import androidx.annotation.NonNull; + +/** + * Settings to connect a single Firebase service to a local emulator. + * + *
TODO(samstern): Un-hide this once Firestore, Database, and Functions are implemented + * + * @see EmulatorSettings + * @hide + */ +public final class EmulatedServiceSettings { + + private final String host; + private final int port; + + public EmulatedServiceSettings(@NonNull String host, int port) { + this.host = host; + this.port = port; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } +} diff --git a/firebase-common/src/main/java/com/google/firebase/emulators/EmulatorSettings.java b/firebase-common/src/main/java/com/google/firebase/emulators/EmulatorSettings.java new file mode 100644 index 00000000000..0ad3c80a32f --- /dev/null +++ b/firebase-common/src/main/java/com/google/firebase/emulators/EmulatorSettings.java @@ -0,0 +1,87 @@ +// 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.emulators; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.firebase.components.Preconditions; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Settings that control which Firebase services should access a local emulator, rather than + * production. + * + *
TODO(samstern): Un-hide this once Firestore, Database, and Functions are implemented
+ *
+ * @see com.google.firebase.FirebaseApp#enableEmulators(EmulatorSettings)
+ * @hide
+ */
+public final class EmulatorSettings {
+
+ /** Empty emulator settings to be used as an internal default */
+ public static final EmulatorSettings DEFAULT = new EmulatorSettings.Builder().build();
+
+ public static final class Builder {
+
+ private final Map TODO(samstern): Un-hide this once Firestore, Database, and Functions are implemented
+ *
+ * @see com.google.firebase.FirebaseApp#enableEmulators(EmulatorSettings)
+ * @see EmulatorSettings
+ * @see EmulatedServiceSettings
+ * @hide
+ */
+public final class FirebaseEmulator {
+
+ private final String name;
+
+ /**
+ * Only to be called by SDKs which support emulators in order to make constants.
+ *
+ * @hide
+ */
+ @NonNull
+ public static FirebaseEmulator forName(String name) {
+ return new FirebaseEmulator(name);
+ }
+
+ private FirebaseEmulator(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/firebase-database/src/androidTest/java/com/google/firebase/database/FirebaseDatabaseTest.java b/firebase-database/src/androidTest/java/com/google/firebase/database/FirebaseDatabaseTest.java
index 121a8a3e93e..61135dfdee5 100644
--- a/firebase-database/src/androidTest/java/com/google/firebase/database/FirebaseDatabaseTest.java
+++ b/firebase-database/src/androidTest/java/com/google/firebase/database/FirebaseDatabaseTest.java
@@ -30,6 +30,8 @@
import com.google.firebase.database.core.persistence.MockPersistenceStorageEngine;
import com.google.firebase.database.core.persistence.PersistenceManager;
import com.google.firebase.database.future.WriteFuture;
+import com.google.firebase.emulators.EmulatedServiceSettings;
+import com.google.firebase.emulators.EmulatorSettings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -64,6 +66,26 @@ public void getInstanceForApp() {
assertEquals(IntegrationTestValues.getAltNamespace(), db.getReference().toString());
}
+ @Test
+ public void getInstanceForAppWithEmulator() {
+ FirebaseApp app =
+ appForDatabaseUrl(IntegrationTestValues.getAltNamespace(), "getInstanceForAppWithEmulator");
+
+ EmulatedServiceSettings serviceSettings = new EmulatedServiceSettings("10.0.2.2", 9000);
+ EmulatorSettings emulatorSettings =
+ new EmulatorSettings.Builder()
+ .addEmulatedService(FirebaseDatabase.EMULATOR, serviceSettings)
+ .build();
+ app.enableEmulators(emulatorSettings);
+
+ FirebaseDatabase db = FirebaseDatabase.getInstance(app);
+ DatabaseReference rootRef = db.getReference();
+ assertEquals(rootRef.toString(), "http://10.0.2.2:9000");
+
+ DatabaseReference urlReference = db.getReferenceFromUrl("https://otherns.firebaseio.com");
+ assertEquals(urlReference.toString(), "http://10.0.2.2:9000");
+ }
+
@Test
public void getInstanceForAppWithUrl() {
FirebaseApp app =
diff --git a/firebase-database/src/main/java/com/google/firebase/database/FirebaseDatabase.java b/firebase-database/src/main/java/com/google/firebase/database/FirebaseDatabase.java
index d9a79a3d4bb..d8bca73485f 100644
--- a/firebase-database/src/main/java/com/google/firebase/database/FirebaseDatabase.java
+++ b/firebase-database/src/main/java/com/google/firebase/database/FirebaseDatabase.java
@@ -20,6 +20,7 @@
import androidx.annotation.NonNull;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
+import com.google.firebase.database.annotations.Nullable;
import com.google.firebase.database.core.DatabaseConfig;
import com.google.firebase.database.core.Path;
import com.google.firebase.database.core.Repo;
@@ -28,6 +29,8 @@
import com.google.firebase.database.core.utilities.ParsedUrl;
import com.google.firebase.database.core.utilities.Utilities;
import com.google.firebase.database.core.utilities.Validation;
+import com.google.firebase.emulators.EmulatedServiceSettings;
+import com.google.firebase.emulators.FirebaseEmulator;
/**
* The entry point for accessing a Firebase Database. You can get an instance by calling {@link
@@ -36,6 +39,8 @@
*/
public class FirebaseDatabase {
+ public static final FirebaseEmulator EMULATOR = FirebaseEmulator.forName("database");
+
private static final String SDK_VERSION = BuildConfig.VERSION_NAME;
private final FirebaseApp app;
@@ -99,7 +104,7 @@ public static synchronized FirebaseDatabase getInstance(
+ "FirebaseApp or from your getInstance() call.");
}
- ParsedUrl parsedUrl = Utilities.parseUrl(url);
+ ParsedUrl parsedUrl = Utilities.parseUrl(url, getEmulatorServiceSettings(app));
if (!parsedUrl.path.isEmpty()) {
throw new DatabaseException(
"Specified Database URL '"
@@ -112,6 +117,7 @@ public static synchronized FirebaseDatabase getInstance(
checkNotNull(app, "Provided FirebaseApp must not be null.");
FirebaseDatabaseComponent component = app.get(FirebaseDatabaseComponent.class);
checkNotNull(component, "Firebase Database component is not present.");
+
return component.get(parsedUrl.repoInfo);
}
@@ -188,7 +194,7 @@ public DatabaseReference getReferenceFromUrl(@NonNull String url) {
"Can't pass null for argument 'url' in " + "FirebaseDatabase.getReferenceFromUrl()");
}
- ParsedUrl parsedUrl = Utilities.parseUrl(url);
+ ParsedUrl parsedUrl = Utilities.parseUrl(url, getEmulatorServiceSettings(this.app));
if (!parsedUrl.repoInfo.host.equals(this.repo.getRepoInfo().host)) {
throw new DatabaseException(
"Invalid URL ("
@@ -288,6 +294,11 @@ public synchronized void setPersistenceCacheSizeBytes(long cacheSizeInBytes) {
this.config.setPersistenceCacheSizeBytes(cacheSizeInBytes);
}
+ @Nullable
+ private static EmulatedServiceSettings getEmulatorServiceSettings(@NonNull FirebaseApp app) {
+ return app.getEmulatorSettings().getServiceSettings(EMULATOR);
+ }
+
/** @return The semver version for this build of the Firebase Database client */
@NonNull
public static String getSdkVersion() {
diff --git a/firebase-database/src/main/java/com/google/firebase/database/core/utilities/Utilities.java b/firebase-database/src/main/java/com/google/firebase/database/core/utilities/Utilities.java
index 6267d328e22..1ddbd685824 100644
--- a/firebase-database/src/main/java/com/google/firebase/database/core/utilities/Utilities.java
+++ b/firebase-database/src/main/java/com/google/firebase/database/core/utilities/Utilities.java
@@ -16,6 +16,7 @@
import android.net.Uri;
import android.util.Base64;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.TaskCompletionSource;
@@ -24,6 +25,7 @@
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.core.Path;
import com.google.firebase.database.core.RepoInfo;
+import com.google.firebase.emulators.EmulatedServiceSettings;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -32,7 +34,13 @@
public class Utilities {
private static final char[] HEX_CHARACTERS = "0123456789abcdef".toCharArray();
- public static ParsedUrl parseUrl(String url) throws DatabaseException {
+ public static ParsedUrl parseUrl(@NonNull String url) {
+ return Utilities.parseUrl(url, null);
+ }
+
+ public static ParsedUrl parseUrl(
+ @NonNull String url, @Nullable EmulatedServiceSettings emulatorSettings)
+ throws DatabaseException {
try {
Uri uri = Uri.parse(url);
@@ -46,26 +54,29 @@ public static ParsedUrl parseUrl(String url) throws DatabaseException {
throw new IllegalArgumentException("Database URL does not specify a valid host");
}
- RepoInfo repoInfo = new RepoInfo();
- repoInfo.host = host.toLowerCase();
-
- int port = uri.getPort();
- if (port != -1) {
- repoInfo.secure = scheme.equals("https") || scheme.equals("wss");
- repoInfo.host += ":" + port;
- } else {
- repoInfo.secure = true;
+ String namespace = uri.getQueryParameter("ns");
+ if (namespace == null) {
+ String[] parts = host.split("\\.", -1);
+ namespace = parts[0].toLowerCase();
}
- String namespaceParam = uri.getQueryParameter("ns");
- if (namespaceParam != null) {
- repoInfo.namespace = namespaceParam;
+ RepoInfo repoInfo = new RepoInfo();
+ if (emulatorSettings != null) {
+ repoInfo.host = emulatorSettings.getHost() + ":" + emulatorSettings.getPort();
+ repoInfo.secure = false;
} else {
- String[] parts = host.split("\\.", -1);
- repoInfo.namespace = parts[0].toLowerCase();
+ repoInfo.host = host.toLowerCase();
+ int port = uri.getPort();
+ if (port != -1) {
+ repoInfo.secure = scheme.equals("https") || scheme.equals("wss");
+ repoInfo.host += ":" + port;
+ } else {
+ repoInfo.secure = true;
+ }
}
repoInfo.internalHost = repoInfo.host;
+ repoInfo.namespace = namespace;
String originalPathString = extractPathString(url);
// URLEncoding a space turns it into a '+', which is different
diff --git a/firebase-database/src/test/java/com/google/firebase/database/core/utilities/ParseUrlTest.java b/firebase-database/src/test/java/com/google/firebase/database/core/utilities/ParseUrlTest.java
index 9e902d0ebd5..4ac207f5566 100644
--- a/firebase-database/src/test/java/com/google/firebase/database/core/utilities/ParseUrlTest.java
+++ b/firebase-database/src/test/java/com/google/firebase/database/core/utilities/ParseUrlTest.java
@@ -19,6 +19,7 @@
import static org.junit.Assert.assertTrue;
import com.google.firebase.database.DatabaseException;
+import com.google.firebase.emulators.EmulatedServiceSettings;
import org.junit.Test;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@@ -35,7 +36,6 @@ public void testUrlParsing() throws DatabaseException {
assertEquals("gsoltis.fblocal.com:9000", parsed.repoInfo.internalHost);
parsed = Utilities.parseUrl("http://gsoltis.firebaseio.com/foo/bar");
- assertEquals("/foo/bar", parsed.path.toString());
assertEquals("gsoltis.firebaseio.com", parsed.repoInfo.host);
assertEquals("gsoltis.firebaseio.com", parsed.repoInfo.internalHost);
@@ -90,4 +90,14 @@ public void testUrlParsingSslDetection() throws DatabaseException {
parsed = Utilities.parseUrl("http://gsoltis.firebaseio.com");
assertTrue(parsed.repoInfo.secure);
}
+
+ @Test
+ public void testUrlParsingWithEmulator() {
+ EmulatedServiceSettings serviceSettings = new EmulatedServiceSettings("10.0.2.2", 9000);
+
+ ParsedUrl parsedUrl = Utilities.parseUrl("https://myns.firebaseio.com", serviceSettings);
+ assertFalse(parsedUrl.repoInfo.secure);
+ assertEquals(parsedUrl.repoInfo.host, "10.0.2.2:9000");
+ assertEquals(parsedUrl.repoInfo.namespace, "myns");
+ }
}