From e884b900ce10900c4ea29e3304505d8ae79fbda2 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Fri, 15 Sep 2023 12:55:03 -0500 Subject: [PATCH 01/11] Functions non-dagger kotlin --- .../firebase-functions.gradle.kts | 17 + ...ontextProvider.java => ContextProvider.kt} | 13 +- .../functions/FirebaseContextProvider.java | 123 ----- .../functions/FirebaseContextProvider.kt | 105 +++++ .../firebase/functions/FirebaseFunctions.java | 424 ------------------ .../firebase/functions/FirebaseFunctions.kt | 382 ++++++++++++++++ .../functions/FirebaseFunctionsException.java | 262 ----------- .../functions/FirebaseFunctionsException.kt | 229 ++++++++++ ...psCallOptions.java => HttpsCallOptions.kt} | 63 ++- .../functions/HttpsCallableContext.java | 48 -- .../functions/HttpsCallableContext.kt | 20 + .../functions/HttpsCallableOptions.java | 64 --- .../functions/HttpsCallableOptions.kt | 49 ++ .../functions/HttpsCallableReference.java | 160 ------- .../functions/HttpsCallableReference.kt | 159 +++++++ ...ableResult.java => HttpsCallableResult.kt} | 31 +- .../google/firebase/functions/Serializer.java | 192 -------- .../google/firebase/functions/Serializer.kt | 181 ++++++++ 18 files changed, 1188 insertions(+), 1334 deletions(-) rename firebase-functions/src/main/java/com/google/firebase/functions/{ContextProvider.java => ContextProvider.kt} (73%) delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.kt rename firebase-functions/src/main/java/com/google/firebase/functions/{HttpsCallOptions.java => HttpsCallOptions.kt} (53%) delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.kt delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt rename firebase-functions/src/main/java/com/google/firebase/functions/{HttpsCallableResult.java => HttpsCallableResult.kt} (57%) delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/Serializer.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt diff --git a/firebase-functions/firebase-functions.gradle.kts b/firebase-functions/firebase-functions.gradle.kts index 9d836aab7504..5b1af068a2b7 100644 --- a/firebase-functions/firebase-functions.gradle.kts +++ b/firebase-functions/firebase-functions.gradle.kts @@ -49,6 +49,23 @@ dependencies { javadocClasspath("org.codehaus.mojo:animal-sniffer-annotations:1.21") javadocClasspath(libs.autovalue.annotations) javadocClasspath(libs.findbugs.jsr305) + implementation("com.google.firebase:firebase-annotations:16.2.0") + implementation("com.google.firebase:firebase-common:20.3.1") + implementation("com.google.firebase:firebase-components:17.1.0") + implementation(project(":appcheck:firebase-appcheck-interop")) + implementation(libs.kotlin.stdlib) + implementation(libs.playservices.base) + implementation(libs.playservices.basement) + implementation(libs.playservices.tasks) + implementation("com.google.firebase:firebase-iid:21.1.0") { + exclude(group = "com.google.firebase", module = "firebase-common") + exclude(group = "com.google.firebase", module = "firebase-components") + } + implementation("com.google.firebase:firebase-auth-interop:18.0.0") { + exclude(group = "com.google.firebase", module = "firebase-common") + } + implementation("com.google.firebase:firebase-iid-interop:17.1.0") + implementation(libs.okhttp) implementation("com.google.firebase:firebase-appcheck-interop:17.1.0") implementation("com.google.firebase:firebase-common:20.4.2") diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.java b/firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.kt similarity index 73% rename from firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.java rename to firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.kt index a9802a5c3dbb..3ac789cb7b86 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.java +++ b/firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.kt @@ -11,12 +11,11 @@ // 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.functions -package com.google.firebase.functions; +import com.google.android.gms.tasks.Task -import com.google.android.gms.tasks.Task; - -/** The interface for getting metadata about the client. This is an interface for easier testing. */ -interface ContextProvider { - Task getContext(boolean getLimitedUseAppCheckToken); -} +/** The interface for getting metadata about the client. This is an interface for easier testing. */ +internal interface ContextProvider { + fun getContext(getLimitedUseAppCheckToken: Boolean): Task? +} \ No newline at end of file diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.java b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.java deleted file mode 100644 index 68e549464c3c..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.java +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2018 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.functions; - -import android.util.Log; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.annotations.concurrent.Lightweight; -import com.google.firebase.appcheck.AppCheckTokenResult; -import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider; -import com.google.firebase.auth.internal.InternalAuthProvider; -import com.google.firebase.iid.internal.FirebaseInstanceIdInternal; -import com.google.firebase.inject.Deferred; -import com.google.firebase.inject.Provider; -import com.google.firebase.internal.api.FirebaseNoSignedInUserException; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; -import javax.inject.Inject; -import javax.inject.Singleton; - -/** A ContextProvider that uses FirebaseAuth to get the token. */ -@Singleton -class FirebaseContextProvider implements ContextProvider { - private final String TAG = "FirebaseContextProvider"; - - private final Provider tokenProvider; - private final Provider instanceId; - private final AtomicReference appCheckRef = new AtomicReference<>(); - private final Executor executor; - - @Inject - FirebaseContextProvider( - Provider tokenProvider, - Provider instanceId, - Deferred appCheckDeferred, - @Lightweight Executor executor) { - this.tokenProvider = tokenProvider; - this.instanceId = instanceId; - this.executor = executor; - appCheckDeferred.whenAvailable( - p -> { - InteropAppCheckTokenProvider appCheck = p.get(); - appCheckRef.set(appCheck); - - appCheck.addAppCheckTokenListener( - unused -> { - // Do nothing; we just need to register a listener so that the App Check SDK knows - // to auto-refresh the token. - }); - }); - } - - @Override - public Task getContext(boolean limitedUseAppCheckToken) { - Task authToken = getAuthToken(); - Task appCheckToken = getAppCheckToken(limitedUseAppCheckToken); - return Tasks.whenAll(authToken, appCheckToken) - .onSuccessTask( - executor, - v -> - Tasks.forResult( - new HttpsCallableContext( - authToken.getResult(), - instanceId.get().getToken(), - appCheckToken.getResult()))); - } - - private Task getAuthToken() { - InternalAuthProvider auth = tokenProvider.get(); - if (auth == null) { - return Tasks.forResult(null); - } - return auth.getAccessToken(false) - .continueWith( - executor, - task -> { - String authToken = null; - if (!task.isSuccessful()) { - Exception exception = task.getException(); - if (exception instanceof FirebaseNoSignedInUserException) { - // Firebase Auth is linked in, but nobody is signed in, which is fine. - } else { - throw exception; - } - } else { - authToken = task.getResult().getToken(); - } - return authToken; - }); - } - - private Task getAppCheckToken(boolean limitedUseAppCheckToken) { - InteropAppCheckTokenProvider appCheck = appCheckRef.get(); - if (appCheck == null) { - return Tasks.forResult(null); - } - Task tokenTask = - limitedUseAppCheckToken ? appCheck.getLimitedUseToken() : appCheck.getToken(false); - return tokenTask.onSuccessTask( - executor, - result -> { - if (result.getError() != null) { - // If there was an error getting the App Check token, do NOT send the placeholder - // token. Only valid App Check tokens should be sent to the functions backend. - Log.w(TAG, "Error getting App Check token. Error: " + result.getError()); - return Tasks.forResult(null); - } - return Tasks.forResult(result.getToken()); - }); - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt new file mode 100644 index 000000000000..6e8063807199 --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt @@ -0,0 +1,105 @@ +// Copyright 2018 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.functions + +import android.util.Log +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks +import com.google.firebase.annotations.concurrent.Lightweight +import com.google.firebase.appcheck.AppCheckTokenResult +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.GetTokenResult +import com.google.firebase.auth.internal.InternalAuthProvider +import com.google.firebase.iid.internal.FirebaseInstanceIdInternal +import com.google.firebase.inject.Deferred +import com.google.firebase.inject.Provider +import com.google.firebase.internal.api.FirebaseNoSignedInUserException +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject +import javax.inject.Singleton + +/** A ContextProvider that uses FirebaseAuth to get the token. */ +@Singleton +internal class FirebaseContextProvider @Inject constructor( + private val tokenProvider: Provider, + private val instanceId: Provider, + appCheckDeferred: Deferred, + @param:Lightweight private val executor: Executor) : ContextProvider { + private val TAG = "FirebaseContextProvider" + private val appCheckRef = AtomicReference() + + init { + appCheckDeferred.whenAvailable { p: Provider -> + val appCheck = p.get() + appCheckRef.set(appCheck) + appCheck.addAppCheckTokenListener { unused: AppCheckTokenResult? -> } + } + } + + override fun getContext(limitedUseAppCheckToken: Boolean): Task? { + val authToken = authToken + val appCheckToken = getAppCheckToken(limitedUseAppCheckToken) + return Tasks.whenAll(authToken, appCheckToken) + .onSuccessTask( + executor + ) { v: Void? -> + Tasks.forResult( + HttpsCallableContext( + authToken.result, + instanceId.get().token, + appCheckToken.result)) + } + } + + private val authToken: Task + private get() { + val auth = tokenProvider.get() + ?: return Tasks.forResult(null) + return auth.getAccessToken(false) + .continueWith( + executor + ) { task: Task -> + var authToken: String? = null + if (!task.isSuccessful) { + val exception = task.exception + if (exception is FirebaseNoSignedInUserException) { + // Firebase Auth is linked in, but nobody is signed in, which is fine. + } else { + throw exception!! + } + } else { + authToken = task.result.token + } + authToken + } + } + + private fun getAppCheckToken(limitedUseAppCheckToken: Boolean): Task { + val appCheck = appCheckRef.get() + ?: return Tasks.forResult(null) + val tokenTask = if (limitedUseAppCheckToken) appCheck.limitedUseToken else appCheck.getToken(false) + return tokenTask.onSuccessTask( + executor + ) { result: AppCheckTokenResult -> + if (result.error != null) { + // If there was an error getting the App Check token, do NOT send the placeholder + // token. Only valid App Check tokens should be sent to the functions backend. + Log.w(TAG, "Error getting App Check token. Error: " + result.error) + return@onSuccessTask Tasks.forResult(null) + } + Tasks.forResult(result.token) + } + } +} \ No newline at end of file diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java deleted file mode 100644 index 6b15592b1f36..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright 2018 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.functions; - -import android.content.Context; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.android.gms.common.internal.Preconditions; -import com.google.android.gms.security.ProviderInstaller; -import com.google.android.gms.security.ProviderInstaller.ProviderInstallListener; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; -import com.google.firebase.annotations.concurrent.Lightweight; -import com.google.firebase.annotations.concurrent.UiThread; -import com.google.firebase.emulators.EmulatedServiceSettings; -import com.google.firebase.functions.FirebaseFunctionsException.Code; -import dagger.assisted.Assisted; -import dagger.assisted.AssistedInject; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Executor; -import javax.inject.Named; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import org.json.JSONException; -import org.json.JSONObject; - -/** FirebaseFunctions lets you call Cloud Functions for Firebase. */ -public class FirebaseFunctions { - - /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ - private static final TaskCompletionSource providerInstalled = new TaskCompletionSource<>(); - - /** - * Whether the ProviderInstaller async task has been started. This is guarded by the - * providerInstalled lock. - */ - private static boolean providerInstallStarted = false; - - // The network client to use for HTTPS requests. - private final OkHttpClient client; - - // A serializer to encode/decode parameters and return values. - private final Serializer serializer; - - // A provider of client metadata to include with calls. - private final ContextProvider contextProvider; - - private final Executor executor; - - // The projectId to use for all functions references. - private final String projectId; - - // The region to use for all function references. - private final String region; - - // A custom domain for the http trigger, such as "https://mydomain.com" - @Nullable private final String customDomain; - - // The format to use for constructing urls from region, projectId, and name. - private String urlFormat = "https://%1$s-%2$s.cloudfunctions.net/%3$s"; - - // Emulator settings - @Nullable private EmulatedServiceSettings emulatorSettings; - - @AssistedInject - FirebaseFunctions( - Context context, - @Named("projectId") String projectId, - @Assisted String regionOrCustomDomain, - ContextProvider contextProvider, - @Lightweight Executor executor, - @UiThread Executor uiExecutor) { - this.executor = executor; - this.client = new OkHttpClient(); - this.serializer = new Serializer(); - this.contextProvider = Preconditions.checkNotNull(contextProvider); - this.projectId = Preconditions.checkNotNull(projectId); - - boolean isRegion; - try { - new URL(regionOrCustomDomain); - isRegion = false; - } catch (MalformedURLException malformedURLException) { - isRegion = true; - } - - if (isRegion) { - this.region = regionOrCustomDomain; - this.customDomain = null; - } else { - this.region = "us-central1"; - this.customDomain = regionOrCustomDomain; - } - - maybeInstallProviders(context, uiExecutor); - } - - /** - * Runs ProviderInstaller.installIfNeededAsync once per application instance. - * - * @param context The application context. - * @param uiExecutor - */ - private static void maybeInstallProviders(Context context, Executor uiExecutor) { - // Make sure this only runs once. - synchronized (providerInstalled) { - if (providerInstallStarted) { - return; - } - providerInstallStarted = true; - } - - // Package installIfNeededAsync into a Runnable so it can be run on the main thread. - // installIfNeededAsync checks to make sure it is on the main thread, and throws otherwise. - uiExecutor.execute( - () -> - ProviderInstaller.installIfNeededAsync( - context, - new ProviderInstallListener() { - @Override - public void onProviderInstalled() { - providerInstalled.setResult(null); - } - - @Override - public void onProviderInstallFailed(int i, android.content.Intent intent) { - Log.d("FirebaseFunctions", "Failed to update ssl context"); - providerInstalled.setResult(null); - } - })); - } - - /** - * Creates a Cloud Functions client with the given app and region or custom domain. - * - * @param app The app for the Firebase project. - * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as {@code - * "us-central1"} or {@code "https://mydomain.com"}. - */ - @NonNull - public static FirebaseFunctions getInstance( - @NonNull FirebaseApp app, @NonNull String regionOrCustomDomain) { - Preconditions.checkNotNull(app, "You must call FirebaseApp.initializeApp first."); - Preconditions.checkNotNull(regionOrCustomDomain); - - FunctionsMultiResourceComponent component = app.get(FunctionsMultiResourceComponent.class); - Preconditions.checkNotNull(component, "Functions component does not exist."); - - return component.get(regionOrCustomDomain); - } - - /** - * Creates a Cloud Functions client with the given app. - * - * @param app The app for the Firebase project. - */ - @NonNull - public static FirebaseFunctions getInstance(@NonNull FirebaseApp app) { - return getInstance(app, "us-central1"); - } - - /** - * Creates a Cloud Functions client with the default app and given region or custom domain. - * - * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as {@code - * "us-central1"} or {@code "https://mydomain.com"}. - */ - @NonNull - public static FirebaseFunctions getInstance(@NonNull String regionOrCustomDomain) { - return getInstance(FirebaseApp.getInstance(), regionOrCustomDomain); - } - - /** Creates a Cloud Functions client with the default app. */ - @NonNull - public static FirebaseFunctions getInstance() { - return getInstance(FirebaseApp.getInstance(), "us-central1"); - } - - /** Returns a reference to the callable HTTPS trigger with the given name. */ - @NonNull - public HttpsCallableReference getHttpsCallable(@NonNull String name) { - return new HttpsCallableReference(this, name, new HttpsCallOptions()); - } - - /** Returns a reference to the callable HTTPS trigger with the provided URL. */ - @NonNull - public HttpsCallableReference getHttpsCallableFromUrl(@NonNull URL url) { - return new HttpsCallableReference(this, url, new HttpsCallOptions()); - } - - /** Returns a reference to the callable HTTPS trigger with the given name and call options. */ - @NonNull - public HttpsCallableReference getHttpsCallable( - @NonNull String name, @NonNull HttpsCallableOptions options) { - return new HttpsCallableReference(this, name, new HttpsCallOptions(options)); - } - - /** Returns a reference to the callable HTTPS trigger with the provided URL and call options. */ - @NonNull - public HttpsCallableReference getHttpsCallableFromUrl( - @NonNull URL url, @NonNull HttpsCallableOptions options) { - return new HttpsCallableReference(this, url, new HttpsCallOptions(options)); - } - - /** - * Returns the URL for a particular function. - * - * @param function The name of the function. - * @return The URL. - */ - @VisibleForTesting - URL getURL(String function) { - EmulatedServiceSettings emulatorSettings = this.emulatorSettings; - if (emulatorSettings != null) { - urlFormat = - "http://" - + emulatorSettings.getHost() - + ":" - + emulatorSettings.getPort() - + "/%2$s/%1$s/%3$s"; - } - - String str = String.format(urlFormat, region, projectId, function); - - if (customDomain != null && emulatorSettings == null) { - str = customDomain + "/" + function; - } - - try { - return new URL(str); - } catch (MalformedURLException mfe) { - throw new IllegalStateException(mfe); - } - } - - /** @deprecated Use {@link #useEmulator(String, int)} to connect to the emulator. */ - public void useFunctionsEmulator(@NonNull String origin) { - Preconditions.checkNotNull(origin, "origin cannot be null"); - urlFormat = origin + "/%2$s/%1$s/%3$s"; - } - - /** - * Modifies this FirebaseFunctions instance to communicate with the Cloud Functions emulator. - * - *

Note: Call this method before using the instance to do any functions operations. - * - * @param host the emulator host (for example, 10.0.2.2) - * @param port the emulator port (for example, 5001) - */ - public void useEmulator(@NonNull String host, int port) { - this.emulatorSettings = new EmulatedServiceSettings(host, port); - } - - /** - * Calls a Callable HTTPS trigger endpoint. - * - * @param name The name of the HTTPS trigger. - * @param data Parameters to pass to the function. Can be anything encodable as JSON. - * @return A Task that will be completed when the request is complete. - */ - Task call(String name, @Nullable Object data, HttpsCallOptions options) { - return providerInstalled - .getTask() - .continueWithTask( - executor, task -> contextProvider.getContext(options.getLimitedUseAppCheckTokens())) - .continueWithTask( - executor, - task -> { - if (!task.isSuccessful()) { - return Tasks.forException(task.getException()); - } - HttpsCallableContext context = task.getResult(); - URL url = getURL(name); - return call(url, data, context, options); - }); - } - - /** - * Calls a Callable HTTPS trigger endpoint. - * - * @param url The url of the HTTPS trigger - * @param data Parameters to pass to the function. Can be anything encodable as JSON. - * @return A Task that will be completed when the request is complete. - */ - Task call(URL url, @Nullable Object data, HttpsCallOptions options) { - return providerInstalled - .getTask() - .continueWithTask( - executor, task -> contextProvider.getContext(options.getLimitedUseAppCheckTokens())) - .continueWithTask( - executor, - task -> { - if (!task.isSuccessful()) { - return Tasks.forException(task.getException()); - } - HttpsCallableContext context = task.getResult(); - return call(url, data, context, options); - }); - } - - /** - * Calls a Callable HTTPS trigger endpoint. - * - * @param url The name of the HTTPS trigger. - * @param data Parameters to pass to the function. Can be anything encodable as JSON. - * @param context Metadata to supply with the function call. - * @return A Task that will be completed when the request is complete. - */ - private Task call( - @NonNull URL url, - @Nullable Object data, - HttpsCallableContext context, - HttpsCallOptions options) { - Preconditions.checkNotNull(url, "url cannot be null"); - - Map body = new HashMap<>(); - - Object encoded = serializer.encode(data); - body.put("data", encoded); - - JSONObject bodyJSON = new JSONObject(body); - MediaType contentType = MediaType.parse("application/json"); - RequestBody requestBody = RequestBody.create(contentType, bodyJSON.toString()); - - Request.Builder request = new Request.Builder().url(url).post(requestBody); - if (context.getAuthToken() != null) { - request = request.header("Authorization", "Bearer " + context.getAuthToken()); - } - if (context.getInstanceIdToken() != null) { - request = request.header("Firebase-Instance-ID-Token", context.getInstanceIdToken()); - } - if (context.getAppCheckToken() != null) { - request = request.header("X-Firebase-AppCheck", context.getAppCheckToken()); - } - - OkHttpClient callClient = options.apply(client); - Call call = callClient.newCall(request.build()); - - TaskCompletionSource tcs = new TaskCompletionSource<>(); - call.enqueue( - new Callback() { - @Override - public void onFailure(Call ignored, IOException e) { - if (e instanceof InterruptedIOException) { - FirebaseFunctionsException exception = - new FirebaseFunctionsException( - Code.DEADLINE_EXCEEDED.name(), Code.DEADLINE_EXCEEDED, null, e); - tcs.setException(exception); - } else { - FirebaseFunctionsException exception = - new FirebaseFunctionsException(Code.INTERNAL.name(), Code.INTERNAL, null, e); - tcs.setException(exception); - } - } - - @Override - public void onResponse(Call ignored, Response response) throws IOException { - Code code = Code.fromHttpStatus(response.code()); - String body = response.body().string(); - - FirebaseFunctionsException exception = - FirebaseFunctionsException.fromResponse(code, body, serializer); - if (exception != null) { - tcs.setException(exception); - return; - } - - JSONObject bodyJSON; - try { - bodyJSON = new JSONObject(body); - } catch (JSONException je) { - Exception e = - new FirebaseFunctionsException( - "Response is not valid JSON object.", Code.INTERNAL, null, je); - tcs.setException(e); - return; - } - - Object dataJSON = bodyJSON.opt("data"); - // TODO: Allow "result" instead of "data" for now, for backwards compatibility. - if (dataJSON == null) { - dataJSON = bodyJSON.opt("result"); - } - if (dataJSON == null) { - Exception e = - new FirebaseFunctionsException( - "Response is missing data field.", Code.INTERNAL, null); - tcs.setException(e); - return; - } - - HttpsCallableResult result = new HttpsCallableResult(serializer.decode(dataJSON)); - tcs.setResult(result); - } - }); - return tcs.getTask(); - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt new file mode 100644 index 000000000000..973d982613a9 --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt @@ -0,0 +1,382 @@ +// Copyright 2018 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.functions + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.annotation.VisibleForTesting +import com.google.android.gms.common.internal.Preconditions +import com.google.android.gms.security.ProviderInstaller +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.TaskCompletionSource +import com.google.android.gms.tasks.Tasks +import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Lightweight +import com.google.firebase.annotations.concurrent.UiThread +import com.google.firebase.emulators.EmulatedServiceSettings +import com.google.firebase.functions.FirebaseFunctionsException.Code.Companion.fromHttpStatus +import com.google.firebase.functions.FirebaseFunctionsException.Companion.fromResponse +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import okhttp3.Call +import okhttp3.Callback +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.io.InterruptedIOException +import java.net.MalformedURLException +import java.net.URL +import java.util.concurrent.Executor +import javax.inject.Named + +/** FirebaseFunctions lets you call Cloud Functions for Firebase. */ +class FirebaseFunctions @AssistedInject internal constructor( + context: Context, + @Named("projectId") projectId: String?, + @Assisted regionOrCustomDomain: String?, + contextProvider: ContextProvider?, + @param:Lightweight private val executor: Executor, + @UiThread uiExecutor: Executor) { + // The network client to use for HTTPS requests. + private val client: OkHttpClient + + // A serializer to encode/decode parameters and return values. + private val serializer: Serializer + + // A provider of client metadata to include with calls. + private val contextProvider: ContextProvider + + // The projectId to use for all functions references. + private val projectId: String + + // The region to use for all function references. + private var region: String? = null + + // A custom domain for the http trigger, such as "https://mydomain.com" + private var customDomain: String? = null + + // The format to use for constructing urls from region, projectId, and name. + private var urlFormat = "https://%1\$s-%2\$s.cloudfunctions.net/%3\$s" + + // Emulator settings + private var emulatorSettings: EmulatedServiceSettings? = null + + init { + client = OkHttpClient() + serializer = Serializer() + this.contextProvider = Preconditions.checkNotNull(contextProvider) + this.projectId = Preconditions.checkNotNull(projectId) + val isRegion: Boolean + isRegion = try { + URL(regionOrCustomDomain) + false + } catch (malformedURLException: MalformedURLException) { + true + } + if (isRegion) { + region = regionOrCustomDomain + customDomain = null + } else { + region = "us-central1" + customDomain = regionOrCustomDomain + } + maybeInstallProviders(context, uiExecutor) + } + + /** Returns a reference to the callable HTTPS trigger with the given name. */ + fun getHttpsCallable(name: String): HttpsCallableReference { + return HttpsCallableReference(this, name, HttpsCallOptions()) + } + + /** Returns a reference to the callable HTTPS trigger with the provided URL. */ + fun getHttpsCallableFromUrl(url: URL): HttpsCallableReference { + return HttpsCallableReference(this, url, HttpsCallOptions()) + } + + /** Returns a reference to the callable HTTPS trigger with the given name and call options. */ + fun getHttpsCallable( + name: String, options: HttpsCallableOptions): HttpsCallableReference { + return HttpsCallableReference(this, name, HttpsCallOptions(options)) + } + + /** Returns a reference to the callable HTTPS trigger with the provided URL and call options. */ + fun getHttpsCallableFromUrl( + url: URL, options: HttpsCallableOptions): HttpsCallableReference { + return HttpsCallableReference(this, url, HttpsCallOptions(options)) + } + + /** + * Returns the URL for a particular function. + * + * @param function The name of the function. + * @return The URL. + */ + @VisibleForTesting + fun getURL(function: String): URL { + val emulatorSettings = emulatorSettings + if (emulatorSettings != null) { + urlFormat = ("http://" + + emulatorSettings.host + + ":" + + emulatorSettings.port + + "/%2\$s/%1\$s/%3\$s") + } + var str = String.format(urlFormat, region, projectId, function) + if (customDomain != null && emulatorSettings == null) { + str = "$customDomain/$function" + } + return try { + URL(str) + } catch (mfe: MalformedURLException) { + throw IllegalStateException(mfe) + } + } + + @Deprecated("Use {@link #useEmulator(String, int)} to connect to the emulator. ") + fun useFunctionsEmulator(origin: String) { + Preconditions.checkNotNull(origin, "origin cannot be null") + urlFormat = "$origin/%2\$s/%1\$s/%3\$s" + } + + /** + * Modifies this FirebaseFunctions instance to communicate with the Cloud Functions emulator. + * + * + * Note: Call this method before using the instance to do any functions operations. + * + * @param host the emulator host (for example, 10.0.2.2) + * @param port the emulator port (for example, 5001) + */ + fun useEmulator(host: String, port: Int) { + emulatorSettings = EmulatedServiceSettings(host, port) + } + + /** + * Calls a Callable HTTPS trigger endpoint. + * + * @param name The name of the HTTPS trigger. + * @param data Parameters to pass to the function. Can be anything encodable as JSON. + * @return A Task that will be completed when the request is complete. + */ + fun call(name: String, data: Any?, options: HttpsCallOptions): Task { + return providerInstalled + .task + .continueWithTask( + executor) { task: Task? -> contextProvider.getContext(options.limitedUseAppCheckTokens) } + .continueWithTask( + executor + ) { task: Task -> + if (!task.isSuccessful) { + return@continueWithTask Tasks.forException(task.exception!!) + } + val context = task.result + val url = getURL(name) + call(url, data, context, options) + } + } + + /** + * Calls a Callable HTTPS trigger endpoint. + * + * @param url The url of the HTTPS trigger + * @param data Parameters to pass to the function. Can be anything encodable as JSON. + * @return A Task that will be completed when the request is complete. + */ + fun call(url: URL, data: Any?, options: HttpsCallOptions): Task { + return providerInstalled + .task + .continueWithTask( + executor) { task: Task? -> contextProvider.getContext(options.limitedUseAppCheckTokens) } + .continueWithTask( + executor + ) { task: Task -> + if (!task.isSuccessful) { + return@continueWithTask Tasks.forException(task.exception!!) + } + val context = task.result + call(url, data, context, options) + } + } + + /** + * Calls a Callable HTTPS trigger endpoint. + * + * @param url The name of the HTTPS trigger. + * @param data Parameters to pass to the function. Can be anything encodable as JSON. + * @param context Metadata to supply with the function call. + * @return A Task that will be completed when the request is complete. + */ + private fun call( + url: URL, + data: Any?, + context: HttpsCallableContext?, + options: HttpsCallOptions): Task { + Preconditions.checkNotNull(url, "url cannot be null") + val body: MutableMap = HashMap() + val encoded = serializer.encode(data) + body["data"] = encoded + val bodyJSON = JSONObject(body) + val contentType = MediaType.parse("application/json") + val requestBody = RequestBody.create(contentType, bodyJSON.toString()) + var request = Request.Builder().url(url).post(requestBody) + if (context!!.authToken != null) { + request = request.header("Authorization", "Bearer " + context.authToken) + } + if (context.instanceIdToken != null) { + request = request.header("Firebase-Instance-ID-Token", context.instanceIdToken) + } + if (context.appCheckToken != null) { + request = request.header("X-Firebase-AppCheck", context.appCheckToken) + } + val callClient = options.apply(client) + val call = callClient.newCall(request.build()) + val tcs = TaskCompletionSource() + call.enqueue( + object : Callback { + override fun onFailure(ignored: Call, e: IOException) { + if (e is InterruptedIOException) { + val exception = FirebaseFunctionsException( + FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name, FirebaseFunctionsException.Code.DEADLINE_EXCEEDED, null, e) + tcs.setException(exception) + } else { + val exception = FirebaseFunctionsException(FirebaseFunctionsException.Code.INTERNAL.name, FirebaseFunctionsException.Code.INTERNAL, null, e) + tcs.setException(exception) + } + } + + @Throws(IOException::class) + override fun onResponse(ignored: Call, response: Response) { + val code = fromHttpStatus(response.code()) + val body = response.body()!!.string() + val exception = fromResponse(code, body, serializer) + if (exception != null) { + tcs.setException(exception) + return + } + val bodyJSON: JSONObject + bodyJSON = try { + JSONObject(body) + } catch (je: JSONException) { + val e: Exception = FirebaseFunctionsException( + "Response is not valid JSON object.", FirebaseFunctionsException.Code.INTERNAL, null, je) + tcs.setException(e) + return + } + var dataJSON = bodyJSON.opt("data") + // TODO: Allow "result" instead of "data" for now, for backwards compatibility. + if (dataJSON == null) { + dataJSON = bodyJSON.opt("result") + } + if (dataJSON == null) { + val e: Exception = FirebaseFunctionsException( + "Response is missing data field.", FirebaseFunctionsException.Code.INTERNAL, null) + tcs.setException(e) + return + } + val result = HttpsCallableResult(serializer.decode(dataJSON)) + tcs.setResult(result) + } + }) + return tcs.task + } + + companion object { + /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ + private val providerInstalled = TaskCompletionSource() + + /** + * Whether the ProviderInstaller async task has been started. This is guarded by the + * providerInstalled lock. + */ + private var providerInstallStarted = false + + /** + * Runs ProviderInstaller.installIfNeededAsync once per application instance. + * + * @param context The application context. + * @param uiExecutor + */ + private fun maybeInstallProviders(context: Context, uiExecutor: Executor) { + // Make sure this only runs once. + synchronized(providerInstalled) { + if (providerInstallStarted) { + return + } + providerInstallStarted = true + } + + // Package installIfNeededAsync into a Runnable so it can be run on the main thread. + // installIfNeededAsync checks to make sure it is on the main thread, and throws otherwise. + uiExecutor.execute { + ProviderInstaller.installIfNeededAsync( + context, + object : ProviderInstaller.ProviderInstallListener { + override fun onProviderInstalled() { + providerInstalled.setResult(null) + } + + override fun onProviderInstallFailed(i: Int, intent: Intent?) { + Log.d("FirebaseFunctions", "Failed to update ssl context") + providerInstalled.setResult(null) + } + }) + } + } + + /** + * Creates a Cloud Functions client with the given app and region or custom domain. + * + * @param app The app for the Firebase project. + * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as `"us-central1"` or `"https://mydomain.com"`. + */ + @JvmStatic + fun getInstance( + app: FirebaseApp, regionOrCustomDomain: String): FirebaseFunctions { + Preconditions.checkNotNull(app, "You must call FirebaseApp.initializeApp first.") + Preconditions.checkNotNull(regionOrCustomDomain) + val component = app.get(FunctionsMultiResourceComponent::class.java) + Preconditions.checkNotNull(component, "Functions component does not exist.") + return component[regionOrCustomDomain] + } + + /** + * Creates a Cloud Functions client with the given app. + * + * @param app The app for the Firebase project. + */ + @JvmStatic + fun getInstance(app: FirebaseApp): FirebaseFunctions { + return getInstance(app, "us-central1") + } + + /** + * Creates a Cloud Functions client with the default app and given region or custom domain. + * + * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as `"us-central1"` or `"https://mydomain.com"`. + */ + @JvmStatic + fun getInstance(regionOrCustomDomain: String): FirebaseFunctions { + return getInstance(FirebaseApp.getInstance(), regionOrCustomDomain) + } + + @JvmStatic + fun getInstance() = getInstance(FirebaseApp.getInstance(), "us-central1") + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.java b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.java deleted file mode 100644 index 6ea90bda901a..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.java +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright 2018 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.functions; - -import android.util.SparseArray; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.firebase.FirebaseException; -import org.json.JSONException; -import org.json.JSONObject; - -// TODO: This is a copy of FirebaseFirestoreException. -// We should investigate whether we can at least share the Code enum. - -/** The class for all Exceptions thrown by FirebaseFunctions. */ -public class FirebaseFunctionsException extends FirebaseException { - - /** - * The set of error status codes that can be returned from a Callable HTTPS tigger. These are the - * canonical error codes for Google APIs, as documented here: - * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto#L26 - */ - public enum Code { - /** - * The operation completed successfully. FirebaseFunctionsException will never have a status of - * OK. - */ - OK(0), - - /** The operation was cancelled (typically by the caller). */ - CANCELLED(1), - - /** Unknown error or an error from a different error domain. */ - UNKNOWN(2), - - /** - * Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION. - * INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the - * system (e.g., an invalid field name). - */ - INVALID_ARGUMENT(3), - - /** - * Deadline expired before operation could complete. For operations that change the state of the - * system, this error may be returned even if the operation has completed successfully. For - * example, a successful response from a server could have been delayed long enough for the - * deadline to expire. - */ - DEADLINE_EXCEEDED(4), - - /** Some requested document was not found. */ - NOT_FOUND(5), - - /** Some document that we attempted to create already exists. */ - ALREADY_EXISTS(6), - - /** The caller does not have permission to execute the specified operation. */ - PERMISSION_DENIED(7), - - /** - * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system - * is out of space. - */ - RESOURCE_EXHAUSTED(8), - - /** - * Operation was rejected because the system is not in a state required for the operation's - * execution. - */ - FAILED_PRECONDITION(9), - - /** - * The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. - */ - ABORTED(10), - - /** Operation was attempted past the valid range. */ - OUT_OF_RANGE(11), - - /** Operation is not implemented or not supported/enabled. */ - UNIMPLEMENTED(12), - - /** - * Internal errors. Means some invariants expected by underlying system has been broken. If you - * see one of these errors, something is very broken. - */ - INTERNAL(13), - - /** - * The service is currently unavailable. This is a most likely a transient condition and may be - * corrected by retrying with a backoff. - */ - UNAVAILABLE(14), - - /** Unrecoverable data loss or corruption. */ - DATA_LOSS(15), - - /** The request does not have valid authentication credentials for the operation. */ - UNAUTHENTICATED(16); - - private final int value; - - Code(int value) { - this.value = value; - } - - // Create the canonical list of Status instances indexed by their code values. - private static final SparseArray STATUS_LIST = buildStatusList(); - - private static SparseArray buildStatusList() { - SparseArray codes = new SparseArray<>(); - for (Code c : Code.values()) { - Code existingValue = codes.get(c.ordinal()); - if (existingValue != null) { - throw new IllegalStateException( - "Code value duplication between " + existingValue + "&" + c.name()); - } - codes.put(c.ordinal(), c); - } - return codes; - } - - static Code fromValue(int value) { - return STATUS_LIST.get(value, Code.UNKNOWN); - } - - /** - * Takes an HTTP status code and returns the corresponding FUNErrorCode error code. This is the - * standard HTTP status code -> error mapping defined in: - * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto - * - * @param status An HTTP status code. - * @return The corresponding Code, or Code.UNKNOWN if none. - */ - static Code fromHttpStatus(int status) { - switch (status) { - case 200: - return Code.OK; - case 400: - return Code.INVALID_ARGUMENT; - case 401: - return Code.UNAUTHENTICATED; - case 403: - return Code.PERMISSION_DENIED; - case 404: - return Code.NOT_FOUND; - case 409: - return Code.ABORTED; - case 429: - return Code.RESOURCE_EXHAUSTED; - case 499: - return Code.CANCELLED; - case 500: - return Code.INTERNAL; - case 501: - return Code.UNIMPLEMENTED; - case 503: - return Code.UNAVAILABLE; - case 504: - return Code.DEADLINE_EXCEEDED; - } - return Code.UNKNOWN; - } - } - - /** - * Takes an HTTP response and returns the corresponding Exception if any. - * - * @param code An HTTP status code. - * @param body The body of the HTTP response, if any. - * @param serializer A serializer to use for decoding error details. - * @return The corresponding Exception, or null if none. - */ - static @Nullable FirebaseFunctionsException fromResponse( - Code code, @Nullable String body, Serializer serializer) { - // Start with reasonable defaults from the status code. - String description = code.name(); - Object details = null; - - // Then look through the body for explicit details. - try { - JSONObject json = new JSONObject(body); - JSONObject error = json.getJSONObject("error"); - if (error.opt("status") instanceof String) { - code = Code.valueOf(error.getString("status")); - // TODO: Add better default descriptions for error enums. - // The default description needs to be updated for the new code. - description = code.name(); - } - if (error.opt("message") instanceof String && !error.getString("message").isEmpty()) { - description = error.getString("message"); - } - details = error.opt("details"); - if (details != null) { - details = serializer.decode(details); - } - } catch (IllegalArgumentException iae) { - // This most likely means the status string was invalid, so consider this malformed. - code = Code.INTERNAL; - description = code.name(); - } catch (JSONException ioe) { - // If we couldn't parse explicit error data, that's fine. - } - - if (code == Code.OK) { - // Technically, there's an edge case where a developer could explicitly return an error code - // of OK, and we will treat it as success, but that seems reasonable. - return null; - } - - return new FirebaseFunctionsException(description, code, details); - } - - @NonNull private final Code code; - @Nullable private final Object details; - - FirebaseFunctionsException( - @NonNull String message, @NonNull Code code, @Nullable Object details) { - super(message); - this.code = code; - this.details = details; - } - - FirebaseFunctionsException( - @NonNull String message, @NonNull Code code, @Nullable Object details, Throwable cause) { - super(message, cause); - this.code = code; - this.details = details; - } - - /** - * Gets the error code for the operation that failed. - * - * @return the code for the FirebaseFunctionsException - */ - @NonNull - public Code getCode() { - return code; - } - - /** - * Gets the details object, if one was included in the error response. - * - * @return the object included in the "details" field of the response. - */ - @Nullable - public Object getDetails() { - return details; - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.kt new file mode 100644 index 000000000000..f57e12128305 --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.kt @@ -0,0 +1,229 @@ +// Copyright 2018 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.functions + +import android.util.SparseArray +import com.google.firebase.FirebaseException +import org.json.JSONException +import org.json.JSONObject + +// TODO: This is a copy of FirebaseFirestoreException. +// We should investigate whether we can at least share the Code enum. +/** The class for all Exceptions thrown by FirebaseFunctions. */ +class FirebaseFunctionsException : FirebaseException { + /** + * The set of error status codes that can be returned from a Callable HTTPS tigger. These are the + * canonical error codes for Google APIs, as documented here: + * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto#L26 + */ + enum class Code(private val value: Int) { + /** + * The operation completed successfully. FirebaseFunctionsException will never have a status of + * OK. + */ + OK(0), + + /** The operation was cancelled (typically by the caller). */ + CANCELLED(1), + + /** Unknown error or an error from a different error domain. */ + UNKNOWN(2), + + /** + * Client specified an invalid argument. Note that this differs from FAILED_PRECONDITION. + * INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the + * system (e.g., an invalid field name). + */ + INVALID_ARGUMENT(3), + + /** + * Deadline expired before operation could complete. For operations that change the state of the + * system, this error may be returned even if the operation has completed successfully. For + * example, a successful response from a server could have been delayed long enough for the + * deadline to expire. + */ + DEADLINE_EXCEEDED(4), + + /** Some requested document was not found. */ + NOT_FOUND(5), + + /** Some document that we attempted to create already exists. */ + ALREADY_EXISTS(6), + + /** The caller does not have permission to execute the specified operation. */ + PERMISSION_DENIED(7), + + /** + * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system + * is out of space. + */ + RESOURCE_EXHAUSTED(8), + + /** + * Operation was rejected because the system is not in a state required for the operation's + * execution. + */ + FAILED_PRECONDITION(9), + + /** + * The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. + */ + ABORTED(10), + + /** Operation was attempted past the valid range. */ + OUT_OF_RANGE(11), + + /** Operation is not implemented or not supported/enabled. */ + UNIMPLEMENTED(12), + + /** + * Internal errors. Means some invariants expected by underlying system has been broken. If you + * see one of these errors, something is very broken. + */ + INTERNAL(13), + + /** + * The service is currently unavailable. This is a most likely a transient condition and may be + * corrected by retrying with a backoff. + */ + UNAVAILABLE(14), + + /** Unrecoverable data loss or corruption. */ + DATA_LOSS(15), + + /** The request does not have valid authentication credentials for the operation. */ + UNAUTHENTICATED(16); + + companion object { + // Create the canonical list of Status instances indexed by their code values. + private val STATUS_LIST = buildStatusList() + private fun buildStatusList(): SparseArray { + val codes = SparseArray() + for (c in values()) { + val existingValue = codes[c.ordinal] + check(existingValue == null) { "Code value duplication between " + existingValue + "&" + c.name } + codes.put(c.ordinal, c) + } + return codes + } + + @JvmStatic + fun fromValue(value: Int): Code { + return STATUS_LIST[value, UNKNOWN] + } + + /** + * Takes an HTTP status code and returns the corresponding FUNErrorCode error code. This is the + * standard HTTP status code -> error mapping defined in: + * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto + * + * @param status An HTTP status code. + * @return The corresponding Code, or Code.UNKNOWN if none. + */ + @JvmStatic + fun fromHttpStatus(status: Int): Code { + when (status) { + 200 -> return OK + 400 -> return INVALID_ARGUMENT + 401 -> return UNAUTHENTICATED + 403 -> return PERMISSION_DENIED + 404 -> return NOT_FOUND + 409 -> return ABORTED + 429 -> return RESOURCE_EXHAUSTED + 499 -> return CANCELLED + 500 -> return INTERNAL + 501 -> return UNIMPLEMENTED + 503 -> return UNAVAILABLE + 504 -> return DEADLINE_EXCEEDED + } + return UNKNOWN + } + } + } + + /** + * Gets the error code for the operation that failed. + * + * @return the code for the FirebaseFunctionsException + */ + val code: Code + + /** + * Gets the details object, if one was included in the error response. + * + * @return the object included in the "details" field of the response. + */ + val details: Any? + + internal constructor( + message: String, code: Code, details: Any?) : super(message) { + this.code = code + this.details = details + } + + internal constructor( + message: String, code: Code, details: Any?, cause: Throwable?) : super(message, cause!!) { + this.code = code + this.details = details + } + + companion object { + /** + * Takes an HTTP response and returns the corresponding Exception if any. + * + * @param code An HTTP status code. + * @param body The body of the HTTP response, if any. + * @param serializer A serializer to use for decoding error details. + * @return The corresponding Exception, or null if none. + */ + @JvmStatic + fun fromResponse( + code: Code, body: String?, serializer: Serializer): FirebaseFunctionsException? { + // Start with reasonable defaults from the status code. + var code = code + var description = code.name + var details: Any? = null + + // Then look through the body for explicit details. + try { + val json = JSONObject(body) + val error = json.getJSONObject("error") + if (error.opt("status") is String) { + code = Code.valueOf(error.getString("status")) + // TODO: Add better default descriptions for error enums. + // The default description needs to be updated for the new code. + description = code.name + } + if (error.opt("message") is String && !error.getString("message").isEmpty()) { + description = error.getString("message") + } + details = error.opt("details") + if (details != null) { + details = serializer.decode(details) + } + } catch (iae: IllegalArgumentException) { + // This most likely means the status string was invalid, so consider this malformed. + code = Code.INTERNAL + description = code.name + } catch (ioe: JSONException) { + // If we couldn't parse explicit error data, that's fine. + } + return if (code == Code.OK) { + // Technically, there's an edge case where a developer could explicitly return an error code + // of OK, and we will treat it as success, but that seems reasonable. + null + } else FirebaseFunctionsException(description, code, details) + } + } +} \ No newline at end of file diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.java b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt similarity index 53% rename from firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.java rename to firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt index 86ab79889fb8..653ded9b0981 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.java +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt @@ -11,32 +11,25 @@ // 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.functions -package com.google.firebase.functions; +import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeUnit; -import okhttp3.OkHttpClient; - -/** An internal class for keeping track of options applied to an HttpsCallableReference. */ +/** An internal class for keeping track of options applied to an HttpsCallableReference. */ class HttpsCallOptions { - - // The default timeout to use for all calls. - private static final long DEFAULT_TIMEOUT = 70; - private static final TimeUnit DEFAULT_TIMEOUT_UNITS = TimeUnit.SECONDS; - // The timeout to use for calls from references created by this Functions. - private long timeout = DEFAULT_TIMEOUT; - private TimeUnit timeoutUnits = DEFAULT_TIMEOUT_UNITS; + private var timeout = DEFAULT_TIMEOUT + private var timeoutUnits = DEFAULT_TIMEOUT_UNITS + val limitedUseAppCheckTokens: Boolean - private final boolean limitedUseAppCheckTokens; - - /** Creates an (internal) HttpsCallOptions from the (external) {@link HttpsCallableOptions}. */ - HttpsCallOptions(HttpsCallableOptions publicCallableOptions) { - this.limitedUseAppCheckTokens = publicCallableOptions.getLimitedUseAppCheckTokens(); + /** Creates an (internal) HttpsCallOptions from the (external) [HttpsCallableOptions]. */ + constructor(publicCallableOptions: HttpsCallableOptions) { + limitedUseAppCheckTokens = publicCallableOptions.limitedUseAppCheckTokens } - HttpsCallOptions() { - this.limitedUseAppCheckTokens = false; + constructor() { + limitedUseAppCheckTokens = false } /** @@ -45,9 +38,9 @@ class HttpsCallOptions { * @param timeout The length of the timeout, in the given units. * @param units The units for the specified timeout. */ - void setTimeout(long timeout, TimeUnit units) { - this.timeout = timeout; - this.timeoutUnits = units; + fun setTimeout(timeout: Long, units: TimeUnit) { + this.timeout = timeout + timeoutUnits = units } /** @@ -55,20 +48,22 @@ void setTimeout(long timeout, TimeUnit units) { * * @return The timeout, in milliseconds. */ - long getTimeout() { - return timeoutUnits.toMillis(timeout); + fun getTimeout(): Long { + return timeoutUnits.toMillis(timeout) } - boolean getLimitedUseAppCheckTokens() { - return limitedUseAppCheckTokens; + /** Creates a new OkHttpClient with these options applied to it. */ + fun apply(client: OkHttpClient): OkHttpClient { + return client + .newBuilder() + .callTimeout(timeout, timeoutUnits) + .readTimeout(timeout, timeoutUnits) + .build() } - /** Creates a new OkHttpClient with these options applied to it. */ - OkHttpClient apply(OkHttpClient client) { - return client - .newBuilder() - .callTimeout(timeout, timeoutUnits) - .readTimeout(timeout, timeoutUnits) - .build(); + companion object { + // The default timeout to use for all calls. + private const val DEFAULT_TIMEOUT: Long = 70 + private val DEFAULT_TIMEOUT_UNITS = TimeUnit.SECONDS } -} +} \ No newline at end of file diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.java b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.java deleted file mode 100644 index a97fa044096d..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.java +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2018 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.functions; - -import androidx.annotation.Nullable; - -/** The metadata about the client that should automatically be included in function calls. */ -class HttpsCallableContext { - @Nullable private final String authToken; - @Nullable private final String instanceIdToken; - @Nullable private final String appCheckToken; - - HttpsCallableContext( - @Nullable String authToken, - @Nullable String instanceIdToken, - @Nullable String appCheckToken) { - this.authToken = authToken; - this.instanceIdToken = instanceIdToken; - this.appCheckToken = appCheckToken; - } - - @Nullable - public String getAuthToken() { - return authToken; - } - - @Nullable - public String getInstanceIdToken() { - return instanceIdToken; - } - - @Nullable - public String getAppCheckToken() { - return appCheckToken; - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.kt new file mode 100644 index 000000000000..2c79a4a8b6ed --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.kt @@ -0,0 +1,20 @@ +// Copyright 2018 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.functions + +/** The metadata about the client that should automatically be included in function calls. */ +internal class HttpsCallableContext( + val authToken: String?, + val instanceIdToken: String?, + val appCheckToken: String?) \ No newline at end of file diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.java b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.java deleted file mode 100644 index b3dd58116215..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.java +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2023 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.functions; - -import androidx.annotation.NonNull; - -/** - * Options for configuring the callable function. - * - *

These properties are immutable once a callable function reference is instantiated. - */ -public class HttpsCallableOptions { - // If true, request a limited-use token from AppCheck. - private final boolean limitedUseAppCheckTokens; - - private HttpsCallableOptions(boolean limitedUseAppCheckTokens) { - this.limitedUseAppCheckTokens = limitedUseAppCheckTokens; - } - - /** - * Returns the setting indicating if limited-use App Check tokens are enforced for this function. - */ - public boolean getLimitedUseAppCheckTokens() { - return limitedUseAppCheckTokens; - } - - /** Builder class for {@link com.google.firebase.functions.HttpsCallableOptions} */ - public static class Builder { - private boolean limitedUseAppCheckTokens = false; - - /** - * Sets whether or not to use limited-use App Check tokens when invoking the associated - * function. - */ - @NonNull - public Builder setLimitedUseAppCheckTokens(boolean limitedUse) { - limitedUseAppCheckTokens = limitedUse; - return this; - } - - /** Returns the setting indicating if limited-use App Check tokens are enforced. */ - public boolean getLimitedUseAppCheckTokens() { - return limitedUseAppCheckTokens; - } - - /** Builds a new {@link com.google.firebase.functions.HttpsCallableOptions}. */ - @NonNull - public HttpsCallableOptions build() { - return new HttpsCallableOptions(limitedUseAppCheckTokens); - } - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt new file mode 100644 index 000000000000..21169fa38aa9 --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt @@ -0,0 +1,49 @@ +// Copyright 2023 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.functions + +/** + * Options for configuring the callable function. + * + * + * These properties are immutable once a callable function reference is instantiated. + */ +class HttpsCallableOptions private constructor( + /** + * Returns the setting indicating if limited-use App Check tokens are enforced for this function. + */ + // If true, request a limited-use token from AppCheck. + val limitedUseAppCheckTokens: Boolean) { + + /** Builder class for [com.google.firebase.functions.HttpsCallableOptions] */ + class Builder { + /** Returns the setting indicating if limited-use App Check tokens are enforced. */ + var limitedUseAppCheckTokens = false + private set + + /** + * Sets whether or not to use limited-use App Check tokens when invoking the associated + * function. + */ + fun setLimitedUseAppCheckTokens(limitedUse: Boolean): Builder { + limitedUseAppCheckTokens = limitedUse + return this + } + + /** Builds a new [com.google.firebase.functions.HttpsCallableOptions]. */ + fun build(): HttpsCallableOptions { + return HttpsCallableOptions(limitedUseAppCheckTokens) + } + } +} \ No newline at end of file diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.java b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.java deleted file mode 100644 index 8957a9aa1bdd..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.java +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2018 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.functions; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.gms.tasks.Task; -import java.net.URL; -import java.util.concurrent.TimeUnit; - -/** A reference to a particular Callable HTTPS trigger in Cloud Functions. */ -public class HttpsCallableReference { - - // The functions client to use for making calls. - private final FirebaseFunctions functionsClient; - - // The name of the HTTPS endpoint this reference refers to. - // Is null if url is set. - private final String name; - - // The url of the HTTPS endpoint this reference refers to. - // Is null if name is set. - private final URL url; - - // Options for how to do the HTTPS call. - final HttpsCallOptions options; - - /** Creates a new reference with the given options. */ - HttpsCallableReference(FirebaseFunctions functionsClient, String name, HttpsCallOptions options) { - this.functionsClient = functionsClient; - this.name = name; - this.url = null; - this.options = options; - } - - /** Creates a new reference with the given options. */ - HttpsCallableReference(FirebaseFunctions functionsClient, URL url, HttpsCallOptions options) { - this.functionsClient = functionsClient; - this.name = null; - this.url = url; - this.options = options; - } - - /** - * Executes this Callable HTTPS trigger asynchronously. - * - *

The data passed into the trigger can be any of the following types: - * - *

    - *
  • Any primitive type, including null, int, long, float, and boolean. - *
  • {@link String} - *
  • {@link java.util.List List<?>}, where the contained objects are also one of these - * types. - *
  • {@link java.util.Map Map<String, ?>>}, where the values are also one of these - * types. - *
  • {@link org.json.JSONArray} - *
  • {@link org.json.JSONObject} - *
  • {@link org.json.JSONObject#NULL} - *
- * - *

If the returned task fails, the Exception will be one of the following types: - * - *

    - *
  • {@link java.io.IOException} - if the HTTPS request failed to connect. - *
  • {@link FirebaseFunctionsException} - if the request connected, but the function returned - * an error. - *
- * - *

The request to the Cloud Functions backend made by this method automatically includes a - * Firebase Instance ID token to identify the app instance. If a user is logged in with Firebase - * Auth, an auth token for the user will also be automatically included. - * - *

Firebase Instance ID sends data to the Firebase backend periodically to collect information - * regarding the app instance. To stop this, see {@link - * com.google.firebase.iid.FirebaseInstanceId#deleteInstanceId}. It will resume with a new - * Instance ID the next time you call this method. - * - * @param data Parameters to pass to the trigger. - * @return A Task that will be completed when the HTTPS request has completed. - * @see org.json.JSONArray - * @see org.json.JSONObject - * @see java.io.IOException - * @see FirebaseFunctionsException - */ - @NonNull - public Task call(@Nullable Object data) { - if (name != null) { - return functionsClient.call(name, data, options); - } else { - return functionsClient.call(url, data, options); - } - } - - /** - * Executes this HTTPS endpoint asynchronously without arguments. - * - *

The request to the Cloud Functions backend made by this method automatically includes a - * Firebase Instance ID token to identify the app instance. If a user is logged in with Firebase - * Auth, an auth token for the user will also be automatically included. - * - *

Firebase Instance ID sends data to the Firebase backend periodically to collect information - * regarding the app instance. To stop this, see {@link - * com.google.firebase.iid.FirebaseInstanceId#deleteInstanceId}. It will resume with a new - * Instance ID the next time you call this method. - * - * @return A Task that will be completed when the HTTPS request has completed. - */ - @NonNull - public Task call() { - if (name != null) { - return functionsClient.call(name, null, options); - } else { - return functionsClient.call(url, null, options); - } - } - - /** - * Changes the timeout for calls from this instance of Functions. The default is 60 seconds. - * - * @param timeout The length of the timeout, in the given units. - * @param units The units for the specified timeout. - */ - public void setTimeout(long timeout, @NonNull TimeUnit units) { - options.setTimeout(timeout, units); - } - - /** - * Returns the timeout for calls from this instance of Functions. - * - * @return The timeout, in milliseconds. - */ - public long getTimeout() { - return options.getTimeout(); - } - - /** - * Creates a new reference with the given timeout for calls. The default is 60 seconds. - * - * @param timeout The length of the timeout, in the given units. - * @param units The units for the specified timeout. - */ - @NonNull - public HttpsCallableReference withTimeout(long timeout, @NonNull TimeUnit units) { - HttpsCallableReference other = new HttpsCallableReference(functionsClient, name, options); - other.setTimeout(timeout, units); - return other; - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt new file mode 100644 index 000000000000..f7efb064b6ca --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt @@ -0,0 +1,159 @@ +// Copyright 2018 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.functions + +import com.google.android.gms.tasks.Task +import java.net.URL +import java.util.concurrent.TimeUnit + +/** A reference to a particular Callable HTTPS trigger in Cloud Functions. */ +class HttpsCallableReference { + // The functions client to use for making calls. + private val functionsClient: FirebaseFunctions + + // The name of the HTTPS endpoint this reference refers to. + // Is null if url is set. + private val name: String? + + // The url of the HTTPS endpoint this reference refers to. + // Is null if name is set. + private val url: URL? + + // Options for how to do the HTTPS call. + private val options: HttpsCallOptions + + /** Creates a new reference with the given options. */ + internal constructor(functionsClient: FirebaseFunctions, name: String?, options: HttpsCallOptions) { + this.functionsClient = functionsClient + this.name = name + url = null + this.options = options + } + + /** Creates a new reference with the given options. */ + internal constructor(functionsClient: FirebaseFunctions, url: URL?, options: HttpsCallOptions) { + this.functionsClient = functionsClient + name = null + this.url = url + this.options = options + } + + /** + * Executes this Callable HTTPS trigger asynchronously. + * + * + * The data passed into the trigger can be any of the following types: + * + * + * * Any primitive type, including null, int, long, float, and boolean. + * * [String] + * * [List&lt;?&gt;][java.util.List], where the contained objects are also one of these + * types. + * * [Map&lt;String, ?&gt;>][java.util.Map], where the values are also one of these + * types. + * * [org.json.JSONArray] + * * [org.json.JSONObject] + * * [org.json.JSONObject.NULL] + * + * + * + * If the returned task fails, the Exception will be one of the following types: + * + * + * * [java.io.IOException] - if the HTTPS request failed to connect. + * * [FirebaseFunctionsException] - if the request connected, but the function returned + * an error. + * + * + * + * The request to the Cloud Functions backend made by this method automatically includes a + * Firebase Instance ID token to identify the app instance. If a user is logged in with Firebase + * Auth, an auth token for the user will also be automatically included. + * + * + * Firebase Instance ID sends data to the Firebase backend periodically to collect information + * regarding the app instance. To stop this, see [ ][com.google.firebase.iid.FirebaseInstanceId.deleteInstanceId]. It will resume with a new + * Instance ID the next time you call this method. + * + * @param data Parameters to pass to the trigger. + * @return A Task that will be completed when the HTTPS request has completed. + * @see org.json.JSONArray + * + * @see org.json.JSONObject + * + * @see java.io.IOException + * + * @see FirebaseFunctionsException + */ + fun call(data: Any?): Task { + return if (name != null) { + functionsClient.call(name, data, options) + } else { + functionsClient.call(url!!, data, options) + } + } + + /** + * Executes this HTTPS endpoint asynchronously without arguments. + * + * + * The request to the Cloud Functions backend made by this method automatically includes a + * Firebase Instance ID token to identify the app instance. If a user is logged in with Firebase + * Auth, an auth token for the user will also be automatically included. + * + * + * Firebase Instance ID sends data to the Firebase backend periodically to collect information + * regarding the app instance. To stop this, see [ ][com.google.firebase.iid.FirebaseInstanceId.deleteInstanceId]. It will resume with a new + * Instance ID the next time you call this method. + * + * @return A Task that will be completed when the HTTPS request has completed. + */ + fun call(): Task { + return if (name != null) { + functionsClient.call(name, null, options) + } else { + functionsClient.call(url!!, null, options) + } + } + + /** + * Changes the timeout for calls from this instance of Functions. The default is 60 seconds. + * + * @param timeout The length of the timeout, in the given units. + * @param units The units for the specified timeout. + */ + fun setTimeout(timeout: Long, units: TimeUnit) { + options.setTimeout(timeout, units) + } + + val timeout: Long + /** + * Returns the timeout for calls from this instance of Functions. + * + * @return The timeout, in milliseconds. + */ + get() = options.getTimeout() + + /** + * Creates a new reference with the given timeout for calls. The default is 60 seconds. + * + * @param timeout The length of the timeout, in the given units. + * @param units The units for the specified timeout. + */ + fun withTimeout(timeout: Long, units: TimeUnit): HttpsCallableReference { + val other = HttpsCallableReference(functionsClient, name, options) + other.setTimeout(timeout, units) + return other + } +} \ No newline at end of file diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.java b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt similarity index 57% rename from firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.java rename to firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt index 51f37839d9ed..b5892ba8f3f2 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.java +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt @@ -11,29 +11,20 @@ // 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.functions -package com.google.firebase.functions; - -import androidx.annotation.Nullable; - -/** The result of calling a HttpsCallableReference function. */ -public class HttpsCallableResult { - // The actual result data, as generic types decoded from JSON. - private final Object data; - - HttpsCallableResult(Object data) { - this.data = data; - } - +/** The result of calling a HttpsCallableReference function. */ +class HttpsCallableResult internal constructor( // The actual result data, as generic types decoded from JSON. + private val data: Any?) { /** * Returns the data that was returned from the Callable HTTPS trigger. * - *

The data is in the form of native Java objects. For example, if your trigger returned an + * + * The data is in the form of native Java objects. For example, if your trigger returned an * array, this object would be a List. If your trigger returned a JavaScript object with - * keys and values, this object would be a Map. - */ - @Nullable - public Object getData() { - return data; + * keys and values, this object would be a Map, Object>. + */ + fun getData(): Any? { + return data } -} +} \ No newline at end of file diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.java b/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.java deleted file mode 100644 index 52201464b589..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.java +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2018 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.functions; - -import androidx.annotation.VisibleForTesting; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -/** Converts raw Java types into JSON objects. */ -class Serializer { - @VisibleForTesting - static final String LONG_TYPE = "type.googleapis.com/google.protobuf.Int64Value"; - - @VisibleForTesting - static final String UNSIGNED_LONG_TYPE = "type.googleapis.com/google.protobuf.UInt64Value"; - - private final DateFormat dateFormat; - - Serializer() { - // Encode Dates as UTC ISO 8601 strings. - dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - public Object encode(Object obj) { - if (obj == null || obj == JSONObject.NULL) { - return JSONObject.NULL; - } - if (obj instanceof Long) { - // JavaScript can't handle the full range of a long, so we use a wrapped string. - JSONObject wrapped = new JSONObject(); - try { - wrapped.put("@type", LONG_TYPE); - wrapped.put("value", obj.toString()); - } catch (JSONException e) { - // This should never happen. - throw new RuntimeException("Error encoding Long.", e); - } - return wrapped; - } - if (obj instanceof Number) { - return obj; - } - if (obj instanceof String) { - return obj; - } - if (obj instanceof Boolean) { - return obj; - } - if (obj instanceof JSONObject) { - return obj; - } - if (obj instanceof JSONArray) { - return obj; - } - if (obj instanceof Map) { - JSONObject result = new JSONObject(); - Map m = (Map) obj; - for (Object k : m.keySet()) { - if (!(k instanceof String)) { - throw new IllegalArgumentException("Object keys must be strings."); - } - String key = (String) k; - Object value = encode(m.get(k)); - try { - result.put(key, value); - } catch (JSONException e) { - // This should never happen. - throw new RuntimeException(e); - } - } - return result; - } - if (obj instanceof List) { - JSONArray result = new JSONArray(); - List l = (List) obj; - for (Object o : l) { - result.put(encode(o)); - } - return result; - } - if (obj instanceof JSONObject) { - JSONObject result = new JSONObject(); - JSONObject m = (JSONObject) obj; - Iterator keys = m.keys(); - while (keys.hasNext()) { - String k = keys.next(); - if (k == null) { - throw new IllegalArgumentException("Object keys cannot be null."); - } - String key = (String) k; - Object value = encode(m.opt(k)); - try { - result.put(key, value); - } catch (JSONException e) { - // This should never happen. - throw new RuntimeException(e); - } - } - return result; - } - if (obj instanceof JSONArray) { - JSONArray result = new JSONArray(); - JSONArray l = (JSONArray) obj; - for (int i = 0; i < l.length(); i++) { - Object o = l.opt(i); - result.put(encode(o)); - } - return result; - } - throw new IllegalArgumentException("Object cannot be encoded in JSON: " + obj); - } - - // TODO: Maybe this should throw a FirebaseFunctionsException instead? - public Object decode(Object obj) { - if (obj instanceof Number) { - return obj; - } - if (obj instanceof String) { - return obj; - } - if (obj instanceof Boolean) { - return obj; - } - if (obj instanceof JSONObject) { - if (((JSONObject) obj).has("@type")) { - String type = (((JSONObject) obj).optString("@type")); - String value = (((JSONObject) obj).optString("value")); - if (type.equals(LONG_TYPE)) { - // Decode the value as a Long. - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid Long format:" + value); - } - } else if (type.equals(UNSIGNED_LONG_TYPE)) { - // Decode the value as a Long. - // This will fail for numbers outside the normal range for a Long. - // TODO: Once min API version is >26, should switch to Long.parseUnsignedLong. - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid Long format:" + value); - } - } - // If the type is not a known type, just decode it as a map. - } - Map result = new HashMap(); - Iterator keys = ((JSONObject) obj).keys(); - while (keys.hasNext()) { - String key = keys.next(); - Object value = decode(((JSONObject) obj).opt(key)); - result.put(key, value); - } - return result; - } - if (obj instanceof JSONArray) { - List result = new ArrayList(); - for (int i = 0; i < ((JSONArray) obj).length(); i++) { - Object value = decode(((JSONArray) obj).opt(i)); - result.add(value); - } - return result; - } - if (obj == JSONObject.NULL) { - return null; - } - throw new IllegalArgumentException("Object cannot be decoded from JSON: " + obj); - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt b/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt new file mode 100644 index 000000000000..c1832b3559e2 --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt @@ -0,0 +1,181 @@ +// Copyright 2018 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.functions + +import androidx.annotation.VisibleForTesting +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +/** Converts raw Java types into JSON objects. */ +class Serializer { + private val dateFormat: DateFormat + + init { + // Encode Dates as UTC ISO 8601 strings. + dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + dateFormat.timeZone = TimeZone.getTimeZone("UTC") + } + + fun encode(obj: Any?): Any { + if (obj == null || obj === JSONObject.NULL) { + return JSONObject.NULL + } + if (obj is Long) { + // JavaScript can't handle the full range of a long, so we use a wrapped string. + val wrapped = JSONObject() + try { + wrapped.put("@type", LONG_TYPE) + wrapped.put("value", obj.toString()) + } catch (e: JSONException) { + // This should never happen. + throw RuntimeException("Error encoding Long.", e) + } + return wrapped + } + if (obj is Number) { + return obj + } + if (obj is String) { + return obj + } + if (obj is Boolean) { + return obj + } + if (obj is JSONObject) { + return obj + } + if (obj is JSONArray) { + return obj + } + if (obj is Map<*, *>) { + val result = JSONObject() + val m = obj + for (k in m.keys) { + require(k is String) { "Object keys must be strings." } + val value = encode(m[k]) + try { + result.put(k, value) + } catch (e: JSONException) { + // This should never happen. + throw RuntimeException(e) + } + } + return result + } + if (obj is List<*>) { + val result = JSONArray() + for (o in obj) { + result.put(encode(o)) + } + return result + } + if (obj is JSONObject) { + val result = JSONObject() + val m = obj + val keys = m.keys() + while (keys.hasNext()) { + val k = keys.next() + ?: throw IllegalArgumentException("Object keys cannot be null.") + val value = encode(m.opt(k)) + try { + result.put(k, value) + } catch (e: JSONException) { + // This should never happen. + throw RuntimeException(e) + } + } + return result + } + if (obj is JSONArray) { + val result = JSONArray() + val l = obj + for (i in 0 until l.length()) { + val o = l.opt(i) + result.put(encode(o)) + } + return result + } + throw IllegalArgumentException("Object cannot be encoded in JSON: $obj") + } + + // TODO: Maybe this should throw a FirebaseFunctionsException instead? + fun decode(obj: Any): Any? { + if (obj is Number) { + return obj + } + if (obj is String) { + return obj + } + if (obj is Boolean) { + return obj + } + if (obj is JSONObject) { + if (obj.has("@type")) { + val type = obj.optString("@type") + val value = obj.optString("value") + if (type == LONG_TYPE) { + // Decode the value as a Long. + return try { + value.toLong() + } catch (e: NumberFormatException) { + throw IllegalArgumentException("Invalid Long format:$value") + } + } else if (type == UNSIGNED_LONG_TYPE) { + // Decode the value as a Long. + // This will fail for numbers outside the normal range for a Long. + // TODO: Once min API version is >26, should switch to Long.parseUnsignedLong. + return try { + value.toLong() + } catch (e: NumberFormatException) { + throw IllegalArgumentException("Invalid Long format:$value") + } + } + // If the type is not a known type, just decode it as a map. + } + val result: MutableMap = HashMap() + val keys = obj.keys() + while (keys.hasNext()) { + val key = keys.next() + val value = decode(obj.opt(key)) + result[key] = value + } + return result + } + if (obj is JSONArray) { + val result: MutableList = ArrayList() + for (i in 0 until obj.length()) { + val value = decode(obj.opt(i)) + result.add(value) + } + return result + } + if (obj === JSONObject.NULL) { + return null + } + throw IllegalArgumentException("Object cannot be decoded from JSON: $obj") + } + + companion object { + @VisibleForTesting + const val LONG_TYPE = "type.googleapis.com/google.protobuf.Int64Value" + + @VisibleForTesting + const val UNSIGNED_LONG_TYPE = "type.googleapis.com/google.protobuf.UInt64Value" + } +} \ No newline at end of file From e64018af78f3a6bc1c57f290b7114c299fdfe909 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Fri, 20 Oct 2023 11:16:31 -0500 Subject: [PATCH 02/11] Resolve functions Kotlin issues --- .../firebase-functions.gradle.kts | 9 +- .../firebase/functions/ContextProvider.kt | 4 +- .../functions/FirebaseContextProvider.kt | 83 +++--- .../firebase/functions/FirebaseFunctions.kt | 274 ++++++++++-------- .../functions/FirebaseFunctionsException.kt | 44 +-- .../functions/FunctionsComponent.java | 78 ----- .../firebase/functions/FunctionsComponent.kt | 70 +++++ .../FunctionsMultiResourceComponent.java | 56 ---- .../FunctionsMultiResourceComponent.kt | 48 +++ .../functions/FunctionsRegistrar.java | 72 ----- .../firebase/functions/FunctionsRegistrar.kt | 73 +++++ .../firebase/functions/HttpsCallOptions.kt | 18 +- .../functions/HttpsCallableContext.kt | 9 +- .../functions/HttpsCallableOptions.kt | 30 +- .../functions/HttpsCallableReference.kt | 51 ++-- .../firebase/functions/HttpsCallableResult.kt | 18 +- .../google/firebase/functions/Serializer.kt | 16 +- 17 files changed, 481 insertions(+), 472 deletions(-) delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.kt delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.kt delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.kt diff --git a/firebase-functions/firebase-functions.gradle.kts b/firebase-functions/firebase-functions.gradle.kts index 5b1af068a2b7..f88e5771a2d4 100644 --- a/firebase-functions/firebase-functions.gradle.kts +++ b/firebase-functions/firebase-functions.gradle.kts @@ -16,6 +16,7 @@ plugins { id("firebase-library") id("kotlin-android") id("firebase-vendor") + kotlin("kapt") } firebaseLibrary { @@ -103,12 +104,14 @@ dependencies { } androidTestImplementation(project(":integ-testing")) - androidTestImplementation(libs.androidx.test.junit) - androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.junit) + androidTestImplementation(libs.truth) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.junit) androidTestImplementation(libs.mockito.core) androidTestImplementation(libs.mockito.dexmaker) - androidTestImplementation(libs.truth) + kapt("com.google.dagger:dagger-android-processor:2.43.2") + kapt("com.google.dagger:dagger-compiler:2.43.2") } // ========================================================================== diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.kt b/firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.kt index 3ac789cb7b86..9721fc27d63b 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/ContextProvider.kt @@ -15,7 +15,7 @@ package com.google.firebase.functions import com.google.android.gms.tasks.Task -/** The interface for getting metadata about the client. This is an interface for easier testing. */ +/** The interface for getting metadata about the client. This is an interface for easier testing. */ internal interface ContextProvider { fun getContext(getLimitedUseAppCheckToken: Boolean): Task? -} \ No newline at end of file +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt index 6e8063807199..860ce0ee8422 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseContextProvider.kt @@ -30,13 +30,16 @@ import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import javax.inject.Singleton -/** A ContextProvider that uses FirebaseAuth to get the token. */ +/** A ContextProvider that uses FirebaseAuth to get the token. */ @Singleton -internal class FirebaseContextProvider @Inject constructor( - private val tokenProvider: Provider, - private val instanceId: Provider, - appCheckDeferred: Deferred, - @param:Lightweight private val executor: Executor) : ContextProvider { +internal class FirebaseContextProvider +@Inject +constructor( + private val tokenProvider: Provider, + private val instanceId: Provider, + appCheckDeferred: Deferred, + @param:Lightweight private val executor: Executor +) : ContextProvider { private val TAG = "FirebaseContextProvider" private val appCheckRef = AtomicReference() @@ -49,50 +52,38 @@ internal class FirebaseContextProvider @Inject constructor( } override fun getContext(limitedUseAppCheckToken: Boolean): Task? { - val authToken = authToken + val authToken = getAuthToken() val appCheckToken = getAppCheckToken(limitedUseAppCheckToken) - return Tasks.whenAll(authToken, appCheckToken) - .onSuccessTask( - executor - ) { v: Void? -> - Tasks.forResult( - HttpsCallableContext( - authToken.result, - instanceId.get().token, - appCheckToken.result)) - } + return Tasks.whenAll(authToken, appCheckToken).onSuccessTask(executor) { _ -> + Tasks.forResult( + HttpsCallableContext(authToken.result, instanceId.get().token, appCheckToken.result) + ) + } } - private val authToken: Task - private get() { - val auth = tokenProvider.get() - ?: return Tasks.forResult(null) - return auth.getAccessToken(false) - .continueWith( - executor - ) { task: Task -> - var authToken: String? = null - if (!task.isSuccessful) { - val exception = task.exception - if (exception is FirebaseNoSignedInUserException) { - // Firebase Auth is linked in, but nobody is signed in, which is fine. - } else { - throw exception!! - } - } else { - authToken = task.result.token - } - authToken - } + private fun getAuthToken(): Task { + val auth = tokenProvider.get() ?: return Tasks.forResult(null) + return auth.getAccessToken(false).continueWith(executor) { task: Task -> + var authToken: String? = null + if (!task.isSuccessful) { + val exception = task.exception + if (exception is FirebaseNoSignedInUserException) { + // Firebase Auth is linked in, but nobody is signed in, which is fine. + } else { + throw exception!! + } + } else { + authToken = task.result.token + } + authToken } + } - private fun getAppCheckToken(limitedUseAppCheckToken: Boolean): Task { - val appCheck = appCheckRef.get() - ?: return Tasks.forResult(null) - val tokenTask = if (limitedUseAppCheckToken) appCheck.limitedUseToken else appCheck.getToken(false) - return tokenTask.onSuccessTask( - executor - ) { result: AppCheckTokenResult -> + private fun getAppCheckToken(getLimitedUseAppCheckToken: Boolean): Task { + val appCheck = appCheckRef.get() ?: return Tasks.forResult(null) + val tokenTask = + if (getLimitedUseAppCheckToken) appCheck.limitedUseToken else appCheck.getToken(false) + return tokenTask.onSuccessTask(executor) { result: AppCheckTokenResult -> if (result.error != null) { // If there was an error getting the App Check token, do NOT send the placeholder // token. Only valid App Check tokens should be sent to the functions backend. @@ -102,4 +93,4 @@ internal class FirebaseContextProvider @Inject constructor( Tasks.forResult(result.token) } } -} \ No newline at end of file +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt index 973d982613a9..94162a9b855d 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt @@ -30,6 +30,12 @@ import com.google.firebase.functions.FirebaseFunctionsException.Code.Companion.f import com.google.firebase.functions.FirebaseFunctionsException.Companion.fromResponse import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import java.io.IOException +import java.io.InterruptedIOException +import java.net.MalformedURLException +import java.net.URL +import java.util.concurrent.Executor +import javax.inject.Named import okhttp3.Call import okhttp3.Callback import okhttp3.MediaType @@ -39,21 +45,18 @@ import okhttp3.RequestBody import okhttp3.Response import org.json.JSONException import org.json.JSONObject -import java.io.IOException -import java.io.InterruptedIOException -import java.net.MalformedURLException -import java.net.URL -import java.util.concurrent.Executor -import javax.inject.Named -/** FirebaseFunctions lets you call Cloud Functions for Firebase. */ -class FirebaseFunctions @AssistedInject internal constructor( - context: Context, - @Named("projectId") projectId: String?, - @Assisted regionOrCustomDomain: String?, - contextProvider: ContextProvider?, - @param:Lightweight private val executor: Executor, - @UiThread uiExecutor: Executor) { +/** FirebaseFunctions lets you call Cloud Functions for Firebase. */ +class FirebaseFunctions +@AssistedInject +internal constructor( + context: Context, + @Named("projectId") projectId: String?, + @Assisted regionOrCustomDomain: String?, + contextProvider: ContextProvider?, + @param:Lightweight private val executor: Executor, + @UiThread uiExecutor: Executor +) { // The network client to use for HTTPS requests. private val client: OkHttpClient @@ -84,12 +87,13 @@ class FirebaseFunctions @AssistedInject internal constructor( this.contextProvider = Preconditions.checkNotNull(contextProvider) this.projectId = Preconditions.checkNotNull(projectId) val isRegion: Boolean - isRegion = try { - URL(regionOrCustomDomain) - false - } catch (malformedURLException: MalformedURLException) { - true - } + isRegion = + try { + URL(regionOrCustomDomain) + false + } catch (malformedURLException: MalformedURLException) { + true + } if (isRegion) { region = regionOrCustomDomain customDomain = null @@ -100,25 +104,23 @@ class FirebaseFunctions @AssistedInject internal constructor( maybeInstallProviders(context, uiExecutor) } - /** Returns a reference to the callable HTTPS trigger with the given name. */ + /** Returns a reference to the callable HTTPS trigger with the given name. */ fun getHttpsCallable(name: String): HttpsCallableReference { return HttpsCallableReference(this, name, HttpsCallOptions()) } - /** Returns a reference to the callable HTTPS trigger with the provided URL. */ + /** Returns a reference to the callable HTTPS trigger with the provided URL. */ fun getHttpsCallableFromUrl(url: URL): HttpsCallableReference { return HttpsCallableReference(this, url, HttpsCallOptions()) } - /** Returns a reference to the callable HTTPS trigger with the given name and call options. */ - fun getHttpsCallable( - name: String, options: HttpsCallableOptions): HttpsCallableReference { + /** Returns a reference to the callable HTTPS trigger with the given name and call options. */ + fun getHttpsCallable(name: String, options: HttpsCallableOptions): HttpsCallableReference { return HttpsCallableReference(this, name, HttpsCallOptions(options)) } - /** Returns a reference to the callable HTTPS trigger with the provided URL and call options. */ - fun getHttpsCallableFromUrl( - url: URL, options: HttpsCallableOptions): HttpsCallableReference { + /** Returns a reference to the callable HTTPS trigger with the provided URL and call options. */ + fun getHttpsCallableFromUrl(url: URL, options: HttpsCallableOptions): HttpsCallableReference { return HttpsCallableReference(this, url, HttpsCallOptions(options)) } @@ -132,11 +134,8 @@ class FirebaseFunctions @AssistedInject internal constructor( fun getURL(function: String): URL { val emulatorSettings = emulatorSettings if (emulatorSettings != null) { - urlFormat = ("http://" - + emulatorSettings.host - + ":" - + emulatorSettings.port - + "/%2\$s/%1\$s/%3\$s") + urlFormat = + ("http://" + emulatorSettings.host + ":" + emulatorSettings.port + "/%2\$s/%1\$s/%3\$s") } var str = String.format(urlFormat, region, projectId, function) if (customDomain != null && emulatorSettings == null) { @@ -158,7 +157,6 @@ class FirebaseFunctions @AssistedInject internal constructor( /** * Modifies this FirebaseFunctions instance to communicate with the Cloud Functions emulator. * - * * Note: Call this method before using the instance to do any functions operations. * * @param host the emulator host (for example, 10.0.2.2) @@ -176,20 +174,18 @@ class FirebaseFunctions @AssistedInject internal constructor( * @return A Task that will be completed when the request is complete. */ fun call(name: String, data: Any?, options: HttpsCallOptions): Task { - return providerInstalled - .task - .continueWithTask( - executor) { task: Task? -> contextProvider.getContext(options.limitedUseAppCheckTokens) } - .continueWithTask( - executor - ) { task: Task -> - if (!task.isSuccessful) { - return@continueWithTask Tasks.forException(task.exception!!) - } - val context = task.result - val url = getURL(name) - call(url, data, context, options) - } + return providerInstalled.task + .continueWithTask(executor) { task: Task? -> + contextProvider.getContext(options.limitedUseAppCheckTokens) + } + .continueWithTask(executor) { task: Task -> + if (!task.isSuccessful) { + return@continueWithTask Tasks.forException(task.exception!!) + } + val context = task.result + val url = getURL(name) + call(url, data, context, options) + } } /** @@ -200,19 +196,17 @@ class FirebaseFunctions @AssistedInject internal constructor( * @return A Task that will be completed when the request is complete. */ fun call(url: URL, data: Any?, options: HttpsCallOptions): Task { - return providerInstalled - .task - .continueWithTask( - executor) { task: Task? -> contextProvider.getContext(options.limitedUseAppCheckTokens) } - .continueWithTask( - executor - ) { task: Task -> - if (!task.isSuccessful) { - return@continueWithTask Tasks.forException(task.exception!!) - } - val context = task.result - call(url, data, context, options) - } + return providerInstalled.task + .continueWithTask(executor) { task: Task? -> + contextProvider.getContext(options.limitedUseAppCheckTokens) + } + .continueWithTask(executor) { task: Task -> + if (!task.isSuccessful) { + return@continueWithTask Tasks.forException(task.exception!!) + } + val context = task.result + call(url, data, context, options) + } } /** @@ -224,10 +218,11 @@ class FirebaseFunctions @AssistedInject internal constructor( * @return A Task that will be completed when the request is complete. */ private fun call( - url: URL, - data: Any?, - context: HttpsCallableContext?, - options: HttpsCallOptions): Task { + url: URL, + data: Any?, + context: HttpsCallableContext?, + options: HttpsCallOptions + ): Task { Preconditions.checkNotNull(url, "url cannot be null") val body: MutableMap = HashMap() val encoded = serializer.encode(data) @@ -249,56 +244,78 @@ class FirebaseFunctions @AssistedInject internal constructor( val call = callClient.newCall(request.build()) val tcs = TaskCompletionSource() call.enqueue( - object : Callback { - override fun onFailure(ignored: Call, e: IOException) { - if (e is InterruptedIOException) { - val exception = FirebaseFunctionsException( - FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name, FirebaseFunctionsException.Code.DEADLINE_EXCEEDED, null, e) - tcs.setException(exception) - } else { - val exception = FirebaseFunctionsException(FirebaseFunctionsException.Code.INTERNAL.name, FirebaseFunctionsException.Code.INTERNAL, null, e) - tcs.setException(exception) - } - } + object : Callback { + override fun onFailure(ignored: Call, e: IOException) { + if (e is InterruptedIOException) { + val exception = + FirebaseFunctionsException( + FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name, + FirebaseFunctionsException.Code.DEADLINE_EXCEEDED, + null, + e + ) + tcs.setException(exception) + } else { + val exception = + FirebaseFunctionsException( + FirebaseFunctionsException.Code.INTERNAL.name, + FirebaseFunctionsException.Code.INTERNAL, + null, + e + ) + tcs.setException(exception) + } + } - @Throws(IOException::class) - override fun onResponse(ignored: Call, response: Response) { - val code = fromHttpStatus(response.code()) - val body = response.body()!!.string() - val exception = fromResponse(code, body, serializer) - if (exception != null) { - tcs.setException(exception) - return - } - val bodyJSON: JSONObject - bodyJSON = try { - JSONObject(body) - } catch (je: JSONException) { - val e: Exception = FirebaseFunctionsException( - "Response is not valid JSON object.", FirebaseFunctionsException.Code.INTERNAL, null, je) - tcs.setException(e) - return - } - var dataJSON = bodyJSON.opt("data") - // TODO: Allow "result" instead of "data" for now, for backwards compatibility. - if (dataJSON == null) { - dataJSON = bodyJSON.opt("result") - } - if (dataJSON == null) { - val e: Exception = FirebaseFunctionsException( - "Response is missing data field.", FirebaseFunctionsException.Code.INTERNAL, null) - tcs.setException(e) - return - } - val result = HttpsCallableResult(serializer.decode(dataJSON)) - tcs.setResult(result) - } - }) + @Throws(IOException::class) + override fun onResponse(ignored: Call, response: Response) { + val code = fromHttpStatus(response.code()) + val body = response.body()!!.string() + val exception = fromResponse(code, body, serializer) + if (exception != null) { + tcs.setException(exception) + return + } + val bodyJSON: JSONObject + bodyJSON = + try { + JSONObject(body) + } catch (je: JSONException) { + val e: Exception = + FirebaseFunctionsException( + "Response is not valid JSON object.", + FirebaseFunctionsException.Code.INTERNAL, + null, + je + ) + tcs.setException(e) + return + } + var dataJSON = bodyJSON.opt("data") + // TODO: Allow "result" instead of "data" for now, for backwards compatibility. + if (dataJSON == null) { + dataJSON = bodyJSON.opt("result") + } + if (dataJSON == null) { + val e: Exception = + FirebaseFunctionsException( + "Response is missing data field.", + FirebaseFunctionsException.Code.INTERNAL, + null + ) + tcs.setException(e) + return + } + val result = HttpsCallableResult(serializer.decode(dataJSON)) + tcs.setResult(result) + } + } + ) return tcs.task } companion object { - /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ + /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ private val providerInstalled = TaskCompletionSource() /** @@ -326,17 +343,18 @@ class FirebaseFunctions @AssistedInject internal constructor( // installIfNeededAsync checks to make sure it is on the main thread, and throws otherwise. uiExecutor.execute { ProviderInstaller.installIfNeededAsync( - context, - object : ProviderInstaller.ProviderInstallListener { - override fun onProviderInstalled() { - providerInstalled.setResult(null) - } + context, + object : ProviderInstaller.ProviderInstallListener { + override fun onProviderInstalled() { + providerInstalled.setResult(null) + } - override fun onProviderInstallFailed(i: Int, intent: Intent?) { - Log.d("FirebaseFunctions", "Failed to update ssl context") - providerInstalled.setResult(null) - } - }) + override fun onProviderInstallFailed(i: Int, intent: Intent?) { + Log.d("FirebaseFunctions", "Failed to update ssl context") + providerInstalled.setResult(null) + } + } + ) } } @@ -344,16 +362,16 @@ class FirebaseFunctions @AssistedInject internal constructor( * Creates a Cloud Functions client with the given app and region or custom domain. * * @param app The app for the Firebase project. - * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as `"us-central1"` or `"https://mydomain.com"`. + * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as + * `"us-central1"` or `"https://mydomain.com"`. */ @JvmStatic - fun getInstance( - app: FirebaseApp, regionOrCustomDomain: String): FirebaseFunctions { + fun getInstance(app: FirebaseApp, regionOrCustomDomain: String): FirebaseFunctions { Preconditions.checkNotNull(app, "You must call FirebaseApp.initializeApp first.") Preconditions.checkNotNull(regionOrCustomDomain) val component = app.get(FunctionsMultiResourceComponent::class.java) Preconditions.checkNotNull(component, "Functions component does not exist.") - return component[regionOrCustomDomain] + return component[regionOrCustomDomain]!! } /** @@ -369,14 +387,18 @@ class FirebaseFunctions @AssistedInject internal constructor( /** * Creates a Cloud Functions client with the default app and given region or custom domain. * - * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as `"us-central1"` or `"https://mydomain.com"`. + * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as + * `"us-central1"` or `"https://mydomain.com"`. */ @JvmStatic fun getInstance(regionOrCustomDomain: String): FirebaseFunctions { return getInstance(FirebaseApp.getInstance(), regionOrCustomDomain) } + /** Creates a Cloud Functions client with the default app. */ @JvmStatic - fun getInstance() = getInstance(FirebaseApp.getInstance(), "us-central1") + fun getInstance(): FirebaseFunctions { + return getInstance(FirebaseApp.getInstance(), "us-central") + } } } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.kt index f57e12128305..fce7514eca4a 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctionsException.kt @@ -20,7 +20,7 @@ import org.json.JSONObject // TODO: This is a copy of FirebaseFirestoreException. // We should investigate whether we can at least share the Code enum. -/** The class for all Exceptions thrown by FirebaseFunctions. */ +/** The class for all Exceptions thrown by FirebaseFunctions. */ class FirebaseFunctionsException : FirebaseException { /** * The set of error status codes that can be returned from a Callable HTTPS tigger. These are the @@ -34,10 +34,10 @@ class FirebaseFunctionsException : FirebaseException { */ OK(0), - /** The operation was cancelled (typically by the caller). */ + /** The operation was cancelled (typically by the caller). */ CANCELLED(1), - /** Unknown error or an error from a different error domain. */ + /** Unknown error or an error from a different error domain. */ UNKNOWN(2), /** @@ -55,13 +55,13 @@ class FirebaseFunctionsException : FirebaseException { */ DEADLINE_EXCEEDED(4), - /** Some requested document was not found. */ + /** Some requested document was not found. */ NOT_FOUND(5), - /** Some document that we attempted to create already exists. */ + /** Some document that we attempted to create already exists. */ ALREADY_EXISTS(6), - /** The caller does not have permission to execute the specified operation. */ + /** The caller does not have permission to execute the specified operation. */ PERMISSION_DENIED(7), /** @@ -81,10 +81,10 @@ class FirebaseFunctionsException : FirebaseException { */ ABORTED(10), - /** Operation was attempted past the valid range. */ + /** Operation was attempted past the valid range. */ OUT_OF_RANGE(11), - /** Operation is not implemented or not supported/enabled. */ + /** Operation is not implemented or not supported/enabled. */ UNIMPLEMENTED(12), /** @@ -99,10 +99,10 @@ class FirebaseFunctionsException : FirebaseException { */ UNAVAILABLE(14), - /** Unrecoverable data loss or corruption. */ + /** Unrecoverable data loss or corruption. */ DATA_LOSS(15), - /** The request does not have valid authentication credentials for the operation. */ + /** The request does not have valid authentication credentials for the operation. */ UNAUTHENTICATED(16); companion object { @@ -112,7 +112,9 @@ class FirebaseFunctionsException : FirebaseException { val codes = SparseArray() for (c in values()) { val existingValue = codes[c.ordinal] - check(existingValue == null) { "Code value duplication between " + existingValue + "&" + c.name } + check(existingValue == null) { + "Code value duplication between " + existingValue + "&" + c.name + } codes.put(c.ordinal, c) } return codes @@ -124,8 +126,8 @@ class FirebaseFunctionsException : FirebaseException { } /** - * Takes an HTTP status code and returns the corresponding FUNErrorCode error code. This is the - * standard HTTP status code -> error mapping defined in: + * Takes an HTTP status code and returns the corresponding FUNErrorCode error code. This is + * the standard HTTP status code -> error mapping defined in: * https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto * * @param status An HTTP status code. @@ -166,14 +168,17 @@ class FirebaseFunctionsException : FirebaseException { */ val details: Any? - internal constructor( - message: String, code: Code, details: Any?) : super(message) { + internal constructor(message: String, code: Code, details: Any?) : super(message) { this.code = code this.details = details } internal constructor( - message: String, code: Code, details: Any?, cause: Throwable?) : super(message, cause!!) { + message: String, + code: Code, + details: Any?, + cause: Throwable? + ) : super(message, cause!!) { this.code = code this.details = details } @@ -189,7 +194,10 @@ class FirebaseFunctionsException : FirebaseException { */ @JvmStatic fun fromResponse( - code: Code, body: String?, serializer: Serializer): FirebaseFunctionsException? { + code: Code, + body: String?, + serializer: Serializer + ): FirebaseFunctionsException? { // Start with reasonable defaults from the status code. var code = code var description = code.name @@ -226,4 +234,4 @@ class FirebaseFunctionsException : FirebaseException { } else FirebaseFunctionsException(description, code, details) } } -} \ No newline at end of file +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.java b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.java deleted file mode 100644 index f7fd4fc06357..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.java +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2018 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.functions; - -import android.content.Context; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.annotations.concurrent.Lightweight; -import com.google.firebase.annotations.concurrent.UiThread; -import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider; -import com.google.firebase.auth.internal.InternalAuthProvider; -import com.google.firebase.iid.internal.FirebaseInstanceIdInternal; -import com.google.firebase.inject.Deferred; -import com.google.firebase.inject.Provider; -import dagger.Binds; -import dagger.BindsInstance; -import dagger.Component; -import dagger.Module; -import dagger.Provides; -import java.util.concurrent.Executor; -import javax.inject.Named; -import javax.inject.Singleton; - -/** @hide */ -@Component(modules = FunctionsComponent.MainModule.class) -@Singleton -interface FunctionsComponent { - FunctionsMultiResourceComponent getMultiResourceComponent(); - - @Component.Builder - interface Builder { - @BindsInstance - Builder setApplicationContext(Context applicationContext); - - @BindsInstance - Builder setFirebaseOptions(FirebaseOptions options); - - @BindsInstance - Builder setLiteExecutor(@Lightweight Executor executor); - - @BindsInstance - Builder setUiExecutor(@UiThread Executor executor); - - @BindsInstance - Builder setAuth(Provider auth); - - @BindsInstance - Builder setIid(Provider iid); - - @BindsInstance - Builder setAppCheck(Deferred appCheck); - - FunctionsComponent build(); - } - - @Module - interface MainModule { - @Provides - @Named("projectId") - static String bindProjectId(FirebaseOptions options) { - return options.getProjectId(); - } - - @Binds - ContextProvider contextProvider(FirebaseContextProvider provider); - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.kt new file mode 100644 index 000000000000..70ca15c48d84 --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsComponent.kt @@ -0,0 +1,70 @@ +// Copyright 2018 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.functions + +import android.content.Context +import com.google.firebase.FirebaseOptions +import com.google.firebase.annotations.concurrent.Lightweight +import com.google.firebase.annotations.concurrent.UiThread +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import com.google.firebase.iid.internal.FirebaseInstanceIdInternal +import com.google.firebase.inject.Deferred +import com.google.firebase.inject.Provider +import dagger.Binds +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import java.util.concurrent.Executor +import javax.inject.Named +import javax.inject.Singleton + +/** @hide */ +@Component(modules = [FunctionsComponent.MainModule::class]) +@Singleton +internal interface FunctionsComponent { + val multiResourceComponent: FunctionsMultiResourceComponent? + + @Component.Builder + interface Builder { + @BindsInstance fun setApplicationContext(applicationContext: Context): Builder + + @BindsInstance fun setFirebaseOptions(options: FirebaseOptions): Builder + + @BindsInstance fun setLiteExecutor(@Lightweight executor: Executor): Builder + + @BindsInstance fun setUiExecutor(@UiThread executor: Executor): Builder + + @BindsInstance fun setAuth(auth: Provider): Builder + + @BindsInstance fun setIid(iid: Provider): Builder + + @BindsInstance fun setAppCheck(appCheck: Deferred): Builder + fun build(): FunctionsComponent + } + + @Module + interface MainModule { + @Binds fun contextProvider(provider: FirebaseContextProvider): ContextProvider + + companion object { + @Provides + @Named("projectId") + fun bindProjectId(options: FirebaseOptions): String? { + return options.projectId + } + } + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.java b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.java deleted file mode 100644 index 4a36b27297f2..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2018 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.functions; - -import androidx.annotation.GuardedBy; -import dagger.assisted.Assisted; -import dagger.assisted.AssistedFactory; -import java.util.HashMap; -import java.util.Map; -import javax.inject.Inject; -import javax.inject.Singleton; - -/** Multi-resource container for Functions. */ -@Singleton -class FunctionsMultiResourceComponent { - /** - * A static map from instance key to FirebaseFunctions instances. Instance keys region names. - * - *

To ensure thread safety it should only be accessed when it is being synchronized on. - */ - @GuardedBy("this") - private final Map instances = new HashMap<>(); - - private final FirebaseFunctionsFactory functionsFactory; - - @Inject - FunctionsMultiResourceComponent(FirebaseFunctionsFactory functionsFactory) { - this.functionsFactory = functionsFactory; - } - - synchronized FirebaseFunctions get(String regionOrCustomDomain) { - FirebaseFunctions functions = instances.get(regionOrCustomDomain); - if (functions == null) { - functions = functionsFactory.create(regionOrCustomDomain); - instances.put(regionOrCustomDomain, functions); - } - return functions; - } - - @AssistedFactory - interface FirebaseFunctionsFactory { - FirebaseFunctions create(@Assisted String regionOrCustomDomain); - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.kt new file mode 100644 index 000000000000..4483049498a3 --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsMultiResourceComponent.kt @@ -0,0 +1,48 @@ +// Copyright 2018 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.functions + +import androidx.annotation.GuardedBy +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import javax.inject.Inject +import javax.inject.Singleton + +/** Multi-resource container for Functions. */ +@Singleton +internal class FunctionsMultiResourceComponent +@Inject +constructor(private val functionsFactory: FirebaseFunctionsFactory) { + /** + * A static map from instance key to FirebaseFunctions instances. Instance keys region names. + * + * To ensure thread safety it should only be accessed when it is being synchronized on. + */ + @GuardedBy("this") private val instances: MutableMap = HashMap() + + @Synchronized + operator fun get(regionOrCustomDomain: String): FirebaseFunctions? { + var functions = instances[regionOrCustomDomain] + if (functions == null) { + functions = functionsFactory.create(regionOrCustomDomain) + instances[regionOrCustomDomain] = functions + } + return functions + } + + @AssistedFactory + internal interface FirebaseFunctionsFactory { + fun create(@Assisted regionOrCustomDomain: String?): FirebaseFunctions? + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.java b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.java deleted file mode 100644 index d65807502cf7..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.java +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2018 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.functions; - -import android.content.Context; -import androidx.annotation.Keep; -import com.google.firebase.FirebaseOptions; -import com.google.firebase.annotations.concurrent.Lightweight; -import com.google.firebase.annotations.concurrent.UiThread; -import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider; -import com.google.firebase.auth.internal.InternalAuthProvider; -import com.google.firebase.components.Component; -import com.google.firebase.components.ComponentRegistrar; -import com.google.firebase.components.Dependency; -import com.google.firebase.components.Qualified; -import com.google.firebase.iid.internal.FirebaseInstanceIdInternal; -import com.google.firebase.platforminfo.LibraryVersionComponent; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Executor; - -/** - * Registers {@link FunctionsMultiResourceComponent}. - * - * @hide - */ -@Keep -public class FunctionsRegistrar implements ComponentRegistrar { - private static final String LIBRARY_NAME = "fire-fn"; - - @Override - public List> getComponents() { - Qualified liteExecutor = Qualified.qualified(Lightweight.class, Executor.class); - Qualified uiExecutor = Qualified.qualified(UiThread.class, Executor.class); - return Arrays.asList( - Component.builder(FunctionsMultiResourceComponent.class) - .name(LIBRARY_NAME) - .add(Dependency.required(Context.class)) - .add(Dependency.required(FirebaseOptions.class)) - .add(Dependency.optionalProvider(InternalAuthProvider.class)) - .add(Dependency.requiredProvider(FirebaseInstanceIdInternal.class)) - .add(Dependency.deferred(InteropAppCheckTokenProvider.class)) - .add(Dependency.required(liteExecutor)) - .add(Dependency.required(uiExecutor)) - .factory( - c -> - DaggerFunctionsComponent.builder() - .setApplicationContext(c.get(Context.class)) - .setFirebaseOptions(c.get(FirebaseOptions.class)) - .setLiteExecutor(c.get(liteExecutor)) - .setUiExecutor(c.get(uiExecutor)) - .setAuth(c.getProvider(InternalAuthProvider.class)) - .setIid(c.getProvider(FirebaseInstanceIdInternal.class)) - .setAppCheck(c.getDeferred(InteropAppCheckTokenProvider.class)) - .build() - .getMultiResourceComponent()) - .build(), - LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)); - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.kt new file mode 100644 index 000000000000..3e9d33869a50 --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FunctionsRegistrar.kt @@ -0,0 +1,73 @@ +// Copyright 2018 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.functions + +import android.content.Context +import androidx.annotation.Keep +import com.google.firebase.FirebaseOptions +import com.google.firebase.annotations.concurrent.Lightweight +import com.google.firebase.annotations.concurrent.UiThread +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import com.google.firebase.components.Component +import com.google.firebase.components.ComponentContainer +import com.google.firebase.components.ComponentRegistrar +import com.google.firebase.components.Dependency +import com.google.firebase.components.Qualified +import com.google.firebase.iid.internal.FirebaseInstanceIdInternal +import com.google.firebase.platforminfo.LibraryVersionComponent +import java.util.Arrays +import java.util.concurrent.Executor + +/** + * Registers [FunctionsMultiResourceComponent]. + * + * @hide + */ +@Keep +class FunctionsRegistrar : ComponentRegistrar { + override fun getComponents(): List> { + val liteExecutor = Qualified.qualified(Lightweight::class.java, Executor::class.java) + val uiExecutor = Qualified.qualified(UiThread::class.java, Executor::class.java) + return Arrays.asList( + Component.builder(FunctionsMultiResourceComponent::class.java) + .name(LIBRARY_NAME) + .add(Dependency.required(Context::class.java)) + .add(Dependency.required(FirebaseOptions::class.java)) + .add(Dependency.optionalProvider(InternalAuthProvider::class.java)) + .add(Dependency.requiredProvider(FirebaseInstanceIdInternal::class.java)) + .add(Dependency.deferred(InteropAppCheckTokenProvider::class.java)) + .add(Dependency.required(liteExecutor)) + .add(Dependency.required(uiExecutor)) + .factory { c: ComponentContainer -> + DaggerFunctionsComponent.builder() + .setApplicationContext(c.get(Context::class.java)) + .setFirebaseOptions(c.get(FirebaseOptions::class.java)) + .setLiteExecutor(c.get(liteExecutor)) + .setUiExecutor(c.get(uiExecutor)) + .setAuth(c.getProvider(InternalAuthProvider::class.java)) + .setIid(c.getProvider(FirebaseInstanceIdInternal::class.java)) + .setAppCheck(c.getDeferred(InteropAppCheckTokenProvider::class.java)) + .build() + ?.multiResourceComponent + } + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME) + ) + } + + companion object { + private const val LIBRARY_NAME = "fire-fn" + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt index 653ded9b0981..b0ada9f2c095 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt @@ -13,17 +13,17 @@ // limitations under the License. package com.google.firebase.functions -import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit +import okhttp3.OkHttpClient -/** An internal class for keeping track of options applied to an HttpsCallableReference. */ +/** An internal class for keeping track of options applied to an HttpsCallableReference. */ class HttpsCallOptions { // The timeout to use for calls from references created by this Functions. private var timeout = DEFAULT_TIMEOUT private var timeoutUnits = DEFAULT_TIMEOUT_UNITS val limitedUseAppCheckTokens: Boolean - /** Creates an (internal) HttpsCallOptions from the (external) [HttpsCallableOptions]. */ + /** Creates an (internal) HttpsCallOptions from the (external) [HttpsCallableOptions]. */ constructor(publicCallableOptions: HttpsCallableOptions) { limitedUseAppCheckTokens = publicCallableOptions.limitedUseAppCheckTokens } @@ -52,13 +52,13 @@ class HttpsCallOptions { return timeoutUnits.toMillis(timeout) } - /** Creates a new OkHttpClient with these options applied to it. */ + /** Creates a new OkHttpClient with these options applied to it. */ fun apply(client: OkHttpClient): OkHttpClient { return client - .newBuilder() - .callTimeout(timeout, timeoutUnits) - .readTimeout(timeout, timeoutUnits) - .build() + .newBuilder() + .callTimeout(timeout, timeoutUnits) + .readTimeout(timeout, timeoutUnits) + .build() } companion object { @@ -66,4 +66,4 @@ class HttpsCallOptions { private const val DEFAULT_TIMEOUT: Long = 70 private val DEFAULT_TIMEOUT_UNITS = TimeUnit.SECONDS } -} \ No newline at end of file +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.kt index 2c79a4a8b6ed..637fb0a836d2 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableContext.kt @@ -13,8 +13,9 @@ // limitations under the License. package com.google.firebase.functions -/** The metadata about the client that should automatically be included in function calls. */ +/** The metadata about the client that should automatically be included in function calls. */ internal class HttpsCallableContext( - val authToken: String?, - val instanceIdToken: String?, - val appCheckToken: String?) \ No newline at end of file + val authToken: String?, + val instanceIdToken: String?, + val appCheckToken: String? +) diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt index 21169fa38aa9..eb653a5042a4 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt @@ -16,21 +16,25 @@ package com.google.firebase.functions /** * Options for configuring the callable function. * - * * These properties are immutable once a callable function reference is instantiated. */ -class HttpsCallableOptions private constructor( - /** - * Returns the setting indicating if limited-use App Check tokens are enforced for this function. - */ - // If true, request a limited-use token from AppCheck. - val limitedUseAppCheckTokens: Boolean) { +class HttpsCallableOptions +private constructor( + /** + * Returns the setting indicating if limited-use App Check tokens are enforced for this function. + */ + // If true, request a limited-use token from AppCheck. + @JvmField val limitedUseAppCheckTokens: Boolean +) { - /** Builder class for [com.google.firebase.functions.HttpsCallableOptions] */ + /** Builder class for [com.google.firebase.functions.HttpsCallableOptions] */ class Builder { - /** Returns the setting indicating if limited-use App Check tokens are enforced. */ - var limitedUseAppCheckTokens = false - private set + @JvmField var limitedUseAppCheckTokens = false + + /** Returns the setting indicating if limited-use App Check tokens are enforced. */ + fun getLimitedUseAppCheckTokens(): Boolean { + return limitedUseAppCheckTokens + } /** * Sets whether or not to use limited-use App Check tokens when invoking the associated @@ -41,9 +45,9 @@ class HttpsCallableOptions private constructor( return this } - /** Builds a new [com.google.firebase.functions.HttpsCallableOptions]. */ + /** Builds a new [com.google.firebase.functions.HttpsCallableOptions]. */ fun build(): HttpsCallableOptions { return HttpsCallableOptions(limitedUseAppCheckTokens) } } -} \ No newline at end of file +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt index f7efb064b6ca..ca1b069519df 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt @@ -17,7 +17,7 @@ import com.google.android.gms.tasks.Task import java.net.URL import java.util.concurrent.TimeUnit -/** A reference to a particular Callable HTTPS trigger in Cloud Functions. */ +/** A reference to a particular Callable HTTPS trigger in Cloud Functions. */ class HttpsCallableReference { // The functions client to use for making calls. private val functionsClient: FirebaseFunctions @@ -33,15 +33,19 @@ class HttpsCallableReference { // Options for how to do the HTTPS call. private val options: HttpsCallOptions - /** Creates a new reference with the given options. */ - internal constructor(functionsClient: FirebaseFunctions, name: String?, options: HttpsCallOptions) { + /** Creates a new reference with the given options. */ + internal constructor( + functionsClient: FirebaseFunctions, + name: String?, + options: HttpsCallOptions + ) { this.functionsClient = functionsClient this.name = name url = null this.options = options } - /** Creates a new reference with the given options. */ + /** Creates a new reference with the given options. */ internal constructor(functionsClient: FirebaseFunctions, url: URL?, options: HttpsCallOptions) { this.functionsClient = functionsClient name = null @@ -52,38 +56,32 @@ class HttpsCallableReference { /** * Executes this Callable HTTPS trigger asynchronously. * - * * The data passed into the trigger can be any of the following types: * - * - * * Any primitive type, including null, int, long, float, and boolean. - * * [String] - * * [List&lt;?&gt;][java.util.List], where the contained objects are also one of these + * * Any primitive type, including null, int, long, float, and boolean. + * * [String] + * * [List&lt;?&gt;][java.util.List], where the contained objects are also one of these * types. - * * [Map&lt;String, ?&gt;>][java.util.Map], where the values are also one of these + * * [Map&lt;String, ?&gt;>][java.util.Map], where the values are also one of these * types. - * * [org.json.JSONArray] - * * [org.json.JSONObject] - * * [org.json.JSONObject.NULL] - * - * + * * [org.json.JSONArray] + * * [org.json.JSONObject] + * * [org.json.JSONObject.NULL] * * If the returned task fails, the Exception will be one of the following types: * - * - * * [java.io.IOException] - if the HTTPS request failed to connect. - * * [FirebaseFunctionsException] - if the request connected, but the function returned - * an error. - * - * + * * [java.io.IOException] + * - if the HTTPS request failed to connect. + * * [FirebaseFunctionsException] + * - if the request connected, but the function returned an error. * * The request to the Cloud Functions backend made by this method automatically includes a * Firebase Instance ID token to identify the app instance. If a user is logged in with Firebase * Auth, an auth token for the user will also be automatically included. * - * * Firebase Instance ID sends data to the Firebase backend periodically to collect information - * regarding the app instance. To stop this, see [ ][com.google.firebase.iid.FirebaseInstanceId.deleteInstanceId]. It will resume with a new + * regarding the app instance. To stop this, see [ ] + * [com.google.firebase.iid.FirebaseInstanceId.deleteInstanceId]. It will resume with a new * Instance ID the next time you call this method. * * @param data Parameters to pass to the trigger. @@ -107,14 +105,13 @@ class HttpsCallableReference { /** * Executes this HTTPS endpoint asynchronously without arguments. * - * * The request to the Cloud Functions backend made by this method automatically includes a * Firebase Instance ID token to identify the app instance. If a user is logged in with Firebase * Auth, an auth token for the user will also be automatically included. * - * * Firebase Instance ID sends data to the Firebase backend periodically to collect information - * regarding the app instance. To stop this, see [ ][com.google.firebase.iid.FirebaseInstanceId.deleteInstanceId]. It will resume with a new + * regarding the app instance. To stop this, see [ ] + * [com.google.firebase.iid.FirebaseInstanceId.deleteInstanceId]. It will resume with a new * Instance ID the next time you call this method. * * @return A Task that will be completed when the HTTPS request has completed. @@ -156,4 +153,4 @@ class HttpsCallableReference { other.setTimeout(timeout, units) return other } -} \ No newline at end of file +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt index b5892ba8f3f2..4b1db59986f5 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt @@ -13,18 +13,18 @@ // limitations under the License. package com.google.firebase.functions -/** The result of calling a HttpsCallableReference function. */ -class HttpsCallableResult internal constructor( // The actual result data, as generic types decoded from JSON. - private val data: Any?) { +/** The result of calling a HttpsCallableReference function. */ +class HttpsCallableResult +internal constructor( // The actual result data, as generic types decoded from JSON. +private val data: Any?) { /** * Returns the data that was returned from the Callable HTTPS trigger. * - * - * The data is in the form of native Java objects. For example, if your trigger returned an - * array, this object would be a List. If your trigger returned a JavaScript object with - * keys and values, this object would be a Map, Object>. - */ + * The data is in the form of native Java objects. For example, if your trigger returned an array, + * this object would be a List. If your trigger returned a JavaScript object with keys and + * values, this object would be a Map, Object>. + */ fun getData(): Any? { return data } -} \ No newline at end of file +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt b/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt index c1832b3559e2..590e6bfe4b2f 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt @@ -14,15 +14,15 @@ package com.google.firebase.functions import androidx.annotation.VisibleForTesting -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Locale import java.util.TimeZone +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject -/** Converts raw Java types into JSON objects. */ +/** Converts raw Java types into JSON objects. */ class Serializer { private val dateFormat: DateFormat @@ -90,8 +90,7 @@ class Serializer { val m = obj val keys = m.keys() while (keys.hasNext()) { - val k = keys.next() - ?: throw IllegalArgumentException("Object keys cannot be null.") + val k = keys.next() ?: throw IllegalArgumentException("Object keys cannot be null.") val value = encode(m.opt(k)) try { result.put(k, value) @@ -172,10 +171,9 @@ class Serializer { } companion object { - @VisibleForTesting - const val LONG_TYPE = "type.googleapis.com/google.protobuf.Int64Value" + @VisibleForTesting const val LONG_TYPE = "type.googleapis.com/google.protobuf.Int64Value" @VisibleForTesting const val UNSIGNED_LONG_TYPE = "type.googleapis.com/google.protobuf.UInt64Value" } -} \ No newline at end of file +} From 5cfb43f0d6c00da186319e12e641b62d1e1de296 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Mon, 23 Oct 2023 16:04:43 -0500 Subject: [PATCH 03/11] Add removed API method --- .../com/google/firebase/functions/HttpsCallableOptions.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt index eb653a5042a4..e0952bffa2a2 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt @@ -27,6 +27,10 @@ private constructor( @JvmField val limitedUseAppCheckTokens: Boolean ) { + fun getLimitedUseAppCheckTokens(): Boolean { + return limitedUseAppCheckTokens + } + /** Builder class for [com.google.firebase.functions.HttpsCallableOptions] */ class Builder { @JvmField var limitedUseAppCheckTokens = false From 0b2dc759a50fa8403f430f9d710e794c3101d032 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Tue, 9 Jan 2024 15:51:59 -0600 Subject: [PATCH 04/11] Resolve comment issues --- .../java/com/google/firebase/functions/HttpsCallableResult.kt | 2 +- .../src/main/java/com/google/firebase/functions/Serializer.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt index 4b1db59986f5..69b8aaf5d477 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableResult.kt @@ -22,7 +22,7 @@ private val data: Any?) { * * The data is in the form of native Java objects. For example, if your trigger returned an array, * this object would be a List. If your trigger returned a JavaScript object with keys and - * values, this object would be a Map, Object>. + * values, this object would be a Map. */ fun getData(): Any? { return data diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt b/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt index 590e6bfe4b2f..6157cafd22a6 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/Serializer.kt @@ -113,8 +113,8 @@ class Serializer { throw IllegalArgumentException("Object cannot be encoded in JSON: $obj") } - // TODO: Maybe this should throw a FirebaseFunctionsException instead? fun decode(obj: Any): Any? { + // TODO: Maybe this should throw a FirebaseFunctionsException instead? if (obj is Number) { return obj } From cce9791b097199581804b56e852291a509248533 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Thu, 11 Jan 2024 12:06:59 -0600 Subject: [PATCH 05/11] Resolve all test issues --- .../com/google/firebase/functions/FirebaseFunctions.kt | 4 ++-- .../com/google/firebase/functions/HttpsCallOptions.kt | 5 +++++ .../google/firebase/functions/HttpsCallableReference.kt | 8 +++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt index 94162a9b855d..3a53e73464ed 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt @@ -173,7 +173,7 @@ internal constructor( * @param data Parameters to pass to the function. Can be anything encodable as JSON. * @return A Task that will be completed when the request is complete. */ - fun call(name: String, data: Any?, options: HttpsCallOptions): Task { + fun call(name: String, data: Any?, options: HttpsCallOptions): Task { return providerInstalled.task .continueWithTask(executor) { task: Task? -> contextProvider.getContext(options.limitedUseAppCheckTokens) @@ -195,7 +195,7 @@ internal constructor( * @param data Parameters to pass to the function. Can be anything encodable as JSON. * @return A Task that will be completed when the request is complete. */ - fun call(url: URL, data: Any?, options: HttpsCallOptions): Task { + fun call(url: URL, data: Any?, options: HttpsCallOptions): Task { return providerInstalled.task .continueWithTask(executor) { task: Task? -> contextProvider.getContext(options.limitedUseAppCheckTokens) diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt index b0ada9f2c095..5b6c806e3963 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt @@ -21,6 +21,7 @@ class HttpsCallOptions { // The timeout to use for calls from references created by this Functions. private var timeout = DEFAULT_TIMEOUT private var timeoutUnits = DEFAULT_TIMEOUT_UNITS + @JvmField val limitedUseAppCheckTokens: Boolean /** Creates an (internal) HttpsCallOptions from the (external) [HttpsCallableOptions]. */ @@ -32,6 +33,10 @@ class HttpsCallOptions { limitedUseAppCheckTokens = false } + fun getLimitedUseAppCheckTokens(): Boolean { + return limitedUseAppCheckTokens + } + /** * Changes the timeout for calls from this instance of Functions. The default is 60 seconds. * diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt index ca1b069519df..28010b0760cf 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt @@ -13,6 +13,7 @@ // limitations under the License. package com.google.firebase.functions +import androidx.annotation.VisibleForTesting import com.google.android.gms.tasks.Task import java.net.URL import java.util.concurrent.TimeUnit @@ -31,7 +32,8 @@ class HttpsCallableReference { private val url: URL? // Options for how to do the HTTPS call. - private val options: HttpsCallOptions + @VisibleForTesting + val options: HttpsCallOptions /** Creates a new reference with the given options. */ internal constructor( @@ -94,7 +96,7 @@ class HttpsCallableReference { * * @see FirebaseFunctionsException */ - fun call(data: Any?): Task { + fun call(data: Any?): Task { return if (name != null) { functionsClient.call(name, data, options) } else { @@ -116,7 +118,7 @@ class HttpsCallableReference { * * @return A Task that will be completed when the HTTPS request has completed. */ - fun call(): Task { + fun call(): Task { return if (name != null) { functionsClient.call(name, null, options) } else { From c658223c8f939104cccb17945084fe940773dd96 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Thu, 11 Jan 2024 12:21:41 -0600 Subject: [PATCH 06/11] Formatting --- .../java/com/google/firebase/functions/HttpsCallOptions.kt | 3 +-- .../com/google/firebase/functions/HttpsCallableReference.kt | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt index 5b6c806e3963..0a180b289a19 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt @@ -21,8 +21,7 @@ class HttpsCallOptions { // The timeout to use for calls from references created by this Functions. private var timeout = DEFAULT_TIMEOUT private var timeoutUnits = DEFAULT_TIMEOUT_UNITS - @JvmField - val limitedUseAppCheckTokens: Boolean + @JvmField val limitedUseAppCheckTokens: Boolean /** Creates an (internal) HttpsCallOptions from the (external) [HttpsCallableOptions]. */ constructor(publicCallableOptions: HttpsCallableOptions) { diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt index 28010b0760cf..9d80bf69b326 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt @@ -32,8 +32,7 @@ class HttpsCallableReference { private val url: URL? // Options for how to do the HTTPS call. - @VisibleForTesting - val options: HttpsCallOptions + @VisibleForTesting val options: HttpsCallOptions /** Creates a new reference with the given options. */ internal constructor( From fac44a6aa9c97d10877bbc9633ccd492ccf487e2 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Fri, 12 Jan 2024 16:29:02 -0600 Subject: [PATCH 07/11] Update api.txt --- firebase-functions/api.txt | 98 ++++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 19 deletions(-) diff --git a/firebase-functions/api.txt b/firebase-functions/api.txt index 2a59cdc659c4..689d792a49ab 100644 --- a/firebase-functions/api.txt +++ b/firebase-functions/api.txt @@ -1,25 +1,44 @@ // Signature format: 2.0 package com.google.firebase.functions { - public class FirebaseFunctions { - method @NonNull public com.google.firebase.functions.HttpsCallableReference getHttpsCallable(@NonNull String); - method @NonNull public com.google.firebase.functions.HttpsCallableReference getHttpsCallable(@NonNull String, @NonNull com.google.firebase.functions.HttpsCallableOptions); - method @NonNull public com.google.firebase.functions.HttpsCallableReference getHttpsCallableFromUrl(@NonNull java.net.URL); - method @NonNull public com.google.firebase.functions.HttpsCallableReference getHttpsCallableFromUrl(@NonNull java.net.URL, @NonNull com.google.firebase.functions.HttpsCallableOptions); - method @NonNull public static com.google.firebase.functions.FirebaseFunctions getInstance(@NonNull com.google.firebase.FirebaseApp, @NonNull String); - method @NonNull public static com.google.firebase.functions.FirebaseFunctions getInstance(@NonNull com.google.firebase.FirebaseApp); - method @NonNull public static com.google.firebase.functions.FirebaseFunctions getInstance(@NonNull String); + public final class FirebaseFunctions { + method @NonNull public com.google.android.gms.tasks.Task call(@NonNull String name, @Nullable Object data, @NonNull com.google.firebase.functions.HttpsCallOptions options); + method @NonNull public com.google.android.gms.tasks.Task call(@NonNull java.net.URL url, @Nullable Object data, @NonNull com.google.firebase.functions.HttpsCallOptions options); + method @NonNull public com.google.firebase.functions.HttpsCallableReference getHttpsCallable(@NonNull String name); + method @NonNull public com.google.firebase.functions.HttpsCallableReference getHttpsCallable(@NonNull String name, @NonNull com.google.firebase.functions.HttpsCallableOptions options); + method @NonNull public com.google.firebase.functions.HttpsCallableReference getHttpsCallableFromUrl(@NonNull java.net.URL url); + method @NonNull public com.google.firebase.functions.HttpsCallableReference getHttpsCallableFromUrl(@NonNull java.net.URL url, @NonNull com.google.firebase.functions.HttpsCallableOptions options); + method @NonNull public static com.google.firebase.functions.FirebaseFunctions getInstance(@NonNull com.google.firebase.FirebaseApp app, @NonNull String regionOrCustomDomain); + method @NonNull public static com.google.firebase.functions.FirebaseFunctions getInstance(@NonNull com.google.firebase.FirebaseApp app); + method @NonNull public static com.google.firebase.functions.FirebaseFunctions getInstance(@NonNull String regionOrCustomDomain); method @NonNull public static com.google.firebase.functions.FirebaseFunctions getInstance(); - method public void useEmulator(@NonNull String, int); - method @Deprecated public void useFunctionsEmulator(@NonNull String); + method @NonNull @VisibleForTesting public java.net.URL getURL(@NonNull String function); + method public void useEmulator(@NonNull String host, int port); + method @Deprecated public void useFunctionsEmulator(@NonNull String origin); + field @NonNull public static final com.google.firebase.functions.FirebaseFunctions.Companion Companion; } - public class FirebaseFunctionsException extends com.google.firebase.FirebaseException { + public static final class FirebaseFunctions.Companion { + method @NonNull public com.google.firebase.functions.FirebaseFunctions getInstance(@NonNull com.google.firebase.FirebaseApp app, @NonNull String regionOrCustomDomain); + method @NonNull public com.google.firebase.functions.FirebaseFunctions getInstance(@NonNull com.google.firebase.FirebaseApp app); + method @NonNull public com.google.firebase.functions.FirebaseFunctions getInstance(@NonNull String regionOrCustomDomain); + method @NonNull public com.google.firebase.functions.FirebaseFunctions getInstance(); + } + + public final class FirebaseFunctionsException extends com.google.firebase.FirebaseException { + method @Nullable public static com.google.firebase.functions.FirebaseFunctionsException fromResponse(@NonNull com.google.firebase.functions.FirebaseFunctionsException.Code code, @Nullable String body, @NonNull com.google.firebase.functions.Serializer serializer); method @NonNull public com.google.firebase.functions.FirebaseFunctionsException.Code getCode(); method @Nullable public Object getDetails(); + property @NonNull public final com.google.firebase.functions.FirebaseFunctionsException.Code code; + property @Nullable public final Object details; + field @NonNull public static final com.google.firebase.functions.FirebaseFunctionsException.Companion Companion; } public enum FirebaseFunctionsException.Code { + method @NonNull public static final com.google.firebase.functions.FirebaseFunctionsException.Code fromHttpStatus(int status); + method @NonNull public static final com.google.firebase.functions.FirebaseFunctionsException.Code fromValue(int value); + method @NonNull public static com.google.firebase.functions.FirebaseFunctionsException.Code valueOf(@NonNull String name) throws java.lang.IllegalArgumentException; + method @NonNull public static com.google.firebase.functions.FirebaseFunctionsException.Code[] values(); enum_constant public static final com.google.firebase.functions.FirebaseFunctionsException.Code ABORTED; enum_constant public static final com.google.firebase.functions.FirebaseFunctionsException.Code ALREADY_EXISTS; enum_constant public static final com.google.firebase.functions.FirebaseFunctionsException.Code CANCELLED; @@ -37,6 +56,16 @@ package com.google.firebase.functions { enum_constant public static final com.google.firebase.functions.FirebaseFunctionsException.Code UNAVAILABLE; enum_constant public static final com.google.firebase.functions.FirebaseFunctionsException.Code UNIMPLEMENTED; enum_constant public static final com.google.firebase.functions.FirebaseFunctionsException.Code UNKNOWN; + field @NonNull public static final com.google.firebase.functions.FirebaseFunctionsException.Code.Companion Companion; + } + + public static final class FirebaseFunctionsException.Code.Companion { + method @NonNull public com.google.firebase.functions.FirebaseFunctionsException.Code fromHttpStatus(int status); + method @NonNull public com.google.firebase.functions.FirebaseFunctionsException.Code fromValue(int value); + } + + public static final class FirebaseFunctionsException.Companion { + method @Nullable public com.google.firebase.functions.FirebaseFunctionsException fromResponse(@NonNull com.google.firebase.functions.FirebaseFunctionsException.Code code, @Nullable String body, @NonNull com.google.firebase.functions.Serializer serializer); } public final class FunctionsKt { @@ -48,29 +77,60 @@ package com.google.firebase.functions { method @NonNull public static com.google.firebase.functions.HttpsCallableReference getHttpsCallableFromUrl(@NonNull com.google.firebase.functions.FirebaseFunctions, @NonNull java.net.URL url, @NonNull kotlin.jvm.functions.Function1 init); } - public class HttpsCallableOptions { + public final class HttpsCallOptions { + ctor public HttpsCallOptions(@NonNull com.google.firebase.functions.HttpsCallableOptions publicCallableOptions); + ctor public HttpsCallOptions(); + method @NonNull public okhttp3.OkHttpClient apply(@NonNull okhttp3.OkHttpClient client); + method public boolean getLimitedUseAppCheckTokens(); + method public long getTimeout(); + method public void setTimeout(long timeout, @NonNull java.util.concurrent.TimeUnit units); + field @NonNull public static final com.google.firebase.functions.HttpsCallOptions.Companion Companion; + field public final boolean limitedUseAppCheckTokens; + } + + public static final class HttpsCallOptions.Companion { + } + + public final class HttpsCallableOptions { method public boolean getLimitedUseAppCheckTokens(); + field public final boolean limitedUseAppCheckTokens; } - public static class HttpsCallableOptions.Builder { + public static final class HttpsCallableOptions.Builder { ctor public HttpsCallableOptions.Builder(); method @NonNull public com.google.firebase.functions.HttpsCallableOptions build(); method public boolean getLimitedUseAppCheckTokens(); - method @NonNull public com.google.firebase.functions.HttpsCallableOptions.Builder setLimitedUseAppCheckTokens(boolean); + method @NonNull public com.google.firebase.functions.HttpsCallableOptions.Builder setLimitedUseAppCheckTokens(boolean limitedUse); + field public boolean limitedUseAppCheckTokens; } - public class HttpsCallableReference { - method @NonNull public com.google.android.gms.tasks.Task call(@Nullable Object); + public final class HttpsCallableReference { + method @NonNull public com.google.android.gms.tasks.Task call(@Nullable Object data); method @NonNull public com.google.android.gms.tasks.Task call(); + method @NonNull public com.google.firebase.functions.HttpsCallOptions getOptions(); method public long getTimeout(); - method public void setTimeout(long, @NonNull java.util.concurrent.TimeUnit); - method @NonNull public com.google.firebase.functions.HttpsCallableReference withTimeout(long, @NonNull java.util.concurrent.TimeUnit); + method public void setTimeout(long timeout, @NonNull java.util.concurrent.TimeUnit units); + method @NonNull public com.google.firebase.functions.HttpsCallableReference withTimeout(long timeout, @NonNull java.util.concurrent.TimeUnit units); + property @NonNull public final com.google.firebase.functions.HttpsCallOptions options; + property public final long timeout; } - public class HttpsCallableResult { + public final class HttpsCallableResult { method @Nullable public Object getData(); } + public final class Serializer { + ctor public Serializer(); + method @Nullable public Object decode(@NonNull Object obj); + method @NonNull public Object encode(@Nullable Object obj); + field @NonNull public static final com.google.firebase.functions.Serializer.Companion Companion; + field @NonNull @VisibleForTesting public static final String LONG_TYPE = "type.googleapis.com/google.protobuf.Int64Value"; + field @NonNull @VisibleForTesting public static final String UNSIGNED_LONG_TYPE = "type.googleapis.com/google.protobuf.UInt64Value"; + } + + public static final class Serializer.Companion { + } + } package com.google.firebase.functions.ktx { From 8ccf8905c6683520146274f80f2e84a1130e82d8 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Mon, 29 Jul 2024 11:53:20 -0500 Subject: [PATCH 08/11] Temporary java revert for bugtesting --- .../firebase/functions/FirebaseFunctions.java | 424 ++++++++++++++++++ .../firebase/functions/FirebaseFunctions.kt | 404 ----------------- 2 files changed, 424 insertions(+), 404 deletions(-) create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java new file mode 100644 index 000000000000..585aba3cf79b --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java @@ -0,0 +1,424 @@ +// Copyright 2018 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.functions; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import com.google.android.gms.common.internal.Preconditions; +import com.google.android.gms.security.ProviderInstaller; +import com.google.android.gms.security.ProviderInstaller.ProviderInstallListener; +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.FirebaseApp; +import com.google.firebase.annotations.concurrent.Lightweight; +import com.google.firebase.annotations.concurrent.UiThread; +import com.google.firebase.emulators.EmulatedServiceSettings; +import com.google.firebase.functions.FirebaseFunctionsException.Code; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedInject; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; +import javax.inject.Named; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.json.JSONException; +import org.json.JSONObject; + +/** FirebaseFunctions lets you call Cloud Functions for Firebase. */ +public class FirebaseFunctions { + + /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ + private static final TaskCompletionSource providerInstalled = new TaskCompletionSource<>(); + + /** + * Whether the ProviderInstaller async task has been started. This is guarded by the + * providerInstalled lock. + */ + private static boolean providerInstallStarted = false; + + // The network client to use for HTTPS requests. + private final OkHttpClient client; + + // A serializer to encode/decode parameters and return values. + private final Serializer serializer; + + // A provider of client metadata to include with calls. + private final ContextProvider contextProvider; + + private final Executor executor; + + // The projectId to use for all functions references. + private final String projectId; + + // The region to use for all function references. + private final String region; + + // A custom domain for the http trigger, such as "https://mydomain.com" + @Nullable private final String customDomain; + + // The format to use for constructing urls from region, projectId, and name. + private String urlFormat = "https://%1$s-%2$s.cloudfunctions.net/%3$s"; + + // Emulator settings + @Nullable private EmulatedServiceSettings emulatorSettings; + + @AssistedInject + FirebaseFunctions( + Context context, + @Nullable @Named("projectId") String projectId, + @Assisted String regionOrCustomDomain, + ContextProvider contextProvider, + @Lightweight Executor executor, + @UiThread Executor uiExecutor) { + this.executor = executor; + this.client = new OkHttpClient(); + this.serializer = new Serializer(); + this.contextProvider = Preconditions.checkNotNull(contextProvider); + this.projectId = Preconditions.checkNotNull(projectId); + + boolean isRegion; + try { + new URL(regionOrCustomDomain); + isRegion = false; + } catch (MalformedURLException malformedURLException) { + isRegion = true; + } + + if (isRegion) { + this.region = regionOrCustomDomain; + this.customDomain = null; + } else { + this.region = "us-central1"; + this.customDomain = regionOrCustomDomain; + } + + maybeInstallProviders(context, uiExecutor); + } + + /** + * Runs ProviderInstaller.installIfNeededAsync once per application instance. + * + * @param context The application context. + * @param uiExecutor + */ + private static void maybeInstallProviders(Context context, Executor uiExecutor) { + // Make sure this only runs once. + synchronized (providerInstalled) { + if (providerInstallStarted) { + return; + } + providerInstallStarted = true; + } + + // Package installIfNeededAsync into a Runnable so it can be run on the main thread. + // installIfNeededAsync checks to make sure it is on the main thread, and throws otherwise. + uiExecutor.execute( + () -> + ProviderInstaller.installIfNeededAsync( + context, + new ProviderInstallListener() { + @Override + public void onProviderInstalled() { + providerInstalled.setResult(null); + } + + @Override + public void onProviderInstallFailed(int i, android.content.Intent intent) { + Log.d("FirebaseFunctions", "Failed to update ssl context"); + providerInstalled.setResult(null); + } + })); + } + + /** + * Creates a Cloud Functions client with the given app and region or custom domain. + * + * @param app The app for the Firebase project. + * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as {@code + * "us-central1"} or {@code "https://mydomain.com"}. + */ + @NonNull + public static FirebaseFunctions getInstance( + @NonNull FirebaseApp app, @NonNull String regionOrCustomDomain) { + Preconditions.checkNotNull(app, "You must call FirebaseApp.initializeApp first."); + Preconditions.checkNotNull(regionOrCustomDomain); + + FunctionsMultiResourceComponent component = app.get(FunctionsMultiResourceComponent.class); + Preconditions.checkNotNull(component, "Functions component does not exist."); + + return component.get(regionOrCustomDomain); + } + + /** + * Creates a Cloud Functions client with the given app. + * + * @param app The app for the Firebase project. + */ + @NonNull + public static FirebaseFunctions getInstance(@NonNull FirebaseApp app) { + return getInstance(app, "us-central1"); + } + + /** + * Creates a Cloud Functions client with the default app and given region or custom domain. + * + * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as {@code + * "us-central1"} or {@code "https://mydomain.com"}. + */ + @NonNull + public static FirebaseFunctions getInstance(@NonNull String regionOrCustomDomain) { + return getInstance(FirebaseApp.getInstance(), regionOrCustomDomain); + } + + /** Creates a Cloud Functions client with the default app. */ + @NonNull + public static FirebaseFunctions getInstance() { + return getInstance(FirebaseApp.getInstance(), "us-central1"); + } + + /** Returns a reference to the callable HTTPS trigger with the given name. */ + @NonNull + public HttpsCallableReference getHttpsCallable(@NonNull String name) { + return new HttpsCallableReference(this, name, new HttpsCallOptions()); + } + + /** Returns a reference to the callable HTTPS trigger with the provided URL. */ + @NonNull + public HttpsCallableReference getHttpsCallableFromUrl(@NonNull URL url) { + return new HttpsCallableReference(this, url, new HttpsCallOptions()); + } + + /** Returns a reference to the callable HTTPS trigger with the given name and call options. */ + @NonNull + public HttpsCallableReference getHttpsCallable( + @NonNull String name, @NonNull HttpsCallableOptions options) { + return new HttpsCallableReference(this, name, new HttpsCallOptions(options)); + } + + /** Returns a reference to the callable HTTPS trigger with the provided URL and call options. */ + @NonNull + public HttpsCallableReference getHttpsCallableFromUrl( + @NonNull URL url, @NonNull HttpsCallableOptions options) { + return new HttpsCallableReference(this, url, new HttpsCallOptions(options)); + } + + /** + * Returns the URL for a particular function. + * + * @param function The name of the function. + * @return The URL. + */ + @VisibleForTesting + URL getURL(String function) { + EmulatedServiceSettings emulatorSettings = this.emulatorSettings; + if (emulatorSettings != null) { + urlFormat = + "http://" + + emulatorSettings.getHost() + + ":" + + emulatorSettings.getPort() + + "/%2$s/%1$s/%3$s"; + } + + String str = String.format(urlFormat, region, projectId, function); + + if (customDomain != null && emulatorSettings == null) { + str = customDomain + "/" + function; + } + + try { + return new URL(str); + } catch (MalformedURLException mfe) { + throw new IllegalStateException(mfe); + } + } + + /** @deprecated Use {@link #useEmulator(String, int)} to connect to the emulator. */ + public void useFunctionsEmulator(@NonNull String origin) { + Preconditions.checkNotNull(origin, "origin cannot be null"); + urlFormat = origin + "/%2$s/%1$s/%3$s"; + } + + /** + * Modifies this FirebaseFunctions instance to communicate with the Cloud Functions emulator. + * + *

Note: Call this method before using the instance to do any functions operations. + * + * @param host the emulator host (for example, 10.0.2.2) + * @param port the emulator port (for example, 5001) + */ + public void useEmulator(@NonNull String host, int port) { + this.emulatorSettings = new EmulatedServiceSettings(host, port); + } + + /** + * Calls a Callable HTTPS trigger endpoint. + * + * @param name The name of the HTTPS trigger. + * @param data Parameters to pass to the function. Can be anything encodable as JSON. + * @return A Task that will be completed when the request is complete. + */ + Task call(String name, @Nullable Object data, HttpsCallOptions options) { + return providerInstalled + .getTask() + .continueWithTask( + executor, task -> contextProvider.getContext(options.getLimitedUseAppCheckTokens())) + .continueWithTask( + executor, + task -> { + if (!task.isSuccessful()) { + return Tasks.forException(task.getException()); + } + HttpsCallableContext context = task.getResult(); + URL url = getURL(name); + return call(url, data, context, options); + }); + } + + /** + * Calls a Callable HTTPS trigger endpoint. + * + * @param url The url of the HTTPS trigger + * @param data Parameters to pass to the function. Can be anything encodable as JSON. + * @return A Task that will be completed when the request is complete. + */ + Task call(URL url, @Nullable Object data, HttpsCallOptions options) { + return providerInstalled + .getTask() + .continueWithTask( + executor, task -> contextProvider.getContext(options.getLimitedUseAppCheckTokens())) + .continueWithTask( + executor, + task -> { + if (!task.isSuccessful()) { + return Tasks.forException(task.getException()); + } + HttpsCallableContext context = task.getResult(); + return call(url, data, context, options); + }); + } + + /** + * Calls a Callable HTTPS trigger endpoint. + * + * @param url The name of the HTTPS trigger. + * @param data Parameters to pass to the function. Can be anything encodable as JSON. + * @param context Metadata to supply with the function call. + * @return A Task that will be completed when the request is complete. + */ + private Task call( + @NonNull URL url, + @Nullable Object data, + HttpsCallableContext context, + HttpsCallOptions options) { + Preconditions.checkNotNull(url, "url cannot be null"); + + Map body = new HashMap<>(); + + Object encoded = serializer.encode(data); + body.put("data", encoded); + + JSONObject bodyJSON = new JSONObject(body); + MediaType contentType = MediaType.parse("application/json"); + RequestBody requestBody = RequestBody.create(contentType, bodyJSON.toString()); + + Request.Builder request = new Request.Builder().url(url).post(requestBody); + if (context.getAuthToken() != null) { + request = request.header("Authorization", "Bearer " + context.getAuthToken()); + } + if (context.getInstanceIdToken() != null) { + request = request.header("Firebase-Instance-ID-Token", context.getInstanceIdToken()); + } + if (context.getAppCheckToken() != null) { + request = request.header("X-Firebase-AppCheck", context.getAppCheckToken()); + } + + OkHttpClient callClient = options.apply(client); + Call call = callClient.newCall(request.build()); + + TaskCompletionSource tcs = new TaskCompletionSource<>(); + call.enqueue( + new Callback() { + @Override + public void onFailure(Call ignored, IOException e) { + if (e instanceof InterruptedIOException) { + FirebaseFunctionsException exception = + new FirebaseFunctionsException( + Code.DEADLINE_EXCEEDED.name(), Code.DEADLINE_EXCEEDED, null, e); + tcs.setException(exception); + } else { + FirebaseFunctionsException exception = + new FirebaseFunctionsException(Code.INTERNAL.name(), Code.INTERNAL, null, e); + tcs.setException(exception); + } + } + + @Override + public void onResponse(Call ignored, Response response) throws IOException { + Code code = Code.fromHttpStatus(response.code()); + String body = response.body().string(); + + FirebaseFunctionsException exception = + FirebaseFunctionsException.fromResponse(code, body, serializer); + if (exception != null) { + tcs.setException(exception); + return; + } + + JSONObject bodyJSON; + try { + bodyJSON = new JSONObject(body); + } catch (JSONException je) { + Exception e = + new FirebaseFunctionsException( + "Response is not valid JSON object.", Code.INTERNAL, null, je); + tcs.setException(e); + return; + } + + Object dataJSON = bodyJSON.opt("data"); + // TODO: Allow "result" instead of "data" for now, for backwards compatibility. + if (dataJSON == null) { + dataJSON = bodyJSON.opt("result"); + } + if (dataJSON == null) { + Exception e = + new FirebaseFunctionsException( + "Response is missing data field.", Code.INTERNAL, null); + tcs.setException(e); + return; + } + + HttpsCallableResult result = new HttpsCallableResult(serializer.decode(dataJSON)); + tcs.setResult(result); + } + }); + return tcs.getTask(); + } +} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt deleted file mode 100644 index 3a53e73464ed..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt +++ /dev/null @@ -1,404 +0,0 @@ -// Copyright 2018 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.functions - -import android.content.Context -import android.content.Intent -import android.util.Log -import androidx.annotation.VisibleForTesting -import com.google.android.gms.common.internal.Preconditions -import com.google.android.gms.security.ProviderInstaller -import com.google.android.gms.tasks.Task -import com.google.android.gms.tasks.TaskCompletionSource -import com.google.android.gms.tasks.Tasks -import com.google.firebase.FirebaseApp -import com.google.firebase.annotations.concurrent.Lightweight -import com.google.firebase.annotations.concurrent.UiThread -import com.google.firebase.emulators.EmulatedServiceSettings -import com.google.firebase.functions.FirebaseFunctionsException.Code.Companion.fromHttpStatus -import com.google.firebase.functions.FirebaseFunctionsException.Companion.fromResponse -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import java.io.IOException -import java.io.InterruptedIOException -import java.net.MalformedURLException -import java.net.URL -import java.util.concurrent.Executor -import javax.inject.Named -import okhttp3.Call -import okhttp3.Callback -import okhttp3.MediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody -import okhttp3.Response -import org.json.JSONException -import org.json.JSONObject - -/** FirebaseFunctions lets you call Cloud Functions for Firebase. */ -class FirebaseFunctions -@AssistedInject -internal constructor( - context: Context, - @Named("projectId") projectId: String?, - @Assisted regionOrCustomDomain: String?, - contextProvider: ContextProvider?, - @param:Lightweight private val executor: Executor, - @UiThread uiExecutor: Executor -) { - // The network client to use for HTTPS requests. - private val client: OkHttpClient - - // A serializer to encode/decode parameters and return values. - private val serializer: Serializer - - // A provider of client metadata to include with calls. - private val contextProvider: ContextProvider - - // The projectId to use for all functions references. - private val projectId: String - - // The region to use for all function references. - private var region: String? = null - - // A custom domain for the http trigger, such as "https://mydomain.com" - private var customDomain: String? = null - - // The format to use for constructing urls from region, projectId, and name. - private var urlFormat = "https://%1\$s-%2\$s.cloudfunctions.net/%3\$s" - - // Emulator settings - private var emulatorSettings: EmulatedServiceSettings? = null - - init { - client = OkHttpClient() - serializer = Serializer() - this.contextProvider = Preconditions.checkNotNull(contextProvider) - this.projectId = Preconditions.checkNotNull(projectId) - val isRegion: Boolean - isRegion = - try { - URL(regionOrCustomDomain) - false - } catch (malformedURLException: MalformedURLException) { - true - } - if (isRegion) { - region = regionOrCustomDomain - customDomain = null - } else { - region = "us-central1" - customDomain = regionOrCustomDomain - } - maybeInstallProviders(context, uiExecutor) - } - - /** Returns a reference to the callable HTTPS trigger with the given name. */ - fun getHttpsCallable(name: String): HttpsCallableReference { - return HttpsCallableReference(this, name, HttpsCallOptions()) - } - - /** Returns a reference to the callable HTTPS trigger with the provided URL. */ - fun getHttpsCallableFromUrl(url: URL): HttpsCallableReference { - return HttpsCallableReference(this, url, HttpsCallOptions()) - } - - /** Returns a reference to the callable HTTPS trigger with the given name and call options. */ - fun getHttpsCallable(name: String, options: HttpsCallableOptions): HttpsCallableReference { - return HttpsCallableReference(this, name, HttpsCallOptions(options)) - } - - /** Returns a reference to the callable HTTPS trigger with the provided URL and call options. */ - fun getHttpsCallableFromUrl(url: URL, options: HttpsCallableOptions): HttpsCallableReference { - return HttpsCallableReference(this, url, HttpsCallOptions(options)) - } - - /** - * Returns the URL for a particular function. - * - * @param function The name of the function. - * @return The URL. - */ - @VisibleForTesting - fun getURL(function: String): URL { - val emulatorSettings = emulatorSettings - if (emulatorSettings != null) { - urlFormat = - ("http://" + emulatorSettings.host + ":" + emulatorSettings.port + "/%2\$s/%1\$s/%3\$s") - } - var str = String.format(urlFormat, region, projectId, function) - if (customDomain != null && emulatorSettings == null) { - str = "$customDomain/$function" - } - return try { - URL(str) - } catch (mfe: MalformedURLException) { - throw IllegalStateException(mfe) - } - } - - @Deprecated("Use {@link #useEmulator(String, int)} to connect to the emulator. ") - fun useFunctionsEmulator(origin: String) { - Preconditions.checkNotNull(origin, "origin cannot be null") - urlFormat = "$origin/%2\$s/%1\$s/%3\$s" - } - - /** - * Modifies this FirebaseFunctions instance to communicate with the Cloud Functions emulator. - * - * Note: Call this method before using the instance to do any functions operations. - * - * @param host the emulator host (for example, 10.0.2.2) - * @param port the emulator port (for example, 5001) - */ - fun useEmulator(host: String, port: Int) { - emulatorSettings = EmulatedServiceSettings(host, port) - } - - /** - * Calls a Callable HTTPS trigger endpoint. - * - * @param name The name of the HTTPS trigger. - * @param data Parameters to pass to the function. Can be anything encodable as JSON. - * @return A Task that will be completed when the request is complete. - */ - fun call(name: String, data: Any?, options: HttpsCallOptions): Task { - return providerInstalled.task - .continueWithTask(executor) { task: Task? -> - contextProvider.getContext(options.limitedUseAppCheckTokens) - } - .continueWithTask(executor) { task: Task -> - if (!task.isSuccessful) { - return@continueWithTask Tasks.forException(task.exception!!) - } - val context = task.result - val url = getURL(name) - call(url, data, context, options) - } - } - - /** - * Calls a Callable HTTPS trigger endpoint. - * - * @param url The url of the HTTPS trigger - * @param data Parameters to pass to the function. Can be anything encodable as JSON. - * @return A Task that will be completed when the request is complete. - */ - fun call(url: URL, data: Any?, options: HttpsCallOptions): Task { - return providerInstalled.task - .continueWithTask(executor) { task: Task? -> - contextProvider.getContext(options.limitedUseAppCheckTokens) - } - .continueWithTask(executor) { task: Task -> - if (!task.isSuccessful) { - return@continueWithTask Tasks.forException(task.exception!!) - } - val context = task.result - call(url, data, context, options) - } - } - - /** - * Calls a Callable HTTPS trigger endpoint. - * - * @param url The name of the HTTPS trigger. - * @param data Parameters to pass to the function. Can be anything encodable as JSON. - * @param context Metadata to supply with the function call. - * @return A Task that will be completed when the request is complete. - */ - private fun call( - url: URL, - data: Any?, - context: HttpsCallableContext?, - options: HttpsCallOptions - ): Task { - Preconditions.checkNotNull(url, "url cannot be null") - val body: MutableMap = HashMap() - val encoded = serializer.encode(data) - body["data"] = encoded - val bodyJSON = JSONObject(body) - val contentType = MediaType.parse("application/json") - val requestBody = RequestBody.create(contentType, bodyJSON.toString()) - var request = Request.Builder().url(url).post(requestBody) - if (context!!.authToken != null) { - request = request.header("Authorization", "Bearer " + context.authToken) - } - if (context.instanceIdToken != null) { - request = request.header("Firebase-Instance-ID-Token", context.instanceIdToken) - } - if (context.appCheckToken != null) { - request = request.header("X-Firebase-AppCheck", context.appCheckToken) - } - val callClient = options.apply(client) - val call = callClient.newCall(request.build()) - val tcs = TaskCompletionSource() - call.enqueue( - object : Callback { - override fun onFailure(ignored: Call, e: IOException) { - if (e is InterruptedIOException) { - val exception = - FirebaseFunctionsException( - FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name, - FirebaseFunctionsException.Code.DEADLINE_EXCEEDED, - null, - e - ) - tcs.setException(exception) - } else { - val exception = - FirebaseFunctionsException( - FirebaseFunctionsException.Code.INTERNAL.name, - FirebaseFunctionsException.Code.INTERNAL, - null, - e - ) - tcs.setException(exception) - } - } - - @Throws(IOException::class) - override fun onResponse(ignored: Call, response: Response) { - val code = fromHttpStatus(response.code()) - val body = response.body()!!.string() - val exception = fromResponse(code, body, serializer) - if (exception != null) { - tcs.setException(exception) - return - } - val bodyJSON: JSONObject - bodyJSON = - try { - JSONObject(body) - } catch (je: JSONException) { - val e: Exception = - FirebaseFunctionsException( - "Response is not valid JSON object.", - FirebaseFunctionsException.Code.INTERNAL, - null, - je - ) - tcs.setException(e) - return - } - var dataJSON = bodyJSON.opt("data") - // TODO: Allow "result" instead of "data" for now, for backwards compatibility. - if (dataJSON == null) { - dataJSON = bodyJSON.opt("result") - } - if (dataJSON == null) { - val e: Exception = - FirebaseFunctionsException( - "Response is missing data field.", - FirebaseFunctionsException.Code.INTERNAL, - null - ) - tcs.setException(e) - return - } - val result = HttpsCallableResult(serializer.decode(dataJSON)) - tcs.setResult(result) - } - } - ) - return tcs.task - } - - companion object { - /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ - private val providerInstalled = TaskCompletionSource() - - /** - * Whether the ProviderInstaller async task has been started. This is guarded by the - * providerInstalled lock. - */ - private var providerInstallStarted = false - - /** - * Runs ProviderInstaller.installIfNeededAsync once per application instance. - * - * @param context The application context. - * @param uiExecutor - */ - private fun maybeInstallProviders(context: Context, uiExecutor: Executor) { - // Make sure this only runs once. - synchronized(providerInstalled) { - if (providerInstallStarted) { - return - } - providerInstallStarted = true - } - - // Package installIfNeededAsync into a Runnable so it can be run on the main thread. - // installIfNeededAsync checks to make sure it is on the main thread, and throws otherwise. - uiExecutor.execute { - ProviderInstaller.installIfNeededAsync( - context, - object : ProviderInstaller.ProviderInstallListener { - override fun onProviderInstalled() { - providerInstalled.setResult(null) - } - - override fun onProviderInstallFailed(i: Int, intent: Intent?) { - Log.d("FirebaseFunctions", "Failed to update ssl context") - providerInstalled.setResult(null) - } - } - ) - } - } - - /** - * Creates a Cloud Functions client with the given app and region or custom domain. - * - * @param app The app for the Firebase project. - * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as - * `"us-central1"` or `"https://mydomain.com"`. - */ - @JvmStatic - fun getInstance(app: FirebaseApp, regionOrCustomDomain: String): FirebaseFunctions { - Preconditions.checkNotNull(app, "You must call FirebaseApp.initializeApp first.") - Preconditions.checkNotNull(regionOrCustomDomain) - val component = app.get(FunctionsMultiResourceComponent::class.java) - Preconditions.checkNotNull(component, "Functions component does not exist.") - return component[regionOrCustomDomain]!! - } - - /** - * Creates a Cloud Functions client with the given app. - * - * @param app The app for the Firebase project. - */ - @JvmStatic - fun getInstance(app: FirebaseApp): FirebaseFunctions { - return getInstance(app, "us-central1") - } - - /** - * Creates a Cloud Functions client with the default app and given region or custom domain. - * - * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as - * `"us-central1"` or `"https://mydomain.com"`. - */ - @JvmStatic - fun getInstance(regionOrCustomDomain: String): FirebaseFunctions { - return getInstance(FirebaseApp.getInstance(), regionOrCustomDomain) - } - - /** Creates a Cloud Functions client with the default app. */ - @JvmStatic - fun getInstance(): FirebaseFunctions { - return getInstance(FirebaseApp.getInstance(), "us-central") - } - } -} From 8770f0ecf74e7ce3e42bdfa4e8b793cc5253276c Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Tue, 6 Aug 2024 16:04:05 -0500 Subject: [PATCH 09/11] Re-add kotlin file with typo fixed --- .../firebase/functions/FirebaseFunctions.java | 424 ------------------ .../firebase/functions/FirebaseFunctions.kt | 404 +++++++++++++++++ 2 files changed, 404 insertions(+), 424 deletions(-) delete mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java create mode 100644 firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java deleted file mode 100644 index 585aba3cf79b..000000000000 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright 2018 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.functions; - -import android.content.Context; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import com.google.android.gms.common.internal.Preconditions; -import com.google.android.gms.security.ProviderInstaller; -import com.google.android.gms.security.ProviderInstaller.ProviderInstallListener; -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.FirebaseApp; -import com.google.firebase.annotations.concurrent.Lightweight; -import com.google.firebase.annotations.concurrent.UiThread; -import com.google.firebase.emulators.EmulatedServiceSettings; -import com.google.firebase.functions.FirebaseFunctionsException.Code; -import dagger.assisted.Assisted; -import dagger.assisted.AssistedInject; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Executor; -import javax.inject.Named; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import org.json.JSONException; -import org.json.JSONObject; - -/** FirebaseFunctions lets you call Cloud Functions for Firebase. */ -public class FirebaseFunctions { - - /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ - private static final TaskCompletionSource providerInstalled = new TaskCompletionSource<>(); - - /** - * Whether the ProviderInstaller async task has been started. This is guarded by the - * providerInstalled lock. - */ - private static boolean providerInstallStarted = false; - - // The network client to use for HTTPS requests. - private final OkHttpClient client; - - // A serializer to encode/decode parameters and return values. - private final Serializer serializer; - - // A provider of client metadata to include with calls. - private final ContextProvider contextProvider; - - private final Executor executor; - - // The projectId to use for all functions references. - private final String projectId; - - // The region to use for all function references. - private final String region; - - // A custom domain for the http trigger, such as "https://mydomain.com" - @Nullable private final String customDomain; - - // The format to use for constructing urls from region, projectId, and name. - private String urlFormat = "https://%1$s-%2$s.cloudfunctions.net/%3$s"; - - // Emulator settings - @Nullable private EmulatedServiceSettings emulatorSettings; - - @AssistedInject - FirebaseFunctions( - Context context, - @Nullable @Named("projectId") String projectId, - @Assisted String regionOrCustomDomain, - ContextProvider contextProvider, - @Lightweight Executor executor, - @UiThread Executor uiExecutor) { - this.executor = executor; - this.client = new OkHttpClient(); - this.serializer = new Serializer(); - this.contextProvider = Preconditions.checkNotNull(contextProvider); - this.projectId = Preconditions.checkNotNull(projectId); - - boolean isRegion; - try { - new URL(regionOrCustomDomain); - isRegion = false; - } catch (MalformedURLException malformedURLException) { - isRegion = true; - } - - if (isRegion) { - this.region = regionOrCustomDomain; - this.customDomain = null; - } else { - this.region = "us-central1"; - this.customDomain = regionOrCustomDomain; - } - - maybeInstallProviders(context, uiExecutor); - } - - /** - * Runs ProviderInstaller.installIfNeededAsync once per application instance. - * - * @param context The application context. - * @param uiExecutor - */ - private static void maybeInstallProviders(Context context, Executor uiExecutor) { - // Make sure this only runs once. - synchronized (providerInstalled) { - if (providerInstallStarted) { - return; - } - providerInstallStarted = true; - } - - // Package installIfNeededAsync into a Runnable so it can be run on the main thread. - // installIfNeededAsync checks to make sure it is on the main thread, and throws otherwise. - uiExecutor.execute( - () -> - ProviderInstaller.installIfNeededAsync( - context, - new ProviderInstallListener() { - @Override - public void onProviderInstalled() { - providerInstalled.setResult(null); - } - - @Override - public void onProviderInstallFailed(int i, android.content.Intent intent) { - Log.d("FirebaseFunctions", "Failed to update ssl context"); - providerInstalled.setResult(null); - } - })); - } - - /** - * Creates a Cloud Functions client with the given app and region or custom domain. - * - * @param app The app for the Firebase project. - * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as {@code - * "us-central1"} or {@code "https://mydomain.com"}. - */ - @NonNull - public static FirebaseFunctions getInstance( - @NonNull FirebaseApp app, @NonNull String regionOrCustomDomain) { - Preconditions.checkNotNull(app, "You must call FirebaseApp.initializeApp first."); - Preconditions.checkNotNull(regionOrCustomDomain); - - FunctionsMultiResourceComponent component = app.get(FunctionsMultiResourceComponent.class); - Preconditions.checkNotNull(component, "Functions component does not exist."); - - return component.get(regionOrCustomDomain); - } - - /** - * Creates a Cloud Functions client with the given app. - * - * @param app The app for the Firebase project. - */ - @NonNull - public static FirebaseFunctions getInstance(@NonNull FirebaseApp app) { - return getInstance(app, "us-central1"); - } - - /** - * Creates a Cloud Functions client with the default app and given region or custom domain. - * - * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as {@code - * "us-central1"} or {@code "https://mydomain.com"}. - */ - @NonNull - public static FirebaseFunctions getInstance(@NonNull String regionOrCustomDomain) { - return getInstance(FirebaseApp.getInstance(), regionOrCustomDomain); - } - - /** Creates a Cloud Functions client with the default app. */ - @NonNull - public static FirebaseFunctions getInstance() { - return getInstance(FirebaseApp.getInstance(), "us-central1"); - } - - /** Returns a reference to the callable HTTPS trigger with the given name. */ - @NonNull - public HttpsCallableReference getHttpsCallable(@NonNull String name) { - return new HttpsCallableReference(this, name, new HttpsCallOptions()); - } - - /** Returns a reference to the callable HTTPS trigger with the provided URL. */ - @NonNull - public HttpsCallableReference getHttpsCallableFromUrl(@NonNull URL url) { - return new HttpsCallableReference(this, url, new HttpsCallOptions()); - } - - /** Returns a reference to the callable HTTPS trigger with the given name and call options. */ - @NonNull - public HttpsCallableReference getHttpsCallable( - @NonNull String name, @NonNull HttpsCallableOptions options) { - return new HttpsCallableReference(this, name, new HttpsCallOptions(options)); - } - - /** Returns a reference to the callable HTTPS trigger with the provided URL and call options. */ - @NonNull - public HttpsCallableReference getHttpsCallableFromUrl( - @NonNull URL url, @NonNull HttpsCallableOptions options) { - return new HttpsCallableReference(this, url, new HttpsCallOptions(options)); - } - - /** - * Returns the URL for a particular function. - * - * @param function The name of the function. - * @return The URL. - */ - @VisibleForTesting - URL getURL(String function) { - EmulatedServiceSettings emulatorSettings = this.emulatorSettings; - if (emulatorSettings != null) { - urlFormat = - "http://" - + emulatorSettings.getHost() - + ":" - + emulatorSettings.getPort() - + "/%2$s/%1$s/%3$s"; - } - - String str = String.format(urlFormat, region, projectId, function); - - if (customDomain != null && emulatorSettings == null) { - str = customDomain + "/" + function; - } - - try { - return new URL(str); - } catch (MalformedURLException mfe) { - throw new IllegalStateException(mfe); - } - } - - /** @deprecated Use {@link #useEmulator(String, int)} to connect to the emulator. */ - public void useFunctionsEmulator(@NonNull String origin) { - Preconditions.checkNotNull(origin, "origin cannot be null"); - urlFormat = origin + "/%2$s/%1$s/%3$s"; - } - - /** - * Modifies this FirebaseFunctions instance to communicate with the Cloud Functions emulator. - * - *

Note: Call this method before using the instance to do any functions operations. - * - * @param host the emulator host (for example, 10.0.2.2) - * @param port the emulator port (for example, 5001) - */ - public void useEmulator(@NonNull String host, int port) { - this.emulatorSettings = new EmulatedServiceSettings(host, port); - } - - /** - * Calls a Callable HTTPS trigger endpoint. - * - * @param name The name of the HTTPS trigger. - * @param data Parameters to pass to the function. Can be anything encodable as JSON. - * @return A Task that will be completed when the request is complete. - */ - Task call(String name, @Nullable Object data, HttpsCallOptions options) { - return providerInstalled - .getTask() - .continueWithTask( - executor, task -> contextProvider.getContext(options.getLimitedUseAppCheckTokens())) - .continueWithTask( - executor, - task -> { - if (!task.isSuccessful()) { - return Tasks.forException(task.getException()); - } - HttpsCallableContext context = task.getResult(); - URL url = getURL(name); - return call(url, data, context, options); - }); - } - - /** - * Calls a Callable HTTPS trigger endpoint. - * - * @param url The url of the HTTPS trigger - * @param data Parameters to pass to the function. Can be anything encodable as JSON. - * @return A Task that will be completed when the request is complete. - */ - Task call(URL url, @Nullable Object data, HttpsCallOptions options) { - return providerInstalled - .getTask() - .continueWithTask( - executor, task -> contextProvider.getContext(options.getLimitedUseAppCheckTokens())) - .continueWithTask( - executor, - task -> { - if (!task.isSuccessful()) { - return Tasks.forException(task.getException()); - } - HttpsCallableContext context = task.getResult(); - return call(url, data, context, options); - }); - } - - /** - * Calls a Callable HTTPS trigger endpoint. - * - * @param url The name of the HTTPS trigger. - * @param data Parameters to pass to the function. Can be anything encodable as JSON. - * @param context Metadata to supply with the function call. - * @return A Task that will be completed when the request is complete. - */ - private Task call( - @NonNull URL url, - @Nullable Object data, - HttpsCallableContext context, - HttpsCallOptions options) { - Preconditions.checkNotNull(url, "url cannot be null"); - - Map body = new HashMap<>(); - - Object encoded = serializer.encode(data); - body.put("data", encoded); - - JSONObject bodyJSON = new JSONObject(body); - MediaType contentType = MediaType.parse("application/json"); - RequestBody requestBody = RequestBody.create(contentType, bodyJSON.toString()); - - Request.Builder request = new Request.Builder().url(url).post(requestBody); - if (context.getAuthToken() != null) { - request = request.header("Authorization", "Bearer " + context.getAuthToken()); - } - if (context.getInstanceIdToken() != null) { - request = request.header("Firebase-Instance-ID-Token", context.getInstanceIdToken()); - } - if (context.getAppCheckToken() != null) { - request = request.header("X-Firebase-AppCheck", context.getAppCheckToken()); - } - - OkHttpClient callClient = options.apply(client); - Call call = callClient.newCall(request.build()); - - TaskCompletionSource tcs = new TaskCompletionSource<>(); - call.enqueue( - new Callback() { - @Override - public void onFailure(Call ignored, IOException e) { - if (e instanceof InterruptedIOException) { - FirebaseFunctionsException exception = - new FirebaseFunctionsException( - Code.DEADLINE_EXCEEDED.name(), Code.DEADLINE_EXCEEDED, null, e); - tcs.setException(exception); - } else { - FirebaseFunctionsException exception = - new FirebaseFunctionsException(Code.INTERNAL.name(), Code.INTERNAL, null, e); - tcs.setException(exception); - } - } - - @Override - public void onResponse(Call ignored, Response response) throws IOException { - Code code = Code.fromHttpStatus(response.code()); - String body = response.body().string(); - - FirebaseFunctionsException exception = - FirebaseFunctionsException.fromResponse(code, body, serializer); - if (exception != null) { - tcs.setException(exception); - return; - } - - JSONObject bodyJSON; - try { - bodyJSON = new JSONObject(body); - } catch (JSONException je) { - Exception e = - new FirebaseFunctionsException( - "Response is not valid JSON object.", Code.INTERNAL, null, je); - tcs.setException(e); - return; - } - - Object dataJSON = bodyJSON.opt("data"); - // TODO: Allow "result" instead of "data" for now, for backwards compatibility. - if (dataJSON == null) { - dataJSON = bodyJSON.opt("result"); - } - if (dataJSON == null) { - Exception e = - new FirebaseFunctionsException( - "Response is missing data field.", Code.INTERNAL, null); - tcs.setException(e); - return; - } - - HttpsCallableResult result = new HttpsCallableResult(serializer.decode(dataJSON)); - tcs.setResult(result); - } - }); - return tcs.getTask(); - } -} diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt new file mode 100644 index 000000000000..043d35d6e000 --- /dev/null +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt @@ -0,0 +1,404 @@ +// Copyright 2018 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.functions + +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.annotation.VisibleForTesting +import com.google.android.gms.common.internal.Preconditions +import com.google.android.gms.security.ProviderInstaller +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.TaskCompletionSource +import com.google.android.gms.tasks.Tasks +import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Lightweight +import com.google.firebase.annotations.concurrent.UiThread +import com.google.firebase.emulators.EmulatedServiceSettings +import com.google.firebase.functions.FirebaseFunctionsException.Code.Companion.fromHttpStatus +import com.google.firebase.functions.FirebaseFunctionsException.Companion.fromResponse +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import java.io.IOException +import java.io.InterruptedIOException +import java.net.MalformedURLException +import java.net.URL +import java.util.concurrent.Executor +import javax.inject.Named +import okhttp3.Call +import okhttp3.Callback +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import org.json.JSONException +import org.json.JSONObject + +/** FirebaseFunctions lets you call Cloud Functions for Firebase. */ +class FirebaseFunctions +@AssistedInject +internal constructor( + context: Context, + @Named("projectId") projectId: String?, + @Assisted regionOrCustomDomain: String?, + contextProvider: ContextProvider?, + @param:Lightweight private val executor: Executor, + @UiThread uiExecutor: Executor +) { + // The network client to use for HTTPS requests. + private val client: OkHttpClient + + // A serializer to encode/decode parameters and return values. + private val serializer: Serializer + + // A provider of client metadata to include with calls. + private val contextProvider: ContextProvider + + // The projectId to use for all functions references. + private val projectId: String + + // The region to use for all function references. + private var region: String? = null + + // A custom domain for the http trigger, such as "https://mydomain.com" + private var customDomain: String? = null + + // The format to use for constructing urls from region, projectId, and name. + private var urlFormat = "https://%1\$s-%2\$s.cloudfunctions.net/%3\$s" + + // Emulator settings + private var emulatorSettings: EmulatedServiceSettings? = null + + init { + client = OkHttpClient() + serializer = Serializer() + this.contextProvider = Preconditions.checkNotNull(contextProvider) + this.projectId = Preconditions.checkNotNull(projectId) + val isRegion: Boolean + isRegion = + try { + URL(regionOrCustomDomain) + false + } catch (malformedURLException: MalformedURLException) { + true + } + if (isRegion) { + region = regionOrCustomDomain + customDomain = null + } else { + region = "us-central1" + customDomain = regionOrCustomDomain + } + maybeInstallProviders(context, uiExecutor) + } + + /** Returns a reference to the callable HTTPS trigger with the given name. */ + fun getHttpsCallable(name: String): HttpsCallableReference { + return HttpsCallableReference(this, name, HttpsCallOptions()) + } + + /** Returns a reference to the callable HTTPS trigger with the provided URL. */ + fun getHttpsCallableFromUrl(url: URL): HttpsCallableReference { + return HttpsCallableReference(this, url, HttpsCallOptions()) + } + + /** Returns a reference to the callable HTTPS trigger with the given name and call options. */ + fun getHttpsCallable(name: String, options: HttpsCallableOptions): HttpsCallableReference { + return HttpsCallableReference(this, name, HttpsCallOptions(options)) + } + + /** Returns a reference to the callable HTTPS trigger with the provided URL and call options. */ + fun getHttpsCallableFromUrl(url: URL, options: HttpsCallableOptions): HttpsCallableReference { + return HttpsCallableReference(this, url, HttpsCallOptions(options)) + } + + /** + * Returns the URL for a particular function. + * + * @param function The name of the function. + * @return The URL. + */ + @VisibleForTesting + fun getURL(function: String): URL { + val emulatorSettings = emulatorSettings + if (emulatorSettings != null) { + urlFormat = + ("http://" + emulatorSettings.host + ":" + emulatorSettings.port + "/%2\$s/%1\$s/%3\$s") + } + var str = String.format(urlFormat, region, projectId, function) + if (customDomain != null && emulatorSettings == null) { + str = "$customDomain/$function" + } + return try { + URL(str) + } catch (mfe: MalformedURLException) { + throw IllegalStateException(mfe) + } + } + + @Deprecated("Use {@link #useEmulator(String, int)} to connect to the emulator. ") + fun useFunctionsEmulator(origin: String) { + Preconditions.checkNotNull(origin, "origin cannot be null") + urlFormat = "$origin/%2\$s/%1\$s/%3\$s" + } + + /** + * Modifies this FirebaseFunctions instance to communicate with the Cloud Functions emulator. + * + * Note: Call this method before using the instance to do any functions operations. + * + * @param host the emulator host (for example, 10.0.2.2) + * @param port the emulator port (for example, 5001) + */ + fun useEmulator(host: String, port: Int) { + emulatorSettings = EmulatedServiceSettings(host, port) + } + + /** + * Calls a Callable HTTPS trigger endpoint. + * + * @param name The name of the HTTPS trigger. + * @param data Parameters to pass to the function. Can be anything encodable as JSON. + * @return A Task that will be completed when the request is complete. + */ + fun call(name: String, data: Any?, options: HttpsCallOptions): Task { + return providerInstalled.task + .continueWithTask(executor) { task: Task? -> + contextProvider.getContext(options.limitedUseAppCheckTokens) + } + .continueWithTask(executor) { task: Task -> + if (!task.isSuccessful) { + return@continueWithTask Tasks.forException(task.exception!!) + } + val context = task.result + val url = getURL(name) + call(url, data, context, options) + } + } + + /** + * Calls a Callable HTTPS trigger endpoint. + * + * @param url The url of the HTTPS trigger + * @param data Parameters to pass to the function. Can be anything encodable as JSON. + * @return A Task that will be completed when the request is complete. + */ + fun call(url: URL, data: Any?, options: HttpsCallOptions): Task { + return providerInstalled.task + .continueWithTask(executor) { task: Task? -> + contextProvider.getContext(options.limitedUseAppCheckTokens) + } + .continueWithTask(executor) { task: Task -> + if (!task.isSuccessful) { + return@continueWithTask Tasks.forException(task.exception!!) + } + val context = task.result + call(url, data, context, options) + } + } + + /** + * Calls a Callable HTTPS trigger endpoint. + * + * @param url The name of the HTTPS trigger. + * @param data Parameters to pass to the function. Can be anything encodable as JSON. + * @param context Metadata to supply with the function call. + * @return A Task that will be completed when the request is complete. + */ + private fun call( + url: URL, + data: Any?, + context: HttpsCallableContext?, + options: HttpsCallOptions + ): Task { + Preconditions.checkNotNull(url, "url cannot be null") + val body: MutableMap = HashMap() + val encoded = serializer.encode(data) + body["data"] = encoded + val bodyJSON = JSONObject(body) + val contentType = MediaType.parse("application/json") + val requestBody = RequestBody.create(contentType, bodyJSON.toString()) + var request = Request.Builder().url(url).post(requestBody) + if (context!!.authToken != null) { + request = request.header("Authorization", "Bearer " + context.authToken) + } + if (context.instanceIdToken != null) { + request = request.header("Firebase-Instance-ID-Token", context.instanceIdToken) + } + if (context.appCheckToken != null) { + request = request.header("X-Firebase-AppCheck", context.appCheckToken) + } + val callClient = options.apply(client) + val call = callClient.newCall(request.build()) + val tcs = TaskCompletionSource() + call.enqueue( + object : Callback { + override fun onFailure(ignored: Call, e: IOException) { + if (e is InterruptedIOException) { + val exception = + FirebaseFunctionsException( + FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name, + FirebaseFunctionsException.Code.DEADLINE_EXCEEDED, + null, + e + ) + tcs.setException(exception) + } else { + val exception = + FirebaseFunctionsException( + FirebaseFunctionsException.Code.INTERNAL.name, + FirebaseFunctionsException.Code.INTERNAL, + null, + e + ) + tcs.setException(exception) + } + } + + @Throws(IOException::class) + override fun onResponse(ignored: Call, response: Response) { + val code = fromHttpStatus(response.code()) + val body = response.body()!!.string() + val exception = fromResponse(code, body, serializer) + if (exception != null) { + tcs.setException(exception) + return + } + val bodyJSON: JSONObject + bodyJSON = + try { + JSONObject(body) + } catch (je: JSONException) { + val e: Exception = + FirebaseFunctionsException( + "Response is not valid JSON object.", + FirebaseFunctionsException.Code.INTERNAL, + null, + je + ) + tcs.setException(e) + return + } + var dataJSON = bodyJSON.opt("data") + // TODO: Allow "result" instead of "data" for now, for backwards compatibility. + if (dataJSON == null) { + dataJSON = bodyJSON.opt("result") + } + if (dataJSON == null) { + val e: Exception = + FirebaseFunctionsException( + "Response is missing data field.", + FirebaseFunctionsException.Code.INTERNAL, + null + ) + tcs.setException(e) + return + } + val result = HttpsCallableResult(serializer.decode(dataJSON)) + tcs.setResult(result) + } + } + ) + return tcs.task + } + + companion object { + /** A task that will be resolved once ProviderInstaller has installed what it needs to. */ + private val providerInstalled = TaskCompletionSource() + + /** + * Whether the ProviderInstaller async task has been started. This is guarded by the + * providerInstalled lock. + */ + private var providerInstallStarted = false + + /** + * Runs ProviderInstaller.installIfNeededAsync once per application instance. + * + * @param context The application context. + * @param uiExecutor + */ + private fun maybeInstallProviders(context: Context, uiExecutor: Executor) { + // Make sure this only runs once. + synchronized(providerInstalled) { + if (providerInstallStarted) { + return + } + providerInstallStarted = true + } + + // Package installIfNeededAsync into a Runnable so it can be run on the main thread. + // installIfNeededAsync checks to make sure it is on the main thread, and throws otherwise. + uiExecutor.execute { + ProviderInstaller.installIfNeededAsync( + context, + object : ProviderInstaller.ProviderInstallListener { + override fun onProviderInstalled() { + providerInstalled.setResult(null) + } + + override fun onProviderInstallFailed(i: Int, intent: Intent?) { + Log.d("FirebaseFunctions", "Failed to update ssl context") + providerInstalled.setResult(null) + } + } + ) + } + } + + /** + * Creates a Cloud Functions client with the given app and region or custom domain. + * + * @param app The app for the Firebase project. + * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as + * `"us-central1"` or `"https://mydomain.com"`. + */ + @JvmStatic + fun getInstance(app: FirebaseApp, regionOrCustomDomain: String): FirebaseFunctions { + Preconditions.checkNotNull(app, "You must call FirebaseApp.initializeApp first.") + Preconditions.checkNotNull(regionOrCustomDomain) + val component = app.get(FunctionsMultiResourceComponent::class.java) + Preconditions.checkNotNull(component, "Functions component does not exist.") + return component[regionOrCustomDomain]!! + } + + /** + * Creates a Cloud Functions client with the given app. + * + * @param app The app for the Firebase project. + */ + @JvmStatic + fun getInstance(app: FirebaseApp): FirebaseFunctions { + return getInstance(app, "us-central1") + } + + /** + * Creates a Cloud Functions client with the default app and given region or custom domain. + * + * @param regionOrCustomDomain The region or custom domain for the HTTPS trigger, such as + * `"us-central1"` or `"https://mydomain.com"`. + */ + @JvmStatic + fun getInstance(regionOrCustomDomain: String): FirebaseFunctions { + return getInstance(FirebaseApp.getInstance(), regionOrCustomDomain) + } + + /** Creates a Cloud Functions client with the default app. */ + @JvmStatic + fun getInstance(): FirebaseFunctions { + return getInstance(FirebaseApp.getInstance(), "us-central1") + } + } +} From b25b5d9abe0a0dd757c1c40c12c461089a476d16 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Fri, 30 Aug 2024 13:31:17 -0500 Subject: [PATCH 10/11] Add changelog --- firebase-functions/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-functions/CHANGELOG.md b/firebase-functions/CHANGELOG.md index adc5138333fd..eb6c565a42b0 100644 --- a/firebase-functions/CHANGELOG.md +++ b/firebase-functions/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased - +* [changed] Migrated to Kotlin # 21.0.0 * [changed] Bump internal dependencies From 2cefd77bb2d30c0c542d9eda15b3e2b63e8d7604 Mon Sep 17 00:00:00 2001 From: Emily Ploszaj Date: Fri, 30 Aug 2024 15:53:59 -0500 Subject: [PATCH 11/11] Minor version bump --- firebase-functions/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-functions/gradle.properties b/firebase-functions/gradle.properties index b878bad37ffc..d9988d780d30 100644 --- a/firebase-functions/gradle.properties +++ b/firebase-functions/gradle.properties @@ -1,3 +1,3 @@ -version=21.0.1 +version=21.1.0 latestReleasedVersion=21.0.0 android.enableUnitTestBinaryResources=true