From 2c30305afd788176fe3714a6a0399b737a967d9c Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Tue, 10 Oct 2023 16:16:27 -0400 Subject: [PATCH 01/38] Setup scaffolding for SessionMaintainer (#5393) --- .../firebase/sessions/FirebaseSessions.kt | 1 + .../sessions/FirebaseSessionsRegistrar.kt | 18 +++++++--- .../firebase/sessions/SessionMaintainer.kt | 34 +++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionMaintainer.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index 4a23b4af362..0bcab8428ee 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -37,6 +37,7 @@ internal constructor( backgroundDispatcher: CoroutineDispatcher, blockingDispatcher: CoroutineDispatcher, transportFactoryProvider: Provider, + @Suppress("UNUSED_PARAMETER") sessionMaintainer: SessionMaintainer, ) { private val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp) private val sessionSettings = diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index 2989dee83a3..27e7c3e9ea3 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -29,7 +29,7 @@ import com.google.firebase.platforminfo.LibraryVersionComponent import kotlinx.coroutines.CoroutineDispatcher /** - * [ComponentRegistrar] for setting up [FirebaseSessions]. + * [ComponentRegistrar] for setting up [FirebaseSessions] and [SessionMaintainer]. * * @hide */ @@ -38,12 +38,13 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { override fun getComponents() = listOf( Component.builder(FirebaseSessions::class.java) - .name(LIBRARY_NAME) + .name(SESSIONS_LIBRARY_NAME) .add(Dependency.required(firebaseApp)) .add(Dependency.required(firebaseInstallationsApi)) .add(Dependency.required(backgroundDispatcher)) .add(Dependency.required(blockingDispatcher)) .add(Dependency.requiredProvider(transportFactory)) + .add(Dependency.required(sessionMaintainer)) .factory { container -> FirebaseSessions( container.get(firebaseApp), @@ -51,15 +52,22 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { container.get(backgroundDispatcher), container.get(blockingDispatcher), container.getProvider(transportFactory), + container.get(sessionMaintainer) ) } .build(), - LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME) + Component.builder(SessionMaintainer::class.java) + .name(MAINTAINER_LIBRARY_NAME) + .factory { SessionMaintainer() } + .build(), + LibraryVersionComponent.create(SESSIONS_LIBRARY_NAME, BuildConfig.VERSION_NAME), ) - companion object { - private const val LIBRARY_NAME = "fire-sessions" + private companion object { + private const val SESSIONS_LIBRARY_NAME = "fire-sessions" + private const val MAINTAINER_LIBRARY_NAME = "fire-session-maintainer" + private val sessionMaintainer = unqualified(SessionMaintainer::class.java) private val firebaseApp = unqualified(FirebaseApp::class.java) private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java) private val backgroundDispatcher = diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionMaintainer.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionMaintainer.kt new file mode 100644 index 00000000000..5bff0ffdb74 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionMaintainer.kt @@ -0,0 +1,34 @@ +/* + * 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.sessions + +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.app + +/** Maintainer for Sessions. */ +internal class SessionMaintainer { + + internal companion object { + @JvmStatic + val instance: SessionMaintainer + get() = getInstance(Firebase.app) + + @JvmStatic + fun getInstance(app: FirebaseApp): SessionMaintainer = app.get(SessionMaintainer::class.java) + } +} From f85056c16d289244051603b5d6ba8d40312e2049 Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Wed, 11 Oct 2023 15:13:44 -0400 Subject: [PATCH 02/38] =?UTF-8?q?Introduces=20a=20SessionDataService=20whi?= =?UTF-8?q?ch=20is=20bound=20to=20by=20clients=20in=20each=20=E2=80=A6=20(?= =?UTF-8?q?#5397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new SessionDataService that is bound to by each process and used to deliver session-ids. --- .../src/main/AndroidManifest.xml | 4 + .../firebase/sessions/FirebaseSessions.kt | 1 + .../firebase/sessions/SessionDataService.kt | 198 ++++++++++++++++++ .../firebase/sessions/SessionInitiator.kt | 4 +- 4 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataService.kt diff --git a/firebase-sessions/src/main/AndroidManifest.xml b/firebase-sessions/src/main/AndroidManifest.xml index 662efdb1d7c..6e6267631a3 100644 --- a/firebase-sessions/src/main/AndroidManifest.xml +++ b/firebase-sessions/src/main/AndroidManifest.xml @@ -18,6 +18,10 @@ + diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataService.kt new file mode 100644 index 00000000000..9394c297de8 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataService.kt @@ -0,0 +1,198 @@ +/* + * 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.sessions + +import android.app.Activity +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Build +import android.os.Bundle +import android.os.DeadObjectException +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.Message +import android.os.Messenger +import android.os.RemoteException +import android.util.Log +import java.util.LinkedList +import java.util.UUID + +/** Service for providing access to session data */ +internal class SessionDataService : Service() { + /** Target we publish for clients to send messages to IncomingHandler. */ + private lateinit var messenger: Messenger + + val boundClients = mutableListOf() + + /** Handler of incoming messages from clients. */ + internal inner class IncomingHandler( + context: Context, + private val appContext: Context = context.applicationContext, + ) : Handler(Looper.getMainLooper()) { // TODO(rothbutter) probably want to use our own executor + + private var curSessionId: String? = null + + override fun handleMessage(msg: Message) { + when (msg.what) { + FOREGROUNDED -> handleForegrounding() + BACKGROUNDED -> handleBackgrounding() + else -> super.handleMessage(msg) + } + } + + fun handleForegrounding() { + Log.i(TAG, "SERVICE: Activity foregrounding - updating ${boundClients.size} clients") + broadcastSession(UUID.randomUUID().toString()) + } + + fun handleBackgrounding() { + Log.i(TAG, "SERVICE: Activity backgrounding") + } + + fun broadcastSession(sessionId: String) { + Log.i(TAG, "SERVICE: Broadcasting new session $sessionId") + boundClients.forEach { sendNewSession(it, sessionId) } + } + + fun sendNewSession(client: Messenger, sessionId: String) { + try { + client.send( + Message.obtain(null, SESSION_UPDATED, 0, 0).also { + it.setData(Bundle().also { it.putString(SESSION_UPDATE_EXTRA, sessionId) }) + } + ) + } catch (e: Exception) { + if (e is DeadObjectException) { + Log.i(TAG, "SERVICE: Removing dead client from list: $client") + boundClients.remove(client) + } else { + Log.e(TAG, "SERVICE: Unable to push new session to $client.", e) + } + } + } + } + + override fun onBind(intent: Intent): IBinder? { + Log.i(TAG, "SERVICE: Service bound") + messenger = Messenger(IncomingHandler(this)) + val callbackMessenger = getCallback(intent) + if (callbackMessenger != null) { + boundClients.add(callbackMessenger) + Log.i(TAG, "SERVICE: Stored callback to $callbackMessenger. Size: ${boundClients.size}") + } + return messenger.binder + } + + private fun getCallback(intent: Intent): Messenger? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER, Messenger::class.java) + } else { + intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER) + } + + internal companion object { + const val TAG = "SessionDataService" + const val CLIENT_CALLBACK_MESSENGER = "ClientCallbackMessenger" + const val SESSION_UPDATE_EXTRA = "SessionUpdateExtra" + + const val FOREGROUNDED = 1 + const val BACKGROUNDED = 2 + const val SESSION_UPDATED = 3 + + private var testService: Messenger? = null + private var testServiceBound: Boolean = false + private val queuedMessages = LinkedList() + + internal class ClientUpdateHandler(appContext: Context) : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + when (msg.what) { + SESSION_UPDATED -> + handleSessionUpdate(msg.data?.getString(SESSION_UPDATE_EXTRA) ?: "no-id-given") + else -> super.handleMessage(msg) + } + } + + fun handleSessionUpdate(sessionId: String) { + Log.i(TAG, "CLIENT: Session update received: $sessionId") + } + } + + private val testServiceConnection = + object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + Log.i(TAG, "CLIENT: Connected to SessionDataService. Queue size ${queuedMessages.size}") + testService = Messenger(service) + testServiceBound = true + val queueItr = queuedMessages.iterator() + for (msg in queueItr) { + Log.i(TAG, "CLIENT: sending queued message ${msg.what}") + sendMessage(msg) + queueItr.remove() + } + } + + override fun onServiceDisconnected(className: ComponentName) { + Log.i(TAG, "CLIENT: Disconnected from SessionDataService") + testService = null + testServiceBound = false + } + } + + fun bind(appContext: Context): Unit { + Intent(appContext, SessionDataService::class.java).also { intent -> + Log.i(TAG, "CLIENT: Binding service to application.") + // This is necessary for the onBind() to be called by each process + intent.setAction(android.os.Process.myPid().toString()) + intent.putExtra(CLIENT_CALLBACK_MESSENGER, Messenger(ClientUpdateHandler(appContext))) + appContext.bindService( + intent, + testServiceConnection, + Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE + ) + } + } + + fun foregrounded(activity: Activity): Unit { + sendMessage(FOREGROUNDED) + } + + fun backgrounded(activity: Activity): Unit { + sendMessage(BACKGROUNDED) + } + + private fun sendMessage(messageCode: Int) { + sendMessage(Message.obtain(null, messageCode, 0, 0)) + } + + private fun sendMessage(msg: Message) { + if (testService != null) { + try { + testService?.send(msg) + } catch (e: RemoteException) { + Log.e(TAG, "CLIENT: Unable to deliver message: ${msg.what}") + } + } else { + Log.i(TAG, "CLIENT: Queueing message ${msg.what}") + queuedMessages.add(msg) + } + } + } +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt index 84fbd99c72d..ccdd6c07c80 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt @@ -66,9 +66,9 @@ internal class SessionInitiator( internal val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks { - override fun onActivityResumed(activity: Activity) = appForegrounded() + override fun onActivityResumed(activity: Activity) = SessionDataService.foregrounded(activity) - override fun onActivityPaused(activity: Activity) = appBackgrounded() + override fun onActivityPaused(activity: Activity) = SessionDataService.backgrounded(activity) override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit From 8e2255cfa13d8a76b46b6e39846e0d7adf38f2eb Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Wed, 11 Oct 2023 15:37:43 -0400 Subject: [PATCH 03/38] New datastore for holding session id and timestamp (#5399) Co-authored-by: jrothfeder --- .../firebase/sessions/SessionDatastore.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt new file mode 100644 index 00000000000..e3d534514b2 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt @@ -0,0 +1,71 @@ +/* + * 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.sessions + +import android.content.Context +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +/** Datastore for sessions information */ +internal data class FirebaseSessionsData(val sessionId: String?, val timestampMicroseconds: Long?) + +internal class SessionDatastore(private val context: Context) { + private val tag = "FirebaseSessionsRepo" + + private object FirebaseSessionDataKeys { + val SESSION_ID = stringPreferencesKey("session_id") + val TIMESTAMP_MICROSECONDS = longPreferencesKey("timestamp_microseconds") + } + + internal val firebaseSessionDataFlow: Flow = + context.dataStore.data + .catch { exception -> + Log.e(tag, "Error reading stored session data.", exception) + emit(emptyPreferences()) + } + .map { preferences -> mapSessionsData(preferences) } + + suspend fun updateSessionId(sessionId: String) { + context.dataStore.edit { preferences -> + preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId + } + } + + suspend fun updateTimestamp(timestampMicroseconds: Long) { + context.dataStore.edit { preferences -> + preferences[FirebaseSessionDataKeys.TIMESTAMP_MICROSECONDS] = timestampMicroseconds + } + } + + private fun mapSessionsData(preferences: Preferences): FirebaseSessionsData = + FirebaseSessionsData( + preferences[FirebaseSessionDataKeys.SESSION_ID], + preferences[FirebaseSessionDataKeys.TIMESTAMP_MICROSECONDS] + ) +} + +private val Context.dataStore: DataStore by + preferencesDataStore(name = "firebase_session_settings") From c7e23f6194f3eb839d885e82c5a2c128703dba37 Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Thu, 12 Oct 2023 13:47:53 -0400 Subject: [PATCH 04/38] Splits the SessionDataService into a service and client (#5403) Created a SessionLifecycleService class which contains all the service-side code, and a SessionLifecycleClient object which handles the connection to the server and the sending and receiving of events. --- .../src/main/AndroidManifest.xml | 2 +- .../firebase/sessions/FirebaseSessions.kt | 2 +- .../firebase/sessions/SessionDataService.kt | 198 ------------------ .../firebase/sessions/SessionInitiator.kt | 6 +- .../sessions/SessionLifecycleClient.kt | 198 ++++++++++++++++++ .../sessions/SessionLifecycleService.kt | 174 +++++++++++++++ 6 files changed, 378 insertions(+), 202 deletions(-) delete mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataService.kt create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt diff --git a/firebase-sessions/src/main/AndroidManifest.xml b/firebase-sessions/src/main/AndroidManifest.xml index 6e6267631a3..55e05b0ba80 100644 --- a/firebase-sessions/src/main/AndroidManifest.xml +++ b/firebase-sessions/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataService.kt deleted file mode 100644 index 9394c297de8..00000000000 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataService.kt +++ /dev/null @@ -1,198 +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.sessions - -import android.app.Activity -import android.app.Service -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.Build -import android.os.Bundle -import android.os.DeadObjectException -import android.os.Handler -import android.os.IBinder -import android.os.Looper -import android.os.Message -import android.os.Messenger -import android.os.RemoteException -import android.util.Log -import java.util.LinkedList -import java.util.UUID - -/** Service for providing access to session data */ -internal class SessionDataService : Service() { - /** Target we publish for clients to send messages to IncomingHandler. */ - private lateinit var messenger: Messenger - - val boundClients = mutableListOf() - - /** Handler of incoming messages from clients. */ - internal inner class IncomingHandler( - context: Context, - private val appContext: Context = context.applicationContext, - ) : Handler(Looper.getMainLooper()) { // TODO(rothbutter) probably want to use our own executor - - private var curSessionId: String? = null - - override fun handleMessage(msg: Message) { - when (msg.what) { - FOREGROUNDED -> handleForegrounding() - BACKGROUNDED -> handleBackgrounding() - else -> super.handleMessage(msg) - } - } - - fun handleForegrounding() { - Log.i(TAG, "SERVICE: Activity foregrounding - updating ${boundClients.size} clients") - broadcastSession(UUID.randomUUID().toString()) - } - - fun handleBackgrounding() { - Log.i(TAG, "SERVICE: Activity backgrounding") - } - - fun broadcastSession(sessionId: String) { - Log.i(TAG, "SERVICE: Broadcasting new session $sessionId") - boundClients.forEach { sendNewSession(it, sessionId) } - } - - fun sendNewSession(client: Messenger, sessionId: String) { - try { - client.send( - Message.obtain(null, SESSION_UPDATED, 0, 0).also { - it.setData(Bundle().also { it.putString(SESSION_UPDATE_EXTRA, sessionId) }) - } - ) - } catch (e: Exception) { - if (e is DeadObjectException) { - Log.i(TAG, "SERVICE: Removing dead client from list: $client") - boundClients.remove(client) - } else { - Log.e(TAG, "SERVICE: Unable to push new session to $client.", e) - } - } - } - } - - override fun onBind(intent: Intent): IBinder? { - Log.i(TAG, "SERVICE: Service bound") - messenger = Messenger(IncomingHandler(this)) - val callbackMessenger = getCallback(intent) - if (callbackMessenger != null) { - boundClients.add(callbackMessenger) - Log.i(TAG, "SERVICE: Stored callback to $callbackMessenger. Size: ${boundClients.size}") - } - return messenger.binder - } - - private fun getCallback(intent: Intent): Messenger? = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER, Messenger::class.java) - } else { - intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER) - } - - internal companion object { - const val TAG = "SessionDataService" - const val CLIENT_CALLBACK_MESSENGER = "ClientCallbackMessenger" - const val SESSION_UPDATE_EXTRA = "SessionUpdateExtra" - - const val FOREGROUNDED = 1 - const val BACKGROUNDED = 2 - const val SESSION_UPDATED = 3 - - private var testService: Messenger? = null - private var testServiceBound: Boolean = false - private val queuedMessages = LinkedList() - - internal class ClientUpdateHandler(appContext: Context) : Handler(Looper.getMainLooper()) { - override fun handleMessage(msg: Message) { - when (msg.what) { - SESSION_UPDATED -> - handleSessionUpdate(msg.data?.getString(SESSION_UPDATE_EXTRA) ?: "no-id-given") - else -> super.handleMessage(msg) - } - } - - fun handleSessionUpdate(sessionId: String) { - Log.i(TAG, "CLIENT: Session update received: $sessionId") - } - } - - private val testServiceConnection = - object : ServiceConnection { - override fun onServiceConnected(className: ComponentName, service: IBinder) { - Log.i(TAG, "CLIENT: Connected to SessionDataService. Queue size ${queuedMessages.size}") - testService = Messenger(service) - testServiceBound = true - val queueItr = queuedMessages.iterator() - for (msg in queueItr) { - Log.i(TAG, "CLIENT: sending queued message ${msg.what}") - sendMessage(msg) - queueItr.remove() - } - } - - override fun onServiceDisconnected(className: ComponentName) { - Log.i(TAG, "CLIENT: Disconnected from SessionDataService") - testService = null - testServiceBound = false - } - } - - fun bind(appContext: Context): Unit { - Intent(appContext, SessionDataService::class.java).also { intent -> - Log.i(TAG, "CLIENT: Binding service to application.") - // This is necessary for the onBind() to be called by each process - intent.setAction(android.os.Process.myPid().toString()) - intent.putExtra(CLIENT_CALLBACK_MESSENGER, Messenger(ClientUpdateHandler(appContext))) - appContext.bindService( - intent, - testServiceConnection, - Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE - ) - } - } - - fun foregrounded(activity: Activity): Unit { - sendMessage(FOREGROUNDED) - } - - fun backgrounded(activity: Activity): Unit { - sendMessage(BACKGROUNDED) - } - - private fun sendMessage(messageCode: Int) { - sendMessage(Message.obtain(null, messageCode, 0, 0)) - } - - private fun sendMessage(msg: Message) { - if (testService != null) { - try { - testService?.send(msg) - } catch (e: RemoteException) { - Log.e(TAG, "CLIENT: Unable to deliver message: ${msg.what}") - } - } else { - Log.i(TAG, "CLIENT: Queueing message ${msg.what}") - queuedMessages.add(msg) - } - } - } -} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt index ccdd6c07c80..f808b4bcc4f 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt @@ -66,9 +66,11 @@ internal class SessionInitiator( internal val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks { - override fun onActivityResumed(activity: Activity) = SessionDataService.foregrounded(activity) + override fun onActivityResumed(activity: Activity) = + SessionLifecycleClient.foregrounded(activity) - override fun onActivityPaused(activity: Activity) = SessionDataService.backgrounded(activity) + override fun onActivityPaused(activity: Activity) = + SessionLifecycleClient.backgrounded(activity) override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt new file mode 100644 index 00000000000..bfa3d43a9ca --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt @@ -0,0 +1,198 @@ +/* + * 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.sessions + +import android.app.Activity +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.Message +import android.os.Messenger +import android.os.RemoteException +import android.util.Log +import java.util.concurrent.LinkedBlockingDeque + +/** + * Client for binding to the [SessionLifecycleService]. This client will receive updated sessions + * through a callback whenever a new session is generated by the service, or after the initial + * binding. + * + * Note: this client will be connected in every application process that uses Firebase, and is + * intended to maintain that connection for the lifetime of the process. + */ +internal object SessionLifecycleClient { + const val TAG = "SessionLifecycleClient" + + /** + * The maximum number of messages that we should queue up for delivery to the + * [SessionLifecycleService] in the event that we have lost the connection. + */ + const val MAX_QUEUED_MESSAGES = 20 + + private var service: Messenger? = null + private var serviceBound: Boolean = false + private val queuedMessages = LinkedBlockingDeque(MAX_QUEUED_MESSAGES) + private var curSessionId: String = "" + + /** + * The callback class that will be used to receive updated session events from the + * [SessionLifecycleService]. + */ + internal class ClientUpdateHandler() : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + when (msg.what) { + SessionLifecycleService.SESSION_UPDATED -> + handleSessionUpdate( + msg.data?.getString(SessionLifecycleService.SESSION_UPDATE_EXTRA) ?: "" + ) + else -> { + Log.w(TAG, "Received unexpected event from the SessionLifecycleService: $msg") + super.handleMessage(msg) + } + } + } + + fun handleSessionUpdate(sessionId: String) { + Log.i(TAG, "Session update received: $sessionId") + curSessionId = sessionId + } + } + + /** The connection object to the [SessionLifecycleService]. */ + private val serviceConnection = + object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, serviceBinder: IBinder) { + Log.i(TAG, "Connected to SessionLifecycleService. Queue size ${queuedMessages.size}") + service = Messenger(serviceBinder) + serviceBound = true + sendLifecycleEvents(drainQueue()) + } + + override fun onServiceDisconnected(className: ComponentName) { + Log.i(TAG, "Disconnected from SessionLifecycleService") + service = null + serviceBound = false + } + } + + /** + * Binds to the [SessionLifecycleService] and passes a callback [Messenger] that will be used to + * relay session updates to this client. + */ + fun bindToService(appContext: Context): Unit { + Intent(appContext, SessionLifecycleService::class.java).also { intent -> + Log.i(TAG, "Binding service to application.") + // This is necessary for the onBind() to be called by each process + intent.setAction(android.os.Process.myPid().toString()) + intent.putExtra( + SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, + Messenger(ClientUpdateHandler()) + ) + appContext.bindService( + intent, + serviceConnection, + Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE + ) + } + } + + /** + * Should be called when any activity in this application process goes to the foreground. This + * will relay the event to the [SessionLifecycleService] where it can make the determination of + * whether or not this foregrounding event should result in a new session being generated. + */ + fun foregrounded(activity: Activity): Unit { + sendLifecycleEvent(SessionLifecycleService.FOREGROUNDED) + } + + /** + * Should be called when any activity in this application process goes from the foreground to the + * background. This will relay the event to the [SessionLifecycleService] where it will be used to + * determine when a new session should be generated. + */ + fun backgrounded(activity: Activity): Unit { + sendLifecycleEvent(SessionLifecycleService.BACKGROUNDED) + } + + /** + * Sends a message to the [SessionLifecycleService] with the given event code. This will + * potentially also send any messages that have been queued up but not successfully delivered to + * thes service since the previous send. + */ + private fun sendLifecycleEvent(messageCode: Int) { + val allMessages = drainQueue() + allMessages.add(Message.obtain(null, messageCode, 0, 0)) + sendLifecycleEvents(allMessages) + } + + /** + * Sends lifecycle events to the [SessionLifecycleService]. This will only send the latest + * FOREGROUND and BACKGROUND events to the service that are included in the given list. Running + * through the full backlog of messages is not useful since the service only cares about the + * current state and transitions from background -> foreground. + */ + private fun sendLifecycleEvents(messages: List) { + val latest = ArrayList() + getLatestByCode(messages, SessionLifecycleService.BACKGROUNDED)?.let { latest.add(it) } + getLatestByCode(messages, SessionLifecycleService.FOREGROUNDED)?.let { latest.add(it) } + latest.sortBy { it.getWhen() } + + latest.forEach { sendMessageToServer(it) } + } + + /** Sends the given [Message] to the [SessionLifecycleService]. */ + private fun sendMessageToServer(msg: Message) { + if (service != null) { + try { + Log.i(TAG, "Sending lifecycle ${msg.what} at time ${msg.getWhen()} to service") + service?.send(msg) + } catch (e: RemoteException) { + Log.e(TAG, "Unable to deliver message: ${msg.what}") + queueMessage(msg) + } + } else { + queueMessage(msg) + } + } + + /** + * Queues the given [Message] up for delivery to the [SessionLifecycleService] once the connection + * is established. + */ + private fun queueMessage(msg: Message) { + if (queuedMessages.offer(msg)) { + Log.i(TAG, "Queued message ${msg.what} at ${msg.getWhen()}") + } else { + Log.i(TAG, "Failed to enqueue message ${msg.what} at ${msg.getWhen()}. Dropping.") + } + } + + /** Drains the queue of messages into a new list in a thread-safe manner. */ + private fun drainQueue(): MutableList { + val messages = ArrayList() + queuedMessages.drainTo(messages) + return messages + } + + /** Gets the message in the given list with the given code that has the latest timestamp. */ + private fun getLatestByCode(messages: List, msgCode: Int): Message? = + messages.filter { it.what == msgCode }.maxByOrNull { it.getWhen() } +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt new file mode 100644 index 00000000000..1e4bd230c7f --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -0,0 +1,174 @@ +/* + * 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.sessions + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.DeadObjectException +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.Message +import android.os.Messenger +import android.util.Log +import java.util.UUID + +/** + * Service for monitoring application lifecycle events and determining when/if a new session should + * be generated. When this happens, the service will broadcast the updated session id to all + * connected clients. + */ +internal class SessionLifecycleService : Service() { + + private val boundClients = mutableListOf() + private var curSessionId: String? = null + private var lastMsgTimeMs: Long = 0 + + /** + * Handler of incoming activity lifecycle events being received from [SessionLifecycleClient]s. + * All incoming communication from connected clients comes through this class and will be used to + * determine when new sessions should be created. + */ + internal inner class IncomingHandler( + context: Context, + private val appContext: Context = context.applicationContext, + ) : Handler(Looper.getMainLooper()) { // TODO(rothbutter) probably want to use our own executor + + override fun handleMessage(msg: Message) { + if (lastMsgTimeMs > msg.getWhen()) { + Log.i(TAG, "Received old message $msg. Ignoring") + return + } + when (msg.what) { + FOREGROUNDED -> handleForegrounding(msg) + BACKGROUNDED -> handleBackgrounding(msg) + else -> { + Log.w(TAG, "Received unexpected event from the SessionLifecycleClient: $msg") + super.handleMessage(msg) + } + } + lastMsgTimeMs = msg.getWhen() + } + } + + /** Called when a new [SessionLifecycleClient] binds to this service. */ + override fun onBind(intent: Intent): IBinder? { + Log.i(TAG, "Service bound") + val messenger = Messenger(IncomingHandler(this)) + val callbackMessenger = getClientCallback(intent) + if (callbackMessenger != null) { + boundClients.add(callbackMessenger) + sendSessionToClient(callbackMessenger) + Log.i(TAG, "Stored callback to $callbackMessenger. Size: ${boundClients.size}") + } + return messenger.binder + } + + /** + * Handles a foregrounding event by any activity owned by the aplication as specified by the given + * [Message]. This will determine if the foregrounding should result in the creation of a new + * session. + */ + private fun handleForegrounding(msg: Message) { + Log.i(TAG, "Activity foregrounding at ${msg.getWhen()}") + + if (curSessionId == null) { + Log.i(TAG, "Cold start detected.") + newSession() + } else if (msg.getWhen() - lastMsgTimeMs > MAX_BACKGROUND_MS) { + Log.i(TAG, "Session too long in background. Creating new session.") + newSession() + } + } + + /** + * Handles a backgrounding event by any activity owned by the application as specified by the + * given [Message]. This will keep track of the backgrounding and be used to determine if future + * foregrounding events should result in the creation of a new session. + */ + private fun handleBackgrounding(msg: Message) { + Log.i(TAG, "Activity backgrounding at ${msg.getWhen()}") + } + + /** + * Triggers the creation of a new session, and broadcasts that session to all connected clients. + */ + private fun newSession() { + curSessionId = UUID.randomUUID().toString() + Log.i(TAG, "Broadcasting session $curSessionId to ${boundClients.size} clients") + boundClients.forEach { sendSessionToClient(it) } + } + + /** Sends the current session id to the client connected through the given [Messenger]. */ + private fun sendSessionToClient(client: Messenger) { + try { + client.send( + Message.obtain(null, SESSION_UPDATED, 0, 0).also { + it.setData(Bundle().also { it.putString(SESSION_UPDATE_EXTRA, curSessionId) }) + } + ) + } catch (e: DeadObjectException) { + Log.i(TAG, "Removing dead client from list: $client") + boundClients.remove(client) + } catch (e: Exception) { + Log.e(TAG, "Unable to push new session to $client.", e) + } + } + + /** + * Extracts the callback [Messenger] from the given [Intent] which will be used to push session + * updates back to the [SessionLifecycleClient] that created this [Intent]. + */ + private fun getClientCallback(intent: Intent): Messenger? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER, Messenger::class.java) + } else { + intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER) + } + + internal companion object { + const val TAG = "SessionLifecycleService" + + /** + * Key for the [Messenger] callback extra included in the [Intent] used by the + * [SessionLifecycleClient] to bind to this service. + */ + const val CLIENT_CALLBACK_MESSENGER = "ClientCallbackMessenger" + + /** + * Key for the extra String included in the [SESSION_UPDATED] message, sent to all connected + * clients, containing an updated session id. + */ + const val SESSION_UPDATE_EXTRA = "SessionUpdateExtra" + + // TODO(bryanatkinson): Swap this out for the value coming from settings + const val MAX_BACKGROUND_MS = 30000 + + /** [Message] code indicating that an application activity has gone to the foreground */ + const val FOREGROUNDED = 1 + /** [Message] code indicating that an application activity has gone to the background */ + const val BACKGROUNDED = 2 + /** + * [Message] code indicating that a new session has been started, and containing the new session + * id in the [SESSION_UPDATE_EXTRA] extra field. + */ + const val SESSION_UPDATED = 3 + } +} From e80929f495cf270b94b5fa20b38a81f4af0c0fa3 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Thu, 12 Oct 2023 14:13:33 -0400 Subject: [PATCH 05/38] Avoid changing api surface (#5406) Avoid changing the api surface. Having `val`s at the file level causes Kotlin to generate a public class with the filenameKt which adds a public class to the public api surface: "The public api surface has changed for the subproject firebase-sessions: error: Added class com.google.firebase.sessions.SessionDatastoreKt [AddedClass]". So we have to put them in a class or companion object. --- .../com/google/firebase/sessions/SessionDatastore.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt index e3d534514b2..6e9bedaed2c 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt @@ -65,7 +65,9 @@ internal class SessionDatastore(private val context: Context) { preferences[FirebaseSessionDataKeys.SESSION_ID], preferences[FirebaseSessionDataKeys.TIMESTAMP_MICROSECONDS] ) -} -private val Context.dataStore: DataStore by - preferencesDataStore(name = "firebase_session_settings") + private companion object { + private val Context.dataStore: DataStore by + preferencesDataStore(name = "firebase_session_settings") + } +} From 2a7e35443afa4dc7dd79e3f93bc095e7fe84f1e4 Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Thu, 12 Oct 2023 14:23:44 -0400 Subject: [PATCH 06/38] Rename SessionCoordinator to SessionFirelogPublisher and add `getInstance` so it can be accessed from a bound service. (#5407) Co-authored-by: jrothfeder --- .../firebase/sessions/FirebaseSessions.kt | 8 ++------ .../sessions/FirebaseSessionsRegistrar.kt | 17 ++++++++++++++-- ...rdinator.kt => SessionFirelogPublisher.kt} | 20 ++++++++++++++----- ...Test.kt => SessionFirelogPublisherTest.kt} | 4 ++-- 4 files changed, 34 insertions(+), 15 deletions(-) rename firebase-sessions/src/main/kotlin/com/google/firebase/sessions/{SessionCoordinator.kt => SessionFirelogPublisher.kt} (74%) rename firebase-sessions/src/test/kotlin/com/google/firebase/sessions/{SessionCoordinatorTest.kt => SessionFirelogPublisherTest.kt} (97%) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index ac4233cdc0e..df3017727d0 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -18,11 +18,9 @@ package com.google.firebase.sessions import android.app.Application import android.util.Log -import com.google.android.datatransport.TransportFactory import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app -import com.google.firebase.inject.Provider import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.api.SessionSubscriber @@ -36,7 +34,7 @@ internal constructor( firebaseInstallations: FirebaseInstallationsApi, backgroundDispatcher: CoroutineDispatcher, blockingDispatcher: CoroutineDispatcher, - transportFactoryProvider: Provider, + private val sessionFirelogPublisher: SessionFirelogPublisher, @Suppress("UNUSED_PARAMETER") sessionMaintainer: SessionMaintainer, ) { private val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp) @@ -50,8 +48,6 @@ internal constructor( ) private val timeProvider: TimeProvider = Time() private val sessionGenerator: SessionGenerator - private val eventGDTLogger = EventGDTLogger(transportFactoryProvider) - private val sessionCoordinator = SessionCoordinator(firebaseInstallations, eventGDTLogger) init { sessionGenerator = SessionGenerator(collectEvents = shouldCollectEvents(), timeProvider) @@ -148,7 +144,7 @@ internal constructor( try { val sessionEvent = SessionEvents.startSession(firebaseApp, sessionDetails, sessionSettings, subscribers) - sessionCoordinator.attemptLoggingSessionEvent(sessionEvent) + sessionFirelogPublisher.attemptLoggingSessionEvent(sessionEvent) } catch (ex: IllegalStateException) { // This can happen if the app suddenly deletes the instance of FirebaseApp. Log.w( diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index 27e7c3e9ea3..80ca3684976 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -43,15 +43,15 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { .add(Dependency.required(firebaseInstallationsApi)) .add(Dependency.required(backgroundDispatcher)) .add(Dependency.required(blockingDispatcher)) - .add(Dependency.requiredProvider(transportFactory)) .add(Dependency.required(sessionMaintainer)) + .add(Dependency.required(sessionFirelogPublisher)) .factory { container -> FirebaseSessions( container.get(firebaseApp), container.get(firebaseInstallationsApi), container.get(backgroundDispatcher), container.get(blockingDispatcher), - container.getProvider(transportFactory), + container.get(sessionFirelogPublisher), container.get(sessionMaintainer) ) } @@ -60,12 +60,24 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { .name(MAINTAINER_LIBRARY_NAME) .factory { SessionMaintainer() } .build(), + Component.builder(SessionFirelogPublisher::class.java) + .name(SESSIONS_PUBLISHER) + .add(Dependency.required(firebaseInstallationsApi)) + .add(Dependency.requiredProvider(transportFactory)) + .factory { container -> + SessionFirelogPublisher( + container.get(firebaseInstallationsApi), + EventGDTLogger(container.getProvider(transportFactory)) + ) + } + .build(), LibraryVersionComponent.create(SESSIONS_LIBRARY_NAME, BuildConfig.VERSION_NAME), ) private companion object { private const val SESSIONS_LIBRARY_NAME = "fire-sessions" private const val MAINTAINER_LIBRARY_NAME = "fire-session-maintainer" + private const val SESSIONS_PUBLISHER = "sessions-publisher" private val sessionMaintainer = unqualified(SessionMaintainer::class.java) private val firebaseApp = unqualified(FirebaseApp::class.java) @@ -75,5 +87,6 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { private val blockingDispatcher = qualified(Blocking::class.java, CoroutineDispatcher::class.java) private val transportFactory = unqualified(TransportFactory::class.java) + private val sessionFirelogPublisher = unqualified(SessionFirelogPublisher::class.java) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionCoordinator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt similarity index 74% rename from firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionCoordinator.kt rename to firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt index 3cf9f13a3ff..53c63089e55 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionCoordinator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt @@ -17,16 +17,18 @@ package com.google.firebase.sessions import android.util.Log +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.app import com.google.firebase.installations.FirebaseInstallationsApi import kotlinx.coroutines.tasks.await /** - * [SessionCoordinator] is responsible for coordinating the systems in this SDK involved with - * sending a [SessionEvent]. + * [SessionFirelogPublisher] is responsible for publishing sessions to firelog * * @hide */ -internal class SessionCoordinator( +internal class SessionFirelogPublisher( private val firebaseInstallations: FirebaseInstallationsApi, private val eventGDTLogger: EventGDTLoggerInterface, ) { @@ -49,7 +51,15 @@ internal class SessionCoordinator( } } - companion object { - private const val TAG = "SessionCoordinator" + internal companion object { + const val TAG = "SessionFirelogPublisher" + + @JvmStatic + fun getInstance(app: FirebaseApp): SessionFirelogPublisher = + app.get(SessionFirelogPublisher::class.java) + + @JvmStatic + val instance: SessionFirelogPublisher + get() = getInstance(Firebase.app) } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionCoordinatorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionFirelogPublisherTest.kt similarity index 97% rename from firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionCoordinatorTest.kt rename to firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionFirelogPublisherTest.kt index fbf17a72083..96ebf92582f 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionCoordinatorTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionFirelogPublisherTest.kt @@ -34,13 +34,13 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) -class SessionCoordinatorTest { +class SessionFirelogPublisherTest { @Test fun attemptLoggingSessionEvent_populatesFid() = runTest { val fakeEventGDTLogger = FakeEventGDTLogger() val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val sessionCoordinator = - SessionCoordinator( + SessionFirelogPublisher( firebaseInstallations, eventGDTLogger = fakeEventGDTLogger, ) From 3cb8371dc00a1526c34006944fddddf1362eff85 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Fri, 13 Oct 2023 06:10:23 -0400 Subject: [PATCH 07/38] Make SessionGenerator injectable (#5412) Make `SessionGenerator` injectable. Also made `WallClock` an object to avoid needing to pass it around. Moved `collectEvents` out of SessionGenerator since it will need to be handled in the service. --- .../com/google/firebase/sessions/FirebaseSessions.kt | 12 +++++++----- .../firebase/sessions/FirebaseSessionsRegistrar.kt | 10 +++++++++- .../com/google/firebase/sessions/SessionGenerator.kt | 11 ++++++++++- .../firebase/sessions/{Time.kt => TimeProvider.kt} | 8 +++----- .../google/firebase/sessions/SessionGeneratorTest.kt | 6 ------ .../google/firebase/sessions/SessionInitiatorTest.kt | 10 +++++----- 6 files changed, 34 insertions(+), 23 deletions(-) rename firebase-sessions/src/main/kotlin/com/google/firebase/sessions/{Time.kt => TimeProvider.kt} (92%) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index df3017727d0..63a4d615250 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -36,6 +36,7 @@ internal constructor( blockingDispatcher: CoroutineDispatcher, private val sessionFirelogPublisher: SessionFirelogPublisher, @Suppress("UNUSED_PARAMETER") sessionMaintainer: SessionMaintainer, + private val sessionGenerator: SessionGenerator, ) { private val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp) private val sessionSettings = @@ -46,11 +47,12 @@ internal constructor( firebaseInstallations, applicationInfo, ) - private val timeProvider: TimeProvider = Time() - private val sessionGenerator: SessionGenerator + + // TODO: This needs to be moved into the service to be consistent across multiple processes. + private val collectEvents: Boolean init { - sessionGenerator = SessionGenerator(collectEvents = shouldCollectEvents(), timeProvider) + collectEvents = shouldCollectEvents() val sessionInitiateListener = object : SessionInitiateListener { @@ -62,7 +64,7 @@ internal constructor( val sessionInitiator = SessionInitiator( - timeProvider, + timeProvider = WallClock, backgroundDispatcher, sessionInitiateListener, sessionSettings, @@ -136,7 +138,7 @@ internal constructor( return } - if (!sessionGenerator.collectEvents) { + if (!collectEvents) { Log.d(TAG, "Sessions SDK has dropped this session due to sampling.") return } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index 80ca3684976..559e0721520 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -45,6 +45,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { .add(Dependency.required(blockingDispatcher)) .add(Dependency.required(sessionMaintainer)) .add(Dependency.required(sessionFirelogPublisher)) + .add(Dependency.required(sessionGenerator)) .factory { container -> FirebaseSessions( container.get(firebaseApp), @@ -52,7 +53,8 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { container.get(backgroundDispatcher), container.get(blockingDispatcher), container.get(sessionFirelogPublisher), - container.get(sessionMaintainer) + container.get(sessionMaintainer), + container.get(sessionGenerator), ) } .build(), @@ -60,6 +62,10 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { .name(MAINTAINER_LIBRARY_NAME) .factory { SessionMaintainer() } .build(), + Component.builder(SessionGenerator::class.java) + .name(SESSION_GENERATOR) + .factory { SessionGenerator(timeProvider = WallClock) } + .build(), Component.builder(SessionFirelogPublisher::class.java) .name(SESSIONS_PUBLISHER) .add(Dependency.required(firebaseInstallationsApi)) @@ -78,6 +84,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { private const val SESSIONS_LIBRARY_NAME = "fire-sessions" private const val MAINTAINER_LIBRARY_NAME = "fire-session-maintainer" private const val SESSIONS_PUBLISHER = "sessions-publisher" + private const val SESSION_GENERATOR = "session-generator" private val sessionMaintainer = unqualified(SessionMaintainer::class.java) private val firebaseApp = unqualified(FirebaseApp::class.java) @@ -88,5 +95,6 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { qualified(Blocking::class.java, CoroutineDispatcher::class.java) private val transportFactory = unqualified(TransportFactory::class.java) private val sessionFirelogPublisher = unqualified(SessionFirelogPublisher::class.java) + private val sessionGenerator = unqualified(SessionGenerator::class.java) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt index b526dc0558d..28920c3d497 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt @@ -16,6 +16,9 @@ package com.google.firebase.sessions +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.app import java.util.UUID /** @@ -33,7 +36,6 @@ internal data class SessionDetails( * [SessionDetails] up to date with the latest values. */ internal class SessionGenerator( - val collectEvents: Boolean, private val timeProvider: TimeProvider, private val uuidGenerator: () -> UUID = UUID::randomUUID ) { @@ -62,4 +64,11 @@ internal class SessionGenerator( } private fun generateSessionId() = uuidGenerator().toString().replace("-", "").lowercase() + + internal companion object { + val instance: SessionGenerator + get() = getInstance(Firebase.app) + + fun getInstance(app: FirebaseApp): SessionGenerator = app.get(SessionGenerator::class.java) + } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Time.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt similarity index 92% rename from firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Time.kt rename to firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt index d7216b40937..706285de337 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Time.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt @@ -27,7 +27,7 @@ internal interface TimeProvider { } /** "Wall clock" time provider. */ -internal class Time : TimeProvider { +internal object WallClock : TimeProvider { /** * Gets the [Duration] elapsed in "wall clock" time since device boot. * @@ -45,8 +45,6 @@ internal class Time : TimeProvider { */ override fun currentTimeUs(): Long = System.currentTimeMillis() * US_PER_MILLIS - companion object { - /** Microseconds per millisecond. */ - private const val US_PER_MILLIS = 1000L - } + /** Microseconds per millisecond. */ + private const val US_PER_MILLIS = 1000L } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt index 59be72ea4b4..7f29fb66ae7 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt @@ -42,7 +42,6 @@ class SessionGeneratorTest { fun currentSession_beforeGenerate_throwsUninitialized() { val sessionGenerator = SessionGenerator( - collectEvents = false, timeProvider = FakeTimeProvider(), ) @@ -53,7 +52,6 @@ class SessionGeneratorTest { fun hasGenerateSession_beforeGenerate_returnsFalse() { val sessionGenerator = SessionGenerator( - collectEvents = false, timeProvider = FakeTimeProvider(), ) @@ -64,7 +62,6 @@ class SessionGeneratorTest { fun hasGenerateSession_afterGenerate_returnsTrue() { val sessionGenerator = SessionGenerator( - collectEvents = false, timeProvider = FakeTimeProvider(), ) @@ -77,7 +74,6 @@ class SessionGeneratorTest { fun generateNewSession_generatesValidSessionIds() { val sessionGenerator = SessionGenerator( - collectEvents = true, timeProvider = FakeTimeProvider(), ) @@ -96,7 +92,6 @@ class SessionGeneratorTest { fun generateNewSession_generatesValidSessionDetails() { val sessionGenerator = SessionGenerator( - collectEvents = true, timeProvider = FakeTimeProvider(), uuidGenerator = UUIDs()::next, ) @@ -123,7 +118,6 @@ class SessionGeneratorTest { fun generateNewSession_incrementsSessionIndex_keepsFirstSessionId() { val sessionGenerator = SessionGenerator( - collectEvents = true, timeProvider = FakeTimeProvider(), uuidGenerator = UUIDs()::next, ) diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionInitiatorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionInitiatorTest.kt index 891c18796ec..6a62dfc7023 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionInitiatorTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionInitiatorTest.kt @@ -60,7 +60,7 @@ class SessionInitiatorTest { TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, sessionInitiateListener = sessionInitiateCounter, settings, - SessionGenerator(collectEvents = false, fakeTimeProvider), + SessionGenerator(fakeTimeProvider), ) // Run onInitiateSession suspend function. @@ -86,7 +86,7 @@ class SessionInitiatorTest { TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, sessionInitiateListener = sessionInitiateCounter, settings, - SessionGenerator(collectEvents = false, fakeTimeProvider), + SessionGenerator(fakeTimeProvider), ) // Run onInitiateSession suspend function. @@ -121,7 +121,7 @@ class SessionInitiatorTest { TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, sessionInitiateListener = sessionInitiateCounter, settings, - SessionGenerator(collectEvents = false, fakeTimeProvider), + SessionGenerator(fakeTimeProvider), ) // Run onInitiateSession suspend function. @@ -156,7 +156,7 @@ class SessionInitiatorTest { TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, sessionInitiateListener = sessionInitiateCounter, settings, - SessionGenerator(collectEvents = false, fakeTimeProvider), + SessionGenerator(fakeTimeProvider), ) // Run onInitiateSession suspend function. @@ -196,7 +196,7 @@ class SessionInitiatorTest { TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, sessionInitiateListener = sessionInitiateCounter, settings, - SessionGenerator(collectEvents = false, fakeTimeProvider), + SessionGenerator(fakeTimeProvider), ) // Run onInitiateSession suspend function. From ddfa4f558f076d3a42be5cf533e53e44b4b40328 Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Fri, 13 Oct 2023 09:32:32 -0400 Subject: [PATCH 08/38] Notify subscribers on session update. (#5411) Co-authored-by: jrothfeder --- .../google/firebase/sessions/FirebaseSessions.kt | 10 +--------- .../firebase/sessions/SessionLifecycleClient.kt | 13 ++++++++++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index 63a4d615250..371e4042ae8 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -32,7 +32,7 @@ class FirebaseSessions internal constructor( private val firebaseApp: FirebaseApp, firebaseInstallations: FirebaseInstallationsApi, - backgroundDispatcher: CoroutineDispatcher, + internal val backgroundDispatcher: CoroutineDispatcher, blockingDispatcher: CoroutineDispatcher, private val sessionFirelogPublisher: SessionFirelogPublisher, @Suppress("UNUSED_PARAMETER") sessionMaintainer: SessionMaintainer, @@ -97,14 +97,6 @@ internal constructor( "Registering Sessions SDK subscriber with name: ${subscriber.sessionSubscriberName}, " + "data collection enabled: ${subscriber.isDataCollectionEnabled}" ) - - // Immediately call the callback if Sessions generated a session before the - // subscriber subscribed, otherwise subscribers might miss the first session. - if (sessionGenerator.hasGenerateSession) { - subscriber.onSessionChanged( - SessionSubscriber.SessionDetails(sessionGenerator.currentSession.sessionId) - ) - } } private suspend fun initiateSessionStart(sessionDetails: SessionDetails) { diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt index bfa3d43a9ca..f70d0645348 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt @@ -28,7 +28,11 @@ import android.os.Message import android.os.Messenger import android.os.RemoteException import android.util.Log +import com.google.firebase.sessions.api.FirebaseSessionsDependencies +import com.google.firebase.sessions.api.SessionSubscriber import java.util.concurrent.LinkedBlockingDeque +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Client for binding to the [SessionLifecycleService]. This client will receive updated sessions @@ -70,9 +74,16 @@ internal object SessionLifecycleClient { } } - fun handleSessionUpdate(sessionId: String) { + private fun handleSessionUpdate(sessionId: String) { Log.i(TAG, "Session update received: $sessionId") curSessionId = sessionId + + CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { + FirebaseSessionsDependencies.getRegisteredSubscribers().values.forEach { subscriber -> + // Notify subscribers, regardless of sampling and data collection state. + subscriber.onSessionChanged(SessionSubscriber.SessionDetails(sessionId)) + } + } } } From 42ec418ad57a40230636481937fca541b0c6c8ad Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Fri, 13 Oct 2023 09:32:52 -0400 Subject: [PATCH 09/38] Fix a bunch of warnings on this class so that bryan can continue to have vimean superpowers. (#5413) Co-authored-by: jrothfeder --- .../google/firebase/sessions/SessionInitiator.kt | 6 ++---- .../firebase/sessions/SessionLifecycleClient.kt | 15 +++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt index f808b4bcc4f..c10ed18c2da 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt @@ -66,11 +66,9 @@ internal class SessionInitiator( internal val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks { - override fun onActivityResumed(activity: Activity) = - SessionLifecycleClient.foregrounded(activity) + override fun onActivityResumed(activity: Activity) = SessionLifecycleClient.foregrounded() - override fun onActivityPaused(activity: Activity) = - SessionLifecycleClient.backgrounded(activity) + override fun onActivityPaused(activity: Activity) = SessionLifecycleClient.backgrounded() override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt index f70d0645348..de9036730f0 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt @@ -16,7 +16,6 @@ package com.google.firebase.sessions -import android.app.Activity import android.content.ComponentName import android.content.Context import android.content.Intent @@ -49,7 +48,7 @@ internal object SessionLifecycleClient { * The maximum number of messages that we should queue up for delivery to the * [SessionLifecycleService] in the event that we have lost the connection. */ - const val MAX_QUEUED_MESSAGES = 20 + private const val MAX_QUEUED_MESSAGES = 20 private var service: Messenger? = null private var serviceBound: Boolean = false @@ -60,7 +59,7 @@ internal object SessionLifecycleClient { * The callback class that will be used to receive updated session events from the * [SessionLifecycleService]. */ - internal class ClientUpdateHandler() : Handler(Looper.getMainLooper()) { + internal class ClientUpdateHandler : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { when (msg.what) { SessionLifecycleService.SESSION_UPDATED -> @@ -108,11 +107,11 @@ internal object SessionLifecycleClient { * Binds to the [SessionLifecycleService] and passes a callback [Messenger] that will be used to * relay session updates to this client. */ - fun bindToService(appContext: Context): Unit { + fun bindToService(appContext: Context) { Intent(appContext, SessionLifecycleService::class.java).also { intent -> Log.i(TAG, "Binding service to application.") // This is necessary for the onBind() to be called by each process - intent.setAction(android.os.Process.myPid().toString()) + intent.action = android.os.Process.myPid().toString() intent.putExtra( SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, Messenger(ClientUpdateHandler()) @@ -130,7 +129,7 @@ internal object SessionLifecycleClient { * will relay the event to the [SessionLifecycleService] where it can make the determination of * whether or not this foregrounding event should result in a new session being generated. */ - fun foregrounded(activity: Activity): Unit { + fun foregrounded() { sendLifecycleEvent(SessionLifecycleService.FOREGROUNDED) } @@ -139,14 +138,14 @@ internal object SessionLifecycleClient { * background. This will relay the event to the [SessionLifecycleService] where it will be used to * determine when a new session should be generated. */ - fun backgrounded(activity: Activity): Unit { + fun backgrounded() { sendLifecycleEvent(SessionLifecycleService.BACKGROUNDED) } /** * Sends a message to the [SessionLifecycleService] with the given event code. This will * potentially also send any messages that have been queued up but not successfully delivered to - * thes service since the previous send. + * this service since the previous send. */ private fun sendLifecycleEvent(messageCode: Int) { val allMessages = drainQueue() From cdc78b7277e45b1ec212995b781927ec72609fab Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Fri, 13 Oct 2023 11:27:29 -0400 Subject: [PATCH 10/38] Hooks up the SessionGenerator to the SessionLifecycleService (#5416) Using the SessionGenerator to generate new sessions and create SessionDetails instead of directly creating UUID session ids in the service. Also updated the boundClients queue to be a LinkedBlockingQueue to avoid ConcurrentModificationExceptions if a client binds while a message is being handled by the server. --- .../sessions/SessionLifecycleService.kt | 56 +++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index 1e4bd230c7f..d695b95e6fb 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -28,7 +28,7 @@ import android.os.Looper import android.os.Message import android.os.Messenger import android.util.Log -import java.util.UUID +import java.util.concurrent.LinkedBlockingQueue /** * Service for monitoring application lifecycle events and determining when/if a new session should @@ -37,8 +37,26 @@ import java.util.UUID */ internal class SessionLifecycleService : Service() { - private val boundClients = mutableListOf() - private var curSessionId: String? = null + /** + * Queue of connected clients. + * + * Note that this needs to be a [LinkedBlockingQueue] since it is modified in the service [onBind] + * method and read in the message handler. Although the [IncomingHandler] is guaranteed to execute + * sequentially on a single thread, the [onBind] method is not guaranteed to run on that same + * thread. + */ + private val boundClients = LinkedBlockingQueue() + + /** + * Flag indicating whether or not the app has ever come into the foreground during the lifetime of + * the service. If it has not, we can infer that the first foreground event is a cold-start + */ + private var hasForegrounded: Boolean = false + + /** + * The timestamp of the last activity lifecycle message we've received from a client. Used to + * determine when the app has been idle for long enough to require a new session. + */ private var lastMsgTimeMs: Long = 0 /** @@ -89,12 +107,14 @@ internal class SessionLifecycleService : Service() { private fun handleForegrounding(msg: Message) { Log.i(TAG, "Activity foregrounding at ${msg.getWhen()}") - if (curSessionId == null) { + if (!hasForegrounded) { Log.i(TAG, "Cold start detected.") - newSession() + hasForegrounded = true + broadcastSession() } else if (msg.getWhen() - lastMsgTimeMs > MAX_BACKGROUND_MS) { Log.i(TAG, "Session too long in background. Creating new session.") - newSession() + SessionGenerator.instance.generateNewSession() + broadcastSession() } } @@ -108,22 +128,26 @@ internal class SessionLifecycleService : Service() { } /** - * Triggers the creation of a new session, and broadcasts that session to all connected clients. + * Broadcasts the current session to by uploading to Firelog and all sending a message to all + * connected clients. */ - private fun newSession() { - curSessionId = UUID.randomUUID().toString() - Log.i(TAG, "Broadcasting session $curSessionId to ${boundClients.size} clients") + private fun broadcastSession() { + Log.i(TAG, "Broadcasting new session: ${SessionGenerator.instance.currentSession}") boundClients.forEach { sendSessionToClient(it) } } - /** Sends the current session id to the client connected through the given [Messenger]. */ private fun sendSessionToClient(client: Messenger) { + if (hasForegrounded) { + sendSessionToClient(client, SessionGenerator.instance.currentSession.sessionId) + } + // TODO: Send the value from the datastore before the first foregrounding + } + + /** Sends the current session id to the client connected through the given [Messenger]. */ + private fun sendSessionToClient(client: Messenger, sessionId: String) { try { - client.send( - Message.obtain(null, SESSION_UPDATED, 0, 0).also { - it.setData(Bundle().also { it.putString(SESSION_UPDATE_EXTRA, curSessionId) }) - } - ) + val msgData = Bundle().also { it.putString(SESSION_UPDATE_EXTRA, sessionId) } + client.send(Message.obtain(null, SESSION_UPDATED, 0, 0).also { it.setData(msgData) }) } catch (e: DeadObjectException) { Log.i(TAG, "Removing dead client from list: $client") boundClients.remove(client) From 716b65cf9432842245b4ee8ed62d97afefdf0516 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Fri, 13 Oct 2023 12:56:18 -0400 Subject: [PATCH 11/38] Make SessionsSettings injectable (#5415) Made SessionsSettings injectable. Also removed SessionMaintainer. --- .../sessions/FirebaseSessionsTests.kt | 8 ++++ .../firebase/sessions/FirebaseSessions.kt | 24 +--------- .../sessions/FirebaseSessionsRegistrar.kt | 47 ++++++++++--------- .../sessions/SessionFirelogPublisher.kt | 2 - .../sessions/SessionInitiateListener.kt | 2 +- .../firebase/sessions/SessionMaintainer.kt | 34 -------------- .../sessions/settings/SessionsSettings.kt | 28 +++++++++-- 7 files changed, 62 insertions(+), 83 deletions(-) delete mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionMaintainer.kt diff --git a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt b/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt index a37687fc703..1cf67e0c5e1 100644 --- a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt +++ b/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt @@ -23,6 +23,7 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions import com.google.firebase.initialize +import com.google.firebase.sessions.settings.SessionsSettings import org.junit.After import org.junit.Before import org.junit.Test @@ -57,6 +58,13 @@ class FirebaseSessionsTests { assertThat(FirebaseSessions.instance).isNotNull() } + @Test + fun firebaseSessionsDependenciesDoInitialize() { + assertThat(SessionFirelogPublisher.instance).isNotNull() + assertThat(SessionGenerator.instance).isNotNull() + assertThat(SessionsSettings.instance).isNotNull() + } + companion object { private const val APP_ID = "1:1:android:1a" private const val API_KEY = "API-KEY-API-KEY-API-KEY-API-KEY-API-KEY" diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index 371e4042ae8..501f7d05838 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -21,7 +21,6 @@ import android.util.Log import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.app -import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.api.SessionSubscriber import com.google.firebase.sessions.settings.SessionsSettings @@ -31,22 +30,11 @@ import kotlinx.coroutines.CoroutineDispatcher class FirebaseSessions internal constructor( private val firebaseApp: FirebaseApp, - firebaseInstallations: FirebaseInstallationsApi, internal val backgroundDispatcher: CoroutineDispatcher, - blockingDispatcher: CoroutineDispatcher, private val sessionFirelogPublisher: SessionFirelogPublisher, - @Suppress("UNUSED_PARAMETER") sessionMaintainer: SessionMaintainer, private val sessionGenerator: SessionGenerator, + private val sessionSettings: SessionsSettings, ) { - private val applicationInfo = SessionEvents.getApplicationInfo(firebaseApp) - private val sessionSettings = - SessionsSettings( - firebaseApp.applicationContext, - blockingDispatcher, - backgroundDispatcher, - firebaseInstallations, - applicationInfo, - ) // TODO: This needs to be moved into the service to be consistent across multiple processes. private val collectEvents: Boolean @@ -54,19 +42,11 @@ internal constructor( init { collectEvents = shouldCollectEvents() - val sessionInitiateListener = - object : SessionInitiateListener { - // Avoid making a public function in FirebaseSessions for onInitiateSession. - override suspend fun onInitiateSession(sessionDetails: SessionDetails) { - initiateSessionStart(sessionDetails) - } - } - val sessionInitiator = SessionInitiator( timeProvider = WallClock, backgroundDispatcher, - sessionInitiateListener, + ::initiateSessionStart, sessionSettings, sessionGenerator, ) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index 559e0721520..a6bb73c7d9b 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -26,10 +26,11 @@ import com.google.firebase.components.Qualified.qualified import com.google.firebase.components.Qualified.unqualified import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.platforminfo.LibraryVersionComponent +import com.google.firebase.sessions.settings.SessionsSettings import kotlinx.coroutines.CoroutineDispatcher /** - * [ComponentRegistrar] for setting up [FirebaseSessions] and [SessionMaintainer]. + * [ComponentRegistrar] for setting up [FirebaseSessions] and its internal dependencies. * * @hide */ @@ -38,55 +39,58 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { override fun getComponents() = listOf( Component.builder(FirebaseSessions::class.java) - .name(SESSIONS_LIBRARY_NAME) + .name(LIBRARY_NAME) .add(Dependency.required(firebaseApp)) - .add(Dependency.required(firebaseInstallationsApi)) .add(Dependency.required(backgroundDispatcher)) - .add(Dependency.required(blockingDispatcher)) - .add(Dependency.required(sessionMaintainer)) .add(Dependency.required(sessionFirelogPublisher)) .add(Dependency.required(sessionGenerator)) + .add(Dependency.required(sessionsSettings)) .factory { container -> FirebaseSessions( container.get(firebaseApp), - container.get(firebaseInstallationsApi), container.get(backgroundDispatcher), - container.get(blockingDispatcher), container.get(sessionFirelogPublisher), - container.get(sessionMaintainer), container.get(sessionGenerator), + container.get(sessionsSettings), ) } .build(), - Component.builder(SessionMaintainer::class.java) - .name(MAINTAINER_LIBRARY_NAME) - .factory { SessionMaintainer() } - .build(), Component.builder(SessionGenerator::class.java) - .name(SESSION_GENERATOR) + .name("session-generator") .factory { SessionGenerator(timeProvider = WallClock) } .build(), Component.builder(SessionFirelogPublisher::class.java) - .name(SESSIONS_PUBLISHER) + .name("session-publisher") .add(Dependency.required(firebaseInstallationsApi)) .add(Dependency.requiredProvider(transportFactory)) .factory { container -> SessionFirelogPublisher( container.get(firebaseInstallationsApi), - EventGDTLogger(container.getProvider(transportFactory)) + EventGDTLogger(container.getProvider(transportFactory)), + ) + } + .build(), + Component.builder(SessionsSettings::class.java) + .name("sessions-settings") + .add(Dependency.required(firebaseApp)) + .add(Dependency.required(blockingDispatcher)) + .add(Dependency.required(backgroundDispatcher)) + .add(Dependency.required(firebaseInstallationsApi)) + .factory { container -> + SessionsSettings( + container.get(firebaseApp), + container.get(blockingDispatcher), + container.get(backgroundDispatcher), + container.get(firebaseInstallationsApi), ) } .build(), - LibraryVersionComponent.create(SESSIONS_LIBRARY_NAME, BuildConfig.VERSION_NAME), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), ) private companion object { - private const val SESSIONS_LIBRARY_NAME = "fire-sessions" - private const val MAINTAINER_LIBRARY_NAME = "fire-session-maintainer" - private const val SESSIONS_PUBLISHER = "sessions-publisher" - private const val SESSION_GENERATOR = "session-generator" + private const val LIBRARY_NAME = "fire-sessions" - private val sessionMaintainer = unqualified(SessionMaintainer::class.java) private val firebaseApp = unqualified(FirebaseApp::class.java) private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java) private val backgroundDispatcher = @@ -96,5 +100,6 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { private val transportFactory = unqualified(TransportFactory::class.java) private val sessionFirelogPublisher = unqualified(SessionFirelogPublisher::class.java) private val sessionGenerator = unqualified(SessionGenerator::class.java) + private val sessionsSettings = unqualified(SessionsSettings::class.java) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt index 53c63089e55..f25fa298f96 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt @@ -54,11 +54,9 @@ internal class SessionFirelogPublisher( internal companion object { const val TAG = "SessionFirelogPublisher" - @JvmStatic fun getInstance(app: FirebaseApp): SessionFirelogPublisher = app.get(SessionFirelogPublisher::class.java) - @JvmStatic val instance: SessionFirelogPublisher get() = getInstance(Firebase.app) } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiateListener.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiateListener.kt index 13d37d91703..55ab9d128e9 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiateListener.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiateListener.kt @@ -17,7 +17,7 @@ package com.google.firebase.sessions /** Interface for listening to the initiation of a new session. */ -internal interface SessionInitiateListener { +internal fun interface SessionInitiateListener { /** To be called whenever a new session is initiated. */ suspend fun onInitiateSession(sessionDetails: SessionDetails) } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionMaintainer.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionMaintainer.kt deleted file mode 100644 index 5bff0ffdb74..00000000000 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionMaintainer.kt +++ /dev/null @@ -1,34 +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.sessions - -import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp -import com.google.firebase.app - -/** Maintainer for Sessions. */ -internal class SessionMaintainer { - - internal companion object { - @JvmStatic - val instance: SessionMaintainer - get() = getInstance(Firebase.app) - - @JvmStatic - fun getInstance(app: FirebaseApp): SessionMaintainer = app.get(SessionMaintainer::class.java) - } -} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt index 36fa222b77b..bce836295fa 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt @@ -20,8 +20,12 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.app import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.ApplicationInfo +import com.google.firebase.sessions.SessionEvents import kotlin.coroutines.CoroutineContext import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes @@ -31,7 +35,7 @@ internal class SessionsSettings( private val localOverrideSettings: SettingsProvider, private val remoteSettings: SettingsProvider, ) { - constructor( + private constructor( context: Context, blockingDispatcher: CoroutineContext, backgroundDispatcher: CoroutineContext, @@ -53,6 +57,19 @@ internal class SessionsSettings( ), ) + constructor( + firebaseApp: FirebaseApp, + blockingDispatcher: CoroutineContext, + backgroundDispatcher: CoroutineContext, + firebaseInstallationsApi: FirebaseInstallationsApi + ) : this( + firebaseApp.applicationContext, + blockingDispatcher, + backgroundDispatcher, + firebaseInstallationsApi, + SessionEvents.getApplicationInfo(firebaseApp), + ) + // Order of preference for all the configs below: // 1. Honor local overrides // 2. If no local overrides, use remote config @@ -117,8 +134,13 @@ internal class SessionsSettings( remoteSettings.updateSettings() } - private companion object { - const val SESSION_CONFIGS_NAME = "firebase_session_settings" + internal companion object { + private const val SESSION_CONFIGS_NAME = "firebase_session_settings" + + val instance: SessionsSettings + get() = getInstance(Firebase.app) + + fun getInstance(app: FirebaseApp): SessionsSettings = app.get(SessionsSettings::class.java) private val Context.dataStore: DataStore by preferencesDataStore(name = SESSION_CONFIGS_NAME) From 83d407e7651e77196da52175eb15e551ed15486e Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Mon, 16 Oct 2023 13:17:33 -0400 Subject: [PATCH 12/38] Wire datastore in to FirebaseLifecycleService (#5420) Co-authored-by: jrothfeder --- .../firebase/sessions/SessionDatastore.kt | 11 +--- .../sessions/SessionLifecycleClient.kt | 1 + .../sessions/SessionLifecycleService.kt | 52 ++++++++++++++----- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt index 6e9bedaed2c..1064aa71776 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt @@ -22,7 +22,6 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow @@ -30,14 +29,13 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map /** Datastore for sessions information */ -internal data class FirebaseSessionsData(val sessionId: String?, val timestampMicroseconds: Long?) +internal data class FirebaseSessionsData(val sessionId: String?) internal class SessionDatastore(private val context: Context) { private val tag = "FirebaseSessionsRepo" private object FirebaseSessionDataKeys { val SESSION_ID = stringPreferencesKey("session_id") - val TIMESTAMP_MICROSECONDS = longPreferencesKey("timestamp_microseconds") } internal val firebaseSessionDataFlow: Flow = @@ -54,16 +52,9 @@ internal class SessionDatastore(private val context: Context) { } } - suspend fun updateTimestamp(timestampMicroseconds: Long) { - context.dataStore.edit { preferences -> - preferences[FirebaseSessionDataKeys.TIMESTAMP_MICROSECONDS] = timestampMicroseconds - } - } - private fun mapSessionsData(preferences: Preferences): FirebaseSessionsData = FirebaseSessionsData( preferences[FirebaseSessionDataKeys.SESSION_ID], - preferences[FirebaseSessionDataKeys.TIMESTAMP_MICROSECONDS] ) private companion object { diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt index de9036730f0..5104568135f 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt @@ -59,6 +59,7 @@ internal object SessionLifecycleClient { * The callback class that will be used to receive updated session events from the * [SessionLifecycleService]. */ + // TODO(rothbutter) should we use the main looper or is there one available in this SDK? internal class ClientUpdateHandler : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { when (msg.what) { diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index d695b95e6fb..79c004d187c 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -17,7 +17,6 @@ package com.google.firebase.sessions import android.app.Service -import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle @@ -28,14 +27,21 @@ import android.os.Looper import android.os.Message import android.os.Messenger import android.util.Log +import com.google.firebase.Firebase +import com.google.firebase.app import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** * Service for monitoring application lifecycle events and determining when/if a new session should * be generated. When this happens, the service will broadcast the updated session id to all * connected clients. */ -internal class SessionLifecycleService : Service() { +internal class SessionLifecycleService( + private var datastore: SessionDatastore = SessionDatastore(Firebase.app.applicationContext) +) : Service() { /** * Queue of connected clients. @@ -59,15 +65,23 @@ internal class SessionLifecycleService : Service() { */ private var lastMsgTimeMs: Long = 0 + /** Most recent session from datastore is updated asynchronously whenever it changes */ + private val currentSessionFromDatastore: AtomicReference = AtomicReference() + + init { + CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { + datastore.firebaseSessionDataFlow.collect { currentSessionFromDatastore.set(it) } + } + } + /** * Handler of incoming activity lifecycle events being received from [SessionLifecycleClient]s. * All incoming communication from connected clients comes through this class and will be used to * determine when new sessions should be created. */ - internal inner class IncomingHandler( - context: Context, - private val appContext: Context = context.applicationContext, - ) : Handler(Looper.getMainLooper()) { // TODO(rothbutter) probably want to use our own executor + // TODO(rothbutter) there's a warning that this needs to be static and leaks may occur. Need to + // look in to this + internal inner class IncomingHandler : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { if (lastMsgTimeMs > msg.getWhen()) { @@ -89,11 +103,11 @@ internal class SessionLifecycleService : Service() { /** Called when a new [SessionLifecycleClient] binds to this service. */ override fun onBind(intent: Intent): IBinder? { Log.i(TAG, "Service bound") - val messenger = Messenger(IncomingHandler(this)) + val messenger = Messenger(IncomingHandler()) val callbackMessenger = getClientCallback(intent) if (callbackMessenger != null) { boundClients.add(callbackMessenger) - sendSessionToClient(callbackMessenger) + maybeSendSessionToClient(callbackMessenger) Log.i(TAG, "Stored callback to $callbackMessenger. Size: ${boundClients.size}") } return messenger.binder @@ -106,15 +120,22 @@ internal class SessionLifecycleService : Service() { */ private fun handleForegrounding(msg: Message) { Log.i(TAG, "Activity foregrounding at ${msg.getWhen()}") - if (!hasForegrounded) { Log.i(TAG, "Cold start detected.") hasForegrounded = true broadcastSession() + updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) } else if (msg.getWhen() - lastMsgTimeMs > MAX_BACKGROUND_MS) { Log.i(TAG, "Session too long in background. Creating new session.") SessionGenerator.instance.generateNewSession() broadcastSession() + updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) + } + } + + private fun updateSessionStorage(sessionId: String) { + CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { + sessionId.let { datastore.updateSessionId(it) } } } @@ -133,21 +154,24 @@ internal class SessionLifecycleService : Service() { */ private fun broadcastSession() { Log.i(TAG, "Broadcasting new session: ${SessionGenerator.instance.currentSession}") - boundClients.forEach { sendSessionToClient(it) } + boundClients.forEach { maybeSendSessionToClient(it) } } - private fun sendSessionToClient(client: Messenger) { + private fun maybeSendSessionToClient(client: Messenger) { if (hasForegrounded) { sendSessionToClient(client, SessionGenerator.instance.currentSession.sessionId) + } else { + // Send the value from the datastore before the first foregrounding it exists + val sessionData = currentSessionFromDatastore.get() + sessionData?.sessionId?.let { sendSessionToClient(client, it) } } - // TODO: Send the value from the datastore before the first foregrounding } /** Sends the current session id to the client connected through the given [Messenger]. */ private fun sendSessionToClient(client: Messenger, sessionId: String) { try { val msgData = Bundle().also { it.putString(SESSION_UPDATE_EXTRA, sessionId) } - client.send(Message.obtain(null, SESSION_UPDATED, 0, 0).also { it.setData(msgData) }) + client.send(Message.obtain(null, SESSION_UPDATED, 0, 0).also { it.data = msgData }) } catch (e: DeadObjectException) { Log.i(TAG, "Removing dead client from list: $client") boundClients.remove(client) @@ -164,7 +188,7 @@ internal class SessionLifecycleService : Service() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER, Messenger::class.java) } else { - intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER) + @Suppress("DEPRECATION") intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER) } internal companion object { From bcca2110efb8ccdb1c6596a1d68a10b6377c5c0e Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Tue, 17 Oct 2023 09:27:28 -0400 Subject: [PATCH 13/38] Only support the default Firebase app (#5422) This will make it safe to call `Firebase.app` any time from anywhere in this SDK. --- .../google/firebase/sessions/FirebaseSessions.kt | 15 ++++++++++++--- .../firebase/sessions/SessionFirelogPublisher.kt | 6 +----- .../google/firebase/sessions/SessionGenerator.kt | 5 +---- .../sessions/settings/SessionsSettings.kt | 4 +--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index 501f7d05838..ca6c127080b 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -32,7 +32,7 @@ internal constructor( private val firebaseApp: FirebaseApp, internal val backgroundDispatcher: CoroutineDispatcher, private val sessionFirelogPublisher: SessionFirelogPublisher, - private val sessionGenerator: SessionGenerator, + sessionGenerator: SessionGenerator, private val sessionSettings: SessionsSettings, ) { @@ -141,9 +141,18 @@ internal constructor( @JvmStatic val instance: FirebaseSessions - get() = getInstance(Firebase.app) + get() = Firebase.app.get(FirebaseSessions::class.java) @JvmStatic - fun getInstance(app: FirebaseApp): FirebaseSessions = app.get(FirebaseSessions::class.java) + @Deprecated( + "Firebase Sessions only supports the Firebase default app.", + ReplaceWith("FirebaseSessions.instance"), + ) + fun getInstance(app: FirebaseApp): FirebaseSessions = + if (app == Firebase.app) { + app.get(FirebaseSessions::class.java) + } else { + throw IllegalArgumentException("Firebase Sessions only supports the Firebase default app.") + } } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt index f25fa298f96..4138b6ee0e8 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt @@ -18,7 +18,6 @@ package com.google.firebase.sessions import android.util.Log import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.firebase.installations.FirebaseInstallationsApi import kotlinx.coroutines.tasks.await @@ -54,10 +53,7 @@ internal class SessionFirelogPublisher( internal companion object { const val TAG = "SessionFirelogPublisher" - fun getInstance(app: FirebaseApp): SessionFirelogPublisher = - app.get(SessionFirelogPublisher::class.java) - val instance: SessionFirelogPublisher - get() = getInstance(Firebase.app) + get() = Firebase.app.get(SessionFirelogPublisher::class.java) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt index 28920c3d497..a266362233a 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt @@ -17,7 +17,6 @@ package com.google.firebase.sessions import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp import com.google.firebase.app import java.util.UUID @@ -67,8 +66,6 @@ internal class SessionGenerator( internal companion object { val instance: SessionGenerator - get() = getInstance(Firebase.app) - - fun getInstance(app: FirebaseApp): SessionGenerator = app.get(SessionGenerator::class.java) + get() = Firebase.app.get(SessionGenerator::class.java) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt index bce836295fa..c51918e1d9c 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionsSettings.kt @@ -138,9 +138,7 @@ internal class SessionsSettings( private const val SESSION_CONFIGS_NAME = "firebase_session_settings" val instance: SessionsSettings - get() = getInstance(Firebase.app) - - fun getInstance(app: FirebaseApp): SessionsSettings = app.get(SessionsSettings::class.java) + get() = Firebase.app.get(SessionsSettings::class.java) private val Context.dataStore: DataStore by preferencesDataStore(name = SESSION_CONFIGS_NAME) From 771ffb722fd9f57c56fa41e94b72d166b505eadb Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Tue, 17 Oct 2023 10:21:25 -0400 Subject: [PATCH 14/38] =?UTF-8?q?Hooks=20up=20the=20SessionLifecycleServic?= =?UTF-8?q?e=20to=20the=20FirelogPublisher,=20and=20has=E2=80=A6=20(#5427)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … the FirelogPublisher handle the full SessionEvent creation since that's only relevant to Firelog. --- .../firebase/sessions/FirebaseSessions.kt | 13 ----- .../sessions/FirebaseSessionsRegistrar.kt | 6 +++ .../google/firebase/sessions/SessionEvents.kt | 2 +- .../sessions/SessionFirelogPublisher.kt | 50 +++++++++++++++---- .../sessions/SessionLifecycleService.kt | 14 ++++-- .../firebase/sessions/EventGDTLoggerTest.kt | 2 +- .../sessions/SessionEventEncoderTest.kt | 2 +- .../firebase/sessions/SessionEventTest.kt | 4 +- .../sessions/SessionFirelogPublisherTest.kt | 30 +++++------ 9 files changed, 76 insertions(+), 47 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index ca6c127080b..f583f98dabd 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -114,19 +114,6 @@ internal constructor( Log.d(TAG, "Sessions SDK has dropped this session due to sampling.") return } - - try { - val sessionEvent = - SessionEvents.startSession(firebaseApp, sessionDetails, sessionSettings, subscribers) - sessionFirelogPublisher.attemptLoggingSessionEvent(sessionEvent) - } catch (ex: IllegalStateException) { - // This can happen if the app suddenly deletes the instance of FirebaseApp. - Log.w( - TAG, - "FirebaseApp is not initialized. Sessions library will not collect session data.", - ex - ) - } } /** Calculate whether we should sample events using [sessionSettings] data. */ diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index a6bb73c7d9b..f8e0e24125e 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -61,12 +61,18 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { .build(), Component.builder(SessionFirelogPublisher::class.java) .name("session-publisher") + .add(Dependency.required(firebaseApp)) .add(Dependency.required(firebaseInstallationsApi)) + .add(Dependency.required(sessionsSettings)) .add(Dependency.requiredProvider(transportFactory)) + .add(Dependency.required(backgroundDispatcher)) .factory { container -> SessionFirelogPublisher( + container.get(firebaseApp), container.get(firebaseInstallationsApi), + container.get(sessionsSettings), EventGDTLogger(container.getProvider(transportFactory)), + container.get(backgroundDispatcher), ) } .build(), diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt index 1769c3ab978..64588a52c79 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionEvents.kt @@ -37,7 +37,7 @@ internal object SessionEvents { * * Some mutable fields, e.g. firebaseInstallationId, get populated later. */ - fun startSession( + fun buildSession( firebaseApp: FirebaseApp, sessionDetails: SessionDetails, sessionsSettings: SessionsSettings, diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt index 4138b6ee0e8..152e836084d 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt @@ -20,6 +20,11 @@ import android.util.Log import com.google.firebase.Firebase import com.google.firebase.app import com.google.firebase.installations.FirebaseInstallationsApi +import com.google.firebase.sessions.api.FirebaseSessionsDependencies +import com.google.firebase.sessions.settings.SessionsSettings +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await /** @@ -28,28 +33,53 @@ import kotlinx.coroutines.tasks.await * @hide */ internal class SessionFirelogPublisher( + private val firebaseApp: FirebaseApp, private val firebaseInstallations: FirebaseInstallationsApi, + private val sessionSettings: SessionsSettings, private val eventGDTLogger: EventGDTLoggerInterface, + private val backgroundDispatcher: CoroutineContext, ) { - suspend fun attemptLoggingSessionEvent(sessionEvent: SessionEvent) { - sessionEvent.sessionData.firebaseInstallationId = - try { - firebaseInstallations.id.await() - } catch (ex: Exception) { - Log.e(TAG, "Error getting Firebase Installation ID: ${ex}. Using an empty ID") - // Use an empty fid if there is any failure. - "" - } + /** + * Logs the session represented by the given [SessionDetails] to Firelog on a background thread. + * + * This will pull all the necessary information about the device in order to create a full + * [SessionEvent], and then upload that through the Firelog interface. + */ + fun logSession(sessionDetails: SessionDetails) { + CoroutineScope(backgroundDispatcher).launch { + val sessionEvent = + SessionEvents.buildSession( + firebaseApp, + sessionDetails, + sessionSettings, + FirebaseSessionsDependencies.getRegisteredSubscribers(), + ) + sessionEvent.sessionData.firebaseInstallationId = getFid() + attemptLoggingSessionEvent(sessionEvent) + } + } + + /** Attempts to write the given [SessionEvent] to firelog. Failures are logged and ignored. */ + private suspend fun attemptLoggingSessionEvent(sessionEvent: SessionEvent) { try { eventGDTLogger.log(sessionEvent) - Log.i(TAG, "Successfully logged Session Start event: ${sessionEvent.sessionData.sessionId}") } catch (ex: RuntimeException) { Log.e(TAG, "Error logging Session Start event to DataTransport: ", ex) } } + /** Gets the Firebase Installation ID for the current app installation. */ + private suspend fun getFid() = + try { + firebaseInstallations.id.await() + } catch (ex: Exception) { + Log.e(TAG, "Error getting Firebase Installation ID: ${ex}. Using an empty ID") + // Use an empty fid if there is any failure. + "" + } + internal companion object { const val TAG = "SessionFirelogPublisher" diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index 79c004d187c..c5c8c8fbaf0 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -29,6 +29,7 @@ import android.os.Messenger import android.util.Log import com.google.firebase.Firebase import com.google.firebase.app +import com.google.firebase.sessions.settings.SessionsSettings import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.atomic.AtomicReference import kotlinx.coroutines.CoroutineScope @@ -125,7 +126,6 @@ internal class SessionLifecycleService( hasForegrounded = true broadcastSession() updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) - } else if (msg.getWhen() - lastMsgTimeMs > MAX_BACKGROUND_MS) { Log.i(TAG, "Session too long in background. Creating new session.") SessionGenerator.instance.generateNewSession() broadcastSession() @@ -154,6 +154,7 @@ internal class SessionLifecycleService( */ private fun broadcastSession() { Log.i(TAG, "Broadcasting new session: ${SessionGenerator.instance.currentSession}") + SessionFirelogPublisher.instance.logSession(SessionGenerator.instance.currentSession) boundClients.forEach { maybeSendSessionToClient(it) } } @@ -191,6 +192,14 @@ internal class SessionLifecycleService( @Suppress("DEPRECATION") intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER) } + /** + * Determines if the foregrounding that occurred at the given time should trigger a new session + * because the app has been idle for too long. + */ + private fun isSessionRestart(foregroundTimeMs: Long) = + (foregroundTimeMs - lastMsgTimeMs) > + SessionsSettings.instance.sessionRestartTimeout.inWholeMilliseconds + internal companion object { const val TAG = "SessionLifecycleService" @@ -206,9 +215,6 @@ internal class SessionLifecycleService( */ const val SESSION_UPDATE_EXTRA = "SessionUpdateExtra" - // TODO(bryanatkinson): Swap this out for the value coming from settings - const val MAX_BACKGROUND_MS = 30000 - /** [Message] code indicating that an application activity has gone to the foreground */ const val FOREGROUNDED = 1 /** [Message] code indicating that an application activity has gone to the background */ diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/EventGDTLoggerTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/EventGDTLoggerTest.kt index 3ed1b958b89..b636d53e3dc 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/EventGDTLoggerTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/EventGDTLoggerTest.kt @@ -42,7 +42,7 @@ class EventGDTLoggerTest { fun event_logsToGoogleDataTransport() = runTest { val fakeFirebaseApp = FakeFirebaseApp() val sessionEvent = - SessionEvents.startSession( + SessionEvents.buildSession( fakeFirebaseApp.firebaseApp, TestSessionEventData.TEST_SESSION_DETAILS, SessionsSettings( diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventEncoderTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventEncoderTest.kt index e4173cb89a1..e8a1965ed0e 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventEncoderTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventEncoderTest.kt @@ -46,7 +46,7 @@ class SessionEventEncoderTest { fun sessionEvent_encodesToJson() = runTest { val fakeFirebaseApp = FakeFirebaseApp() val sessionEvent = - SessionEvents.startSession( + SessionEvents.buildSession( fakeFirebaseApp.firebaseApp, TestSessionEventData.TEST_SESSION_DETAILS, SessionsSettings( diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventTest.kt index 4ee92f8acc1..682371491df 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionEventTest.kt @@ -41,7 +41,7 @@ class SessionEventTest { fun sessionStart_populatesSessionDetailsCorrectly() = runTest { val fakeFirebaseApp = FakeFirebaseApp() val sessionEvent = - SessionEvents.startSession( + SessionEvents.buildSession( fakeFirebaseApp.firebaseApp, TEST_SESSION_DETAILS, SessionsSettings( @@ -61,7 +61,7 @@ class SessionEventTest { val context = firebaseApp.applicationContext val sessionEvent = - SessionEvents.startSession( + SessionEvents.buildSession( firebaseApp, TEST_SESSION_DETAILS, SessionsSettings( diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionFirelogPublisherTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionFirelogPublisherTest.kt index 96ebf92582f..918d146d762 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionFirelogPublisherTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionFirelogPublisherTest.kt @@ -19,6 +19,7 @@ package com.google.firebase.sessions import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp +import com.google.firebase.concurrent.TestOnlyExecutors import com.google.firebase.sessions.settings.SessionsSettings import com.google.firebase.sessions.testing.FakeEventGDTLogger import com.google.firebase.sessions.testing.FakeFirebaseApp @@ -26,6 +27,7 @@ import com.google.firebase.sessions.testing.FakeFirebaseInstallations import com.google.firebase.sessions.testing.FakeSettingsProvider import com.google.firebase.sessions.testing.TestSessionEventData import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.After @@ -36,32 +38,30 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SessionFirelogPublisherTest { @Test - fun attemptLoggingSessionEvent_populatesFid() = runTest { + fun logSession_populatesFid() = runTest { + val fakeFirebaseApp = FakeFirebaseApp() val fakeEventGDTLogger = FakeEventGDTLogger() val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") - val sessionCoordinator = + val sessionsSettings = + SessionsSettings( + localOverrideSettings = FakeSettingsProvider(), + remoteSettings = FakeSettingsProvider(), + ) + val publisher = SessionFirelogPublisher( + fakeFirebaseApp.firebaseApp, firebaseInstallations, + sessionsSettings, eventGDTLogger = fakeEventGDTLogger, + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, ) // Construct an event with no fid set. - val fakeFirebaseApp = FakeFirebaseApp() - val sessionEvent = - SessionEvents.startSession( - fakeFirebaseApp.firebaseApp, - TestSessionEventData.TEST_SESSION_DETAILS, - SessionsSettings( - localOverrideSettings = FakeSettingsProvider(), - remoteSettings = FakeSettingsProvider(), - ), - ) - - sessionCoordinator.attemptLoggingSessionEvent(sessionEvent) + publisher.logSession(TestSessionEventData.TEST_SESSION_DETAILS) runCurrent() - assertThat(sessionEvent.sessionData.firebaseInstallationId).isEqualTo("FaKeFiD") + System.out.println("FakeEventGDTLogger: $fakeEventGDTLogger") assertThat(fakeEventGDTLogger.loggedEvent!!.sessionData.firebaseInstallationId) .isEqualTo("FaKeFiD") } From 92767707cbda3aff9910c13491780780676efb74 Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Tue, 17 Oct 2023 11:02:58 -0400 Subject: [PATCH 15/38] Update SessionDatastore.kt's name (#5433) --- .../kotlin/com/google/firebase/sessions/SessionDatastore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt index 1064aa71776..7845efd6c29 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt @@ -59,6 +59,6 @@ internal class SessionDatastore(private val context: Context) { private companion object { private val Context.dataStore: DataStore by - preferencesDataStore(name = "firebase_session_settings") + preferencesDataStore(name = "firebase_session_data") } } From cd58103d4ef1d8d28c02da8bf506d9f03d0d6498 Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Tue, 17 Oct 2023 11:33:44 -0400 Subject: [PATCH 16/38] =?UTF-8?q?Updates=20the=20service=20to=20run=20on?= =?UTF-8?q?=20a=20looper=20on=20it's=20own=20thread=20and=20not=20use?= =?UTF-8?q?=E2=80=A6=20(#5431)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … the MainLooper which is the application main thread and so could interfere with things there. Also fixes issue with missing import. --- .../sessions/SessionFirelogPublisher.kt | 1 + .../sessions/SessionLifecycleService.kt | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt index 152e836084d..ef24acae11b 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionFirelogPublisher.kt @@ -18,6 +18,7 @@ package com.google.firebase.sessions import android.util.Log import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.api.FirebaseSessionsDependencies diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index c5c8c8fbaf0..cbac4c3da7f 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -22,6 +22,7 @@ import android.os.Build import android.os.Bundle import android.os.DeadObjectException import android.os.Handler +import android.os.HandlerThread import android.os.IBinder import android.os.Looper import android.os.Message @@ -69,6 +70,9 @@ internal class SessionLifecycleService( /** Most recent session from datastore is updated asynchronously whenever it changes */ private val currentSessionFromDatastore: AtomicReference = AtomicReference() + private var handlerThread: HandlerThread = HandlerThread("FirebaseSessions_HandlerThread") + private var messageHandler: Handler? = null + init { CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { datastore.firebaseSessionDataFlow.collect { currentSessionFromDatastore.set(it) } @@ -82,7 +86,7 @@ internal class SessionLifecycleService( */ // TODO(rothbutter) there's a warning that this needs to be static and leaks may occur. Need to // look in to this - internal inner class IncomingHandler : Handler(Looper.getMainLooper()) { + internal inner class IncomingHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { if (lastMsgTimeMs > msg.getWhen()) { @@ -101,10 +105,16 @@ internal class SessionLifecycleService( } } + override fun onCreate() { + super.onCreate() + handlerThread.start() + messageHandler = IncomingHandler(handlerThread.getLooper()) + } + /** Called when a new [SessionLifecycleClient] binds to this service. */ override fun onBind(intent: Intent): IBinder? { Log.i(TAG, "Service bound") - val messenger = Messenger(IncomingHandler()) + val messenger = Messenger(messageHandler) val callbackMessenger = getClientCallback(intent) if (callbackMessenger != null) { boundClients.add(callbackMessenger) @@ -114,6 +124,11 @@ internal class SessionLifecycleService( return messenger.binder } + override fun onDestroy() { + super.onDestroy() + handlerThread.quit() + } + /** * Handles a foregrounding event by any activity owned by the aplication as specified by the given * [Message]. This will determine if the foregrounding should result in the creation of a new From 113b771aa7666484d5bff97a5e2d83d9d54aaf4a Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Tue, 17 Oct 2023 14:09:02 -0400 Subject: [PATCH 17/38] =?UTF-8?q?Refactors=20SessionLifecycleService=20to?= =?UTF-8?q?=20make=20the=20message=20handler=20static=20=E2=80=A6=20(#5434?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …and use the same Messenger instance for every client. This makes sure that all state manipulation happens on the same looper, so we can get rid of the LinkedBlockingQueue or worry about volatile booleans. --- .../sessions/SessionLifecycleService.kt | 261 ++++++++++-------- 1 file changed, 142 insertions(+), 119 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index cbac4c3da7f..c78865495e9 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -31,7 +31,6 @@ import android.util.Log import com.google.firebase.Firebase import com.google.firebase.app import com.google.firebase.sessions.settings.SessionsSettings -import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.atomic.AtomicReference import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -41,52 +40,52 @@ import kotlinx.coroutines.launch * be generated. When this happens, the service will broadcast the updated session id to all * connected clients. */ -internal class SessionLifecycleService( - private var datastore: SessionDatastore = SessionDatastore(Firebase.app.applicationContext) -) : Service() { - - /** - * Queue of connected clients. - * - * Note that this needs to be a [LinkedBlockingQueue] since it is modified in the service [onBind] - * method and read in the message handler. Although the [IncomingHandler] is guaranteed to execute - * sequentially on a single thread, the [onBind] method is not guaranteed to run on that same - * thread. - */ - private val boundClients = LinkedBlockingQueue() - - /** - * Flag indicating whether or not the app has ever come into the foreground during the lifetime of - * the service. If it has not, we can infer that the first foreground event is a cold-start - */ - private var hasForegrounded: Boolean = false - - /** - * The timestamp of the last activity lifecycle message we've received from a client. Used to - * determine when the app has been idle for long enough to require a new session. - */ - private var lastMsgTimeMs: Long = 0 - - /** Most recent session from datastore is updated asynchronously whenever it changes */ - private val currentSessionFromDatastore: AtomicReference = AtomicReference() +internal class SessionLifecycleService() : Service() { + /** The thread that will be used to process all lifecycle messages from connected clients. */ private var handlerThread: HandlerThread = HandlerThread("FirebaseSessions_HandlerThread") - private var messageHandler: Handler? = null - init { - CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { - datastore.firebaseSessionDataFlow.collect { currentSessionFromDatastore.set(it) } - } - } + /** The handler that will process all lifecycle messages from connected clients . */ + private var messageHandler: MessageHandler? = null + + /** The single messenger that will be sent to all connected clients of this service . */ + private var messenger: Messenger? = null /** * Handler of incoming activity lifecycle events being received from [SessionLifecycleClient]s. * All incoming communication from connected clients comes through this class and will be used to * determine when new sessions should be created. */ - // TODO(rothbutter) there's a warning that this needs to be static and leaks may occur. Need to - // look in to this - internal inner class IncomingHandler(looper: Looper) : Handler(looper) { + internal class MessageHandler(looper: Looper) : Handler(looper) { + + private var datastore: SessionDatastore = SessionDatastore(Firebase.app.applicationContext) + /** + * Flag indicating whether or not the app has ever come into the foreground during the lifetime + * of the service. If it has not, we can infer that the first foreground event is a cold-start + * + * Note: this is made volatile because we attempt to send the current session ID to newly bound + * clients, and this binding happens + */ + private var hasForegrounded: Boolean = false + + /** + * The timestamp of the last activity lifecycle message we've received from a client. Used to + * determine when the app has been idle for long enough to require a new session. + */ + private var lastMsgTimeMs: Long = 0 + + /** Queue of connected clients. */ + private val boundClients = ArrayList() + + /** Most recent session from datastore is updated asynchronously whenever it changes */ + private val currentSessionFromDatastore: AtomicReference = + AtomicReference() + + init { + CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { + datastore.firebaseSessionDataFlow.collect { currentSessionFromDatastore.set(it) } + } + } override fun handleMessage(msg: Message) { if (lastMsgTimeMs > msg.getWhen()) { @@ -96,6 +95,7 @@ internal class SessionLifecycleService( when (msg.what) { FOREGROUNDED -> handleForegrounding(msg) BACKGROUNDED -> handleBackgrounding(msg) + CLIENT_BOUND -> handleClientBound(msg) else -> { Log.w(TAG, "Received unexpected event from the SessionLifecycleClient: $msg") super.handleMessage(msg) @@ -103,25 +103,117 @@ internal class SessionLifecycleService( } lastMsgTimeMs = msg.getWhen() } + + internal fun addClient(client: Messenger) { + boundClients.add(client) + maybeSendSessionToClient(client) + Log.i(TAG, "Stored callback to $client. Size: ${boundClients.size}") + } + + /** + * Handles a foregrounding event by any activity owned by the aplication as specified by the + * given [Message]. This will determine if the foregrounding should result in the creation of a + * new session. + */ + private fun handleForegrounding(msg: Message) { + Log.i(TAG, "Activity foregrounding at ${msg.getWhen()}") + if (!hasForegrounded) { + Log.i(TAG, "Cold start detected.") + hasForegrounded = true + broadcastSession() + updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) + } else if (isSessionRestart(msg.getWhen())) { + Log.i(TAG, "Session too long in background. Creating new session.") + SessionGenerator.instance.generateNewSession() + broadcastSession() + updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) + } + } + + /** + * Handles a backgrounding event by any activity owned by the application as specified by the + * given [Message]. This will keep track of the backgrounding and be used to determine if future + * foregrounding events should result in the creation of a new session. + */ + private fun handleBackgrounding(msg: Message) { + Log.i(TAG, "Activity backgrounding at ${msg.getWhen()}") + } + + /** + * Handles a newly bound client to this service by adding it to the list of callback clients and + * attempting to send it the latest session id immediately. + */ + private fun handleClientBound(msg: Message) { + boundClients.add(msg.replyTo) + maybeSendSessionToClient(msg.replyTo) + Log.i(TAG, "Stored callback to ${msg.replyTo}. Size: ${boundClients.size}") + } + + /** + * Broadcasts the current session to by uploading to Firelog and all sending a message to all + * connected clients. + */ + private fun broadcastSession() { + Log.i(TAG, "Broadcasting new session: ${SessionGenerator.instance.currentSession}") + SessionFirelogPublisher.instance.logSession(SessionGenerator.instance.currentSession) + boundClients.forEach { maybeSendSessionToClient(it) } + } + + private fun maybeSendSessionToClient(client: Messenger) { + if (hasForegrounded) { + sendSessionToClient(client, SessionGenerator.instance.currentSession.sessionId) + } else { + // Send the value from the datastore before the first foregrounding it exists + val sessionData = currentSessionFromDatastore.get() + sessionData?.sessionId?.let { sendSessionToClient(client, it) } + } + } + + /** Sends the current session id to the client connected through the given [Messenger]. */ + private fun sendSessionToClient(client: Messenger, sessionId: String) { + try { + val msgData = Bundle().also { it.putString(SESSION_UPDATE_EXTRA, sessionId) } + client.send(Message.obtain(null, SESSION_UPDATED, 0, 0).also { it.data = msgData }) + } catch (e: DeadObjectException) { + Log.i(TAG, "Removing dead client from list: $client") + boundClients.remove(client) + } catch (e: Exception) { + Log.e(TAG, "Unable to push new session to $client.", e) + } + } + + /** + * Determines if the foregrounding that occurred at the given time should trigger a new session + * because the app has been idle for too long. + */ + private fun isSessionRestart(foregroundTimeMs: Long) = + (foregroundTimeMs - lastMsgTimeMs) > + SessionsSettings.instance.sessionRestartTimeout.inWholeMilliseconds + + private fun updateSessionStorage(sessionId: String) { + CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { + sessionId.let { datastore.updateSessionId(it) } + } + } } override fun onCreate() { super.onCreate() handlerThread.start() - messageHandler = IncomingHandler(handlerThread.getLooper()) + messageHandler = MessageHandler(handlerThread.getLooper()) + messenger = Messenger(messageHandler) } /** Called when a new [SessionLifecycleClient] binds to this service. */ override fun onBind(intent: Intent): IBinder? { Log.i(TAG, "Service bound") - val messenger = Messenger(messageHandler) val callbackMessenger = getClientCallback(intent) if (callbackMessenger != null) { - boundClients.add(callbackMessenger) - maybeSendSessionToClient(callbackMessenger) - Log.i(TAG, "Stored callback to $callbackMessenger. Size: ${boundClients.size}") + val clientBoundMsg = Message.obtain(null, CLIENT_BOUND, 0, 0) + clientBoundMsg.replyTo = callbackMessenger + messageHandler?.sendMessage(clientBoundMsg) } - return messenger.binder + return messenger?.binder } override fun onDestroy() { @@ -129,73 +221,6 @@ internal class SessionLifecycleService( handlerThread.quit() } - /** - * Handles a foregrounding event by any activity owned by the aplication as specified by the given - * [Message]. This will determine if the foregrounding should result in the creation of a new - * session. - */ - private fun handleForegrounding(msg: Message) { - Log.i(TAG, "Activity foregrounding at ${msg.getWhen()}") - if (!hasForegrounded) { - Log.i(TAG, "Cold start detected.") - hasForegrounded = true - broadcastSession() - updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) - Log.i(TAG, "Session too long in background. Creating new session.") - SessionGenerator.instance.generateNewSession() - broadcastSession() - updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) - } - } - - private fun updateSessionStorage(sessionId: String) { - CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { - sessionId.let { datastore.updateSessionId(it) } - } - } - - /** - * Handles a backgrounding event by any activity owned by the application as specified by the - * given [Message]. This will keep track of the backgrounding and be used to determine if future - * foregrounding events should result in the creation of a new session. - */ - private fun handleBackgrounding(msg: Message) { - Log.i(TAG, "Activity backgrounding at ${msg.getWhen()}") - } - - /** - * Broadcasts the current session to by uploading to Firelog and all sending a message to all - * connected clients. - */ - private fun broadcastSession() { - Log.i(TAG, "Broadcasting new session: ${SessionGenerator.instance.currentSession}") - SessionFirelogPublisher.instance.logSession(SessionGenerator.instance.currentSession) - boundClients.forEach { maybeSendSessionToClient(it) } - } - - private fun maybeSendSessionToClient(client: Messenger) { - if (hasForegrounded) { - sendSessionToClient(client, SessionGenerator.instance.currentSession.sessionId) - } else { - // Send the value from the datastore before the first foregrounding it exists - val sessionData = currentSessionFromDatastore.get() - sessionData?.sessionId?.let { sendSessionToClient(client, it) } - } - } - - /** Sends the current session id to the client connected through the given [Messenger]. */ - private fun sendSessionToClient(client: Messenger, sessionId: String) { - try { - val msgData = Bundle().also { it.putString(SESSION_UPDATE_EXTRA, sessionId) } - client.send(Message.obtain(null, SESSION_UPDATED, 0, 0).also { it.data = msgData }) - } catch (e: DeadObjectException) { - Log.i(TAG, "Removing dead client from list: $client") - boundClients.remove(client) - } catch (e: Exception) { - Log.e(TAG, "Unable to push new session to $client.", e) - } - } - /** * Extracts the callback [Messenger] from the given [Intent] which will be used to push session * updates back to the [SessionLifecycleClient] that created this [Intent]. @@ -207,14 +232,6 @@ internal class SessionLifecycleService( @Suppress("DEPRECATION") intent.getParcelableExtra(CLIENT_CALLBACK_MESSENGER) } - /** - * Determines if the foregrounding that occurred at the given time should trigger a new session - * because the app has been idle for too long. - */ - private fun isSessionRestart(foregroundTimeMs: Long) = - (foregroundTimeMs - lastMsgTimeMs) > - SessionsSettings.instance.sessionRestartTimeout.inWholeMilliseconds - internal companion object { const val TAG = "SessionLifecycleService" @@ -239,5 +256,11 @@ internal class SessionLifecycleService( * id in the [SESSION_UPDATE_EXTRA] extra field. */ const val SESSION_UPDATED = 3 + + /** + * [Message] code indicating that a new client has been bound to the service. The + * [Message.replyTo] field will contain the new client callback interface. + */ + private const val CLIENT_BOUND = 4 } } From f3543de44bf2f1783d49b3dfe66fbf829dc58a12 Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Wed, 18 Oct 2023 09:14:57 -0400 Subject: [PATCH 18/38] =?UTF-8?q?Removes=20unecessary=20setup=20code=20fro?= =?UTF-8?q?m=20FirebaseSessions=20and=20SessionInitia=E2=80=A6=20(#5436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …tor. --- ...sionInitiateListener.kt => Dispatchers.kt} | 19 +- .../firebase/sessions/FirebaseSessions.kt | 72 +----- .../sessions/FirebaseSessionsRegistrar.kt | 16 +- .../firebase/sessions/SessionInitiator.kt | 83 ------- .../sessions/SessionLifecycleClient.kt | 2 +- .../sessions/SessionLifecycleService.kt | 19 +- .../SessionsActivityLifecycleCallbacks.kt | 41 +++ .../firebase/sessions/SessionInitiatorTest.kt | 233 ------------------ 8 files changed, 80 insertions(+), 405 deletions(-) rename firebase-sessions/src/main/kotlin/com/google/firebase/sessions/{SessionInitiateListener.kt => Dispatchers.kt} (61%) delete mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt delete mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionInitiatorTest.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiateListener.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Dispatchers.kt similarity index 61% rename from firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiateListener.kt rename to firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Dispatchers.kt index 55ab9d128e9..86aa5ac1a5f 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiateListener.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Dispatchers.kt @@ -16,8 +16,19 @@ package com.google.firebase.sessions -/** Interface for listening to the initiation of a new session. */ -internal fun interface SessionInitiateListener { - /** To be called whenever a new session is initiated. */ - suspend fun onInitiateSession(sessionDetails: SessionDetails) +import com.google.firebase.Firebase +import com.google.firebase.app +import kotlin.coroutines.CoroutineContext + +/** Container for injecting dispatchers. */ +internal data class Dispatchers +constructor( + val blockingDispatcher: CoroutineContext, + val backgroundDispatcher: CoroutineContext, +) { + + companion object { + val instance: Dispatchers + get() = Firebase.app.get(Dispatchers::class.java) + } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index f583f98dabd..8b17bb58de1 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -23,42 +23,19 @@ import com.google.firebase.FirebaseApp import com.google.firebase.app import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.api.SessionSubscriber -import com.google.firebase.sessions.settings.SessionsSettings -import kotlinx.coroutines.CoroutineDispatcher /** The [FirebaseSessions] API provides methods to register a [SessionSubscriber]. */ -class FirebaseSessions -internal constructor( - private val firebaseApp: FirebaseApp, - internal val backgroundDispatcher: CoroutineDispatcher, - private val sessionFirelogPublisher: SessionFirelogPublisher, - sessionGenerator: SessionGenerator, - private val sessionSettings: SessionsSettings, -) { - - // TODO: This needs to be moved into the service to be consistent across multiple processes. - private val collectEvents: Boolean +class FirebaseSessions internal constructor(private val firebaseApp: FirebaseApp) { init { - collectEvents = shouldCollectEvents() - - val sessionInitiator = - SessionInitiator( - timeProvider = WallClock, - backgroundDispatcher, - ::initiateSessionStart, - sessionSettings, - sessionGenerator, - ) - val appContext = firebaseApp.applicationContext.applicationContext if (appContext is Application) { SessionLifecycleClient.bindToService(appContext) - appContext.registerActivityLifecycleCallbacks(sessionInitiator.activityLifecycleCallbacks) + appContext.registerActivityLifecycleCallbacks(SessionsActivityLifecycleCallbacks) firebaseApp.addLifecycleEventListener { _, _ -> Log.w(TAG, "FirebaseApp instance deleted. Sessions library will not collect session data.") - appContext.unregisterActivityLifecycleCallbacks(sessionInitiator.activityLifecycleCallbacks) + appContext.unregisterActivityLifecycleCallbacks(SessionsActivityLifecycleCallbacks) } } else { Log.e( @@ -79,50 +56,7 @@ internal constructor( ) } - private suspend fun initiateSessionStart(sessionDetails: SessionDetails) { - val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers() - - if (subscribers.isEmpty()) { - Log.d( - TAG, - "Sessions SDK did not have any dependent SDKs register as dependencies. Events will not be sent." - ) - return - } - - subscribers.values.forEach { subscriber -> - // Notify subscribers, regardless of sampling and data collection state. - subscriber.onSessionChanged(SessionSubscriber.SessionDetails(sessionDetails.sessionId)) - } - - if (subscribers.values.none { it.isDataCollectionEnabled }) { - Log.d(TAG, "Data Collection is disabled for all subscribers. Skipping this Session Event") - return - } - - Log.d(TAG, "Data Collection is enabled for at least one Subscriber") - - // This will cause remote settings to be fetched if the cache is expired. - sessionSettings.updateSettings() - - if (!sessionSettings.sessionsEnabled) { - Log.d(TAG, "Sessions SDK disabled. Events will not be sent.") - return - } - - if (!collectEvents) { - Log.d(TAG, "Sessions SDK has dropped this session due to sampling.") - return - } - } - /** Calculate whether we should sample events using [sessionSettings] data. */ - private fun shouldCollectEvents(): Boolean { - // Sampling rate of 1 means the SDK will send every event. - val randomValue = Math.random() - return randomValue <= sessionSettings.samplingRate - } - companion object { private const val TAG = "FirebaseSessions" diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index f8e0e24125e..b74a3595cf9 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -41,17 +41,9 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { Component.builder(FirebaseSessions::class.java) .name(LIBRARY_NAME) .add(Dependency.required(firebaseApp)) - .add(Dependency.required(backgroundDispatcher)) - .add(Dependency.required(sessionFirelogPublisher)) - .add(Dependency.required(sessionGenerator)) - .add(Dependency.required(sessionsSettings)) .factory { container -> FirebaseSessions( container.get(firebaseApp), - container.get(backgroundDispatcher), - container.get(sessionFirelogPublisher), - container.get(sessionGenerator), - container.get(sessionsSettings), ) } .build(), @@ -91,6 +83,14 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { ) } .build(), + Component.builder(Dispatchers::class.java) + .name("sessions-dispatchers") + .add(Dependency.required(blockingDispatcher)) + .add(Dependency.required(backgroundDispatcher)) + .factory { container -> + Dispatchers(container.get(blockingDispatcher), container.get(backgroundDispatcher)) + } + .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), ) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt deleted file mode 100644 index c10ed18c2da..00000000000 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionInitiator.kt +++ /dev/null @@ -1,83 +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.sessions - -import android.app.Activity -import android.app.Application.ActivityLifecycleCallbacks -import android.os.Bundle -import com.google.firebase.sessions.settings.SessionsSettings -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -/** - * The [SessionInitiator] is responsible for calling [SessionInitiateListener.onInitiateSession] - * with a generated [SessionDetails] on the [backgroundDispatcher] whenever a new session initiates. - * This will happen at a cold start of the app, and when the app has been in the background for a - * period of time (default 30 min) and then comes back to the foreground. - */ -internal class SessionInitiator( - private val timeProvider: TimeProvider, - private val backgroundDispatcher: CoroutineContext, - private val sessionInitiateListener: SessionInitiateListener, - private val sessionsSettings: SessionsSettings, - private val sessionGenerator: SessionGenerator, -) { - private var backgroundTime = timeProvider.elapsedRealtime() - - init { - initiateSession() - } - - fun appBackgrounded() { - backgroundTime = timeProvider.elapsedRealtime() - } - - fun appForegrounded() { - val interval = timeProvider.elapsedRealtime() - backgroundTime - val sessionTimeout = sessionsSettings.sessionRestartTimeout - if (interval > sessionTimeout) { - initiateSession() - } - } - - private fun initiateSession() { - // Generate the session details on main thread so the timestamp is as current as possible. - val sessionDetails = sessionGenerator.generateNewSession() - - CoroutineScope(backgroundDispatcher).launch { - sessionInitiateListener.onInitiateSession(sessionDetails) - } - } - - internal val activityLifecycleCallbacks = - object : ActivityLifecycleCallbacks { - override fun onActivityResumed(activity: Activity) = SessionLifecycleClient.foregrounded() - - override fun onActivityPaused(activity: Activity) = SessionLifecycleClient.backgrounded() - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit - - override fun onActivityStarted(activity: Activity) = Unit - - override fun onActivityStopped(activity: Activity) = Unit - - override fun onActivityDestroyed(activity: Activity) = Unit - - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit - } -} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt index 5104568135f..0e7ced375d5 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt @@ -78,7 +78,7 @@ internal object SessionLifecycleClient { Log.i(TAG, "Session update received: $sessionId") curSessionId = sessionId - CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { + CoroutineScope(Dispatchers.instance.backgroundDispatcher).launch { FirebaseSessionsDependencies.getRegisteredSubscribers().values.forEach { subscriber -> // Notify subscribers, regardless of sampling and data collection state. subscriber.onSessionChanged(SessionSubscriber.SessionDetails(sessionId)) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index c78865495e9..42fd6e693e1 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -59,6 +59,7 @@ internal class SessionLifecycleService() : Service() { internal class MessageHandler(looper: Looper) : Handler(looper) { private var datastore: SessionDatastore = SessionDatastore(Firebase.app.applicationContext) + /** * Flag indicating whether or not the app has ever come into the foreground during the lifetime * of the service. If it has not, we can infer that the first foreground event is a cold-start @@ -82,7 +83,7 @@ internal class SessionLifecycleService() : Service() { AtomicReference() init { - CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { + CoroutineScope(Dispatchers.instance.backgroundDispatcher).launch { datastore.firebaseSessionDataFlow.collect { currentSessionFromDatastore.set(it) } } } @@ -120,13 +121,10 @@ internal class SessionLifecycleService() : Service() { if (!hasForegrounded) { Log.i(TAG, "Cold start detected.") hasForegrounded = true - broadcastSession() - updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) + newSession() } else if (isSessionRestart(msg.getWhen())) { Log.i(TAG, "Session too long in background. Creating new session.") - SessionGenerator.instance.generateNewSession() - broadcastSession() - updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) + newSession() } } @@ -149,6 +147,13 @@ internal class SessionLifecycleService() : Service() { Log.i(TAG, "Stored callback to ${msg.replyTo}. Size: ${boundClients.size}") } + /** Generates a new session id and sends it everywhere it's needed */ + private fun newSession() { + SessionGenerator.instance.generateNewSession() + broadcastSession() + updateSessionStorage(SessionGenerator.instance.currentSession.sessionId) + } + /** * Broadcasts the current session to by uploading to Firelog and all sending a message to all * connected clients. @@ -191,7 +196,7 @@ internal class SessionLifecycleService() : Service() { SessionsSettings.instance.sessionRestartTimeout.inWholeMilliseconds private fun updateSessionStorage(sessionId: String) { - CoroutineScope(FirebaseSessions.instance.backgroundDispatcher).launch { + CoroutineScope(Dispatchers.instance.backgroundDispatcher).launch { sessionId.let { datastore.updateSessionId(it) } } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt new file mode 100644 index 00000000000..c6f91d597b2 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt @@ -0,0 +1,41 @@ +/* + * 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.sessions + +import android.app.Activity +import android.app.Application.ActivityLifecycleCallbacks +import android.os.Bundle + +/** + * Lifecycle callbacks that will inform the [SessionLifecycleClient] whenever an [Activity] in this + * application process goes foreground or background. + */ +internal object SessionsActivityLifecycleCallbacks : ActivityLifecycleCallbacks { + override fun onActivityResumed(activity: Activity) = SessionLifecycleClient.foregrounded() + + override fun onActivityPaused(activity: Activity) = SessionLifecycleClient.backgrounded() + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit + + override fun onActivityStarted(activity: Activity) = Unit + + override fun onActivityStopped(activity: Activity) = Unit + + override fun onActivityDestroyed(activity: Activity) = Unit + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionInitiatorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionInitiatorTest.kt deleted file mode 100644 index 6a62dfc7023..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionInitiatorTest.kt +++ /dev/null @@ -1,233 +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.sessions - -import com.google.common.truth.Truth.assertThat -import com.google.firebase.FirebaseApp -import com.google.firebase.concurrent.TestOnlyExecutors -import com.google.firebase.sessions.settings.SessionsSettings -import com.google.firebase.sessions.testing.FakeSettingsProvider -import com.google.firebase.sessions.testing.FakeTimeProvider -import kotlin.time.Duration.Companion.minutes -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricTestRunner::class) -class SessionInitiatorTest { - private class SessionInitiateCounter : SessionInitiateListener { - var count = 0 - private set - - override suspend fun onInitiateSession(sessionDetails: SessionDetails) { - count++ - } - } - - @Test - fun coldStart_initiatesSession() = runTest { - val sessionInitiateCounter = SessionInitiateCounter() - val fakeTimeProvider = FakeTimeProvider() - val settings = - SessionsSettings( - localOverrideSettings = FakeSettingsProvider(), - remoteSettings = FakeSettingsProvider(), - ) - - // Simulate a cold start by simply constructing the SessionInitiator object - SessionInitiator( - fakeTimeProvider, - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, - sessionInitiateListener = sessionInitiateCounter, - settings, - SessionGenerator(fakeTimeProvider), - ) - - // Run onInitiateSession suspend function. - runCurrent() - - // Session on cold start - assertThat(sessionInitiateCounter.count).isEqualTo(1) - } - - @Test - fun appForegrounded_largeInterval_initiatesSession() = runTest { - val fakeTimeProvider = FakeTimeProvider() - val sessionInitiateCounter = SessionInitiateCounter() - val settings = - SessionsSettings( - localOverrideSettings = FakeSettingsProvider(), - remoteSettings = FakeSettingsProvider(), - ) - - val sessionInitiator = - SessionInitiator( - fakeTimeProvider, - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, - sessionInitiateListener = sessionInitiateCounter, - settings, - SessionGenerator(fakeTimeProvider), - ) - - // Run onInitiateSession suspend function. - runCurrent() - - // First session on cold start - assertThat(sessionInitiateCounter.count).isEqualTo(1) - - // Enough tome to initiate a new session, and then foreground - fakeTimeProvider.addInterval(LARGE_INTERVAL) - sessionInitiator.appForegrounded() - - runCurrent() - - // Another session initiated - assertThat(sessionInitiateCounter.count).isEqualTo(2) - } - - @Test - fun appForegrounded_smallInterval_doesNotInitiatesSession() = runTest { - val fakeTimeProvider = FakeTimeProvider() - val sessionInitiateCounter = SessionInitiateCounter() - val settings = - SessionsSettings( - localOverrideSettings = FakeSettingsProvider(), - remoteSettings = FakeSettingsProvider(), - ) - - val sessionInitiator = - SessionInitiator( - fakeTimeProvider, - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, - sessionInitiateListener = sessionInitiateCounter, - settings, - SessionGenerator(fakeTimeProvider), - ) - - // Run onInitiateSession suspend function. - runCurrent() - - // First session on cold start - assertThat(sessionInitiateCounter.count).isEqualTo(1) - - // Not enough time to initiate a new session, and then foreground - fakeTimeProvider.addInterval(SMALL_INTERVAL) - sessionInitiator.appForegrounded() - - runCurrent() - - // No new session - assertThat(sessionInitiateCounter.count).isEqualTo(1) - } - - @Test - fun appForegrounded_background_foreground_largeIntervals_initiatesSessions() = runTest { - val fakeTimeProvider = FakeTimeProvider() - val sessionInitiateCounter = SessionInitiateCounter() - val settings = - SessionsSettings( - localOverrideSettings = FakeSettingsProvider(), - remoteSettings = FakeSettingsProvider(), - ) - - val sessionInitiator = - SessionInitiator( - fakeTimeProvider, - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, - sessionInitiateListener = sessionInitiateCounter, - settings, - SessionGenerator(fakeTimeProvider), - ) - - // Run onInitiateSession suspend function. - runCurrent() - - assertThat(sessionInitiateCounter.count).isEqualTo(1) - - fakeTimeProvider.addInterval(LARGE_INTERVAL) - sessionInitiator.appForegrounded() - - runCurrent() - - assertThat(sessionInitiateCounter.count).isEqualTo(2) - - sessionInitiator.appBackgrounded() - fakeTimeProvider.addInterval(LARGE_INTERVAL) - sessionInitiator.appForegrounded() - - runCurrent() - - assertThat(sessionInitiateCounter.count).isEqualTo(3) - } - - @Test - fun appForegrounded_background_foreground_smallIntervals_doesNotInitiateNewSessions() = runTest { - val fakeTimeProvider = FakeTimeProvider() - val sessionInitiateCounter = SessionInitiateCounter() - val settings = - SessionsSettings( - localOverrideSettings = FakeSettingsProvider(), - remoteSettings = FakeSettingsProvider(), - ) - - val sessionInitiator = - SessionInitiator( - fakeTimeProvider, - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, - sessionInitiateListener = sessionInitiateCounter, - settings, - SessionGenerator(fakeTimeProvider), - ) - - // Run onInitiateSession suspend function. - runCurrent() - - // First session on cold start - assertThat(sessionInitiateCounter.count).isEqualTo(1) - - fakeTimeProvider.addInterval(SMALL_INTERVAL) - sessionInitiator.appForegrounded() - - runCurrent() - - assertThat(sessionInitiateCounter.count).isEqualTo(1) - - sessionInitiator.appBackgrounded() - fakeTimeProvider.addInterval(SMALL_INTERVAL) - sessionInitiator.appForegrounded() - - runCurrent() - - assertThat(sessionInitiateCounter.count).isEqualTo(1) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } - - companion object { - private val SMALL_INTERVAL = 29.minutes // not enough time to initiate a new session - private val LARGE_INTERVAL = 31.minutes // enough to initiate another session - } -} From c515585c7425ac9370b1d8cfeb685aaa3bf19610 Mon Sep 17 00:00:00 2001 From: Jamie Rothfeder Date: Wed, 18 Oct 2023 09:49:35 -0400 Subject: [PATCH 19/38] Rebase sessions-nine. (#5440) Co-authored-by: Mila <107142260+milaGGL@users.noreply.github.com> Co-authored-by: Rodrigo Lazo Co-authored-by: Greg Sakakihara Co-authored-by: David Motsonashvili Co-authored-by: David Motsonashvili Co-authored-by: Vinay Guthal Co-authored-by: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Co-authored-by: Mark Duckworth <1124037+MarkDuckworth@users.noreply.github.com> Co-authored-by: Ehsan Nasiri Co-authored-by: Rosalyn Tan Co-authored-by: themiswang --- .../firebase-appcheck-debug-testing.gradle | 5 +- .../firebase-appcheck-debug.gradle | 5 +- .../CHANGELOG.md | 3 + .../firebase-appcheck-playintegrity.gradle | 7 +- .../firebase-appcheck-safetynet.gradle | 5 +- appcheck/firebase-appcheck/CHANGELOG.md | 7 +- .../firebase-appcheck.gradle | 6 +- appcheck/firebase-appcheck/ktx/ktx.gradle | 6 +- .../internal/DefaultFirebaseAppCheck.java | 47 ++- .../appcheck/internal/NetworkClient.java | 15 +- .../appcheck/internal/RetryManager.java | 5 +- .../firebase/appcheck/ktx/FirebaseAppCheck.kt | 36 +- .../appcheck/internal/NetworkClientTest.java | 2 +- .../appcheck/internal/RetryManagerTest.java | 21 +- .../test-app/src/main/AndroidManifest.xml | 2 +- .../test-app/test-app.gradle | 2 - contributor-docs/onboarding/new_sdk.md | 4 +- .../firebase-appdistribution-api.gradle | 6 +- firebase-appdistribution-api/ktx/ktx.gradle | 6 +- .../ktx/FirebaseAppDistribution.kt | 71 +--- firebase-common/gradle.properties | 4 +- firebase-common/ktx/ktx.gradle.kts | 2 +- .../java/com/google/firebase/ktx/Firebase.kt | 58 +-- firebase-components/gradle.properties | 4 +- .../bandwagoner/bandwagoner.gradle | 7 +- firebase-config/firebase-config.gradle | 6 +- firebase-config/ktx/ktx.gradle | 6 +- .../firebase/remoteconfig/ktx/RemoteConfig.kt | 42 +-- .../firebase-crashlytics-ndk.gradle | 6 +- firebase-crashlytics/CHANGELOG.md | 2 + .../firebase-crashlytics.gradle | 6 +- firebase-crashlytics/ktx/ktx.gradle | 6 +- .../common/CrashlyticsControllerTest.java | 38 +- .../internal/metadata/MetaDataStoreTest.java | 52 +++ .../common/CrashlyticsController.java | 14 +- .../internal/metadata/UserMetadata.java | 22 +- .../crashlytics/ktx/FirebaseCrashlytics.kt | 32 +- .../crashlytics/ktx/KeyValueBuilder.kt | 36 +- .../firebase-database.gradle.kts | 6 +- firebase-database/ktx/ktx.gradle.kts | 6 +- .../firebase/database/ktx/ChildEvent.kt | 32 +- .../google/firebase/database/ktx/Database.kt | 83 ++--- .../firebase-dynamic-links.gradle | 6 +- firebase-dynamic-links/ktx/ktx.gradle | 6 +- .../dynamiclinks/ktx/FirebaseDynamicLinks.kt | 159 +++------ firebase-firestore/CHANGELOG.md | 2 +- firebase-firestore/api.txt | 27 ++ firebase-firestore/firebase-firestore.gradle | 6 +- firebase-firestore/ktx/ktx.gradle | 6 +- .../firebase/firestore/AggregationTest.java | 331 +++++------------- .../google/firebase/firestore/QueryTest.java | 42 +++ .../firebase/firestore/AggregateField.java | 85 ++++- .../firebase/firestore/AggregateQuery.java | 2 - .../firestore/AggregateQuerySnapshot.java | 16 - .../com/google/firebase/firestore/Query.java | 6 +- .../google/firebase/firestore/core/Query.java | 122 ++++--- .../firebase/firestore/ktx/Firestore.kt | 157 +++------ .../firebase/firestore/remote/Datastore.java | 2 +- .../firebase/firestore/core/QueryTest.java | 104 +++++- .../firebase-functions.gradle.kts | 6 +- firebase-functions/ktx/ktx.gradle.kts | 6 +- .../firebase/functions/ktx/Functions.kt | 66 +--- .../firebase-inappmessaging-display.gradle | 6 +- .../ktx/ktx.gradle | 6 +- .../display/ktx/InAppMessagingDisplay.kt | 25 +- .../firebase-inappmessaging.gradle | 6 +- firebase-inappmessaging/ktx/ktx.gradle | 6 +- .../inappmessaging/ktx/InAppMessaging.kt | 21 +- .../firebase-installations.gradle | 6 +- firebase-installations/ktx/ktx.gradle | 6 +- .../installations/ktx/Installations.kt | 29 +- firebase-messaging/CHANGELOG.md | 6 + firebase-messaging/firebase-messaging.gradle | 6 +- firebase-messaging/ktx/ktx.gradle | 6 +- .../src/main/AndroidManifest.xml | 3 + .../messaging/CommonNotificationBuilder.java | 10 +- .../firebase/messaging/ktx/Messaging.kt | 28 +- .../CommonNotificationBuilderRoboTest.java | 9 +- .../FirebaseMessagingServiceRoboTest.java | 6 +- firebase-ml-modeldownloader/CHANGELOG.md | 2 + .../firebase-ml-modeldownloader.gradle | 8 +- firebase-ml-modeldownloader/ktx/ktx.gradle | 6 +- .../ml-data-collection-tests.gradle | 3 +- .../FirebaseModelDownloaderRegistrar.java | 7 +- .../ml/modeldownloader/ktx/ModelDownloader.kt | 44 +-- firebase-perf/dev-app/dev-app.gradle | 4 +- firebase-perf/firebase-perf.gradle | 6 +- firebase-perf/ktx/ktx.gradle | 6 +- .../google/firebase/perf/ktx/Performance.kt | 38 +- .../firebase-sessions.gradle.kts | 6 +- firebase-storage/firebase-storage.gradle | 6 +- firebase-storage/ktx/ktx.gradle | 6 +- .../google/firebase/storage/ktx/Storage.kt | 146 +++----- .../google/firebase/storage/ktx/TaskState.kt | 21 +- integ-testing/integ-testing.gradle.kts | 6 +- 95 files changed, 1023 insertions(+), 1365 deletions(-) diff --git a/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle b/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle index 984488a1d9e..2bc6df5a5e6 100644 --- a/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle +++ b/appcheck/firebase-appcheck-debug-testing/firebase-appcheck-debug-testing.gradle @@ -46,8 +46,9 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-common:20.3.1' - implementation 'com.google.firebase:firebase-components:17.1.0' + implementation 'com.google.firebase:firebase-common:20.4.2' + implementation 'com.google.firebase:firebase-common-ktx:20.4.2' + implementation 'com.google.firebase:firebase-components:17.1.5' implementation project(':appcheck:firebase-appcheck') implementation project(':appcheck:firebase-appcheck-debug') implementation 'com.google.firebase:firebase-appcheck-interop:17.0.0' diff --git a/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle b/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle index 7ae987d4b2d..fddc9dc61f2 100644 --- a/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle +++ b/appcheck/firebase-appcheck-debug/firebase-appcheck-debug.gradle @@ -44,8 +44,9 @@ android { dependencies { 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 'com.google.firebase:firebase-common:20.4.2' + implementation 'com.google.firebase:firebase-common-ktx:20.4.2' + implementation 'com.google.firebase:firebase-components:17.1.5' implementation project(':appcheck:firebase-appcheck') implementation 'com.google.android.gms:play-services-base:18.0.1' implementation 'com.google.android.gms:play-services-tasks:18.0.1' diff --git a/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md b/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md index e2d42165ab8..31ab374f069 100644 --- a/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md +++ b/appcheck/firebase-appcheck-playintegrity/CHANGELOG.md @@ -1,4 +1,7 @@ # Unreleased +* [fixed] Fixed client-side throttling in Play Integrity flows. +* [changed] Bumped Play Integrity API Library dependency version. + * [unchanged] Updated to keep [app_check] SDK versions aligned. # 17.0.0 diff --git a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle index fdad5887f1e..c90f1f00937 100644 --- a/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle +++ b/appcheck/firebase-appcheck-playintegrity/firebase-appcheck-playintegrity.gradle @@ -44,12 +44,13 @@ android { dependencies { 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 'com.google.firebase:firebase-common:20.4.2' + implementation 'com.google.firebase:firebase-common-ktx:20.4.2' + implementation 'com.google.firebase:firebase-components:17.1.5' implementation project(':appcheck:firebase-appcheck') implementation 'com.google.android.gms:play-services-base:18.0.1' implementation 'com.google.android.gms:play-services-tasks:18.0.1' - implementation 'com.google.android.play:integrity:1.0.1' + implementation 'com.google.android.play:integrity:1.2.0' javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' diff --git a/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle b/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle index 483664d658d..210d4eb7c5f 100644 --- a/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle +++ b/appcheck/firebase-appcheck-safetynet/firebase-appcheck-safetynet.gradle @@ -42,8 +42,9 @@ android { dependencies { 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 'com.google.firebase:firebase-common:20.4.2' + implementation 'com.google.firebase:firebase-common-ktx:20.4.2' + implementation 'com.google.firebase:firebase-components:17.1.5' implementation project(':appcheck:firebase-appcheck') implementation 'com.google.android.gms:play-services-base:18.0.1' implementation 'com.google.android.gms:play-services-tasks:18.0.1' diff --git a/appcheck/firebase-appcheck/CHANGELOG.md b/appcheck/firebase-appcheck/CHANGELOG.md index c70c73de6ab..cfb2a3ad447 100644 --- a/appcheck/firebase-appcheck/CHANGELOG.md +++ b/appcheck/firebase-appcheck/CHANGELOG.md @@ -1,4 +1,8 @@ # Unreleased +* [fixed] Fixed a bug causing internal tests to depend directly on `firebase-common`. + +* [fixed] Fixed client-side throttling in Play Integrity flows. + * [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-appcheck-ktx` to `com.google.firebase:firebase-appcheck` under the `com.google.firebase.appcheck` package. For details, see the @@ -10,8 +14,6 @@ now deprecated. As early as April 2024, we'll no longer release KTX modules. For details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) - - # 17.0.1 * [changed] Internal updates to allow Firebase SDKs to obtain limited-use tokens. @@ -105,4 +107,3 @@ additional updates: # 16.0.0-beta01 * [feature] Initial beta release of the [app_check] SDK with abuse reduction features. - diff --git a/appcheck/firebase-appcheck/firebase-appcheck.gradle b/appcheck/firebase-appcheck/firebase-appcheck.gradle index 93852346f57..2b946afe46f 100644 --- a/appcheck/firebase-appcheck/firebase-appcheck.gradle +++ b/appcheck/firebase-appcheck/firebase-appcheck.gradle @@ -65,9 +65,9 @@ dependencies { implementation 'com.google.firebase:firebase-annotations:16.2.0' implementation project(':appcheck:firebase-appcheck-interop') implementation project(path: ':appcheck:firebase-appcheck-interop') - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' testImplementation "androidx.test:core:$androidxTestCoreVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/appcheck/firebase-appcheck/ktx/ktx.gradle b/appcheck/firebase-appcheck/ktx/ktx.gradle index bfbd0adc2d0..bfdceda40d1 100644 --- a/appcheck/firebase-appcheck/ktx/ktx.gradle +++ b/appcheck/firebase-appcheck/ktx/ktx.gradle @@ -58,9 +58,9 @@ dependencies { androidTestImplementation 'junit:junit:4.12' api(project(":appcheck:firebase-appcheck")) androidTestImplementation(project(":appcheck:firebase-appcheck-interop")) - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") testImplementation "com.google.truth:truth:$googleTruthVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" testImplementation 'junit:junit:4.12' diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java index 58e0fdd0ac8..737b314457a 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/DefaultFirebaseAppCheck.java @@ -61,6 +61,7 @@ public class DefaultFirebaseAppCheck extends FirebaseAppCheck { private AppCheckProviderFactory appCheckProviderFactory; private AppCheckProvider appCheckProvider; private AppCheckToken cachedToken; + private Task cachedTokenTask; public DefaultFirebaseAppCheck( @NonNull FirebaseApp firebaseApp, @@ -192,24 +193,27 @@ public Task getToken(boolean forceRefresh) { DefaultAppCheckTokenResult.constructFromError( new FirebaseException("No AppCheckProvider installed."))); } - // TODO: Cache the in-flight task. - return fetchTokenFromProvider() - .continueWithTask( - liteExecutor, - appCheckTokenTask -> { - if (appCheckTokenTask.isSuccessful()) { - return Tasks.forResult( - DefaultAppCheckTokenResult.constructFromAppCheckToken( - appCheckTokenTask.getResult())); - } - // If the token exchange failed, return a dummy token for integrators to attach - // in their headers. - return Tasks.forResult( - DefaultAppCheckTokenResult.constructFromError( - new FirebaseException( - appCheckTokenTask.getException().getMessage(), - appCheckTokenTask.getException()))); - }); + if (cachedTokenTask == null + || cachedTokenTask.isComplete() + || cachedTokenTask.isCanceled()) { + cachedTokenTask = fetchTokenFromProvider(); + } + return cachedTokenTask.continueWithTask( + liteExecutor, + appCheckTokenTask -> { + if (appCheckTokenTask.isSuccessful()) { + return Tasks.forResult( + DefaultAppCheckTokenResult.constructFromAppCheckToken( + appCheckTokenTask.getResult())); + } + // If the token exchange failed, return a dummy token for integrators to attach + // in their headers. + return Tasks.forResult( + DefaultAppCheckTokenResult.constructFromError( + new FirebaseException( + appCheckTokenTask.getException().getMessage(), + appCheckTokenTask.getException()))); + }); }); } @@ -247,7 +251,12 @@ public Task getAppCheckToken(boolean forceRefresh) { if (appCheckProvider == null) { return Tasks.forException(new FirebaseException("No AppCheckProvider installed.")); } - return fetchTokenFromProvider(); + if (cachedTokenTask == null + || cachedTokenTask.isComplete() + || cachedTokenTask.isCanceled()) { + cachedTokenTask = fetchTokenFromProvider(); + } + return cachedTokenTask; }); } diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java index 7317524dfc2..539b493bd6d 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/NetworkClient.java @@ -121,7 +121,8 @@ public AppCheckTokenResponse exchangeAttestationForAppCheckToken( throw new FirebaseException("Too many attempts."); } URL url = new URL(String.format(getUrlTemplate(tokenType), projectId, appId, apiKey)); - String response = makeNetworkRequest(url, requestBytes, retryManager); + String response = + makeNetworkRequest(url, requestBytes, retryManager, /* resetRetryManagerOnSuccess= */ true); return AppCheckTokenResponse.fromJsonString(response); } @@ -138,11 +139,15 @@ public String generatePlayIntegrityChallenge( } URL url = new URL(String.format(PLAY_INTEGRITY_CHALLENGE_URL_TEMPLATE, projectId, appId, apiKey)); - return makeNetworkRequest(url, requestBytes, retryManager); + return makeNetworkRequest( + url, requestBytes, retryManager, /* resetRetryManagerOnSuccess= */ false); } private String makeNetworkRequest( - @NonNull URL url, @NonNull byte[] requestBytes, @NonNull RetryManager retryManager) + @NonNull URL url, + @NonNull byte[] requestBytes, + @NonNull RetryManager retryManager, + boolean resetRetryManagerOnSuccess) throws FirebaseException, IOException, JSONException { HttpURLConnection urlConnection = createHttpUrlConnection(url); @@ -187,7 +192,9 @@ private String makeNetworkRequest( + " body: " + httpErrorResponse.getErrorMessage()); } - retryManager.resetBackoffOnSuccess(); + if (resetRetryManagerOnSuccess) { + retryManager.resetBackoffOnSuccess(); + } return responseBody; } finally { urlConnection.disconnect(); diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/RetryManager.java b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/RetryManager.java index ea8c6cde029..8798c655512 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/RetryManager.java +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/internal/RetryManager.java @@ -26,7 +26,6 @@ public class RetryManager { @VisibleForTesting static final int BAD_REQUEST_ERROR_CODE = 400; - @VisibleForTesting static final int FORBIDDEN_ERROR_CODE = 403; @VisibleForTesting static final int NOT_FOUND_ERROR_CODE = 404; @VisibleForTesting static final long MAX_EXP_BACKOFF_MILLIS = 4 * 60 * 60 * 1000; // 4 hours @VisibleForTesting static final long ONE_DAY_MILLIS = 24 * 60 * 60 * 1000; // 24 hours @@ -91,9 +90,7 @@ public void updateBackoffOnFailure(int errorCode) { @BackoffStrategyType private static int getBackoffStrategyByErrorCode(int errorCode) { - if (errorCode == BAD_REQUEST_ERROR_CODE - || errorCode == FORBIDDEN_ERROR_CODE - || errorCode == NOT_FOUND_ERROR_CODE) { + if (errorCode == BAD_REQUEST_ERROR_CODE || errorCode == NOT_FOUND_ERROR_CODE) { return ONE_DAY; } return EXPONENTIAL; diff --git a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt index a054b932dc2..5cabcba0f55 100644 --- a/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt +++ b/appcheck/firebase-appcheck/src/main/java/com/google/firebase/appcheck/ktx/FirebaseAppCheck.kt @@ -23,6 +23,9 @@ import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.app /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseAppCheck] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -30,17 +33,13 @@ import com.google.firebase.ktx.app * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.appcheck.Firebase.appCheck` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.appCheck", - imports = ["com.google.firebase.Firebase", "com.google.firebase.appcheck.appCheck"] - ) -) val Firebase.appCheck: FirebaseAppCheck get() = FirebaseAppCheck.getInstance() /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseAppCheck] instance of a given [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -48,13 +47,6 @@ val Firebase.appCheck: FirebaseAppCheck * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.appcheck.Firebase.appCheck(app)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.appCheck(app)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.appcheck.appCheck"] - ) -) fun Firebase.appCheck(app: FirebaseApp) = FirebaseAppCheck.getInstance(app) /** @@ -68,8 +60,8 @@ fun Firebase.appCheck(app: FirebaseApp) = FirebaseAppCheck.getInstance(app) * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appcheck.AppCheckToken.component1` from the main module instead.", - ReplaceWith(expression = "component1()", imports = ["com.google.firebase.appcheck.component1"]) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun AppCheckToken.component1() = token @@ -84,8 +76,8 @@ operator fun AppCheckToken.component1() = token * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appcheck.AppCheckToken.component2` from the main module instead.", - ReplaceWith(expression = "component2()", imports = ["com.google.firebase.appcheck.component2"]) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun AppCheckToken.component2() = expireTimeMillis @@ -98,12 +90,8 @@ operator fun AppCheckToken.component2() = expireTimeMillis * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appcheck.FirebaseAppCheckKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseAppCheckKtxRegistrar", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.appcheck.FirebaseAppCheckKtxRegistrar"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) class FirebaseAppCheckKtxRegistrar : ComponentRegistrar { override fun getComponents(): List> = listOf() diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java index 9a2441f97b6..1b933cde274 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/NetworkClientTest.java @@ -325,7 +325,7 @@ public void generatePlayIntegrityChallenge_successResponse_returnsJsonString() t verify(mockOutputStream) .write(JSON_REQUEST.getBytes(), /* off= */ 0, JSON_REQUEST.getBytes().length); verify(mockRetryManager, never()).updateBackoffOnFailure(anyInt()); - verify(mockRetryManager).resetBackoffOnSuccess(); + verify(mockRetryManager, never()).resetBackoffOnSuccess(); verifyRequestHeaders(); } diff --git a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/RetryManagerTest.java b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/RetryManagerTest.java index 92c87f70c91..aaaf6750662 100644 --- a/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/RetryManagerTest.java +++ b/appcheck/firebase-appcheck/src/test/java/com/google/firebase/appcheck/internal/RetryManagerTest.java @@ -16,7 +16,6 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.firebase.appcheck.internal.RetryManager.BAD_REQUEST_ERROR_CODE; -import static com.google.firebase.appcheck.internal.RetryManager.FORBIDDEN_ERROR_CODE; import static com.google.firebase.appcheck.internal.RetryManager.MAX_EXP_BACKOFF_MILLIS; import static com.google.firebase.appcheck.internal.RetryManager.NOT_FOUND_ERROR_CODE; import static com.google.firebase.appcheck.internal.RetryManager.ONE_DAY_MILLIS; @@ -60,14 +59,6 @@ public void updateBackoffOnFailure_badRequestError_oneDayRetryStrategy() { .isEqualTo(CURRENT_TIME_MILLIS + ONE_DAY_MILLIS); } - @Test - public void updateBackoffOnFailure_forbiddenError_oneDayRetryStrategy() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); - - assertThat(retryManager.getNextRetryTimeMillis()) - .isEqualTo(CURRENT_TIME_MILLIS + ONE_DAY_MILLIS); - } - @Test public void updateBackoffOnFailure_notFoundError_oneDayRetryStrategy() { retryManager.updateBackoffOnFailure(NOT_FOUND_ERROR_CODE); @@ -78,9 +69,9 @@ public void updateBackoffOnFailure_notFoundError_oneDayRetryStrategy() { @Test public void updateBackoffOnFailure_oneDayRetryStrategy_multipleRetries() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); // The backoff period should not increase for consecutive failed retries with the ONE_DAY // strategy. @@ -133,7 +124,7 @@ public void updateBackoffOnFailure_exponentialRetryStrategy() { @Test public void canRetry_beforeNextRetryTime() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); // Sanity check. assertThat(mockClock.currentTimeMillis()).isEqualTo(CURRENT_TIME_MILLIS); @@ -145,7 +136,7 @@ public void canRetry_beforeNextRetryTime() { @Test public void canRetry_afterNextRetryTime() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); long nextRetryMillis = retryManager.getNextRetryTimeMillis(); when(mockClock.currentTimeMillis()).thenReturn(nextRetryMillis + 1); @@ -154,7 +145,7 @@ public void canRetry_afterNextRetryTime() { @Test public void resetBackoffOnSuccess() { - retryManager.updateBackoffOnFailure(FORBIDDEN_ERROR_CODE); + retryManager.updateBackoffOnFailure(BAD_REQUEST_ERROR_CODE); // Sanity check. assertThat(retryManager.getNextRetryTimeMillis()) .isEqualTo(CURRENT_TIME_MILLIS + ONE_DAY_MILLIS); diff --git a/appcheck/firebase-appcheck/test-app/src/main/AndroidManifest.xml b/appcheck/firebase-appcheck/test-app/src/main/AndroidManifest.xml index fbb5ebb6240..95347824781 100644 --- a/appcheck/firebase-appcheck/test-app/src/main/AndroidManifest.xml +++ b/appcheck/firebase-appcheck/test-app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ - + diff --git a/appcheck/firebase-appcheck/test-app/test-app.gradle b/appcheck/firebase-appcheck/test-app/test-app.gradle index 03d310035f7..2374c207c5d 100644 --- a/appcheck/firebase-appcheck/test-app/test-app.gradle +++ b/appcheck/firebase-appcheck/test-app/test-app.gradle @@ -40,8 +40,6 @@ dependencies { implementation 'com.google.android.gms:play-services-tasks:18.0.1' // Firebase dependencies - implementation project(':firebase-common') - implementation project(':firebase-components') implementation project(":appcheck:firebase-appcheck") implementation project(":appcheck:firebase-appcheck-debug") implementation project(":appcheck:firebase-appcheck-interop") diff --git a/contributor-docs/onboarding/new_sdk.md b/contributor-docs/onboarding/new_sdk.md index af94743e213..3faa36e7431 100644 --- a/contributor-docs/onboarding/new_sdk.md +++ b/contributor-docs/onboarding/new_sdk.md @@ -96,8 +96,8 @@ android { } dependencies { - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") } ``` diff --git a/firebase-appdistribution-api/firebase-appdistribution-api.gradle b/firebase-appdistribution-api/firebase-appdistribution-api.gradle index 99fc66a5132..a244045af26 100644 --- a/firebase-appdistribution-api/firebase-appdistribution-api.gradle +++ b/firebase-appdistribution-api/firebase-appdistribution-api.gradle @@ -54,9 +54,9 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation 'androidx.annotation:annotation:1.1.0' implementation 'com.google.android.gms:play-services-tasks:18.0.1' - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") testImplementation "androidx.test:core:$androidxTestCoreVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" diff --git a/firebase-appdistribution-api/ktx/ktx.gradle b/firebase-appdistribution-api/ktx/ktx.gradle index 5146dd4ef84..6b49ef6b94e 100644 --- a/firebase-appdistribution-api/ktx/ktx.gradle +++ b/firebase-appdistribution-api/ktx/ktx.gradle @@ -57,9 +57,9 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'junit:junit:4.12' api(project(":firebase-appdistribution-api")) - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") testImplementation "com.google.truth:truth:$googleTruthVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" testImplementation 'junit:junit:4.12' diff --git a/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt b/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt index 9161a1d5aca..181d8626a81 100644 --- a/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt +++ b/firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/ktx/FirebaseAppDistribution.kt @@ -24,6 +24,9 @@ import com.google.firebase.components.ComponentRegistrar import com.google.firebase.ktx.Firebase /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseAppDistribution] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -31,14 +34,6 @@ import com.google.firebase.ktx.Firebase * 2024, we'll no longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.appDistribution` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.appDistribution", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.appdistribution.appDistribution"] - ) -) val Firebase.appDistribution: FirebaseAppDistribution get() = FirebaseAppDistribution.getInstance() @@ -53,11 +48,8 @@ val Firebase.appDistribution: FirebaseAppDistribution * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appdistribution.AppDistributionRelease.component1()` from the main module instead.", - ReplaceWith( - expression = "component1()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.appdistribution.component1"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun AppDistributionRelease.component1() = binaryType @@ -72,11 +64,8 @@ operator fun AppDistributionRelease.component1() = binaryType * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appdistribution.AppDistributionRelease.component2()` from the main module instead.", - ReplaceWith( - expression = "component2()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.appdistribution.component2"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun AppDistributionRelease.component2() = displayVersion @@ -91,11 +80,8 @@ operator fun AppDistributionRelease.component2() = displayVersion * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appdistribution.AppDistributionRelease.component3()` from the main module instead.", - ReplaceWith( - expression = "component3()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.appdistribution.component3"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun AppDistributionRelease.component3() = versionCode @@ -110,11 +96,8 @@ operator fun AppDistributionRelease.component3() = versionCode * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appdistribution.AppDistributionRelease.component4()` from the main module instead.", - ReplaceWith( - expression = "component4()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.appdistribution.component4"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun AppDistributionRelease.component4() = releaseNotes @@ -129,11 +112,8 @@ operator fun AppDistributionRelease.component4() = releaseNotes * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appdistribution.UpdateProgress.component1()` from the main module instead.", - ReplaceWith( - expression = "component1()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.appdistribution.component1"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun UpdateProgress.component1() = apkBytesDownloaded @@ -148,11 +128,8 @@ operator fun UpdateProgress.component1() = apkBytesDownloaded * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appdistribution.UpdateProgress.component2()` from the main module instead.", - ReplaceWith( - expression = "component2()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.appdistribution.component2"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun UpdateProgress.component2() = apkFileTotalBytes @@ -167,11 +144,8 @@ operator fun UpdateProgress.component2() = apkFileTotalBytes * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appdistribution.UpdateProgress.component3()` from the main module instead.", - ReplaceWith( - expression = "component3()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.appdistribution.component3"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun UpdateProgress.component3() = updateStatus @@ -184,15 +158,8 @@ operator fun UpdateProgress.component3() = updateStatus * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.appdistribution.FirebaseAppDistributionKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseAppDistributionKtxRegistrar", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.appdistribution.FirebaseAppDistributionKtxRegistrar" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep class FirebaseAppDistributionKtxRegistrar : ComponentRegistrar { diff --git a/firebase-common/gradle.properties b/firebase-common/gradle.properties index 0ecf13653c5..97f41337e63 100644 --- a/firebase-common/gradle.properties +++ b/firebase-common/gradle.properties @@ -1,3 +1,3 @@ -version=20.4.2 -latestReleasedVersion=20.4.1 +version=20.4.3 +latestReleasedVersion=20.4.2 android.enableUnitTestBinaryResources=true diff --git a/firebase-common/ktx/ktx.gradle.kts b/firebase-common/ktx/ktx.gradle.kts index ab29238bd8f..86056bc799d 100644 --- a/firebase-common/ktx/ktx.gradle.kts +++ b/firebase-common/ktx/ktx.gradle.kts @@ -44,7 +44,7 @@ android { dependencies { api(project(":firebase-common")) - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-components:17.1.5") implementation("com.google.firebase:firebase-annotations:16.2.0") testImplementation(libs.androidx.test.core) testImplementation(libs.junit) diff --git a/firebase-common/src/main/java/com/google/firebase/ktx/Firebase.kt b/firebase-common/src/main/java/com/google/firebase/ktx/Firebase.kt index 509724c27d1..1f28ad0f9d1 100644 --- a/firebase-common/src/main/java/com/google/firebase/ktx/Firebase.kt +++ b/firebase-common/src/main/java/com/google/firebase/ktx/Firebase.kt @@ -44,6 +44,9 @@ import kotlinx.coroutines.asCoroutineDispatcher object Firebase /** + * Accessing this object for Kotlin apps has changed; see the migration guide: + * https://firebase.google.com/docs/android/kotlin-migration. + * * Returns the default firebase app instance. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -51,17 +54,13 @@ object Firebase * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration). */ -@Deprecated( - "Use `com.google.firebase.Firebase.app` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.app", - imports = ["com.google.firebase.Firebase", "com.google.firebase.app"], - ) -) val Firebase.app: FirebaseApp get() = FirebaseApp.getInstance() /** + * Accessing this object for Kotlin apps has changed; see the migration guide: + * https://firebase.google.com/docs/android/kotlin-migration. + * * Returns a named firebase app instance. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -69,13 +68,6 @@ val Firebase.app: FirebaseApp * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration). */ -@Deprecated( - "Use `com.google.firebase.Firebase.app(name)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.app(name)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.app"] - ) -) fun Firebase.app(name: String): FirebaseApp = FirebaseApp.getInstance(name) /** @@ -87,11 +79,8 @@ fun Firebase.app(name: String): FirebaseApp = FirebaseApp.getInstance(name) * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.Firebase.initialize(context)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.initialize(context)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.initialize"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun Firebase.initialize(context: Context): FirebaseApp? = FirebaseApp.initializeApp(context) @@ -104,11 +93,8 @@ fun Firebase.initialize(context: Context): FirebaseApp? = FirebaseApp.initialize * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.Firebase.initialize(context, options)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.initialize(context, options)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.initialize"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun Firebase.initialize(context: Context, options: FirebaseOptions): FirebaseApp = FirebaseApp.initializeApp(context, options) @@ -122,16 +108,16 @@ fun Firebase.initialize(context: Context, options: FirebaseOptions): FirebaseApp * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.Firebase.initialize(context, options, name)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.initialize(context, options, name)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.initialize"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun Firebase.initialize(context: Context, options: FirebaseOptions, name: String): FirebaseApp = FirebaseApp.initializeApp(context, options, name) /** + * Accessing this object for Kotlin apps has changed; see the migration guide: + * https://firebase.google.com/docs/android/kotlin-migration. + * * Returns options of default FirebaseApp * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -139,23 +125,13 @@ fun Firebase.initialize(context: Context, options: FirebaseOptions, name: String * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.options` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.options", - imports = ["com.google.firebase.Firebase", "com.google.firebase.options"] - ) -) val Firebase.options: FirebaseOptions get() = Firebase.app.options /** @suppress */ @Deprecated( - "Use `com.google.firebase.FirebaseCommonKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseCommonKtxRegistrar", - imports = ["com.google.firebase.Firebase", "com.google.firebase.FirebaseCommonKtxRegistrar"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep class FirebaseCommonKtxRegistrar : ComponentRegistrar { diff --git a/firebase-components/gradle.properties b/firebase-components/gradle.properties index 4c73e5e8ae1..574204fb68c 100644 --- a/firebase-components/gradle.properties +++ b/firebase-components/gradle.properties @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -version=17.1.5 -latestReleasedVersion=17.1.4 +version=17.1.6 +latestReleasedVersion=17.1.5 diff --git a/firebase-config/bandwagoner/bandwagoner.gradle b/firebase-config/bandwagoner/bandwagoner.gradle index c5cb62c6abc..3ab1ce42bf0 100644 --- a/firebase-config/bandwagoner/bandwagoner.gradle +++ b/firebase-config/bandwagoner/bandwagoner.gradle @@ -77,10 +77,9 @@ dependencies { // "implementation" dependencies. The alternative would be to make common an "api" dep of remote-config. // Released artifacts don't need these dependencies since they don't use `project` to refer // to Remote Config. - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") implementation(project(":firebase-installations-interop")) { exclude group: 'com.google.firebase', module: 'firebase-common' diff --git a/firebase-config/firebase-config.gradle b/firebase-config/firebase-config.gradle index 671d0865718..9b6c0cd0132 100644 --- a/firebase-config/firebase-config.gradle +++ b/firebase-config/firebase-config.gradle @@ -76,9 +76,9 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-components' } - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") implementation(project(":firebase-installations")) javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' testImplementation "androidx.test:core:$androidxTestCoreVersion" diff --git a/firebase-config/ktx/ktx.gradle b/firebase-config/ktx/ktx.gradle index ec32df0873e..142b8b40083 100644 --- a/firebase-config/ktx/ktx.gradle +++ b/firebase-config/ktx/ktx.gradle @@ -43,9 +43,9 @@ android { } dependencies { - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") api(project(":firebase-config")) api(project(":firebase-installations")) implementation 'com.google.firebase:firebase-installations-interop:17.1.0' diff --git a/firebase-config/src/main/java/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt b/firebase-config/src/main/java/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt index d676be789d3..5c7c1417308 100644 --- a/firebase-config/src/main/java/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt +++ b/firebase-config/src/main/java/com/google/firebase/remoteconfig/ktx/RemoteConfig.kt @@ -32,6 +32,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseRemoteConfig] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -39,17 +42,13 @@ import kotlinx.coroutines.flow.callbackFlow * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.remoteConfig` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.remoteConfig", - imports = ["com.google.firebase.Firebase", "com.google.firebase.remoteconfig.remoteConfig"] - ) -) val Firebase.remoteConfig: FirebaseRemoteConfig get() = FirebaseRemoteConfig.getInstance() /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseRemoteConfig] instance of a given [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -57,13 +56,6 @@ val Firebase.remoteConfig: FirebaseRemoteConfig * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.remoteConfig(app)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.remoteConfig(app)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.remoteconfig.remoteConfig"] - ) -) fun Firebase.remoteConfig(app: FirebaseApp): FirebaseRemoteConfig = FirebaseRemoteConfig.getInstance(app) @@ -76,8 +68,8 @@ fun Firebase.remoteConfig(app: FirebaseApp): FirebaseRemoteConfig = * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.remoteconfig.FirebaseRemoteConfig.get(key).` from the main module instead.", - ReplaceWith(expression = "get(key)", imports = ["com.google.firebase.remoteconfig.get"]) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun FirebaseRemoteConfig.get(key: String): FirebaseRemoteConfigValue { return this.getValue(key) @@ -105,11 +97,8 @@ fun remoteConfigSettings( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.remoteconfig.FirebaseRemoteConfig.configUpdates` from the main module instead.", - ReplaceWith( - expression = "configUpdates", - imports = ["com.google.firebase.remoteconfig.configUpdates"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) val FirebaseRemoteConfig.configUpdates get() = callbackFlow { @@ -137,15 +126,8 @@ val FirebaseRemoteConfig.configUpdates * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.remoteconfig.FirebaseRemoteConfigKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseRemoteConfigKtxRegistrar", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.remoteconfig.FirebaseRemoteConfigKtxRegistrar" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep class FirebaseRemoteConfigKtxRegistrar : ComponentRegistrar { diff --git a/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle b/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle index dc9db2a87bc..4b9e4d2cf23 100644 --- a/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle +++ b/firebase-crashlytics-ndk/firebase-crashlytics-ndk.gradle @@ -105,9 +105,9 @@ thirdPartyLicenses { } dependencies { - implementation "com.google.firebase:firebase-common:20.4.1" - implementation "com.google.firebase:firebase-common-ktx:20.4.1" - implementation "com.google.firebase:firebase-components:17.1.4" + implementation "com.google.firebase:firebase-common:20.4.2" + implementation "com.google.firebase:firebase-common-ktx:20.4.2" + implementation "com.google.firebase:firebase-components:17.1.5" implementation project(':firebase-crashlytics') implementation 'com.google.android.gms:play-services-basement:18.1.0' diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index 0d7c876bd92..2f728d037e7 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -10,6 +10,8 @@ now deprecated. As early as April 2024, we'll no longer release KTX modules. For details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) +* [fixed] Fixed Flutter and Unity on-demand fatal `setUserIdentifier` behaviour. Github + [#10759](https://github.com/firebase/flutterfire/issues/10759) # 18.4.3 * [fixed] Disabled `GradleMetadataPublishing` to fix breakage of the Kotlin extensions library. [#5337] diff --git a/firebase-crashlytics/firebase-crashlytics.gradle b/firebase-crashlytics/firebase-crashlytics.gradle index 1e83dc01d21..532689e3a5f 100644 --- a/firebase-crashlytics/firebase-crashlytics.gradle +++ b/firebase-crashlytics/firebase-crashlytics.gradle @@ -88,9 +88,9 @@ dependencies { exclude group: 'com.google.firebase', module: 'firebase-components' } implementation(libs.androidx.annotation) - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") implementation(project(":firebase-installations")) implementation(project(':firebase-sessions')) { exclude group: 'com.google.firebase', module: 'firebase-common' diff --git a/firebase-crashlytics/ktx/ktx.gradle b/firebase-crashlytics/ktx/ktx.gradle index 263679d8e60..193d75e7be1 100644 --- a/firebase-crashlytics/ktx/ktx.gradle +++ b/firebase-crashlytics/ktx/ktx.gradle @@ -48,8 +48,8 @@ dependencies { androidTestImplementation(libs.androidx.test.junit) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.truth) - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") api(project(":firebase-crashlytics")) } \ No newline at end of file diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java index 4254152975f..a35bd5f443e 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java @@ -14,6 +14,7 @@ package com.google.firebase.crashlytics.internal.common; +import static org.mockito.AdditionalMatchers.not; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyString; @@ -38,6 +39,7 @@ import com.google.firebase.crashlytics.internal.NativeSessionFileProvider; import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger; import com.google.firebase.crashlytics.internal.metadata.LogFileManager; +import com.google.firebase.crashlytics.internal.metadata.UserMetadata; import com.google.firebase.crashlytics.internal.model.CrashlyticsReport; import com.google.firebase.crashlytics.internal.persistence.FileStore; import com.google.firebase.crashlytics.internal.settings.Settings; @@ -52,6 +54,7 @@ import java.util.TreeSet; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import org.junit.Test; import org.mockito.ArgumentCaptor; public class CrashlyticsControllerTest extends CrashlyticsTestCase { @@ -101,8 +104,12 @@ private class ControllerBuilder { private CrashlyticsNativeComponent nativeComponent = null; private AnalyticsEventLogger analyticsEventLogger; private SessionReportingCoordinator sessionReportingCoordinator; + + private CrashlyticsBackgroundWorker backgroundWorker; private LogFileManager logFileManager = null; + private UserMetadata userMetadata = null; + ControllerBuilder() { dataCollectionArbiter = mockDataCollectionArbiter; nativeComponent = mockNativeComponent; @@ -110,6 +117,8 @@ private class ControllerBuilder { analyticsEventLogger = mock(AnalyticsEventLogger.class); sessionReportingCoordinator = mockSessionReportingCoordinator; + + backgroundWorker = new CrashlyticsBackgroundWorker(new SameThreadExecutorService()); } ControllerBuilder setDataCollectionArbiter(DataCollectionArbiter arbiter) { @@ -117,6 +126,11 @@ ControllerBuilder setDataCollectionArbiter(DataCollectionArbiter arbiter) { return this; } + ControllerBuilder setUserMetadata(UserMetadata userMetadata) { + this.userMetadata = userMetadata; + return this; + } + public ControllerBuilder setNativeComponent(CrashlyticsNativeComponent nativeComponent) { this.nativeComponent = nativeComponent; return this; @@ -153,13 +167,13 @@ public CrashlyticsController build() { final CrashlyticsController controller = new CrashlyticsController( testContext.getApplicationContext(), - new CrashlyticsBackgroundWorker(new SameThreadExecutorService()), + backgroundWorker, idManager, dataCollectionArbiter, testFileStore, crashMarker, appData, - null, + userMetadata, logFileManager, sessionReportingCoordinator, nativeComponent, @@ -211,6 +225,26 @@ public void testFatalException_callsSessionReportingCoordinatorPersistFatal() th .persistFatalEvent(eq(fatal), eq(thread), eq(sessionId), anyLong()); } + @Test + public void testOnDemandFatal_callLogFatalException() { + Thread thread = Thread.currentThread(); + Exception fatal = new RuntimeException("Fatal"); + Thread.UncaughtExceptionHandler exceptionHandler = mock(Thread.UncaughtExceptionHandler.class); + UserMetadata mockUserMetadata = mock(UserMetadata.class); + when(mockSessionReportingCoordinator.listSortedOpenSessionIds()) + .thenReturn(new TreeSet<>(Collections.singleton(SESSION_ID)).descendingSet()); + + final CrashlyticsController controller = + builder() + .setLogFileManager(new LogFileManager(testFileStore)) + .setUserMetadata(mockUserMetadata) + .build(); + controller.enableExceptionHandling(SESSION_ID, exceptionHandler, testSettingsProvider); + controller.logFatalException(thread, fatal); + + verify(mockUserMetadata).setNewSession(not(eq(SESSION_ID))); + } + public void testNativeCrashDataCausesNativeReport() throws Exception { final String sessionId = "sessionId_1_new"; final String previousSessionId = "sessionId_0_previous"; diff --git a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/metadata/MetaDataStoreTest.java b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/metadata/MetaDataStoreTest.java index 2b63b3c5146..64828d3c78a 100644 --- a/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/metadata/MetaDataStoreTest.java +++ b/firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/metadata/MetaDataStoreTest.java @@ -14,6 +14,8 @@ package com.google.firebase.crashlytics.internal.metadata; +import static com.google.common.truth.Truth.assertThat; + import com.google.firebase.crashlytics.internal.CrashlyticsTestCase; import com.google.firebase.crashlytics.internal.common.CrashlyticsBackgroundWorker; import com.google.firebase.crashlytics.internal.persistence.FileStore; @@ -23,6 +25,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.junit.Test; @SuppressWarnings("ResultOfMethodCallIgnored") // Convenient use of files. public class MetaDataStoreTest extends CrashlyticsTestCase { @@ -139,6 +142,55 @@ public void testReadUserData_noStoredData() { assertNull(userData.getUserId()); } + @Test + public void testUpdateSessionId_notPersistUserIdToNewSessionIfNoUserIdSet() { + UserMetadata userMetadata = new UserMetadata(SESSION_ID_1, fileStore, worker); + userMetadata.setNewSession(SESSION_ID_2); + assertThat(fileStore.getSessionFile(SESSION_ID_2, UserMetadata.USERDATA_FILENAME).exists()) + .isFalse(); + } + + @Test + public void testUpdateSessionId_notPersistCustomKeysToNewSessionIfNoCustomKeysSet() { + UserMetadata userMetadata = new UserMetadata(SESSION_ID_1, fileStore, worker); + userMetadata.setNewSession(SESSION_ID_2); + assertThat(fileStore.getSessionFile(SESSION_ID_2, UserMetadata.KEYDATA_FILENAME).exists()) + .isFalse(); + } + + @Test + public void testUpdateSessionId_persistCustomKeysToNewSessionIfCustomKeysSet() { + UserMetadata userMetadata = new UserMetadata(SESSION_ID_1, fileStore, worker); + final Map keys = + new HashMap() { + { + put(KEY_1, VALUE_1); + put(KEY_2, VALUE_2); + put(KEY_3, VALUE_3); + } + }; + userMetadata.setCustomKeys(keys); + userMetadata.setNewSession(SESSION_ID_2); + assertThat(fileStore.getSessionFile(SESSION_ID_2, UserMetadata.KEYDATA_FILENAME).exists()) + .isTrue(); + + MetaDataStore metaDataStore = new MetaDataStore(fileStore); + assertThat(metaDataStore.readKeyData(SESSION_ID_2)).isEqualTo(keys); + } + + @Test + public void testUpdateSessionId_persistUserIdToNewSessionIfUserIdSet() { + String userId = "ThemisWang"; + UserMetadata userMetadata = new UserMetadata(SESSION_ID_1, fileStore, worker); + userMetadata.setUserId(userId); + userMetadata.setNewSession(SESSION_ID_2); + assertThat(fileStore.getSessionFile(SESSION_ID_2, UserMetadata.USERDATA_FILENAME).exists()) + .isTrue(); + + MetaDataStore metaDataStore = new MetaDataStore(fileStore); + assertThat(metaDataStore.readUserId(SESSION_ID_2)).isEqualTo(userId); + } + // Keys public void testWriteKeys() { diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java index 831c08edea7..dd08991eb59 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java @@ -216,7 +216,7 @@ public Task call() throws Exception { doWriteAppExceptionMarker(timestampMillis); doCloseSessions(settingsProvider); - doOpenSession(new CLSUUID(idManager).toString()); + doOpenSession(new CLSUUID(idManager).toString(), isOnDemand); // If automatic data collection is disabled, we'll need to wait until the next run // of the app. @@ -498,7 +498,7 @@ void openSession(String sessionIdentifier) { new Callable() { @Override public Void call() throws Exception { - doOpenSession(sessionIdentifier); + doOpenSession(sessionIdentifier, /*isOnDemand=*/ false); return null; } }); @@ -550,7 +550,7 @@ boolean finalizeSessions(SettingsProvider settingsProvider) { * Not synchronized/locked. Must be executed from the single thread executor service used by this * class. */ - private void doOpenSession(String sessionIdentifier) { + private void doOpenSession(String sessionIdentifier, Boolean isOnDemand) { final long startedAtSeconds = getCurrentTimestampSeconds(); Logger.getLogger().d("Opening a new session with ID " + sessionIdentifier); @@ -568,6 +568,14 @@ private void doOpenSession(String sessionIdentifier) { startedAtSeconds, StaticSessionData.create(appData, osData, deviceData)); + // If is on-demand fatal, we need to update the session id for userMetadata + // as well(since we don't really change the object to a new one for a new session). + // all the information in the previous session is still in memory, but we do need to + // manually writing them into persistence for the new session. + if (isOnDemand && sessionIdentifier != null) { + userMetadata.setNewSession(sessionIdentifier); + } + logFileManager.setCurrentSession(sessionIdentifier); sessionsSubscriber.setSessionId(sessionIdentifier); reportingCoordinator.onBeginSession(sessionIdentifier, startedAtSeconds); diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/UserMetadata.java b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/UserMetadata.java index 90aec643ae1..7bc72271993 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/UserMetadata.java +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/metadata/UserMetadata.java @@ -41,7 +41,7 @@ public class UserMetadata { private final MetaDataStore metaDataStore; private final CrashlyticsBackgroundWorker backgroundWorker; - private final String sessionIdentifier; + private String sessionIdentifier; // The following references contain a marker bit, which is true if the data maintained in the // associated reference has been serialized since the last time it was updated. @@ -77,6 +77,26 @@ public UserMetadata( this.backgroundWorker = backgroundWorker; } + /** + * Refresh the userMetadata to reflect the status of the new session. This API is mainly for + * on-demand fatal feature since we need to close and update to a new session. UserMetadata also + * need to make this update instead of updating session id, we also need to manually writing the + * into persistence for the new session. + */ + public void setNewSession(String sessionId) { + synchronized (sessionIdentifier) { + sessionIdentifier = sessionId; + Map keyData = customKeys.getKeys(); + if (getUserId() != null) { + metaDataStore.writeUserData(sessionId, getUserId()); + } + if (!keyData.isEmpty()) { + metaDataStore.writeKeyData(sessionId, keyData); + } + // TODO(themis): adding feature rollouts later + } + } + @Nullable public String getUserId() { return userId.getReference(); diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/ktx/FirebaseCrashlytics.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/ktx/FirebaseCrashlytics.kt index 24db92be29f..c71dbbf1fd0 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/ktx/FirebaseCrashlytics.kt +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/ktx/FirebaseCrashlytics.kt @@ -22,6 +22,9 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.ktx.Firebase /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseCrashlytics] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -29,13 +32,6 @@ import com.google.firebase.ktx.Firebase * no longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.crashlytics` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.crashlytics", - imports = ["com.google.firebase.Firebase", "com.google.firebase.crashlytics.crashlytics"] - ) -) val Firebase.crashlytics: FirebaseCrashlytics get() = FirebaseCrashlytics.getInstance() @@ -48,15 +44,8 @@ val Firebase.crashlytics: FirebaseCrashlytics * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.crashlytics.FirebaseCrashlytics.setCustomKeys(init)` from the main module instead.", - ReplaceWith( - expression = "FirebaseCrashlytics.setCustomKeys(init)", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.crashlytics.FirebaseCrashlytics.setCustomKeys" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun FirebaseCrashlytics.setCustomKeys(init: KeyValueBuilder.() -> Unit) { val builder = KeyValueBuilder(this) @@ -72,15 +61,8 @@ fun FirebaseCrashlytics.setCustomKeys(init: KeyValueBuilder.() -> Unit) { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.crashlytics.FirebaseCrashlyticsKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseCrashlyticsKtxRegistrar", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.crashlytics.FirebaseCrashlyticsKtxRegistrar" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep internal class FirebaseCrashlyticsKtxRegistrar : ComponentRegistrar { diff --git a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/ktx/KeyValueBuilder.kt b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/ktx/KeyValueBuilder.kt index 0eb43fd6f6f..8638b98922c 100644 --- a/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/ktx/KeyValueBuilder.kt +++ b/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/ktx/KeyValueBuilder.kt @@ -32,11 +32,7 @@ class KeyValueBuilder(private val crashlytics: FirebaseCrashlytics) { */ @Deprecated( "Use `com.google.firebase.crashlytics.KeyValueBuilder.key(key, value)` from the main module.", - ReplaceWith( - expression = "key(key, value)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.crashlytics.KeyValueBuilder.key"] - ) + ReplaceWith("") ) fun key(key: String, value: Boolean) = crashlytics.setCustomKey(key, value) @@ -50,11 +46,7 @@ class KeyValueBuilder(private val crashlytics: FirebaseCrashlytics) { */ @Deprecated( "Use `com.google.firebase.crashlytics.KeyValueBuilder.key(key, value)` from the main module.", - ReplaceWith( - expression = "key(key, value)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.crashlytics.KeyValueBuilder.key"] - ) + ReplaceWith("") ) fun key(key: String, value: Double) = crashlytics.setCustomKey(key, value) @@ -68,11 +60,7 @@ class KeyValueBuilder(private val crashlytics: FirebaseCrashlytics) { */ @Deprecated( "Use `com.google.firebase.crashlytics.KeyValueBuilder.key(key, value)` from the main module.", - ReplaceWith( - expression = "key(key, value)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.crashlytics.KeyValueBuilder.key"] - ) + ReplaceWith("") ) fun key(key: String, value: Float) = crashlytics.setCustomKey(key, value) @@ -86,11 +74,7 @@ class KeyValueBuilder(private val crashlytics: FirebaseCrashlytics) { */ @Deprecated( "Use `com.google.firebase.crashlytics.KeyValueBuilder.key(key, value)` from the main module.", - ReplaceWith( - expression = "key(key, value)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.crashlytics.KeyValueBuilder.key"] - ) + ReplaceWith("") ) fun key(key: String, value: Int) = crashlytics.setCustomKey(key, value) @@ -104,11 +88,7 @@ class KeyValueBuilder(private val crashlytics: FirebaseCrashlytics) { */ @Deprecated( "Use `com.google.firebase.crashlytics.KeyValueBuilder.key(key, value)` from the main module.", - ReplaceWith( - expression = "key(key, value)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.crashlytics.KeyValueBuilder.key"] - ) + ReplaceWith("") ) fun key(key: String, value: Long) = crashlytics.setCustomKey(key, value) @@ -122,11 +102,7 @@ class KeyValueBuilder(private val crashlytics: FirebaseCrashlytics) { */ @Deprecated( "Use `com.google.firebase.crashlytics.KeyValueBuilder.key(key, value)` from the main module.", - ReplaceWith( - expression = "key(key, value)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.crashlytics.KeyValueBuilder.key"] - ) + ReplaceWith("") ) fun key(key: String, value: String) = crashlytics.setCustomKey(key, value) } diff --git a/firebase-database/firebase-database.gradle.kts b/firebase-database/firebase-database.gradle.kts index 90068154f3c..9d72a599e7b 100644 --- a/firebase-database/firebase-database.gradle.kts +++ b/firebase-database/firebase-database.gradle.kts @@ -56,9 +56,9 @@ android { dependencies { implementation(project(":appcheck:firebase-appcheck-interop")) - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") implementation("com.google.firebase:firebase-auth-interop:20.0.0") { exclude(group = "com.google.firebase", module = "firebase-common") exclude(group = "com.google.firebase", module = "firebase-components") diff --git a/firebase-database/ktx/ktx.gradle.kts b/firebase-database/ktx/ktx.gradle.kts index af8cb3299ad..86482c672ee 100644 --- a/firebase-database/ktx/ktx.gradle.kts +++ b/firebase-database/ktx/ktx.gradle.kts @@ -48,11 +48,11 @@ android { } dependencies { - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") api(project(":firebase-database")) - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-components:17.1.5") testImplementation(libs.androidx.test.core) testImplementation(libs.junit) diff --git a/firebase-database/src/main/java/com/google/firebase/database/ktx/ChildEvent.kt b/firebase-database/src/main/java/com/google/firebase/database/ktx/ChildEvent.kt index 0062a893e51..a8b1e9d0813 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/ktx/ChildEvent.kt +++ b/firebase-database/src/main/java/com/google/firebase/database/ktx/ChildEvent.kt @@ -26,8 +26,8 @@ import com.google.firebase.database.DataSnapshot * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.ChildEvent` from the main module instead.", - ReplaceWith(expression = "ChildEvent", imports = ["com.google.firebase.database.ChildEvent"]) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) sealed class ChildEvent { /** @@ -45,11 +45,8 @@ sealed class ChildEvent { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.ChildEvent.Added` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.database.ChildEvent.Added", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.ChildEvent.Added"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) data class Added(val snapshot: DataSnapshot, val previousChildName: String?) : ChildEvent() @@ -68,11 +65,8 @@ sealed class ChildEvent { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.ChildEvent.Changed` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.database.ChildEvent.Changed", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.ChildEvent.Changed"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) data class Changed(val snapshot: DataSnapshot, val previousChildName: String?) : ChildEvent() @@ -87,11 +81,8 @@ sealed class ChildEvent { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.ChildEvent.Removed` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.database.ChildEvent.Removed", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.ChildEvent.Removed"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) data class Removed(val snapshot: DataSnapshot) : ChildEvent() @@ -110,11 +101,8 @@ sealed class ChildEvent { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.ChildEvent.Moved` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.database.ChildEvent.Moved", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.ChildEvent.Moved"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) data class Moved(val snapshot: DataSnapshot, val previousChildName: String?) : ChildEvent() } diff --git a/firebase-database/src/main/java/com/google/firebase/database/ktx/Database.kt b/firebase-database/src/main/java/com/google/firebase/database/ktx/Database.kt index 62e6a2e3706..4b56a2d4f7d 100644 --- a/firebase-database/src/main/java/com/google/firebase/database/ktx/Database.kt +++ b/firebase-database/src/main/java/com/google/firebase/database/ktx/Database.kt @@ -35,6 +35,9 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.map /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseDatabase] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -42,17 +45,13 @@ import kotlinx.coroutines.flow.map * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.database` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.database", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.database"] - ) -) val Firebase.database: FirebaseDatabase get() = FirebaseDatabase.getInstance() /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseDatabase] instance for the specified [url]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -60,16 +59,12 @@ val Firebase.database: FirebaseDatabase * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.database(url)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.database(url)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.database"] - ) -) fun Firebase.database(url: String): FirebaseDatabase = FirebaseDatabase.getInstance(url) /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseDatabase] instance of the given [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -77,16 +72,12 @@ fun Firebase.database(url: String): FirebaseDatabase = FirebaseDatabase.getInsta * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.database(app)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.database(app)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.database"] - ) -) fun Firebase.database(app: FirebaseApp): FirebaseDatabase = FirebaseDatabase.getInstance(app) /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseDatabase] instance of the given [FirebaseApp] and [url]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -94,13 +85,6 @@ fun Firebase.database(app: FirebaseApp): FirebaseDatabase = FirebaseDatabase.get * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.database(app, url)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.database(app, url)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.database"] - ) -) fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase = FirebaseDatabase.getInstance(app, url) @@ -116,11 +100,8 @@ fun Firebase.database(app: FirebaseApp, url: String): FirebaseDatabase = * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.DataSnapshot.getValue` from the main module instead.", - ReplaceWith( - expression = "getValue()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.getValue"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun DataSnapshot.getValue(): T? { return getValue(object : GenericTypeIndicator() {}) @@ -138,11 +119,8 @@ inline fun DataSnapshot.getValue(): T? { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.MutableData.getValue` from the main module instead.", - ReplaceWith( - expression = "getValue()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.getValue"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun MutableData.getValue(): T? { return getValue(object : GenericTypeIndicator() {}) @@ -160,11 +138,8 @@ inline fun MutableData.getValue(): T? { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.Query.snapshots` from the main module instead.", - ReplaceWith( - expression = "snapshots", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.snapshots"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) val Query.snapshots get() = @@ -195,11 +170,8 @@ val Query.snapshots * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.Query.childEvents` from the main module instead.", - ReplaceWith( - expression = "childEvents", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.childEvents"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) val Query.childEvents get() = @@ -243,11 +215,8 @@ val Query.childEvents * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.Query.values` from the main module instead.", - ReplaceWith( - expression = "values()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.database.values"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun Query.values(): Flow { return snapshots.map { it.getValue(T::class.java) } @@ -262,12 +231,8 @@ inline fun Query.values(): Flow { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.database.FirebaseDatabaseKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseDatabaseKtxRegistrar", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.database.FirebaseDatabaseKtxRegistrar"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep class FirebaseDatabaseKtxRegistrar : ComponentRegistrar { diff --git a/firebase-dynamic-links/firebase-dynamic-links.gradle b/firebase-dynamic-links/firebase-dynamic-links.gradle index b55dcd1f102..dfe4a616798 100644 --- a/firebase-dynamic-links/firebase-dynamic-links.gradle +++ b/firebase-dynamic-links/firebase-dynamic-links.gradle @@ -62,9 +62,9 @@ dependencies { implementation('com.google.firebase:firebase-measurement-connector:19.0.0') { exclude group: 'com.google.firebase', module: 'firebase-common' } - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' javadocClasspath 'com.google.code.findbugs:jsr305:3.0.2' javadocClasspath 'org.checkerframework:checker-qual:2.5.2' diff --git a/firebase-dynamic-links/ktx/ktx.gradle b/firebase-dynamic-links/ktx/ktx.gradle index 51dee9ffdba..26e5ef27886 100644 --- a/firebase-dynamic-links/ktx/ktx.gradle +++ b/firebase-dynamic-links/ktx/ktx.gradle @@ -43,9 +43,9 @@ android { } dependencies { - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") api(project(":firebase-dynamic-links")) testImplementation "androidx.test:core:$androidxTestCoreVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/ktx/FirebaseDynamicLinks.kt b/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/ktx/FirebaseDynamicLinks.kt index e99f00662bf..379331092f1 100644 --- a/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/ktx/FirebaseDynamicLinks.kt +++ b/firebase-dynamic-links/src/main/java/com/google/firebase/dynamiclinks/ktx/FirebaseDynamicLinks.kt @@ -26,6 +26,9 @@ import com.google.firebase.dynamiclinks.ShortDynamicLink import com.google.firebase.ktx.Firebase /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseDynamicLinks] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -33,17 +36,13 @@ import com.google.firebase.ktx.Firebase * we'll no longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.dynamicLinks` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.dynamicLinks", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.dynamicLinks"] - ) -) val Firebase.dynamicLinks: FirebaseDynamicLinks get() = FirebaseDynamicLinks.getInstance() /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseDynamicLinks] instance of a given [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -51,13 +50,6 @@ val Firebase.dynamicLinks: FirebaseDynamicLinks * we'll no longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.dynamicLinks(app)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.dynamicLinks(app)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.dynamicLinks"] - ) -) fun Firebase.dynamicLinks(app: FirebaseApp): FirebaseDynamicLinks { return FirebaseDynamicLinks.getInstance(app) } @@ -72,11 +64,8 @@ fun Firebase.dynamicLinks(app: FirebaseApp): FirebaseDynamicLinks { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.DynamicLink.Builder.androidParameters(init)` from the main module instead.", - ReplaceWith( - expression = "androidParameters(init)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.androidParameters"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun DynamicLink.Builder.androidParameters(init: DynamicLink.AndroidParameters.Builder.() -> Unit) { val builder = DynamicLink.AndroidParameters.Builder() @@ -94,11 +83,8 @@ fun DynamicLink.Builder.androidParameters(init: DynamicLink.AndroidParameters.Bu * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.DynamicLink.Builder.androidParameters(packageName, init)` from the main module instead.", - ReplaceWith( - expression = "androidParameters(packageName, init)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.androidParameters"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun DynamicLink.Builder.androidParameters( packageName: String, @@ -119,11 +105,8 @@ fun DynamicLink.Builder.androidParameters( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.DynamicLink.Builder.iosParameters(bundleId, init)` from the main module instead.", - ReplaceWith( - expression = "iosParameters(bundleId, init)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.iosParameters"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun DynamicLink.Builder.iosParameters( bundleId: String, @@ -144,12 +127,8 @@ fun DynamicLink.Builder.iosParameters( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.DynamicLink.Builder.googleAnalyticsParameters(init)` from the main module instead.", - ReplaceWith( - expression = "googleAnalyticsParameters(init)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.googleAnalyticsParameters"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun DynamicLink.Builder.googleAnalyticsParameters( init: DynamicLink.GoogleAnalyticsParameters.Builder.() -> Unit @@ -168,14 +147,7 @@ fun DynamicLink.Builder.googleAnalyticsParameters( * we'll no longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "com.google.firebase.dynam", - ReplaceWith( - expression = "googleAnalyticsParameters(source, medium, campaign, init)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.googleAnalyticsParameters"] - ) -) +@Deprecated("com.google.firebase.dynam", ReplaceWith("")) fun DynamicLink.Builder.googleAnalyticsParameters( source: String, medium: String, @@ -197,15 +169,8 @@ fun DynamicLink.Builder.googleAnalyticsParameters( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.DynamicLink.Builder.itunesConnectAnalyticsParameter(init)` from the main module instead.", - ReplaceWith( - expression = "itunesConnectAnalyticsParameters(init)", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.dynamiclinks.itunesConnectAnalyticsParameters" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun DynamicLink.Builder.itunesConnectAnalyticsParameters( init: DynamicLink.ItunesConnectAnalyticsParameters.Builder.() -> Unit @@ -225,12 +190,8 @@ fun DynamicLink.Builder.itunesConnectAnalyticsParameters( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.DynamicLink.Builder.socialMetaTagParameters(init)` from the main module instead.", - ReplaceWith( - expression = "socialMetaTagParameters(init)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.socialMetaTagParameters"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun DynamicLink.Builder.socialMetaTagParameters( init: DynamicLink.SocialMetaTagParameters.Builder.() -> Unit @@ -250,12 +211,8 @@ fun DynamicLink.Builder.socialMetaTagParameters( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.DynamicLink.Builder.navigationInfoParameters(init)` from the main module instead.", - ReplaceWith( - expression = "navigationInfoParameters(init)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.navigationInfoParameters"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun DynamicLink.Builder.navigationInfoParameters( init: DynamicLink.NavigationInfoParameters.Builder.() -> Unit @@ -274,11 +231,8 @@ fun DynamicLink.Builder.navigationInfoParameters( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.FirebaseDynamicLinks.dynamicLink` from the main module instead.", - ReplaceWith( - expression = "dynamicLink(init)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.dynamicLink"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun FirebaseDynamicLinks.dynamicLink(init: DynamicLink.Builder.() -> Unit): DynamicLink { val builder = FirebaseDynamicLinks.getInstance().createDynamicLink() @@ -295,11 +249,8 @@ fun FirebaseDynamicLinks.dynamicLink(init: DynamicLink.Builder.() -> Unit): Dyna * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.FirebaseDynamicLinks.shortLinkAsync(init)` from the main module instead.", - ReplaceWith( - expression = "shortLinkAsync(init)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.shortLinkAsync"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun FirebaseDynamicLinks.shortLinkAsync( init: DynamicLink.Builder.() -> Unit @@ -318,11 +269,8 @@ fun FirebaseDynamicLinks.shortLinkAsync( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.FirebaseDynamicLinks.shortLinkAsync(suffix, init)` from the main module instead.", - ReplaceWith( - expression = "shortLinkAsync(suffix, init)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.shortLinkAsync"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun FirebaseDynamicLinks.shortLinkAsync( suffix: Int, @@ -342,11 +290,8 @@ fun FirebaseDynamicLinks.shortLinkAsync( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.ShortDynamicLink.component1` from the main module instead.", - ReplaceWith( - expression = "component1()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.component1()"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun ShortDynamicLink.component1() = shortLink @@ -359,11 +304,8 @@ operator fun ShortDynamicLink.component1() = shortLink * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.ShortDynamicLink.component2` from the main module instead.", - ReplaceWith( - expression = "component2()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.component2"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun ShortDynamicLink.component2() = previewLink @@ -376,11 +318,8 @@ operator fun ShortDynamicLink.component2() = previewLink * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.ShortDynamicLink.component3` from the main module instead.", - ReplaceWith( - expression = "component3()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.component3"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun ShortDynamicLink.component3(): List = warnings @@ -393,11 +332,8 @@ operator fun ShortDynamicLink.component3(): List = war * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.PendingDynamicLinkData.component1` from the main module instead.", - ReplaceWith( - expression = "component1()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.component1"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun PendingDynamicLinkData.component1() = link @@ -410,11 +346,8 @@ operator fun PendingDynamicLinkData.component1() = link * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.PendingDynamicLinkData.component2` from the main module instead.", - ReplaceWith( - expression = "component2()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.component2"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun PendingDynamicLinkData.component2() = minimumAppVersion @@ -427,11 +360,8 @@ operator fun PendingDynamicLinkData.component2() = minimumAppVersion * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.PendingDynamicLinkData.component3` from the main module instead.", - ReplaceWith( - expression = "component3()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.dynamiclinks.component3"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) operator fun PendingDynamicLinkData.component3() = clickTimestamp @@ -444,15 +374,8 @@ operator fun PendingDynamicLinkData.component3() = clickTimestamp * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.dynamiclinks.FirebaseDynamicLinksKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseDynamicLinksKtxRegistrar", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.dynamiclinks.FirebaseDynamicLinksKtxRegistrar" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep class FirebaseDynamicLinksKtxRegistrar : ComponentRegistrar { diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 5edcc007985..ef5df4e27ef 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,9 +1,9 @@ # Unreleased +* [feature] Expose Sum/Average aggregate query support in API. [#5217](//github.com/firebase/firebase-android-sdk/pull/5217) * [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-firestore-ktx` to `com.google.firebase:firebase-firestore` under the `com.google.firebase.firestore` package. For details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) - * [deprecated] All the APIs from `com.google.firebase:firebase-firestore-ktx` have been added to `com.google.firebase:firebase-firestore` under the `com.google.firebase.firestore` package, and all the Kotlin extensions (KTX) APIs in `com.google.firebase:firebase-firestore-ktx` are diff --git a/firebase-firestore/api.txt b/firebase-firestore/api.txt index c437f67a225..3af7b9a306a 100644 --- a/firebase-firestore/api.txt +++ b/firebase-firestore/api.txt @@ -19,13 +19,39 @@ package com.google.firebase { package com.google.firebase.firestore { + public abstract class AggregateField { + method @NonNull public static com.google.firebase.firestore.AggregateField.AverageAggregateField average(@NonNull String); + method @NonNull public static com.google.firebase.firestore.AggregateField.AverageAggregateField average(@NonNull com.google.firebase.firestore.FieldPath); + method @NonNull public static com.google.firebase.firestore.AggregateField.CountAggregateField count(); + method @NonNull @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public String getAlias(); + method @NonNull @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public String getFieldPath(); + method @NonNull @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public String getOperator(); + method @NonNull public static com.google.firebase.firestore.AggregateField.SumAggregateField sum(@NonNull String); + method @NonNull public static com.google.firebase.firestore.AggregateField.SumAggregateField sum(@NonNull com.google.firebase.firestore.FieldPath); + } + + public static class AggregateField.AverageAggregateField extends com.google.firebase.firestore.AggregateField { + } + + public static class AggregateField.CountAggregateField extends com.google.firebase.firestore.AggregateField { + } + + public static class AggregateField.SumAggregateField extends com.google.firebase.firestore.AggregateField { + } + public class AggregateQuery { method @NonNull public com.google.android.gms.tasks.Task get(@NonNull com.google.firebase.firestore.AggregateSource); + method @NonNull @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public java.util.List getAggregateFields(); method @NonNull public com.google.firebase.firestore.Query getQuery(); } public class AggregateQuerySnapshot { + method @Nullable public Object get(@NonNull com.google.firebase.firestore.AggregateField); + method public long get(@NonNull com.google.firebase.firestore.AggregateField.CountAggregateField); + method @Nullable public Double get(@NonNull com.google.firebase.firestore.AggregateField.AverageAggregateField); method public long getCount(); + method @Nullable public Double getDouble(@NonNull com.google.firebase.firestore.AggregateField); + method @Nullable public Long getLong(@NonNull com.google.firebase.firestore.AggregateField); method @NonNull public com.google.firebase.firestore.AggregateQuery getQuery(); } @@ -411,6 +437,7 @@ package com.google.firebase.firestore { method @NonNull public com.google.firebase.firestore.ListenerRegistration addSnapshotListener(@NonNull com.google.firebase.firestore.MetadataChanges, @NonNull com.google.firebase.firestore.EventListener); method @NonNull public com.google.firebase.firestore.ListenerRegistration addSnapshotListener(@NonNull java.util.concurrent.Executor, @NonNull com.google.firebase.firestore.MetadataChanges, @NonNull com.google.firebase.firestore.EventListener); method @NonNull public com.google.firebase.firestore.ListenerRegistration addSnapshotListener(@NonNull android.app.Activity, @NonNull com.google.firebase.firestore.MetadataChanges, @NonNull com.google.firebase.firestore.EventListener); + method @NonNull public com.google.firebase.firestore.AggregateQuery aggregate(@NonNull com.google.firebase.firestore.AggregateField, @NonNull com.google.firebase.firestore.AggregateField...); method @NonNull public com.google.firebase.firestore.AggregateQuery count(); method @NonNull public com.google.firebase.firestore.Query endAt(@NonNull com.google.firebase.firestore.DocumentSnapshot); method @NonNull public com.google.firebase.firestore.Query endAt(java.lang.Object...); diff --git a/firebase-firestore/firebase-firestore.gradle b/firebase-firestore/firebase-firestore.gradle index fb5b8b7fc39..08758deedfd 100644 --- a/firebase-firestore/firebase-firestore.gradle +++ b/firebase-firestore/firebase-firestore.gradle @@ -150,9 +150,9 @@ dependencies { implementation('com.google.firebase:firebase-auth-interop:19.0.2') { exclude group: "com.google.firebase", module: "firebase-common" } - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' testCompileOnly "com.google.protobuf:protobuf-java:$protocVersion" testImplementation "androidx.test:core:$androidxTestCoreVersion" diff --git a/firebase-firestore/ktx/ktx.gradle b/firebase-firestore/ktx/ktx.gradle index 26faa99a84b..0c6495b3fa5 100644 --- a/firebase-firestore/ktx/ktx.gradle +++ b/firebase-firestore/ktx/ktx.gradle @@ -53,9 +53,9 @@ tasks.withType(Test) { } dependencies { - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") api(project(":firebase-firestore")) testCompileOnly "com.google.protobuf:protobuf-java:$protocVersion" testImplementation "androidx.test:core:$androidxTestCoreVersion" diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AggregationTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AggregationTest.java index cf360b8b3e3..cd8580ea20d 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AggregationTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AggregationTest.java @@ -39,11 +39,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.gms.tasks.Task; import com.google.common.truth.Truth; +import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.Collections; import java.util.Map; import org.junit.After; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -68,11 +68,6 @@ public void tearDown() { @Test public void testAggregateCountQueryEquals() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference coll1 = testCollection("foo"); CollectionReference coll1_same = coll1.firestore.collection(coll1.getPath()); AggregateQuery query1 = coll1.aggregate(AggregateField.count()); @@ -124,11 +119,6 @@ public void testAggregateCountQueryEquals() { @Test public void testAggregateSumQueryEquals() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference coll1 = testCollection("foo"); CollectionReference coll1_same = coll1.firestore.collection(coll1.getPath()); AggregateQuery query1 = coll1.aggregate(sum("baz")); @@ -180,11 +170,6 @@ public void testAggregateSumQueryEquals() { @Test public void testAggregateAvgQueryEquals() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference coll1 = testCollection("foo"); CollectionReference coll1_same = coll1.firestore.collection(coll1.getPath()); AggregateQuery query1 = coll1.aggregate(average("baz")); @@ -236,11 +221,6 @@ public void testAggregateAvgQueryEquals() { @Test public void testAggregateQueryNotEquals() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference coll = testCollection("foo"); AggregateQuery query1 = coll.aggregate(AggregateField.count()); @@ -279,11 +259,6 @@ public void testAggregateQueryNotEquals() { @Test public void testCanRunCountUsingAggregationMethod() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = @@ -294,11 +269,6 @@ public void testCanRunCountUsingAggregationMethod() { @Test public void testCanRunSumUsingAggregationMethod() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshotPages = @@ -318,11 +288,6 @@ public void testCanRunSumUsingAggregationMethod() { @Test public void testCanRunAvgUsingAggregationMethod() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = @@ -339,11 +304,6 @@ public void testCanRunAvgUsingAggregationMethod() { @Test public void testCanGetDuplicateAggregations() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = @@ -359,11 +319,6 @@ public void testCanGetDuplicateAggregations() { @Test public void testCanGetMultipleAggregationsInTheSameQuery() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = @@ -379,11 +334,6 @@ public void testCanGetMultipleAggregationsInTheSameQuery() { @Test public void testTerminateDoesNotCrashWithFlyingAggregateQuery() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); collection @@ -396,8 +346,9 @@ public void testTerminateDoesNotCrashWithFlyingAggregateQuery() { @Test public void testCanGetCorrectTypeForSum() { assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", + "This query requires a composite index, which is not support when testing" + + " against production. So it only runs against emulator which doesn't require " + + "an index. ", isRunningAgainstEmulator()); CollectionReference collection = testCollectionWithDocs(testDocs1); @@ -412,17 +363,45 @@ public void testCanGetCorrectTypeForSum() { Object sumHeight = snapshot.get(sum("height")); Object sumWeight = snapshot.get(sum("weight")); assertTrue(sumPages instanceof Long); - assertTrue(sumHeight instanceof Long); + assertTrue(sumHeight instanceof Double); assertTrue(sumWeight instanceof Double); } @Test - public void testCanGetCorrectTypeForAvg() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); + public void testCanGetCorrectDoubleTypeForSum() { + CollectionReference collection = testCollectionWithDocs(testDocs1); + + AggregateQuerySnapshot snapshot = + waitFor(collection.aggregate(sum("weight")).get(AggregateSource.SERVER)); + Object sumWeight = snapshot.get(sum("weight")); + assertTrue(sumWeight instanceof Double); + } + + @Test + public void testCanGetCorrectDoubleTypeForSumWhenFieldsAddUpToInteger() { + CollectionReference collection = testCollectionWithDocs(testDocs1); + + AggregateQuerySnapshot snapshot = + waitFor(collection.aggregate(sum("height")).get(AggregateSource.SERVER)); + + Object sumWeight = snapshot.get(sum("height")); + assertTrue(sumWeight instanceof Double); + } + + @Test + public void testCanGetCorrectLongTypeForSum() { + CollectionReference collection = testCollectionWithDocs(testDocs1); + + AggregateQuerySnapshot snapshot = + waitFor(collection.aggregate(sum("pages")).get(AggregateSource.SERVER)); + + Object sumPages = snapshot.get(sum("pages")); + assertTrue(sumPages instanceof Long); + } + + @Test + public void testCanGetCorrectTypeForAvg() { CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = @@ -435,8 +414,9 @@ public void testCanGetCorrectTypeForAvg() { @Test public void testCanPerformMaxAggregations() { assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", + "This query requires a composite index, which is not support when testing" + + " against production. So it only runs against emulator which doesn't require " + + "an index. ", isRunningAgainstEmulator()); CollectionReference collection = testCollectionWithDocs(testDocs1); @@ -458,11 +438,6 @@ public void testCanPerformMaxAggregations() { @Test public void testCannotPerformMoreThanMaxAggregations() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateField f1 = sum("pages"); AggregateField f2 = average("pages"); @@ -483,8 +458,9 @@ public void testCannotPerformMoreThanMaxAggregations() { @Test public void testCanRunAggregateCollectionGroupQuery() { assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", + "This query requires a composite index, which is not support when testing" + + " against production. So it only runs against emulator which doesn't require " + + "an index. ", isRunningAgainstEmulator()); FirebaseFirestore db = testFirestore(); @@ -526,11 +502,6 @@ public void testCanRunAggregateCollectionGroupQuery() { @Test public void testPerformsAggregationsWhenNaNExistsForSomeFieldValues() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", @@ -554,28 +525,22 @@ public void testPerformsAggregationsWhenNaNExistsForSomeFieldValues() { CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - waitFor( - collection - .aggregate(sum("rating"), sum("pages"), average("year"), average("rating")) - .get(AggregateSource.SERVER)); + waitFor(collection.aggregate(sum("rating"), average("rating")).get(AggregateSource.SERVER)); assertEquals(snapshot.get(sum("rating")), Double.NaN); - assertEquals(snapshot.get(sum("pages")), 300L); assertEquals(snapshot.get(average("rating")), (Double) Double.NaN); - assertEquals(snapshot.get(average("year")), (Double) 2000.0); } @Test public void testThrowsAnErrorWhenGettingTheResultOfAnUnrequestedAggregation() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = - waitFor(collection.aggregate(sum("pages")).get(AggregateSource.SERVER)); + waitFor( + collection + .whereGreaterThan("pages", 200) + .aggregate(sum("pages")) + .get(AggregateSource.SERVER)); Exception exception = null; try { @@ -599,11 +564,6 @@ public void testThrowsAnErrorWhenGettingTheResultOfAnUnrequestedAggregation() { @Test public void testPerformsAggregationWhenUsingInOperator() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", @@ -620,26 +580,20 @@ public void testPerformsAggregationWhenUsingInOperator() { waitFor( collection .whereIn("rating", asList(5, 3)) - .aggregate( - sum("rating"), - average("rating"), - sum("pages"), - average("pages"), - AggregateField.count()) + .aggregate(sum("rating"), average("rating"), AggregateField.count()) .get(AggregateSource.SERVER)); assertEquals(snapshot.get(sum("rating")), 8L); assertEquals(snapshot.get(average("rating")), (Double) 4.0); - assertEquals(snapshot.get(sum("pages")), 200L); - assertEquals(snapshot.get(average("pages")), (Double) 100.0); assertEquals(snapshot.get(AggregateField.count()), 2L); } @Test public void testPerformsAggregationWhenUsingArrayContainsAnyOperator() { assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", + "This query requires a composite index, which is not support when testing" + + " against production. So it only runs against emulator which doesn't require " + + "an index. ", isRunningAgainstEmulator()); Map> testDocs = @@ -682,16 +636,9 @@ public void testPerformsAggregationWhenUsingArrayContainsAnyOperator() { waitFor( collection .whereArrayContainsAny("rating", asList(5, 3)) - .aggregate( - sum("rating"), - average("rating"), - sum("pages"), - average("pages"), - AggregateField.count()) + .aggregate(sum("pages"), average("pages"), AggregateField.count()) .get(AggregateSource.SERVER)); - assertEquals(snapshot.get(sum("rating")), 0L); - assertNull(snapshot.get(average("rating"))); assertEquals(snapshot.get(sum("pages")), 200L); assertEquals(snapshot.get(average("pages")), (Double) 100.0); assertEquals(snapshot.get(AggregateField.count()), 2L); @@ -699,11 +646,6 @@ public void testPerformsAggregationWhenUsingArrayContainsAnyOperator() { @Test public void testPerformsAggregationsOnNestedMapValues() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", @@ -728,28 +670,16 @@ public void testPerformsAggregationsOnNestedMapValues() { AggregateQuerySnapshot snapshot = waitFor( collection - .aggregate( - sum("metadata.pages"), - average("metadata.pages"), - average("metadata.rating.critic"), - sum("metadata.rating.user"), - AggregateField.count()) + .aggregate(sum("metadata.pages"), average("metadata.pages"), AggregateField.count()) .get(AggregateSource.SERVER)); assertEquals(snapshot.get(sum("metadata.pages")), 150L); assertEquals(snapshot.get(average("metadata.pages")), (Double) 75.0); - assertEquals(snapshot.get(average("metadata.rating.critic")), (Double) 3.0); - assertEquals(snapshot.get(sum("metadata.rating.user")), 9L); assertEquals(snapshot.get(AggregateField.count()), 2L); } @Test public void testPerformsSumThatOverflowsMaxLong() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", Long.MAX_VALUE), @@ -766,11 +696,6 @@ public void testPerformsSumThatOverflowsMaxLong() { @Test public void testPerformsSumThatCanOverflowIntegerValuesDuringAccumulation() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", Long.MAX_VALUE), @@ -788,11 +713,6 @@ public void testPerformsSumThatCanOverflowIntegerValuesDuringAccumulation() { @Test public void testPerformsSumThatIsNegative() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", Long.MAX_VALUE), @@ -809,11 +729,6 @@ public void testPerformsSumThatIsNegative() { @Test public void testPerformsSumThatIsPositiveInfinity() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", Double.MAX_VALUE), @@ -832,11 +747,6 @@ public void testPerformsSumThatIsPositiveInfinity() { @Test public void testPerformsSumThatIsNegativeInfinity() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", -Double.MAX_VALUE), @@ -856,38 +766,28 @@ public void testPerformsSumThatIsNegativeInfinity() { @Test public void testPerformsSumThatIsValidButCouldOverflowDuringAggregation() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - + // Sum of rating would be 0, but if the accumulation overflow, we expect infinity Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", Double.MAX_VALUE), "b", map("author", "authorB", "title", "titleB", "rating", Double.MAX_VALUE), "c", map("author", "authorC", "title", "titleC", "rating", -Double.MAX_VALUE), - "d", map("author", "authorD", "title", "titleD", "rating", -Double.MAX_VALUE), - "e", map("author", "authorE", "title", "titleE", "rating", Double.MAX_VALUE), - "f", map("author", "authorF", "title", "titleF", "rating", -Double.MAX_VALUE), - "g", map("author", "authorG", "title", "titleG", "rating", -Double.MAX_VALUE), - "h", map("author", "authorH", "title", "titleH", "rating", Double.MAX_VALUE)); + "d", map("author", "authorD", "title", "titleD", "rating", -Double.MAX_VALUE)); CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = waitFor(collection.aggregate(sum("rating")).get(AggregateSource.SERVER)); Object sum = snapshot.get(sum("rating")); - assertTrue(sum instanceof Long); - assertEquals(sum, 0L); + assertTrue(sum instanceof Double); + assertTrue( + sum.equals(0.0) + || sum.equals(Double.POSITIVE_INFINITY) + || sum.equals(Double.NEGATIVE_INFINITY)); } @Test public void testPerformsSumOverResultSetOfZeroDocuments() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = @@ -902,11 +802,6 @@ public void testPerformsSumOverResultSetOfZeroDocuments() { @Test public void testPerformsSumOnlyOnNumericFields() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", 5), @@ -927,11 +822,6 @@ public void testPerformsSumOnlyOnNumericFields() { @Test public void testPerformsSumOfMinIEEE754() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map("a", map("author", "authorA", "title", "titleA", "rating", Double.MIN_VALUE)); @@ -945,11 +835,6 @@ public void testPerformsSumOfMinIEEE754() { @Test public void testPerformsAverageOfIntsThatResultsInAnInt() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", 10), @@ -967,11 +852,6 @@ public void testPerformsAverageOfIntsThatResultsInAnInt() { @Test public void testPerformsAverageOfFloatsThatResultsInAnInt() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", 10.5), @@ -988,11 +868,6 @@ public void testPerformsAverageOfFloatsThatResultsInAnInt() { @Test public void testPerformsAverageOfFloatsAndIntsThatResultsInAnInt() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", 10), @@ -1010,11 +885,6 @@ public void testPerformsAverageOfFloatsAndIntsThatResultsInAnInt() { @Test public void testPerformsAverageOfFloatsThatResultsInAFloat() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", 5.5), @@ -1032,11 +902,6 @@ public void testPerformsAverageOfFloatsThatResultsInAFloat() { @Test public void testPerformsAverageOfFloatsAndIntsThatResultsInAFloat() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", 8.6), @@ -1053,11 +918,6 @@ public void testPerformsAverageOfFloatsAndIntsThatResultsInAFloat() { @Test public void testPerformsAverageOfIntsThatResultsInAFloat() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", 10), @@ -1074,11 +934,6 @@ public void testPerformsAverageOfIntsThatResultsInAFloat() { @Test public void testPerformsAverageCausingUnderflow() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", Double.MIN_VALUE), @@ -1095,11 +950,6 @@ public void testPerformsAverageCausingUnderflow() { @Test public void testPerformsAverageOfMinIEEE754() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map("a", map("author", "authorA", "title", "titleA", "rating", Double.MIN_VALUE)); CollectionReference collection = testCollectionWithDocs(testDocs); @@ -1114,11 +964,6 @@ public void testPerformsAverageOfMinIEEE754() { @Test public void testPerformsAverageOverflowIEEE754DuringAccumulation() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", @@ -1137,11 +982,6 @@ public void testPerformsAverageOverflowIEEE754DuringAccumulation() { @Test public void testPerformsAverageThatIncludesNaN() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", @@ -1164,11 +1004,6 @@ public void testPerformsAverageThatIncludesNaN() { @Test public void testPerformsAverageOverResultSetOfZeroDocuments() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = @@ -1185,11 +1020,6 @@ public void testPerformsAverageOverResultSetOfZeroDocuments() { @Test public void testPerformsAverageOnlyOnNumericFields() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - Map> testDocs = map( "a", map("author", "authorA", "title", "titleA", "rating", 5), @@ -1209,11 +1039,11 @@ public void testPerformsAverageOnlyOnNumericFields() { } @Test - @Ignore("TODO: Enable once we have production support for sum/avg.") public void testAggregateFailWithGoodMessageIfMissingIndex() { assumeFalse( - "Skip this test when running against the Firestore emulator because the Firestore emulator " - + "does not use indexes and never fails with a 'missing index' error", + "Skip this test when running against the Firestore emulator because the " + + "Firestore emulator does not use indexes and never fails with a 'missing index'" + + " error", isRunningAgainstEmulator()); CollectionReference collection = testCollectionWithDocs(Collections.emptyMap()); @@ -1225,24 +1055,25 @@ public void testAggregateFailWithGoodMessageIfMissingIndex() { Throwable throwable = assertThrows(Throwable.class, () -> waitFor(task)); Throwable cause = throwable.getCause(); - Truth.assertThat(cause).hasMessageThat().ignoringCase().contains("index"); - Truth.assertThat(cause).hasMessageThat().contains("https://console.firebase.google.com"); + if (collection + .firestore + .getDatabaseId() + .getDatabaseId() + .equals(DatabaseId.DEFAULT_DATABASE_ID)) { + Truth.assertThat(cause).hasMessageThat().contains("https://console.firebase.google.com"); + } else { + Truth.assertThat(cause).hasMessageThat().contains("Missing index configuration"); + } } @Test public void allowsAliasesLongerThan1500Bytes() { - assumeTrue( - "Skip this test if running against production because sum/avg is only support " - + "in emulator currently.", - isRunningAgainstEmulator()); - // The longest field name allowed is 1500. The alias chosen by the client is _. - // If the field name is - // 1500 bytes, the alias will be longer than 1500, which is the limit for aliases. This is to - // make sure the client - // can handle this corner case correctly. - - StringBuilder builder = new StringBuilder(1500); - for (int i = 0; i < 1500; i++) { + // The longest field name allowed is 1500 bytes, and string sizes are calculated as the number + // of UTF-8 encoded bytes + 1 in server. The alias chosen by the client is _. + // If the field name is 1500 bytes, the alias will be longer than 1500, which is the limit for + // aliases. This is to make sure the client can handle this corner case correctly. + StringBuilder builder = new StringBuilder(1499); + for (int i = 0; i < 1499; i++) { builder.append("a"); } String longField = builder.toString(); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java index 48a6841c868..42c16e36088 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java @@ -38,6 +38,7 @@ import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollectionWithDocs; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore; import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor; +import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitForException; import static com.google.firebase.firestore.testutil.TestUtil.expectError; import static com.google.firebase.firestore.testutil.TestUtil.map; import static java.util.Arrays.asList; @@ -2193,4 +2194,45 @@ public void testMultipleInequalityFromCacheAndFromServer() { Query query5 = collection.where(or(greaterThan("a", 2), lessThan("b", 1))); checkOnlineAndOfflineResultsMatch(query5, "doc1", "doc3"); } + + @Test + public void testMultipleInequalityRejectsIfDocumentKeyIsNotTheLastOrderByField() { + // TODO(MIEQ): Enable this test against production when possible. + assumeTrue( + "Skip this test if running against production because multiple inequality is " + + "not supported yet.", + isRunningAgainstEmulator()); + + CollectionReference collection = testCollection(); + + // Implicitly ordered by: __name__ asc, 'key' asc, + Query query = collection.whereNotEqualTo("key", 42).orderBy(FieldPath.documentId()); + Exception e = waitForException(query.get()); + FirebaseFirestoreException firestoreException = (FirebaseFirestoreException) e; + assertTrue( + firestoreException + .getMessage() + .contains("order by clause cannot contain more fields after the key")); + } + + @Test + public void testMultipleInequalityRejectsIfDocumentKeyAppearsOnlyInEqualityFilter() { + // TODO(MIEQ): Enable this test against production when possible. + assumeTrue( + "Skip this test if running against production because multiple inequality is " + + "not supported yet.", + isRunningAgainstEmulator()); + + CollectionReference collection = testCollection(); + + Query query = + collection.whereNotEqualTo("key", 42).whereEqualTo(FieldPath.documentId(), "doc1"); + Exception e = waitForException(query.get()); + FirebaseFirestoreException firestoreException = (FirebaseFirestoreException) e; + assertTrue( + firestoreException + .getMessage() + .contains( + "Equality on key is not allowed if there are other inequality fields and key does not appear in inequalities.")); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java index d461bc42262..555f9ab5a88 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateField.java @@ -19,10 +19,9 @@ import androidx.annotation.RestrictTo; import java.util.Objects; -// TODO(sumavg): Remove the `hide` and scope annotations. -/** @hide */ -@RestrictTo(RestrictTo.Scope.LIBRARY) +/** Represents an aggregation that can be performed by Firestore. */ public abstract class AggregateField { + /** The field over which the aggregation is performed. */ @Nullable private final FieldPath fieldPath; @NonNull private final String operator; @@ -89,43 +88,123 @@ public int hashCode() { return Objects.hash(getOperator(), getFieldPath()); } + /** + * Create a {@link CountAggregateField} object that can be used to compute the count of documents + * in the result set of a query. + * + *

The result of a count operation will always be a 64-bit integer value. + * + * @return The `CountAggregateField` object that can be used to compute the count of documents in + * the result set of a query. + */ @NonNull public static CountAggregateField count() { return new CountAggregateField(); } + /** + * Create a {@link SumAggregateField} object that can be used to compute the sum of a specified + * field over a range of documents in the result set of a query. + * + *

The result of a sum operation will always be a 64-bit integer value, a double, or NaN. + * + *

    + *
  • Summing over zero documents or fields will result in 0L. + *
  • Summing over NaN will result in a double value representing NaN. + *
  • A sum that overflows the maximum representable 64-bit integer value will result in a + * double return value. This may result in lost precision of the result. + *
  • A sum that overflows the maximum representable double value will result in a double + * return value representing infinity. + *
+ * + * @param field Specifies the field to sum across the result set. + * @return The `SumAggregateField` object that can be used to compute the sum of a specified field + * over a range of documents in the result set of a query. + */ @NonNull public static SumAggregateField sum(@NonNull String field) { return new SumAggregateField(FieldPath.fromDotSeparatedPath(field)); } + /** + * Create a {@link SumAggregateField} object that can be used to compute the sum of a specified + * field over a range of documents in the result set of a query. + * + *

The result of a sum operation will always be a 64-bit integer value, a double, or NaN. + * + *

    + *
  • Summing over zero documents or fields will result in 0L. + *
  • Summing over NaN will result in a double value representing NaN. + *
  • A sum that overflows the maximum representable 64-bit integer value will result in a + * double return value. This may result in lost precision of the result. + *
  • A sum that overflows the maximum representable double value will result in a double + * return value representing infinity. + *
+ * + * @param fieldPath Specifies the field to sum across the result set. + * @return The `SumAggregateField` object that can be used to compute the sum of a specified field + * over a range of documents in the result set of a query. + */ @NonNull public static SumAggregateField sum(@NonNull FieldPath fieldPath) { return new SumAggregateField(fieldPath); } + /** + * Create an {@link AverageAggregateField} object that can be used to compute the average of a + * specified field over a range of documents in the result set of a query. + * + *

The result of an average operation will always be a double or NaN. + * + *

    + *
  • Averaging over zero documents or fields will result in a double value representing NaN. + *
  • Averaging over NaN will result in a double value representing NaN. + *
+ * + * @param field Specifies the field to average across the result set. + * @return The `AverageAggregateField` object that can be used to compute the average of a + * specified field over a range of documents in the result set of a query. + */ @NonNull public static AverageAggregateField average(@NonNull String field) { return new AverageAggregateField(FieldPath.fromDotSeparatedPath(field)); } + /** + * Create an {@link AverageAggregateField} object that can be used to compute the average of a + * specified field over a range of documents in the result set of a query. + * + *

The result of an average operation will always be a double or NaN. + * + *

    + *
  • Averaging over zero documents or fields will result in a double value representing NaN. + *
  • Averaging over NaN will result in a double value representing NaN. + *
+ * + * @param fieldPath Specifies the field to average across the result set. + * @return The `AverageAggregateField` object that can be used to compute the average of a + * specified field over a range of documents in the result set of a query. + */ @NonNull public static AverageAggregateField average(@NonNull FieldPath fieldPath) { return new AverageAggregateField(fieldPath); } + /** Represents a "count" aggregation that can be performed by Firestore. */ public static class CountAggregateField extends AggregateField { private CountAggregateField() { super(null, "count"); } } + /** Represents a "sum" aggregation that can be performed by Firestore. */ public static class SumAggregateField extends AggregateField { private SumAggregateField(@NonNull FieldPath fieldPath) { super(fieldPath, "sum"); } } + /** Represents an "average" aggregation that can be performed by Firestore. */ public static class AverageAggregateField extends AggregateField { private AverageAggregateField(@NonNull FieldPath fieldPath) { super(fieldPath, "average"); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java index f465bf92dcc..db4015d6386 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java @@ -48,8 +48,6 @@ public Query getQuery() { } /** Returns the AggregateFields included inside this object. */ - // TODO(sumavg): Remove the `hide` and scope annotations. - /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY) @NonNull public List getAggregateFields() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuerySnapshot.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuerySnapshot.java index effcf21d980..8f1ce3985c1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuerySnapshot.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuerySnapshot.java @@ -18,7 +18,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; import com.google.firestore.v1.Value; import java.util.Collections; import java.util.Map; @@ -73,9 +72,6 @@ public long getCount() { * @param aggregateField The aggregation for which the value is requested. * @return The result of the given aggregation. */ - // TODO(sumavg): Remove the `hide` and scope annotations. - /** @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY) @Nullable public Object get(@Nonnull AggregateField aggregateField) { return getInternal(aggregateField); @@ -87,9 +83,6 @@ public Object get(@Nonnull AggregateField aggregateField) { * @param countAggregateField The count aggregation for which the value is requested. * @return The result of the given count aggregation. */ - // TODO(sumavg): Remove the `hide` and scope annotations. - /** @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY) public long get(@Nonnull AggregateField.CountAggregateField countAggregateField) { Long value = getLong(countAggregateField); if (value == null) { @@ -108,9 +101,6 @@ public long get(@Nonnull AggregateField.CountAggregateField countAggregateField) * @param averageAggregateField The average aggregation for which the value is requested. * @return The result of the given average aggregation. */ - // TODO(sumavg): Remove the `hide` and scope annotations. - /** @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY) @Nullable public Double get(@Nonnull AggregateField.AverageAggregateField averageAggregateField) { return getDouble(averageAggregateField); @@ -125,9 +115,6 @@ public Double get(@Nonnull AggregateField.AverageAggregateField averageAggregate * @param aggregateField The aggregation for which the value is requested. * @return The result of the given average aggregation as a double. */ - // TODO(sumavg): Remove the `hide` and scope annotations. - /** @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY) @Nullable public Double getDouble(@Nonnull AggregateField aggregateField) { Number val = getTypedValue(aggregateField, Number.class); @@ -142,9 +129,6 @@ public Double getDouble(@Nonnull AggregateField aggregateField) { * @param aggregateField The aggregation for which the value is requested. * @return The result of the given average aggregation as a long. */ - // TODO(sumavg): Remove the `hide` and scope annotations. - /** @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY) @Nullable public Long getLong(@Nonnull AggregateField aggregateField) { Number val = getTypedValue(aggregateField, Number.class); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java index a75b91479b3..eabad33b453 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java @@ -22,7 +22,6 @@ import android.app.Activity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RestrictTo; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; @@ -858,7 +857,7 @@ private Bound boundFromDocumentSnapshot( // contain the document key. That way the position becomes unambiguous and the query // continues/ends exactly at the provided document. Without the key (by using the explicit sort // orders), multiple documents could match the position, yielding duplicate results. - for (OrderBy orderBy : query.getOrderBy()) { + for (OrderBy orderBy : query.getNormalizedOrderBy()) { if (orderBy.getField().equals(com.google.firebase.firestore.model.FieldPath.KEY_PATH)) { components.add(Values.refValue(firestore.getDatabaseId(), document.getKey())); } else { @@ -1203,9 +1202,6 @@ public AggregateQuery count() { * @return The {@code AggregateQuery} that performs aggregations on the documents in the result * set of this query. */ - // TODO(sumavg): Remove the `hide` and scope annotations. - /** @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY) @NonNull public AggregateQuery aggregate( @NonNull AggregateField aggregateField, @NonNull AggregateField... aggregateFields) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java index 86a79a8b7fc..52be465b0ba 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/Query.java @@ -58,11 +58,18 @@ public static Query atPath(ResourcePath path) { private final List explicitSortOrder; - private List memoizedOrderBy; + private List memoizedNormalizedOrderBys; - // The corresponding Target of this Query instance. + /** The corresponding `Target` of this `Query` instance, for use with non-aggregate queries. */ private @Nullable Target memoizedTarget; + /** + * The corresponding `Target` of this `Query` instance, for use with aggregate queries. Unlike + * targets for non-aggregate queries, aggregate query targets do not contain normalized order-bys, + * they only contain explicit order-bys. + */ + private @Nullable Target memoizedAggregateTarget; + private final List filters; private final ResourcePath path; @@ -312,8 +319,8 @@ public List getExplicitOrderBy() { *

The returned list is unmodifiable, to prevent ConcurrentModificationExceptions, if one * thread is iterating the list and one thread is modifying the list. */ - public synchronized List getOrderBy() { - if (memoizedOrderBy == null) { + public synchronized List getNormalizedOrderBy() { + if (memoizedNormalizedOrderBys == null) { List res = new ArrayList<>(); HashSet fieldsNormalized = new HashSet(); @@ -347,9 +354,9 @@ public synchronized List getOrderBy() { res.add(lastDirection.equals(Direction.ASCENDING) ? KEY_ORDERING_ASC : KEY_ORDERING_DESC); } - memoizedOrderBy = Collections.unmodifiableList(res); + memoizedNormalizedOrderBys = Collections.unmodifiableList(res); } - return memoizedOrderBy; + return memoizedNormalizedOrderBys; } private boolean matchesPathAndCollectionGroup(Document doc) { @@ -376,13 +383,14 @@ private boolean matchesFilters(Document doc) { /** A document must have a value for every ordering clause in order to show up in the results. */ private boolean matchesOrderBy(Document doc) { - // We must use `getOrderBy()` to get the list of all orderBys (both implicit and explicit). + // We must use `getNormalizedOrderBy()` to get the list of all orderBys (both implicit and + // explicit). // Note that for OR queries, orderBy applies to all disjunction terms and implicit orderBys must // be taken into account. For example, the query "a > 1 || b==1" has an implicit "orderBy a" due // to the inequality, and is evaluated as "a > 1 orderBy a || b==1 orderBy a". // A document with content of {b:1} matches the filters, but does not match the orderBy because // it's missing the field 'a'. - for (OrderBy order : getOrderBy()) { + for (OrderBy order : getNormalizedOrderBy()) { // order by key always matches if (!order.getField().equals(FieldPath.KEY_PATH) && (doc.getField(order.field) == null)) { return false; @@ -393,10 +401,10 @@ private boolean matchesOrderBy(Document doc) { /** Makes sure a document is within the bounds, if provided. */ private boolean matchesBounds(Document doc) { - if (startAt != null && !startAt.sortsBeforeDocument(getOrderBy(), doc)) { + if (startAt != null && !startAt.sortsBeforeDocument(getNormalizedOrderBy(), doc)) { return false; } - if (endAt != null && !endAt.sortsAfterDocument(getOrderBy(), doc)) { + if (endAt != null && !endAt.sortsAfterDocument(getNormalizedOrderBy(), doc)) { return false; } return true; @@ -413,7 +421,7 @@ && matchesFilters(doc) /** Returns a comparator that will sort documents according to this Query's sort order. */ public Comparator comparator() { - return new QueryComparator(getOrderBy()); + return new QueryComparator(getNormalizedOrderBy()); } private static class QueryComparator implements Comparator { @@ -449,50 +457,62 @@ public int compare(Document doc1, Document doc2) { */ public synchronized Target toTarget() { if (this.memoizedTarget == null) { - if (this.limitType == LimitType.LIMIT_TO_FIRST) { - this.memoizedTarget = - new Target( - this.getPath(), - this.getCollectionGroup(), - this.getFilters(), - this.getOrderBy(), - this.limit, - this.getStartAt(), - this.getEndAt()); - } else { - // Flip the orderBy directions since we want the last results - ArrayList newOrderBy = new ArrayList<>(); - for (OrderBy orderBy : this.getOrderBy()) { - Direction dir = - orderBy.getDirection() == Direction.DESCENDING - ? Direction.ASCENDING - : Direction.DESCENDING; - newOrderBy.add(OrderBy.getInstance(dir, orderBy.getField())); - } + memoizedTarget = toTarget(getNormalizedOrderBy()); + } + return this.memoizedTarget; + } - // We need to swap the cursors to match the now-flipped query ordering. - Bound newStartAt = - this.endAt != null - ? new Bound(this.endAt.getPosition(), this.endAt.isInclusive()) - : null; - Bound newEndAt = - this.startAt != null - ? new Bound(this.startAt.getPosition(), this.startAt.isInclusive()) - : null; - - this.memoizedTarget = - new Target( - this.getPath(), - this.getCollectionGroup(), - this.getFilters(), - newOrderBy, - this.limit, - newStartAt, - newEndAt); + private synchronized Target toTarget(List orderBys) { + if (this.limitType == LimitType.LIMIT_TO_FIRST) { + return new Target( + this.getPath(), + this.getCollectionGroup(), + this.getFilters(), + orderBys, + this.limit, + this.getStartAt(), + this.getEndAt()); + } else { + // Flip the orderBy directions since we want the last results + ArrayList newOrderBy = new ArrayList<>(); + for (OrderBy orderBy : orderBys) { + Direction dir = + orderBy.getDirection() == Direction.DESCENDING + ? Direction.ASCENDING + : Direction.DESCENDING; + newOrderBy.add(OrderBy.getInstance(dir, orderBy.getField())); } + + // We need to swap the cursors to match the now-flipped query ordering. + Bound newStartAt = + this.endAt != null ? new Bound(this.endAt.getPosition(), this.endAt.isInclusive()) : null; + Bound newEndAt = + this.startAt != null + ? new Bound(this.startAt.getPosition(), this.startAt.isInclusive()) + : null; + + return new Target( + this.getPath(), + this.getCollectionGroup(), + this.getFilters(), + newOrderBy, + this.limit, + newStartAt, + newEndAt); } + } - return this.memoizedTarget; + /** + * This method is marked as synchronized because it modifies the internal state in some cases. + * + * @return A {@code Target} instance this query will be mapped to in backend and local store, for + * use within an aggregate query. + */ + public synchronized Target toAggregateTarget() { + if (this.memoizedAggregateTarget == null) { + memoizedAggregateTarget = toTarget(explicitSortOrder); + } + return this.memoizedAggregateTarget; } /** diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/ktx/Firestore.kt b/firebase-firestore/src/main/java/com/google/firebase/firestore/ktx/Firestore.kt index 24b73901f2a..1773e50234a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/ktx/Firestore.kt +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/ktx/Firestore.kt @@ -42,6 +42,9 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.map /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseFirestore] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -49,17 +52,13 @@ import kotlinx.coroutines.flow.map * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.firestore` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.firestore", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.firestore"] - ) -) val Firebase.firestore: FirebaseFirestore get() = FirebaseFirestore.getInstance() /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseFirestore] instance of a given [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -67,16 +66,12 @@ val Firebase.firestore: FirebaseFirestore * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.firestore(app)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.firestore(app)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.firestore"] - ) -) fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore = FirebaseFirestore.getInstance(app) /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseFirestore] instance of a given [FirebaseApp] and database name. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -84,17 +79,13 @@ fun Firebase.firestore(app: FirebaseApp): FirebaseFirestore = FirebaseFirestore. * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.firestore(app, database)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.firestore(app, database)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.firestore"] - ) -) fun Firebase.firestore(app: FirebaseApp, database: String): FirebaseFirestore = FirebaseFirestore.getInstance(app, database) /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseFirestore] instance of the default [FirebaseApp], given the database name. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -102,13 +93,6 @@ fun Firebase.firestore(app: FirebaseApp, database: String): FirebaseFirestore = * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.firestore(database)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.firestore(database)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.firestore"] - ) -) fun Firebase.firestore(database: String): FirebaseFirestore = FirebaseFirestore.getInstance(database) @@ -127,11 +111,8 @@ fun Firebase.firestore(database: String): FirebaseFirestore = * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.DocumentSnapshot.toObject` from the main module instead.", - ReplaceWith( - expression = "toObject()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.toObject"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun DocumentSnapshot.toObject(): T? = toObject(T::class.java) @@ -155,11 +136,8 @@ inline fun DocumentSnapshot.toObject(): T? = toObject(T::class.java) * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.DocumentSnapshot.toObject(serverTimestampBehavior).` from the main module instead.", - ReplaceWith( - expression = "toObject(serverTimestampBehavior)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.toObject"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun DocumentSnapshot.toObject( serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior @@ -179,11 +157,8 @@ inline fun DocumentSnapshot.toObject( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.DocumentSnapshot.getField(field)` from the main module instead.", - ReplaceWith( - expression = "getField(field)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.getField"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun DocumentSnapshot.getField(field: String): T? = get(field, T::class.java) @@ -206,11 +181,8 @@ inline fun DocumentSnapshot.getField(field: String): T? = get(field, * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.DocumentSnapshot.getField(field, serverTimestampBehavior) ` from the main module instead.", - ReplaceWith( - expression = "getField(field, serverTimestampBehavior)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.getField"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun DocumentSnapshot.getField( field: String, @@ -231,11 +203,8 @@ inline fun DocumentSnapshot.getField( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.DocumentSnapshot.getField(fieldPath) ` from the main module instead.", - ReplaceWith( - expression = "getField(fieldPath)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.getField"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun DocumentSnapshot.getField(fieldPath: FieldPath): T? = get(fieldPath, T::class.java) @@ -259,11 +228,8 @@ inline fun DocumentSnapshot.getField(fieldPath: FieldPath): T? = * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.DocumentSnapshot.getField(fieldPath, serverTimestampBehavior) ` from the main module instead.", - ReplaceWith( - expression = "getField(fieldPath, serverTimestampBehavior)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.getField"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun DocumentSnapshot.getField( fieldPath: FieldPath, @@ -282,11 +248,8 @@ inline fun DocumentSnapshot.getField( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.QueryDocumentSnapshot.toObject` from the main module instead.", - ReplaceWith( - expression = "toObject()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.toObject"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun QueryDocumentSnapshot.toObject(): T = toObject(T::class.java) @@ -307,11 +270,8 @@ inline fun QueryDocumentSnapshot.toObject(): T = toObject(T::c * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.QueryDocumentSnapshot.toObject(serverTimestampBehavior)` from the main module instead.", - ReplaceWith( - expression = "toObject(serverTimestampBehavior)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.toObject"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun QueryDocumentSnapshot.toObject( serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior @@ -329,11 +289,8 @@ inline fun QueryDocumentSnapshot.toObject( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.QuerySnapshot.toObjects` from the main module instead.", - ReplaceWith( - expression = "toObjects()", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.toObjects"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun QuerySnapshot.toObjects(): List = toObjects(T::class.java) @@ -353,11 +310,8 @@ inline fun QuerySnapshot.toObjects(): List = toObjects(T::c * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.QuerySnapshot.toObjects(serverTimestampBehavior)` from the main module instead.", - ReplaceWith( - expression = "toObjects(serverTimestampBehavior)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.toObjects"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun QuerySnapshot.toObjects( serverTimestampBehavior: DocumentSnapshot.ServerTimestampBehavior @@ -372,11 +326,8 @@ inline fun QuerySnapshot.toObjects( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.firestoreSettings(init)` from the main module instead.", - ReplaceWith( - expression = "firestoreSettings(init)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.firestoreSettings"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun firestoreSettings( init: FirebaseFirestoreSettings.Builder.() -> Unit @@ -421,15 +372,8 @@ fun persistentCacheSettings( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.Firebase.FirestoreKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseFirestoreKtxRegistrar", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.firestore.FirebaseFirestoreKtxRegistrar" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep class FirebaseFirestoreKtxRegistrar : ComponentRegistrar { @@ -450,13 +394,7 @@ class FirebaseFirestoreKtxRegistrar : ComponentRegistrar { * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "com.google.firebase.fires", - ReplaceWith( - expression = "snapshots(metadataChanges)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.snapshots"] - ) -) +@Deprecated("com.google.firebase.fires", ReplaceWith("")) fun DocumentReference.snapshots( metadataChanges: MetadataChanges = MetadataChanges.EXCLUDE ): Flow { @@ -487,11 +425,8 @@ fun DocumentReference.snapshots( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.Query.snapshots(metadataChanges)` from the main module instead.", - ReplaceWith( - expression = "snapshots(metadataChanges)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.snapshots"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun Query.snapshots( metadataChanges: MetadataChanges = MetadataChanges.EXCLUDE @@ -525,11 +460,8 @@ fun Query.snapshots( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.Query.dataObjects(metadataChanges)` from the main module instead.", - ReplaceWith( - expression = "dataObjects(metadataChanges)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.dataObjects"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun Query.dataObjects( metadataChanges: MetadataChanges = MetadataChanges.EXCLUDE @@ -551,11 +483,8 @@ inline fun Query.dataObjects( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.firestore.DocumentReference.dataObjects(metadataChanges)` from the main module instead.", - ReplaceWith( - expression = "dataObjects(metadataChanges)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.firestore.dataObjects"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) inline fun DocumentReference.dataObjects( metadataChanges: MetadataChanges = MetadataChanges.EXCLUDE diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index c3de588b354..8fe62677439 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -225,7 +225,7 @@ public void onClose(Status status) { public Task> runAggregateQuery( Query query, List aggregateFields) { com.google.firestore.v1.Target.QueryTarget encodedQueryTarget = - serializer.encodeQueryTarget(query.toTarget()); + serializer.encodeQueryTarget(query.toAggregateTarget()); HashMap aliasMap = new HashMap<>(); StructuredAggregationQuery structuredAggregationQuery = serializer.encodeStructuredAggregationQuery(encodedQueryTarget, aggregateFields, aliasMap); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java index e59f50d39db..de3de67463c 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/QueryTest.java @@ -582,37 +582,49 @@ public void testHashCode() { public void testImplicitOrderBy() { Query baseQuery = Query.atPath(path("foo")); // Default is ascending - assertEquals(asList(orderBy(KEY_FIELD_NAME, "asc")), baseQuery.getOrderBy()); + assertEquals(asList(orderBy(KEY_FIELD_NAME, "asc")), baseQuery.getNormalizedOrderBy()); // Explicit key ordering is respected assertEquals( asList(orderBy(KEY_FIELD_NAME, "asc")), - baseQuery.orderBy(orderBy(KEY_FIELD_NAME, "asc")).getOrderBy()); + baseQuery.orderBy(orderBy(KEY_FIELD_NAME, "asc")).getNormalizedOrderBy()); assertEquals( asList(orderBy(KEY_FIELD_NAME, "desc")), - baseQuery.orderBy(orderBy(KEY_FIELD_NAME, "desc")).getOrderBy()); + baseQuery.orderBy(orderBy(KEY_FIELD_NAME, "desc")).getNormalizedOrderBy()); assertEquals( asList(orderBy("foo"), orderBy(KEY_FIELD_NAME, "asc")), - baseQuery.orderBy(orderBy("foo")).orderBy(orderBy(KEY_FIELD_NAME, "asc")).getOrderBy()); + baseQuery + .orderBy(orderBy("foo")) + .orderBy(orderBy(KEY_FIELD_NAME, "asc")) + .getNormalizedOrderBy()); assertEquals( asList(orderBy("foo"), orderBy(KEY_FIELD_NAME, "desc")), - baseQuery.orderBy(orderBy("foo")).orderBy(orderBy(KEY_FIELD_NAME, "desc")).getOrderBy()); + baseQuery + .orderBy(orderBy("foo")) + .orderBy(orderBy(KEY_FIELD_NAME, "desc")) + .getNormalizedOrderBy()); // Inequality filters add order bys assertEquals( asList(orderBy("foo"), orderBy(KEY_FIELD_NAME, "asc")), - baseQuery.filter(filter("foo", "<", 5)).getOrderBy()); + baseQuery.filter(filter("foo", "<", 5)).getNormalizedOrderBy()); // Descending order by applies to implicit key ordering assertEquals( asList(orderBy("foo", "desc"), orderBy(KEY_FIELD_NAME, "desc")), - baseQuery.orderBy(orderBy("foo", "desc")).getOrderBy()); + baseQuery.orderBy(orderBy("foo", "desc")).getNormalizedOrderBy()); assertEquals( asList(orderBy("foo", "asc"), orderBy("bar", "desc"), orderBy(KEY_FIELD_NAME, "desc")), - baseQuery.orderBy(orderBy("foo", "asc")).orderBy(orderBy("bar", "desc")).getOrderBy()); + baseQuery + .orderBy(orderBy("foo", "asc")) + .orderBy(orderBy("bar", "desc")) + .getNormalizedOrderBy()); assertEquals( asList(orderBy("foo", "desc"), orderBy("bar", "asc"), orderBy(KEY_FIELD_NAME, "asc")), - baseQuery.orderBy(orderBy("foo", "desc")).orderBy(orderBy("bar", "asc")).getOrderBy()); + baseQuery + .orderBy(orderBy("foo", "desc")) + .orderBy(orderBy("bar", "asc")) + .getNormalizedOrderBy()); } @Test @@ -631,7 +643,7 @@ public void testImplicitOrderByInMultipleInequality() { .filter(filter("aa", "<", 5)) .filter(filter("b", "<", 5)) .filter(filter("A", "<", 5)) - .getOrderBy()); + .getNormalizedOrderBy()); // numbers assertEquals( @@ -646,7 +658,7 @@ public void testImplicitOrderByInMultipleInequality() { .filter(filter("1", "<", 5)) .filter(filter("2", "<", 5)) .filter(filter("19", "<", 5)) - .getOrderBy()); + .getNormalizedOrderBy()); // nested fields assertEquals( @@ -659,7 +671,7 @@ public void testImplicitOrderByInMultipleInequality() { .filter(filter("a", "<", 5)) .filter(filter("aa", "<", 5)) .filter(filter("a.a", "<", 5)) - .getOrderBy()); + .getNormalizedOrderBy()); // special characters assertEquals( @@ -672,7 +684,7 @@ public void testImplicitOrderByInMultipleInequality() { .filter(filter("a", "<", 5)) .filter(filter("_a", "<", 5)) .filter(filter("a.a", "<", 5)) - .getOrderBy()); + .getNormalizedOrderBy()); // field name with dot assertEquals( @@ -685,7 +697,7 @@ public void testImplicitOrderByInMultipleInequality() { .filter(filter("a", "<", 5)) .filter(filter("`a.a`", "<", 5)) // Field name with dot .filter(filter("a.z", "<", 5)) // Nested field - .getOrderBy()); + .getNormalizedOrderBy()); // composite filter assertEquals( @@ -701,7 +713,7 @@ public void testImplicitOrderByInMultipleInequality() { andFilters( orFilters(filter("b", ">=", 1), filter("c", "<=", 1)), orFilters(filter("d", "<=", 1), filter("e", "==", 1)))) - .getOrderBy()); + .getNormalizedOrderBy()); // OrderBy assertEquals( @@ -715,7 +727,7 @@ public void testImplicitOrderByInMultipleInequality() { .filter(filter("a", "<", 5)) .filter(filter("z", "<", 5)) .orderBy(orderBy("z")) - .getOrderBy()); + .getNormalizedOrderBy()); // last explicit order by direction assertEquals( @@ -728,7 +740,7 @@ public void testImplicitOrderByInMultipleInequality() { .filter(filter("b", "<", 5)) .filter(filter("a", "<", 5)) .orderBy(orderBy("z", "desc")) - .getOrderBy()); + .getNormalizedOrderBy()); assertEquals( asList( @@ -742,7 +754,7 @@ public void testImplicitOrderByInMultipleInequality() { .filter(filter("a", "<", 5)) .orderBy(orderBy("z", "desc")) .orderBy(orderBy("c")) - .getOrderBy()); + .getNormalizedOrderBy()); } @Test @@ -1021,11 +1033,65 @@ public void testGetOrderByReturnsUnmodifiableList() { .orderBy(orderBy("e")) .orderBy(orderBy("f")); - List orderByList = query.getOrderBy(); + List orderByList = query.getNormalizedOrderBy(); assertThrows(UnsupportedOperationException.class, () -> orderByList.add(orderBy("g"))); } + @Test + public void testOrderByForAggregateAndNonAggregate() { + Query col = query("collection"); + + // Build two identical queries + Query query1 = col.filter(filter("foo", ">", 1)); + Query query2 = col.filter(filter("foo", ">", 1)); + + // Compute an aggregate and non-aggregate target from the queries + Target aggregateTarget = query1.toAggregateTarget(); + Target target = query2.toTarget(); + + assertEquals(aggregateTarget.getOrderBy().size(), 0); + + assertEquals(target.getOrderBy().size(), 2); + assertEquals(target.getOrderBy().get(0).getDirection(), OrderBy.Direction.ASCENDING); + assertEquals(target.getOrderBy().get(0).getField().toString(), "foo"); + assertEquals(target.getOrderBy().get(1).getDirection(), OrderBy.Direction.ASCENDING); + assertEquals(target.getOrderBy().get(1).getField().toString(), "__name__"); + } + + @Test + public void testGeneratedOrderBysNotAffectedByPreviouslyMemoizedTargets() { + Query col = query("collection"); + + // Build two identical queries + Query query1 = col.filter(filter("foo", ">", 1)); + Query query2 = col.filter(filter("foo", ">", 1)); + + // query1 - first to aggregate target, then to non-aggregate target + Target aggregateTarget1 = query1.toAggregateTarget(); + Target target1 = query1.toTarget(); + + // query2 - first to non-aggregate target, then to aggregate target + Target target2 = query2.toTarget(); + Target aggregateTarget2 = query2.toAggregateTarget(); + + assertEquals(aggregateTarget1.getOrderBy().size(), 0); + + assertEquals(aggregateTarget2.getOrderBy().size(), 0); + + assertEquals(target1.getOrderBy().size(), 2); + assertEquals(target1.getOrderBy().get(0).getDirection(), OrderBy.Direction.ASCENDING); + assertEquals(target1.getOrderBy().get(0).getField().toString(), "foo"); + assertEquals(target1.getOrderBy().get(1).getDirection(), OrderBy.Direction.ASCENDING); + assertEquals(target1.getOrderBy().get(1).getField().toString(), "__name__"); + + assertEquals(target2.getOrderBy().size(), 2); + assertEquals(target2.getOrderBy().get(0).getDirection(), OrderBy.Direction.ASCENDING); + assertEquals(target2.getOrderBy().get(0).getField().toString(), "foo"); + assertEquals(target2.getOrderBy().get(1).getDirection(), OrderBy.Direction.ASCENDING); + assertEquals(target2.getOrderBy().get(1).getField().toString(), "__name__"); + } + private void assertQueryMatches( Query query, List matching, List nonMatching) { for (MutableDocument doc : matching) { diff --git a/firebase-functions/firebase-functions.gradle.kts b/firebase-functions/firebase-functions.gradle.kts index 3b41ed2e8c7..cdc0210de6c 100644 --- a/firebase-functions/firebase-functions.gradle.kts +++ b/firebase-functions/firebase-functions.gradle.kts @@ -51,9 +51,9 @@ dependencies { javadocClasspath(libs.findbugs.jsr305) implementation(project(":appcheck:firebase-appcheck-interop")) - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") implementation("com.google.firebase:firebase-annotations:16.2.0") implementation("com.google.firebase:firebase-auth-interop:18.0.0") { exclude(group = "com.google.firebase", module = "firebase-common") diff --git a/firebase-functions/ktx/ktx.gradle.kts b/firebase-functions/ktx/ktx.gradle.kts index de8ea47cb4c..fb47b87c78c 100644 --- a/firebase-functions/ktx/ktx.gradle.kts +++ b/firebase-functions/ktx/ktx.gradle.kts @@ -44,11 +44,11 @@ android { } dependencies { - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") api(project(":firebase-functions")) - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-components:17.1.5") testImplementation(libs.androidx.test.core) testImplementation(libs.junit) diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/ktx/Functions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/ktx/Functions.kt index 1c73c153c1b..4ec0cf9caba 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/ktx/Functions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/ktx/Functions.kt @@ -25,6 +25,9 @@ import com.google.firebase.ktx.Firebase import java.net.URL /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseFunctions] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -32,17 +35,13 @@ import java.net.URL * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.functions` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.functions", - imports = ["com.google.firebase.Firebase", "com.google.firebase.functions.functions"] - ) -) val Firebase.functions: FirebaseFunctions get() = FirebaseFunctions.getInstance() /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseFunctions] instance of a given [regionOrCustomDomain]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -50,17 +49,13 @@ val Firebase.functions: FirebaseFunctions * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.functions(regionOrCustomDomain)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.functions(regionOrCustomDomain)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.functions.functions"] - ) -) fun Firebase.functions(regionOrCustomDomain: String): FirebaseFunctions = FirebaseFunctions.getInstance(regionOrCustomDomain) /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseFunctions] instance of a given [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -68,16 +63,12 @@ fun Firebase.functions(regionOrCustomDomain: String): FirebaseFunctions = * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.functions.Firebase.functions(app)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.functions(app)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.functions.functions"] - ) -) fun Firebase.functions(app: FirebaseApp): FirebaseFunctions = FirebaseFunctions.getInstance(app) /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseFunctions] instance of a given [FirebaseApp] and [regionOrCustomDomain]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -85,13 +76,6 @@ fun Firebase.functions(app: FirebaseApp): FirebaseFunctions = FirebaseFunctions. * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.functions.Firebase.functions(app, regionOrCustomDomain)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.functions(app, regionOrCustomDomain)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.functions.functions"] - ) -) fun Firebase.functions(app: FirebaseApp, regionOrCustomDomain: String): FirebaseFunctions = FirebaseFunctions.getInstance(app, regionOrCustomDomain) @@ -104,15 +88,8 @@ fun Firebase.functions(app: FirebaseApp, regionOrCustomDomain: String): Firebase * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.functions.FirebaseFunctionsKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseFunctionsKtxRegistrar", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.functions.FirebaseFunctionsKtxRegistrar" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep class FirebaseFunctionsKtxRegistrar : ComponentRegistrar { @@ -128,11 +105,8 @@ class FirebaseFunctionsKtxRegistrar : ComponentRegistrar { * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.functions.FirebaseFunctions.getHttpsCallable(name, init)` from the main module instead.", - ReplaceWith( - expression = "getHttpsCallable(name, init)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.functions.getHttpsCallable"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun FirebaseFunctions.getHttpsCallable( name: String, @@ -152,12 +126,8 @@ fun FirebaseFunctions.getHttpsCallable( * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.functions.FirebaseFunctions.getHttpsCallableFromUrl(url, init)` from the main module instead.", - ReplaceWith( - expression = "getHttpsCallableFromUrl(url, init)", - imports = - ["com.google.firebase.Firebase", "com.google.firebase.functions.getHttpsCallableFromUrl"] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) fun FirebaseFunctions.getHttpsCallableFromUrl( url: URL, diff --git a/firebase-inappmessaging-display/firebase-inappmessaging-display.gradle b/firebase-inappmessaging-display/firebase-inappmessaging-display.gradle index e6692ae8ce4..6e01ce65818 100644 --- a/firebase-inappmessaging-display/firebase-inappmessaging-display.gradle +++ b/firebase-inappmessaging-display/firebase-inappmessaging-display.gradle @@ -111,9 +111,9 @@ dependencies { implementation 'com.google.android.gms:play-services-tasks:18.0.1' implementation 'com.google.auto.value:auto-value-annotations:1.6.6' implementation 'javax.inject:javax.inject:1' - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") implementation(project(":firebase-inappmessaging")) testImplementation "androidx.test:core:$androidxTestCoreVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/firebase-inappmessaging-display/ktx/ktx.gradle b/firebase-inappmessaging-display/ktx/ktx.gradle index 62d2765e030..abe7e828cce 100644 --- a/firebase-inappmessaging-display/ktx/ktx.gradle +++ b/firebase-inappmessaging-display/ktx/ktx.gradle @@ -44,9 +44,9 @@ android { } dependencies { - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") api(project(":firebase-inappmessaging")) api(project(":firebase-inappmessaging-display")) testImplementation "androidx.test:core:$androidxTestCoreVersion" diff --git a/firebase-inappmessaging-display/src/main/java/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplay.kt b/firebase-inappmessaging-display/src/main/java/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplay.kt index 6167a5652dc..c123df4d540 100644 --- a/firebase-inappmessaging-display/src/main/java/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplay.kt +++ b/firebase-inappmessaging-display/src/main/java/com/google/firebase/inappmessaging/display/ktx/InAppMessagingDisplay.kt @@ -22,6 +22,9 @@ import com.google.firebase.inappmessaging.display.FirebaseInAppMessagingDisplay import com.google.firebase.ktx.Firebase /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseInAppMessagingDisplay] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -29,17 +32,6 @@ import com.google.firebase.ktx.Firebase * 2024, we'll no longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.inAppMessagingDisplay` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.inAppMessagingDisplay", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.inappmessaging.display.inAppMessagingDisplay" - ] - ) -) val Firebase.inAppMessagingDisplay: FirebaseInAppMessagingDisplay get() = FirebaseInAppMessagingDisplay.getInstance() @@ -52,15 +44,8 @@ val Firebase.inAppMessagingDisplay: FirebaseInAppMessagingDisplay * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.inappmessaging.display.FirebaseInAppMessagingDisplayKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseInAppMessagingDisplayKtxRegistrar", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.inappmessaging.display.FirebaseInAppMessagingDisplayKtxRegistrar" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep class FirebaseInAppMessagingDisplayKtxRegistrar : ComponentRegistrar { diff --git a/firebase-inappmessaging/firebase-inappmessaging.gradle b/firebase-inappmessaging/firebase-inappmessaging.gradle index dc06a118f1d..116e71d14ee 100644 --- a/firebase-inappmessaging/firebase-inappmessaging.gradle +++ b/firebase-inappmessaging/firebase-inappmessaging.gradle @@ -141,9 +141,9 @@ dependencies { implementation('com.google.firebase:firebase-measurement-connector:18.0.2') { exclude group: 'com.google.firebase', module: 'firebase-common' } - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") implementation("com.google.firebase:firebase-datatransport:18.2.0"){ exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-components' diff --git a/firebase-inappmessaging/ktx/ktx.gradle b/firebase-inappmessaging/ktx/ktx.gradle index e49ff8570f8..e17660b5ce2 100644 --- a/firebase-inappmessaging/ktx/ktx.gradle +++ b/firebase-inappmessaging/ktx/ktx.gradle @@ -43,9 +43,9 @@ android { } dependencies { - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") api(project(":firebase-inappmessaging")) testImplementation "androidx.test:core:$androidxTestCoreVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/ktx/InAppMessaging.kt b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/ktx/InAppMessaging.kt index 946d93f64ad..ba2c3e5c95a 100644 --- a/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/ktx/InAppMessaging.kt +++ b/firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/ktx/InAppMessaging.kt @@ -22,6 +22,9 @@ import com.google.firebase.inappmessaging.FirebaseInAppMessaging import com.google.firebase.ktx.Firebase /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseInAppMessaging] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -29,13 +32,6 @@ import com.google.firebase.ktx.Firebase * we'll no longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.inAppMessaging` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.inAppMessaging", - imports = ["com.google.firebase.Firebase", "com.google.firebase.inappmessaging.inAppMessaging"] - ) -) val Firebase.inAppMessaging: FirebaseInAppMessaging get() = FirebaseInAppMessaging.getInstance() @@ -48,15 +44,8 @@ val Firebase.inAppMessaging: FirebaseInAppMessaging * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ @Deprecated( - "Use `com.google.firebase.inappmessaging.FirebaseInAppMessagingKtxRegistrar` from the main module instead.", - ReplaceWith( - expression = "FirebaseInAppMessagingKtxRegistrar", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.inappmessaging.FirebaseInAppMessagingKtxRegistrar" - ] - ) + "Migrate to use the KTX API from the main module: https://firebase.google.com/docs/android/kotlin-migration.", + ReplaceWith("") ) @Keep class FirebaseInAppMessagingKtxRegistrar : ComponentRegistrar { diff --git a/firebase-installations/firebase-installations.gradle b/firebase-installations/firebase-installations.gradle index fcdd6d95321..4045a931b5d 100644 --- a/firebase-installations/firebase-installations.gradle +++ b/firebase-installations/firebase-installations.gradle @@ -62,9 +62,9 @@ dependencies { implementation 'com.google.android.gms:play-services-tasks:18.0.1' implementation 'com.google.firebase:firebase-annotations:16.2.0' implementation 'com.google.firebase:firebase-installations-interop:17.1.0' - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") javadocClasspath 'com.google.code.findbugs:jsr305:3.0.2' testImplementation "androidx.test:core:$androidxTestCoreVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/firebase-installations/ktx/ktx.gradle b/firebase-installations/ktx/ktx.gradle index f4e7af0b410..4a0988e64e7 100644 --- a/firebase-installations/ktx/ktx.gradle +++ b/firebase-installations/ktx/ktx.gradle @@ -42,9 +42,9 @@ android { } dependencies { - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") api(project(":firebase-installations")) api(project(":firebase-installations-interop")) testImplementation "androidx.test:core:$androidxTestCoreVersion" diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/ktx/Installations.kt b/firebase-installations/src/main/java/com/google/firebase/installations/ktx/Installations.kt index 622ff2a2384..afe9adcbeb0 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/ktx/Installations.kt +++ b/firebase-installations/src/main/java/com/google/firebase/installations/ktx/Installations.kt @@ -21,6 +21,9 @@ import com.google.firebase.installations.FirebaseInstallations import com.google.firebase.ktx.Firebase /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseInstallations] instance of the default [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -28,17 +31,13 @@ import com.google.firebase.ktx.Firebase * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.installations` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.installations", - imports = ["com.google.firebase.Firebase", "com.google.firebase.installations.installations"] - ) -) val Firebase.installations: FirebaseInstallations get() = FirebaseInstallations.getInstance() /** + * Accessing this object for Kotlin apps has changed; see the + * [migration guide](https://firebase.google.com/docs/android/kotlin-migration). + * * Returns the [FirebaseInstallations] instance of a given [FirebaseApp]. * @deprecated **Deprecation Notice:** The Kotlin extensions (KTX) APIs have been added to their * respective main modules, and the Kotlin extension (KTX) APIs in @@ -46,27 +45,13 @@ val Firebase.installations: FirebaseInstallations * longer release KTX modules. For details, see the * [FAQ about this initiative.](https://firebase.google.com/docs/android/kotlin-migration) */ -@Deprecated( - "Use `com.google.firebase.Firebase.installations(app)` from the main module instead.", - ReplaceWith( - expression = "com.google.firebase.Firebase.installations(app)", - imports = ["com.google.firebase.Firebase", "com.google.firebase.installations.installations"] - ) -) fun Firebase.installations(app: FirebaseApp): FirebaseInstallations = FirebaseInstallations.getInstance(app) /** @suppress */ @Deprecated( "com.google.firebase.installations.FirebaseInstallationsKtxRegistrar has been deprecated. Use `com.google.firebase.installationsFirebaseInstallationsKtxRegistrar` instead.", - ReplaceWith( - expression = "FirebaseInstallationsKtxRegistrar", - imports = - [ - "com.google.firebase.Firebase", - "com.google.firebase.installations.FirebaseInstallationsKtxRegistrar" - ] - ) + ReplaceWith("") ) class FirebaseInstallationsKtxRegistrar : ComponentRegistrar { override fun getComponents(): List> = listOf() diff --git a/firebase-messaging/CHANGELOG.md b/firebase-messaging/CHANGELOG.md index 532bf864f93..6930167886b 100644 --- a/firebase-messaging/CHANGELOG.md +++ b/firebase-messaging/CHANGELOG.md @@ -1,4 +1,10 @@ # Unreleased +* [changed] Added metadata to FirebaseInstanceIdReceiver to signal that it + finishes background broadcasts after the message has been handled. + +* [changed] Specified notification's dismiss intent target via action instead of + component name. + * [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-messaging-ktx` to `com.google.firebase:firebase-messaging` under the `com.google.firebase.messaging` package. For details, see the diff --git a/firebase-messaging/firebase-messaging.gradle b/firebase-messaging/firebase-messaging.gradle index 0966c56aaa3..8f6e5e721c7 100644 --- a/firebase-messaging/firebase-messaging.gradle +++ b/firebase-messaging/firebase-messaging.gradle @@ -111,9 +111,9 @@ dependencies { } implementation('com.google.firebase:firebase-installations-interop:17.1.0') implementation('com.google.firebase:firebase-measurement-connector:19.0.0') - implementation("com.google.firebase:firebase-common:20.4.1") - implementation("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + implementation("com.google.firebase:firebase-common:20.4.2") + implementation("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") implementation(project(":firebase-installations")) javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' testAnnotationProcessor "com.google.auto.value:auto-value:1.6.3" diff --git a/firebase-messaging/ktx/ktx.gradle b/firebase-messaging/ktx/ktx.gradle index 69ff1a0cb18..1487775d916 100644 --- a/firebase-messaging/ktx/ktx.gradle +++ b/firebase-messaging/ktx/ktx.gradle @@ -42,9 +42,9 @@ android { } dependencies { - api("com.google.firebase:firebase-common:20.4.1") - api("com.google.firebase:firebase-common-ktx:20.4.1") - implementation("com.google.firebase:firebase-components:17.1.4") + api("com.google.firebase:firebase-common:20.4.2") + api("com.google.firebase:firebase-common-ktx:20.4.2") + implementation("com.google.firebase:firebase-components:17.1.5") api(project(":firebase-messaging")) testImplementation "androidx.test:core:$androidxTestCoreVersion" testImplementation "com.google.truth:truth:$googleTruthVersion" diff --git a/firebase-messaging/src/main/AndroidManifest.xml b/firebase-messaging/src/main/AndroidManifest.xml index 8641d593f8a..453f2bf9aff 100644 --- a/firebase-messaging/src/main/AndroidManifest.xml +++ b/firebase-messaging/src/main/AndroidManifest.xml @@ -28,6 +28,9 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirelogPublisher.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirelogPublisher.kt new file mode 100644 index 00000000000..2975447bbaa --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeFirelogPublisher.kt @@ -0,0 +1,34 @@ +/* + * 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.sessions.testing + +import com.google.firebase.sessions.SessionDetails +import com.google.firebase.sessions.SessionFirelogPublisher + +/** + * Fake implementation of [SessionFirelogPublisher] that allows for inspecting the session details + * that were sent to it. + */ +internal class FakeFirelogPublisher : SessionFirelogPublisher { + + /** All the sessions that were uploaded via this fake [SessionFirelogPublisher] */ + val loggedSessions = ArrayList() + + override fun logSession(sessionDetails: SessionDetails) { + loggedSessions.add(sessionDetails) + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionDatastore.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionDatastore.kt new file mode 100644 index 00000000000..f98852032c8 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionDatastore.kt @@ -0,0 +1,35 @@ +/* + * 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.sessions.testing + +import com.google.firebase.sessions.SessionDatastore + +/** + * Fake implementaiton of the [SessionDatastore] that allows for inspecting and modifying the + * currently stored values in unit tests. + */ +internal class FakeSessionDatastore : SessionDatastore { + + /** The currently stored value */ + private var currentSessionId: String? = null + + override fun updateSessionId(sessionId: String) { + currentSessionId = sessionId + } + + override fun getCurrentSessionId() = currentSessionId +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt new file mode 100644 index 00000000000..cfb7a32aad8 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt @@ -0,0 +1,116 @@ +/* + * 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.sessions.testing + +import androidx.annotation.Keep +import com.google.android.datatransport.TransportFactory +import com.google.firebase.FirebaseApp +import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.components.Component +import com.google.firebase.components.ComponentRegistrar +import com.google.firebase.components.Dependency +import com.google.firebase.components.Qualified.qualified +import com.google.firebase.components.Qualified.unqualified +import com.google.firebase.installations.FirebaseInstallationsApi +import com.google.firebase.platforminfo.LibraryVersionComponent +import com.google.firebase.sessions.BuildConfig +import com.google.firebase.sessions.Dispatchers +import com.google.firebase.sessions.FirebaseSessions +import com.google.firebase.sessions.SessionDatastore +import com.google.firebase.sessions.SessionFirelogPublisher +import com.google.firebase.sessions.SessionGenerator +import com.google.firebase.sessions.WallClock +import com.google.firebase.sessions.settings.SessionsSettings +import kotlinx.coroutines.CoroutineDispatcher + +/** + * [ComponentRegistrar] for setting up Fake components for [FirebaseSessions] and its internal + * dependencies for unit tests. + * + * @hide + */ +@Keep +internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { + override fun getComponents() = + listOf( + Component.builder(SessionGenerator::class.java) + .name("session-generator") + .factory { SessionGenerator(timeProvider = WallClock) } + .build(), + Component.builder(FakeFirelogPublisher::class.java) + .name("fake-session-publisher") + .factory { FakeFirelogPublisher() } + .build(), + Component.builder(SessionFirelogPublisher::class.java) + .name("session-publisher") + .add(Dependency.required(fakeFirelogPublisher)) + .factory { container -> container.get(fakeFirelogPublisher) } + .build(), + Component.builder(SessionsSettings::class.java) + .name("sessions-settings") + .add(Dependency.required(firebaseApp)) + .add(Dependency.required(blockingDispatcher)) + .add(Dependency.required(backgroundDispatcher)) + .add(Dependency.required(firebaseInstallationsApi)) + .factory { container -> + SessionsSettings( + container.get(firebaseApp), + container.get(blockingDispatcher), + container.get(backgroundDispatcher), + fakeFirebaseInstallations, + ) + } + .build(), + Component.builder(Dispatchers::class.java) + .name("sessions-dispatchers") + .add(Dependency.required(blockingDispatcher)) + .add(Dependency.required(backgroundDispatcher)) + .factory { container -> + Dispatchers(container.get(blockingDispatcher), container.get(backgroundDispatcher)) + } + .build(), + Component.builder(FakeSessionDatastore::class.java) + .name("fake-sessions-datastore") + .factory { FakeSessionDatastore() } + .build(), + Component.builder(SessionDatastore::class.java) + .name("sessions-datastore") + .add(Dependency.required(fakeDatastore)) + .factory { container -> container.get(fakeDatastore) } + .build(), + LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), + ) + + private companion object { + private const val LIBRARY_NAME = "fire-sessions" + + private val firebaseApp = unqualified(FirebaseApp::class.java) + private val firebaseInstallationsApi = unqualified(FirebaseInstallationsApi::class.java) + private val backgroundDispatcher = + qualified(Background::class.java, CoroutineDispatcher::class.java) + private val blockingDispatcher = + qualified(Blocking::class.java, CoroutineDispatcher::class.java) + private val transportFactory = unqualified(TransportFactory::class.java) + private val fakeFirelogPublisher = unqualified(FakeFirelogPublisher::class.java) + private val fakeDatastore = unqualified(FakeSessionDatastore::class.java) + private val sessionGenerator = unqualified(SessionGenerator::class.java) + private val sessionsSettings = unqualified(SessionsSettings::class.java) + + private val fakeFirebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + } +} From 9d548b5a5d70b816f2add46e6e84ec7148538bd8 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Fri, 20 Oct 2023 20:25:43 -0400 Subject: [PATCH 28/38] Project level dep on sessions (#5460) --- firebase-crashlytics/CHANGELOG.md | 1 - firebase-crashlytics/firebase-crashlytics.gradle | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/firebase-crashlytics/CHANGELOG.md b/firebase-crashlytics/CHANGELOG.md index b2d4b518e89..d41b5050967 100644 --- a/firebase-crashlytics/CHANGELOG.md +++ b/firebase-crashlytics/CHANGELOG.md @@ -520,4 +520,3 @@ The following release notes describe changes in the new SDK. from your `AndroidManifest.xml` file. * [removed] The `fabric.properties` and `crashlytics.properties` files are no longer supported. Remove them from your app. - diff --git a/firebase-crashlytics/firebase-crashlytics.gradle b/firebase-crashlytics/firebase-crashlytics.gradle index 57e245f639c..3547be174e6 100644 --- a/firebase-crashlytics/firebase-crashlytics.gradle +++ b/firebase-crashlytics/firebase-crashlytics.gradle @@ -92,7 +92,7 @@ dependencies { implementation("com.google.firebase:firebase-common-ktx:20.4.2") implementation("com.google.firebase:firebase-components:17.1.3") implementation("com.google.firebase:firebase-installations:17.2.0") - implementation("com.google.firebase:firebase-sessions:1.1.0") { + implementation(project(':firebase-sessions')) { exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-components' } From ec897945391132c4e90e7ce95e8bf8cd89da70f7 Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Mon, 23 Oct 2023 13:17:48 -0400 Subject: [PATCH 29/38] =?UTF-8?q?Fixes=20issue=20where=20handler=20thread?= =?UTF-8?q?=20is=20started/stopped=20based=20on=20activity=E2=80=A6=20(#54?= =?UTF-8?q?68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … lifecycle events. We can't have the handler thread reacting to lifecycle events because: * There's no guarantee that an activity is ever started on a process, but we still want session ids pushed there (eg. service-only process) * There can be more than one activity per process and the first time one of those activities is stopped, the handlertthread will be stopped and so we'll have no thread to process the callback messages. Updated to use MainLooper --- .../sessions/SessionLifecycleClient.kt | 21 ++----------------- .../SessionsActivityLifecycleCallbacks.kt | 4 ++-- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt index 4211fff61b3..d79a1d020c5 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt @@ -21,8 +21,8 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.Handler -import android.os.HandlerThread import android.os.IBinder +import android.os.Looper import android.os.Message import android.os.Messenger import android.os.RemoteException @@ -54,18 +54,13 @@ internal object SessionLifecycleClient { private var serviceBound: Boolean = false private val queuedMessages = LinkedBlockingDeque(MAX_QUEUED_MESSAGES) private var curSessionId: String = "" - private var handlerThread: HandlerThread = HandlerThread("FirebaseSessionsClient_HandlerThread") - - init { - handlerThread.start() - } /** * The callback class that will be used to receive updated session events from the * [SessionLifecycleService]. */ // TODO(rothbutter) should we use the main looper or is there one available in this SDK? - internal class ClientUpdateHandler : Handler(handlerThread.looper) { + internal class ClientUpdateHandler : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { when (msg.what) { SessionLifecycleService.SESSION_UPDATED -> @@ -149,18 +144,6 @@ internal object SessionLifecycleClient { sendLifecycleEvent(SessionLifecycleService.BACKGROUNDED) } - /** Perform initialization that requires cleanup */ - fun started() { - if (!handlerThread.isAlive) { - handlerThread.start() - } - } - - /** Cleanup initialization */ - fun stopped() { - handlerThread.quit() - } - /** * Sends a message to the [SessionLifecycleService] with the given event code. This will * potentially also send any messages that have been queued up but not successfully delivered to diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt index 6729f902dfd..c6f91d597b2 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt @@ -31,9 +31,9 @@ internal object SessionsActivityLifecycleCallbacks : ActivityLifecycleCallbacks override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit - override fun onActivityStarted(activity: Activity) = SessionLifecycleClient.started() + override fun onActivityStarted(activity: Activity) = Unit - override fun onActivityStopped(activity: Activity) = SessionLifecycleClient.stopped() + override fun onActivityStopped(activity: Activity) = Unit override fun onActivityDestroyed(activity: Activity) = Unit From 6ed2dbbe1947f316df776b29d87ed5f0fb5f717b Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Mon, 23 Oct 2023 16:11:06 -0400 Subject: [PATCH 30/38] Fix sessions test app android test (#5471) --- firebase-perf/firebase-perf.gradle | 2 +- .../firebase/sessions/FirebaseSessions.kt | 13 ------------ .../testing/sessions/FirebaseSessionsTest.kt | 21 ++++++++++--------- .../test-app/test-app.gradle.kts | 2 +- 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle index 1c4756afb26..c1e6216837a 100644 --- a/firebase-perf/firebase-perf.gradle +++ b/firebase-perf/firebase-perf.gradle @@ -114,7 +114,7 @@ dependencies { implementation("com.google.firebase:firebase-components:17.1.3") implementation("com.google.firebase:firebase-config:21.5.0") implementation("com.google.firebase:firebase-installations:17.2.0") - implementation("com.google.firebase:firebase-sessions:1.1.0") { + implementation(project(':firebase-sessions')) { exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-common-ktx' exclude group: 'com.google.firebase', module: 'firebase-components' diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index 30174debb8e..1dd2bd68d7b 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -63,20 +63,7 @@ internal class FirebaseSessions( companion object { private const val TAG = "FirebaseSessions" - @JvmStatic val instance: FirebaseSessions get() = Firebase.app.get(FirebaseSessions::class.java) - - @JvmStatic - @Deprecated( - "Firebase Sessions only supports the Firebase default app.", - ReplaceWith("FirebaseSessions.instance"), - ) - fun getInstance(app: FirebaseApp): FirebaseSessions = - if (app == Firebase.app) { - app.get(FirebaseSessions::class.java) - } else { - throw IllegalArgumentException("Firebase Sessions only supports the Firebase default app.") - } } } diff --git a/firebase-sessions/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsTest.kt b/firebase-sessions/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsTest.kt index 1db255306eb..9631b9776b4 100644 --- a/firebase-sessions/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsTest.kt +++ b/firebase-sessions/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsTest.kt @@ -16,13 +16,13 @@ package com.google.firebase.testing.sessions +import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import com.google.firebase.Firebase import com.google.firebase.FirebaseApp -import com.google.firebase.ktx.Firebase -import com.google.firebase.ktx.initialize -import com.google.firebase.sessions.FirebaseSessions +import com.google.firebase.initialize import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.api.SessionSubscriber import org.junit.After @@ -44,18 +44,19 @@ class FirebaseSessionsTest { @Test fun initializeSessions_generatesSessionEvent() { - // Force the Firebase Sessions SDK to initialize. - assertThat(FirebaseSessions.instance).isNotNull() - // Add a fake dependency and register it, otherwise sessions will never send. val fakeSessionSubscriber = FakeSessionSubscriber() FirebaseSessionsDependencies.register(fakeSessionSubscriber) - // Wait for the session start event to send. - Thread.sleep(TIME_TO_LOG_SESSION) + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + scenario.onActivity { + // Wait for the session start event to send. + Thread.sleep(TIME_TO_LOG_SESSION) - // Assert that some session was generated and sent to the subscriber. - assertThat(fakeSessionSubscriber.sessionDetails).isNotNull() + // Assert that some session was generated and sent to the subscriber. + assertThat(fakeSessionSubscriber.sessionDetails).isNotNull() + } + } } companion object { diff --git a/firebase-sessions/test-app/test-app.gradle.kts b/firebase-sessions/test-app/test-app.gradle.kts index 276f9077052..338ff3d1d4a 100644 --- a/firebase-sessions/test-app/test-app.gradle.kts +++ b/firebase-sessions/test-app/test-app.gradle.kts @@ -66,7 +66,7 @@ dependencies { implementation("com.google.android.material:material:1.9.0") implementation(libs.androidx.core) - androidTestImplementation("com.google.firebase:firebase-common-ktx:20.3.3") + androidTestImplementation("com.google.firebase:firebase-common:20.4.2") androidTestImplementation(libs.androidx.test.junit) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.truth) From 4f5da7e95380255dc758e6efab5c2beca2f48ed2 Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Thu, 26 Oct 2023 10:52:30 -0400 Subject: [PATCH 31/38] =?UTF-8?q?Adds=20unit=20test=20for=20SessionLifecyc?= =?UTF-8?q?leService=20using=20the=20fake=20registrar=20i=E2=80=A6=20(#546?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …mplementations --- .../sessions/SessionLifecycleService.kt | 2 +- .../sessions/SessionLifecycleServiceTest.kt | 244 ++++++++++++++++++ 2 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt index 120d59a84e3..a1cb70be8ee 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleService.kt @@ -38,7 +38,7 @@ import com.google.firebase.sessions.settings.SessionsSettings internal class SessionLifecycleService : Service() { /** The thread that will be used to process all lifecycle messages from connected clients. */ - private val handlerThread: HandlerThread = HandlerThread("FirebaseSessions_HandlerThread") + internal val handlerThread: HandlerThread = HandlerThread("FirebaseSessions_HandlerThread") /** The handler that will process all lifecycle messages from connected clients . */ private var messageHandler: MessageHandler? = null diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt new file mode 100644 index 00000000000..682a9ddfbbb --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleServiceTest.kt @@ -0,0 +1,244 @@ +/* + * 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.sessions + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.Messenger +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.MediumTest +import com.google.common.truth.Truth.assertThat +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.google.firebase.initialize +import com.google.firebase.sessions.testing.FakeFirebaseApp +import com.google.firebase.sessions.testing.FakeFirelogPublisher +import com.google.firebase.sessions.testing.FakeSessionDatastore +import java.time.Duration +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.android.controller.ServiceController +import org.robolectric.annotation.LooperMode +import org.robolectric.annotation.LooperMode.Mode.PAUSED +import org.robolectric.shadows.ShadowSystemClock + +@OptIn(ExperimentalCoroutinesApi::class) +@MediumTest +@LooperMode(PAUSED) +@RunWith(RobolectricTestRunner::class) +internal class SessionLifecycleServiceTest { + + lateinit var service: ServiceController + lateinit var firebaseApp: FirebaseApp + + data class CallbackMessage(val code: Int, val sessionId: String?) + + internal inner class TestCallbackHandler(looper: Looper = Looper.getMainLooper()) : + Handler(looper) { + val callbackMessages = ArrayList() + + override fun handleMessage(msg: Message) { + callbackMessages.add(CallbackMessage(msg.what, getSessionId(msg))) + } + } + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + firebaseApp = + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build() + ) + service = createService() + } + + @After + fun cleanUp() { + FirebaseApp.clearInstancesForTest() + } + + @Test + fun binding_noCallbackOnInitialBindingWhenNoneStored() { + val client = TestCallbackHandler() + + bindToService(client) + + waitForAllMessages() + assertThat(client.callbackMessages).isEmpty() + } + + @Test + fun binding_callbackOnInitialBindWhenSessionIdSet() { + val client = TestCallbackHandler() + firebaseApp.get(FakeSessionDatastore::class.java).updateSessionId("123") + + bindToService(client) + + waitForAllMessages() + assertThat(client.callbackMessages).hasSize(1) + val msg = client.callbackMessages.first() + assertThat(msg.code).isEqualTo(SessionLifecycleService.SESSION_UPDATED) + assertThat(msg.sessionId).isNotEmpty() + // We should not send stored session IDs to firelog + assertThat(getUploadedSessions()).isEmpty() + } + + @Test + fun foregrounding_startsSessionOnFirstForegrounding() { + val client = TestCallbackHandler() + val messenger = bindToService(client) + + messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + + waitForAllMessages() + assertThat(client.callbackMessages).hasSize(1) + assertThat(getUploadedSessions()).hasSize(1) + assertThat(client.callbackMessages.first().code) + .isEqualTo(SessionLifecycleService.SESSION_UPDATED) + assertThat(client.callbackMessages.first().sessionId).isNotEmpty() + assertThat(getUploadedSessions().first().sessionId) + .isEqualTo(client.callbackMessages.first().sessionId) + } + + @Test + fun foregrounding_onlyOneSessionOnMultipleForegroundings() { + val client = TestCallbackHandler() + val messenger = bindToService(client) + + messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + + waitForAllMessages() + assertThat(client.callbackMessages).hasSize(1) + assertThat(getUploadedSessions()).hasSize(1) + } + + @Test + fun foregrounding_newSessionAfterLongDelay() { + val client = TestCallbackHandler() + val messenger = bindToService(client) + + messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + ShadowSystemClock.advanceBy(Duration.ofMinutes(31)) + messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + + waitForAllMessages() + assertThat(client.callbackMessages).hasSize(2) + assertThat(getUploadedSessions()).hasSize(2) + assertThat(client.callbackMessages.first().sessionId) + .isNotEqualTo(client.callbackMessages.last().sessionId) + assertThat(getUploadedSessions().first().sessionId) + .isEqualTo(client.callbackMessages.first().sessionId) + assertThat(getUploadedSessions().last().sessionId) + .isEqualTo(client.callbackMessages.last().sessionId) + } + + @Test + fun sendsSessionsToMultipleClients() { + val client1 = TestCallbackHandler() + val client2 = TestCallbackHandler() + val client3 = TestCallbackHandler() + bindToService(client1) + val messenger = bindToService(client2) + bindToService(client3) + waitForAllMessages() + + messenger.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + + waitForAllMessages() + assertThat(client1.callbackMessages).hasSize(1) + assertThat(client1.callbackMessages).isEqualTo(client2.callbackMessages) + assertThat(client1.callbackMessages).isEqualTo(client3.callbackMessages) + assertThat(getUploadedSessions()).hasSize(1) + } + + @Test + fun onlyOneSessionForMultipleClientsForegrounding() { + val client1 = TestCallbackHandler() + val client2 = TestCallbackHandler() + val client3 = TestCallbackHandler() + val messenger1 = bindToService(client1) + val messenger2 = bindToService(client2) + val messenger3 = bindToService(client3) + waitForAllMessages() + + messenger1.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + messenger1.send(Message.obtain(null, SessionLifecycleService.BACKGROUNDED, 0, 0)) + messenger2.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + messenger2.send(Message.obtain(null, SessionLifecycleService.BACKGROUNDED, 0, 0)) + messenger3.send(Message.obtain(null, SessionLifecycleService.FOREGROUNDED, 0, 0)) + + waitForAllMessages() + assertThat(client1.callbackMessages).hasSize(1) + assertThat(client1.callbackMessages).isEqualTo(client2.callbackMessages) + assertThat(client1.callbackMessages).isEqualTo(client3.callbackMessages) + assertThat(getUploadedSessions()).hasSize(1) + } + + @Test + fun backgrounding_doesNotStartSession() { + val client = TestCallbackHandler() + val messenger = bindToService(client) + + messenger.send(Message.obtain(null, SessionLifecycleService.BACKGROUNDED, 0, 0)) + + waitForAllMessages() + assertThat(client.callbackMessages).isEmpty() + assertThat(getUploadedSessions()).isEmpty() + } + + private fun bindToService(client: TestCallbackHandler): Messenger { + return Messenger(service.get()?.onBind(createServiceLaunchIntent(client))) + } + + private fun createServiceLaunchIntent(client: TestCallbackHandler) = + Intent( + ApplicationProvider.getApplicationContext(), + SessionLifecycleService::class.java + ) + .apply { putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, Messenger(client)) } + + private fun createService() = + Robolectric.buildService(SessionLifecycleService::class.java).create() + + private fun waitForAllMessages() { + shadowOf(service.get()?.handlerThread?.getLooper()).idle() + shadowOf(Looper.getMainLooper()).idle() + } + + private fun getUploadedSessions() = + firebaseApp.get(FakeFirelogPublisher::class.java).loggedSessions + + private fun getSessionId(msg: Message) = + msg.data?.getString(SessionLifecycleService.SESSION_UPDATE_EXTRA) +} From 26990acb8d5ff7ffc2e2efe588db7e1a1072cc96 Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Thu, 26 Oct 2023 11:43:42 -0400 Subject: [PATCH 32/38] =?UTF-8?q?Refactors=20the=20SessionLifecycleClient?= =?UTF-8?q?=20to=20be=20a=20class=20instead=20ofan=20objec=E2=80=A6=20(#54?= =?UTF-8?q?72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …t, and pulls the service binding into a different class to improve testability of the client. --- .../google/firebase/sessions/Dispatchers.kt | 34 ---------- .../firebase/sessions/FirebaseSessions.kt | 13 ++-- .../sessions/FirebaseSessionsRegistrar.kt | 8 --- .../sessions/SessionLifecycleClient.kt | 66 +++++++++---------- .../sessions/SessionLifecycleServiceBinder.kt | 63 ++++++++++++++++++ .../SessionsActivityLifecycleCallbacks.kt | 8 ++- .../testing/FirebaseSessionsFakeRegistrar.kt | 9 --- 7 files changed, 104 insertions(+), 97 deletions(-) delete mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Dispatchers.kt create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Dispatchers.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Dispatchers.kt deleted file mode 100644 index 86aa5ac1a5f..00000000000 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/Dispatchers.kt +++ /dev/null @@ -1,34 +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.sessions - -import com.google.firebase.Firebase -import com.google.firebase.app -import kotlin.coroutines.CoroutineContext - -/** Container for injecting dispatchers. */ -internal data class Dispatchers -constructor( - val blockingDispatcher: CoroutineContext, - val backgroundDispatcher: CoroutineContext, -) { - - companion object { - val instance: Dispatchers - get() = Firebase.app.get(Dispatchers::class.java) - } -} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index 1dd2bd68d7b..419b5b8b567 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -41,15 +41,14 @@ internal class FirebaseSessions( if (!settings.sessionsEnabled) { Log.d(TAG, "Sessions SDK disabled. Not listening to lifecycle events.") } else if (appContext is Application) { - SessionLifecycleClient.bindToService(appContext) - appContext.registerActivityLifecycleCallbacks(SessionsActivityLifecycleCallbacks) + val lifecycleClient = SessionLifecycleClient(backgroundDispatcher) + val activityCallbacks = SessionsActivityLifecycleCallbacks(lifecycleClient) + appContext.registerActivityLifecycleCallbacks(activityCallbacks) + lifecycleClient.bindToService() firebaseApp.addLifecycleEventListener { _, _ -> - Log.w( - TAG, - "FirebaseApp instance deleted. Sessions library will not collect session data." - ) - appContext.unregisterActivityLifecycleCallbacks(SessionsActivityLifecycleCallbacks) + Log.w(TAG, "FirebaseApp instance deleted. Sessions library will stop collecting data.") + appContext.unregisterActivityLifecycleCallbacks(activityCallbacks) } } else { Log.e( diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index edecc7624af..e704a51ffcb 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -87,14 +87,6 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { ) } .build(), - Component.builder(Dispatchers::class.java) - .name("sessions-dispatchers") - .add(Dependency.required(blockingDispatcher)) - .add(Dependency.required(backgroundDispatcher)) - .factory { container -> - Dispatchers(container.get(blockingDispatcher), container.get(backgroundDispatcher)) - } - .build(), Component.builder(SessionDatastore::class.java) .name("sessions-datastore") .add(Dependency.required(firebaseApp)) diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt index d79a1d020c5..e9e42f05837 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleClient.kt @@ -17,8 +17,6 @@ package com.google.firebase.sessions import android.content.ComponentName -import android.content.Context -import android.content.Intent import android.content.ServiceConnection import android.os.Handler import android.os.IBinder @@ -30,6 +28,7 @@ import android.util.Log import com.google.firebase.sessions.api.FirebaseSessionsDependencies import com.google.firebase.sessions.api.SessionSubscriber import java.util.concurrent.LinkedBlockingDeque +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -41,26 +40,19 @@ import kotlinx.coroutines.launch * Note: this client will be connected in every application process that uses Firebase, and is * intended to maintain that connection for the lifetime of the process. */ -internal object SessionLifecycleClient { - const val TAG = "SessionLifecycleClient" - - /** - * The maximum number of messages that we should queue up for delivery to the - * [SessionLifecycleService] in the event that we have lost the connection. - */ - private const val MAX_QUEUED_MESSAGES = 20 +internal class SessionLifecycleClient(private val backgroundDispatcher: CoroutineContext) { private var service: Messenger? = null private var serviceBound: Boolean = false private val queuedMessages = LinkedBlockingDeque(MAX_QUEUED_MESSAGES) - private var curSessionId: String = "" /** * The callback class that will be used to receive updated session events from the * [SessionLifecycleService]. */ - // TODO(rothbutter) should we use the main looper or is there one available in this SDK? - internal class ClientUpdateHandler : Handler(Looper.getMainLooper()) { + internal class ClientUpdateHandler(private val backgroundDispatcher: CoroutineContext) : + Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { when (msg.what) { SessionLifecycleService.SESSION_UPDATED -> @@ -76,9 +68,8 @@ internal object SessionLifecycleClient { private fun handleSessionUpdate(sessionId: String) { Log.d(TAG, "Session update received: $sessionId") - curSessionId = sessionId - CoroutineScope(Dispatchers.instance.backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher).launch { FirebaseSessionsDependencies.getRegisteredSubscribers().values.forEach { subscriber -> // Notify subscribers, regardless of sampling and data collection state. subscriber.onSessionChanged(SessionSubscriber.SessionDetails(sessionId)) @@ -109,21 +100,11 @@ internal object SessionLifecycleClient { * Binds to the [SessionLifecycleService] and passes a callback [Messenger] that will be used to * relay session updates to this client. */ - fun bindToService(appContext: Context) { - Intent(appContext, SessionLifecycleService::class.java).also { intent -> - Log.d(TAG, "Binding service to application.") - // This is necessary for the onBind() to be called by each process - intent.action = android.os.Process.myPid().toString() - intent.putExtra( - SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, - Messenger(ClientUpdateHandler()) - ) - appContext.bindService( - intent, - serviceConnection, - Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE - ) - } + fun bindToService() { + SessionLifecycleServiceBinder.instance.bindToService( + Messenger(ClientUpdateHandler(backgroundDispatcher)), + serviceConnection + ) } /** @@ -164,7 +145,7 @@ internal object SessionLifecycleClient { * Does not send events unless data collection is enabled for at least one subscriber. */ private fun sendLifecycleEvents(messages: List) = - CoroutineScope(Dispatchers.instance.backgroundDispatcher).launch { + CoroutineScope(backgroundDispatcher).launch { val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers() if (subscribers.isEmpty()) { Log.d( @@ -174,10 +155,13 @@ internal object SessionLifecycleClient { } else if (subscribers.values.none { it.isDataCollectionEnabled }) { Log.d(TAG, "Data Collection is disabled for all subscribers. Skipping this Event") } else { - val latest = ArrayList(2) - getLatestByCode(messages, SessionLifecycleService.BACKGROUNDED)?.let { latest.add(it) } - getLatestByCode(messages, SessionLifecycleService.FOREGROUNDED)?.let { latest.add(it) } - latest.sortedBy { it.getWhen() }.forEach { sendMessageToServer(it) } + mutableListOf( + getLatestByCode(messages, SessionLifecycleService.BACKGROUNDED), + getLatestByCode(messages, SessionLifecycleService.FOREGROUNDED), + ) + .filterNotNull() + .sortedBy { it.getWhen() } + .forEach { sendMessageToServer(it) } } } @@ -210,7 +194,7 @@ internal object SessionLifecycleClient { /** Drains the queue of messages into a new list in a thread-safe manner. */ private fun drainQueue(): MutableList { - val messages = ArrayList() + val messages = mutableListOf() queuedMessages.drainTo(messages) return messages } @@ -218,4 +202,14 @@ internal object SessionLifecycleClient { /** Gets the message in the given list with the given code that has the latest timestamp. */ private fun getLatestByCode(messages: List, msgCode: Int): Message? = messages.filter { it.what == msgCode }.maxByOrNull { it.getWhen() } + + companion object { + const val TAG = "SessionLifecycleClient" + + /** + * The maximum number of messages that we should queue up for delivery to the + * [SessionLifecycleService] in the event that we have lost the connection. + */ + private const val MAX_QUEUED_MESSAGES = 20 + } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt new file mode 100644 index 00000000000..b8d4f9e4f27 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionLifecycleServiceBinder.kt @@ -0,0 +1,63 @@ +/* + * 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.sessions + +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Messenger +import android.util.Log +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.app + +/** Interface for binding with the [SessionLifecycleService]. */ +internal interface SessionLifecycleServiceBinder { + /** + * Binds the given client callback [Messenger] to the [SessionLifecycleService]. The given + * callback will be used to relay session updates to this client. + */ + fun bindToService(callback: Messenger, serviceConnection: ServiceConnection): Unit + + companion object { + val instance: SessionLifecycleServiceBinder + get() = Firebase.app.get(SessionLifecycleServiceBinder::class.java) + } +} + +internal class SessionLifecycleServiceBinderImpl(private val firebaseApp: FirebaseApp) : + SessionLifecycleServiceBinder { + + override fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) { + val appContext = firebaseApp.applicationContext.applicationContext + Intent(appContext, SessionLifecycleService::class.java).also { intent -> + Log.d(TAG, "Binding service to application.") + // This is necessary for the onBind() to be called by each process + intent.action = android.os.Process.myPid().toString() + intent.putExtra(SessionLifecycleService.CLIENT_CALLBACK_MESSENGER, callback) + appContext.bindService( + intent, + serviceConnection, + Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE + ) + } + } + + companion object { + const val TAG = "SessionLifecycleServiceBinder" + } +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt index c6f91d597b2..7f9ecd383a4 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt @@ -24,10 +24,12 @@ import android.os.Bundle * Lifecycle callbacks that will inform the [SessionLifecycleClient] whenever an [Activity] in this * application process goes foreground or background. */ -internal object SessionsActivityLifecycleCallbacks : ActivityLifecycleCallbacks { - override fun onActivityResumed(activity: Activity) = SessionLifecycleClient.foregrounded() +internal class SessionsActivityLifecycleCallbacks(private val client: SessionLifecycleClient) : + ActivityLifecycleCallbacks { - override fun onActivityPaused(activity: Activity) = SessionLifecycleClient.backgrounded() + override fun onActivityResumed(activity: Activity) = client.foregrounded() + + override fun onActivityPaused(activity: Activity) = client.backgrounded() override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt index cfb7a32aad8..bf06e60ab6e 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt @@ -29,7 +29,6 @@ import com.google.firebase.components.Qualified.unqualified import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.platforminfo.LibraryVersionComponent import com.google.firebase.sessions.BuildConfig -import com.google.firebase.sessions.Dispatchers import com.google.firebase.sessions.FirebaseSessions import com.google.firebase.sessions.SessionDatastore import com.google.firebase.sessions.SessionFirelogPublisher @@ -76,14 +75,6 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { ) } .build(), - Component.builder(Dispatchers::class.java) - .name("sessions-dispatchers") - .add(Dependency.required(blockingDispatcher)) - .add(Dependency.required(backgroundDispatcher)) - .factory { container -> - Dispatchers(container.get(blockingDispatcher), container.get(backgroundDispatcher)) - } - .build(), Component.builder(FakeSessionDatastore::class.java) .name("fake-sessions-datastore") .factory { FakeSessionDatastore() } From 7a1b9f7b31e13973c2f86029f1674a41b3cc3bff Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Thu, 26 Oct 2023 11:44:28 -0400 Subject: [PATCH 33/38] Adds unit test for SessionLifecycleClient. (#5475) --- .../sessions/FirebaseSessionsRegistrar.kt | 7 + .../sessions/SessionLifecycleClientTest.kt | 279 ++++++++++++++++++ .../FakeSessionLifecycleServiceBinder.kt | 88 ++++++ .../sessions/testing/FakeSessionSubscriber.kt | 7 +- .../testing/FirebaseSessionsFakeRegistrar.kt | 13 +- 5 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionLifecycleServiceBinder.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index e704a51ffcb..ed06c17a4bd 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -98,6 +98,13 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { ) } .build(), + Component.builder(SessionLifecycleServiceBinder::class.java) + .name("sessions-service-binder") + .add(Dependency.required(firebaseApp)) + .factory { container -> + SessionLifecycleServiceBinderImpl(container.get(firebaseApp)) + } + .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), ) diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt new file mode 100644 index 00000000000..2ccd7dbb20a --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionLifecycleClientTest.kt @@ -0,0 +1,279 @@ +/* + * 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.sessions + +import android.os.Looper +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.MediumTest +import com.google.common.truth.Truth.assertThat +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.google.firebase.concurrent.TestOnlyExecutors +import com.google.firebase.initialize +import com.google.firebase.sessions.api.FirebaseSessionsDependencies +import com.google.firebase.sessions.api.SessionSubscriber +import com.google.firebase.sessions.api.SessionSubscriber.SessionDetails +import com.google.firebase.sessions.testing.FakeFirebaseApp +import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder +import com.google.firebase.sessions.testing.FakeSessionSubscriber +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf + +@OptIn(ExperimentalCoroutinesApi::class) +@MediumTest +@RunWith(RobolectricTestRunner::class) +internal class SessionLifecycleClientTest { + + lateinit var fakeService: FakeSessionLifecycleServiceBinder + + @Before + fun setUp() { + val firebaseApp = + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build() + ) + fakeService = firebaseApp.get(FakeSessionLifecycleServiceBinder::class.java) + } + + @After + fun cleanUp() { + fakeService.serviceDisconnected() + FirebaseApp.clearInstancesForTest() + fakeService.clearForTest() + FirebaseSessionsDependencies.reset() + } + + @Test + fun bindToService_registersCallbacks() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) + client.bindToService() + + waitForMessages() + assertThat(fakeService.clientCallbacks).hasSize(1) + assertThat(fakeService.connectionCallbacks).hasSize(1) + } + + @Test + fun onServiceConnected_sendsQueuedMessages() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) + client.bindToService() + client.foregrounded() + client.backgrounded() + + fakeService.serviceConnected() + + waitForMessages() + assertThat(fakeService.receivedMessageCodes) + .containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED) + } + + @Test + fun onServiceConnected_sendsOnlyLatestMessages() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) + client.bindToService() + client.foregrounded() + client.backgrounded() + client.foregrounded() + client.backgrounded() + client.foregrounded() + client.backgrounded() + + fakeService.serviceConnected() + + waitForMessages() + assertThat(fakeService.receivedMessageCodes) + .containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED) + } + + @Test + fun onServiceDisconnected_noMoreEventsSent() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) + client.bindToService() + + fakeService.serviceConnected() + fakeService.serviceDisconnected() + client.foregrounded() + client.backgrounded() + + waitForMessages() + assertThat(fakeService.receivedMessageCodes).isEmpty() + } + + @Test + fun serviceReconnection_handlesNewMessages() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) + client.bindToService() + + fakeService.serviceConnected() + fakeService.serviceDisconnected() + fakeService.serviceConnected() + client.foregrounded() + client.backgrounded() + + waitForMessages() + assertThat(fakeService.receivedMessageCodes) + .containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED) + } + + @Test + fun serviceReconnection_queuesOldMessages() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) + client.bindToService() + + fakeService.serviceConnected() + fakeService.serviceDisconnected() + client.foregrounded() + client.backgrounded() + fakeService.serviceConnected() + + waitForMessages() + assertThat(fakeService.receivedMessageCodes) + .containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED) + } + + @Test + fun doesNotSendLifecycleEventsWithoutSubscribers() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + client.bindToService() + + fakeService.serviceConnected() + client.foregrounded() + client.backgrounded() + + waitForMessages() + assertThat(fakeService.receivedMessageCodes).isEmpty() + } + + @Test + fun doesNotSendLifecycleEventsWithoutEnabledSubscribers() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + val crashlyticsSubscriber = addSubscriber(false, SessionSubscriber.Name.CRASHLYTICS) + val perfSubscriber = addSubscriber(false, SessionSubscriber.Name.PERFORMANCE) + client.bindToService() + + fakeService.serviceConnected() + client.foregrounded() + client.backgrounded() + + waitForMessages() + assertThat(fakeService.receivedMessageCodes).isEmpty() + } + + @Test + fun sendsLifecycleEventsWhenAtLeastOneEnabledSubscriber() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + val crashlyticsSubscriber = addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) + val perfSubscriber = addSubscriber(false, SessionSubscriber.Name.PERFORMANCE) + client.bindToService() + + fakeService.serviceConnected() + client.foregrounded() + client.backgrounded() + + waitForMessages() + assertThat(fakeService.receivedMessageCodes).hasSize(2) + } + + @Test + fun handleSessionUpdate_noSubscribers() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + client.bindToService() + + fakeService.serviceConnected() + fakeService.broadcastSession("123") + + waitForMessages() + } + + @Test + fun handleSessionUpdate_sendsToSubscribers() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + val crashlyticsSubscriber = addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) + val perfSubscriber = addSubscriber(true, SessionSubscriber.Name.PERFORMANCE) + client.bindToService() + + fakeService.serviceConnected() + fakeService.broadcastSession("123") + + waitForMessages() + assertThat(crashlyticsSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123")) + assertThat(perfSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123")) + } + + @Test + fun handleSessionUpdate_sendsToAllSubscribersAsLongAsOneIsEnabled() = + runTest(UnconfinedTestDispatcher()) { + val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext) + val crashlyticsSubscriber = addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS) + val perfSubscriber = addSubscriber(false, SessionSubscriber.Name.PERFORMANCE) + client.bindToService() + + fakeService.serviceConnected() + fakeService.broadcastSession("123") + + waitForMessages() + assertThat(crashlyticsSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123")) + assertThat(perfSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123")) + } + + private fun addSubscriber( + collectionEnabled: Boolean, + name: SessionSubscriber.Name + ): FakeSessionSubscriber { + val fakeSubscriber = FakeSessionSubscriber(collectionEnabled, sessionSubscriberName = name) + FirebaseSessionsDependencies.addDependency(name) + FirebaseSessionsDependencies.register(fakeSubscriber) + return fakeSubscriber + } + + private fun waitForMessages() { + shadowOf(Looper.getMainLooper()).idle() + } + + private fun backgroundDispatcher() = TestOnlyExecutors.background().asCoroutineDispatcher() +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionLifecycleServiceBinder.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionLifecycleServiceBinder.kt new file mode 100644 index 00000000000..0d4e58e2014 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionLifecycleServiceBinder.kt @@ -0,0 +1,88 @@ +/* + * 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.sessions.testing + +import android.content.ComponentName +import android.content.ServiceConnection +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.Messenger +import com.google.firebase.sessions.SessionLifecycleService +import com.google.firebase.sessions.SessionLifecycleServiceBinder +import java.util.concurrent.LinkedBlockingQueue +import org.robolectric.Shadows.shadowOf + +/** + * Fake implementation of the [SessionLifecycleServiceBinder] that allows for inspecting the + * callbacks and received messages of the service in unit tests. + */ +internal class FakeSessionLifecycleServiceBinder : SessionLifecycleServiceBinder { + + val clientCallbacks = mutableListOf() + val connectionCallbacks = mutableListOf() + val receivedMessageCodes = LinkedBlockingQueue() + var service = Messenger(FakeServiceHandler()) + + internal inner class FakeServiceHandler() : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + receivedMessageCodes.add(msg.what) + } + } + + override fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) { + clientCallbacks.add(callback) + connectionCallbacks.add(serviceConnection) + } + + fun serviceConnected() { + connectionCallbacks.forEach { it.onServiceConnected(componentName, service.getBinder()) } + } + + fun serviceDisconnected() { + connectionCallbacks.forEach { it.onServiceDisconnected(componentName) } + } + + fun broadcastSession(sessionId: String) { + clientCallbacks.forEach { client -> + val msgData = + Bundle().also { it.putString(SessionLifecycleService.SESSION_UPDATE_EXTRA, sessionId) } + client.send( + Message.obtain(null, SessionLifecycleService.SESSION_UPDATED, 0, 0).also { + it.data = msgData + } + ) + } + } + + fun waitForAllMessages() { + shadowOf(Looper.getMainLooper()).idle() + } + + fun clearForTest() { + clientCallbacks.clear() + connectionCallbacks.clear() + receivedMessageCodes.clear() + service = Messenger(FakeServiceHandler()) + } + + companion object { + val componentName = + ComponentName("com.google.firebase.sessions.testing", "FakeSessionLifecycleServiceBinder") + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionSubscriber.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionSubscriber.kt index 86b8ecbccf6..e95059b8691 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionSubscriber.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSessionSubscriber.kt @@ -24,5 +24,10 @@ internal class FakeSessionSubscriber( override val isDataCollectionEnabled: Boolean = true, override val sessionSubscriberName: SessionSubscriber.Name = CRASHLYTICS, ) : SessionSubscriber { - override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) = Unit + + val sessionChangedEvents = mutableListOf() + + override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) { + sessionChangedEvents.add(sessionDetails) + } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt index bf06e60ab6e..b038b7abbb8 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt @@ -33,6 +33,7 @@ import com.google.firebase.sessions.FirebaseSessions import com.google.firebase.sessions.SessionDatastore import com.google.firebase.sessions.SessionFirelogPublisher import com.google.firebase.sessions.SessionGenerator +import com.google.firebase.sessions.SessionLifecycleServiceBinder import com.google.firebase.sessions.WallClock import com.google.firebase.sessions.settings.SessionsSettings import kotlinx.coroutines.CoroutineDispatcher @@ -63,8 +64,6 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { Component.builder(SessionsSettings::class.java) .name("sessions-settings") .add(Dependency.required(firebaseApp)) - .add(Dependency.required(blockingDispatcher)) - .add(Dependency.required(backgroundDispatcher)) .add(Dependency.required(firebaseInstallationsApi)) .factory { container -> SessionsSettings( @@ -84,6 +83,15 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { .add(Dependency.required(fakeDatastore)) .factory { container -> container.get(fakeDatastore) } .build(), + Component.builder(FakeSessionLifecycleServiceBinder::class.java) + .name("fake-sessions-service-binder") + .factory { FakeSessionLifecycleServiceBinder() } + .build(), + Component.builder(SessionLifecycleServiceBinder::class.java) + .name("sessions-service-binder") + .add(Dependency.required(fakeServiceBinder)) + .factory { container -> container.get(fakeServiceBinder) } + .build(), LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME), ) @@ -99,6 +107,7 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { private val transportFactory = unqualified(TransportFactory::class.java) private val fakeFirelogPublisher = unqualified(FakeFirelogPublisher::class.java) private val fakeDatastore = unqualified(FakeSessionDatastore::class.java) + private val fakeServiceBinder = unqualified(FakeSessionLifecycleServiceBinder::class.java) private val sessionGenerator = unqualified(SessionGenerator::class.java) private val sessionsSettings = unqualified(SessionsSettings::class.java) From 25e89565bef4ef3f6b57f56fb52fc763d30d7b28 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Thu, 26 Oct 2023 12:53:03 -0400 Subject: [PATCH 34/38] Fix tests and network exception in gradle task config (#5476) --- .../com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.kt | 2 +- .../firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.kt index d6f079ba25f..d27bf28322b 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/FirebaseLibraryPlugin.kt @@ -85,7 +85,7 @@ class FirebaseLibraryPlugin : BaseFirebaseLibraryPlugin() { android.testServer(FirebaseTestServer(project, firebaseLibrary.testLab, android)) setupStaticAnalysis(project, firebaseLibrary) getIsPomValidTask(project, firebaseLibrary) - setupVersionCheckTasks(project, firebaseLibrary) + // setupVersionCheckTasks(project, firebaseLibrary) configurePublishing(project, firebaseLibrary, android) } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt index b038b7abbb8..1ae328c5329 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FirebaseSessionsFakeRegistrar.kt @@ -65,6 +65,7 @@ internal class FirebaseSessionsFakeRegistrar : ComponentRegistrar { .name("sessions-settings") .add(Dependency.required(firebaseApp)) .add(Dependency.required(firebaseInstallationsApi)) + .add(Dependency.required(backgroundDispatcher)) .factory { container -> SessionsSettings( container.get(firebaseApp), From 4d935cd54f7d7f3dab0edf2fe2034a4becfcd950 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Fri, 27 Oct 2023 13:48:13 -0400 Subject: [PATCH 35/38] Keep track of pending foreground before settings fetched (#5482) Keep track of pending foreground before settings fetched to catch the first session in cold start with no settings cache. --- .../firebase/sessions/FirebaseSessions.kt | 39 +++--- .../SessionsActivityLifecycleCallbacks.kt | 29 +++- .../SessionsActivityLifecycleCallbacksTest.kt | 129 ++++++++++++++++++ 3 files changed, 173 insertions(+), 24 deletions(-) create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt index 419b5b8b567..55876ac88f3 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt @@ -35,27 +35,30 @@ internal class FirebaseSessions( init { Log.d(TAG, "Initializing Firebase Sessions SDK.") - CoroutineScope(backgroundDispatcher).launch { - val appContext = firebaseApp.applicationContext.applicationContext - settings.updateSettings() - if (!settings.sessionsEnabled) { - Log.d(TAG, "Sessions SDK disabled. Not listening to lifecycle events.") - } else if (appContext is Application) { - val lifecycleClient = SessionLifecycleClient(backgroundDispatcher) - val activityCallbacks = SessionsActivityLifecycleCallbacks(lifecycleClient) - appContext.registerActivityLifecycleCallbacks(activityCallbacks) - lifecycleClient.bindToService() + val appContext = firebaseApp.applicationContext.applicationContext + if (appContext is Application) { + appContext.registerActivityLifecycleCallbacks(SessionsActivityLifecycleCallbacks) - firebaseApp.addLifecycleEventListener { _, _ -> - Log.w(TAG, "FirebaseApp instance deleted. Sessions library will stop collecting data.") - appContext.unregisterActivityLifecycleCallbacks(activityCallbacks) + CoroutineScope(backgroundDispatcher).launch { + settings.updateSettings() + if (!settings.sessionsEnabled) { + Log.d(TAG, "Sessions SDK disabled. Not listening to lifecycle events.") + } else { + val lifecycleClient = SessionLifecycleClient(backgroundDispatcher) + lifecycleClient.bindToService() + SessionsActivityLifecycleCallbacks.lifecycleClient = lifecycleClient + + firebaseApp.addLifecycleEventListener { _, _ -> + Log.w(TAG, "FirebaseApp instance deleted. Sessions library will stop collecting data.") + SessionsActivityLifecycleCallbacks.lifecycleClient = null + } } - } else { - Log.e( - TAG, - "Failed to register lifecycle callbacks, unexpected context ${appContext.javaClass}." - ) } + } else { + Log.e( + TAG, + "Failed to register lifecycle callbacks, unexpected context ${appContext.javaClass}." + ) } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt index 7f9ecd383a4..b72c1da5cf3 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacks.kt @@ -19,17 +19,34 @@ package com.google.firebase.sessions import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks import android.os.Bundle +import androidx.annotation.VisibleForTesting /** * Lifecycle callbacks that will inform the [SessionLifecycleClient] whenever an [Activity] in this * application process goes foreground or background. */ -internal class SessionsActivityLifecycleCallbacks(private val client: SessionLifecycleClient) : - ActivityLifecycleCallbacks { - - override fun onActivityResumed(activity: Activity) = client.foregrounded() - - override fun onActivityPaused(activity: Activity) = client.backgrounded() +internal object SessionsActivityLifecycleCallbacks : ActivityLifecycleCallbacks { + @VisibleForTesting internal var hasPendingForeground: Boolean = false + + var lifecycleClient: SessionLifecycleClient? = null + /** Sets the client and calls [SessionLifecycleClient.foregrounded] for pending foreground. */ + set(lifecycleClient) { + field = lifecycleClient + lifecycleClient?.let { + if (hasPendingForeground) { + hasPendingForeground = false + it.foregrounded() + } + } + } + + override fun onActivityResumed(activity: Activity) { + lifecycleClient?.foregrounded() ?: run { hasPendingForeground = true } + } + + override fun onActivityPaused(activity: Activity) { + lifecycleClient?.backgrounded() + } override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt new file mode 100644 index 00000000000..3f9ac95b21c --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionsActivityLifecycleCallbacksTest.kt @@ -0,0 +1,129 @@ +/* + * 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.sessions + +import android.app.Activity +import android.os.Looper +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import com.google.firebase.Firebase +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.google.firebase.concurrent.TestOnlyExecutors +import com.google.firebase.initialize +import com.google.firebase.sessions.api.FirebaseSessionsDependencies +import com.google.firebase.sessions.api.SessionSubscriber +import com.google.firebase.sessions.testing.FakeFirebaseApp +import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder +import com.google.firebase.sessions.testing.FakeSessionSubscriber +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Shadows + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +internal class SessionsActivityLifecycleCallbacksTest { + private lateinit var fakeService: FakeSessionLifecycleServiceBinder + private val fakeActivity = Activity() + + @Before + fun setUp() { + // Reset the state of the SessionsActivityLifecycleCallbacks object. + SessionsActivityLifecycleCallbacks.hasPendingForeground = false + SessionsActivityLifecycleCallbacks.lifecycleClient = null + + FirebaseSessionsDependencies.addDependency(SessionSubscriber.Name.MATT_SAYS_HI) + FirebaseSessionsDependencies.register( + FakeSessionSubscriber( + isDataCollectionEnabled = true, + sessionSubscriberName = SessionSubscriber.Name.MATT_SAYS_HI, + ) + ) + + val firebaseApp = + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(FakeFirebaseApp.MOCK_APP_ID) + .setApiKey(FakeFirebaseApp.MOCK_API_KEY) + .setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID) + .build() + ) + fakeService = firebaseApp.get(FakeSessionLifecycleServiceBinder::class.java) + } + + @After + fun cleanUp() { + fakeService.serviceDisconnected() + FirebaseApp.clearInstancesForTest() + fakeService.clearForTest() + FirebaseSessionsDependencies.reset() + } + + @Test + fun hasPendingForeground_thenSetLifecycleClient_callsBackgrounded() = + runTest(UnconfinedTestDispatcher()) { + val lifecycleClient = SessionLifecycleClient(backgroundDispatcher(coroutineContext)) + + // Activity comes to foreground before the lifecycle client was set due to no settings. + SessionsActivityLifecycleCallbacks.onActivityResumed(fakeActivity) + + // Settings fetched and set the lifecycle client. + lifecycleClient.bindToService() + fakeService.serviceConnected() + SessionsActivityLifecycleCallbacks.lifecycleClient = lifecycleClient + + // Assert lifecycleClient.foregrounded got called. + waitForMessages() + assertThat(fakeService.receivedMessageCodes).hasSize(1) + } + + @Test + fun noPendingForeground_thenSetLifecycleClient_doesNotCallBackgrounded() = + runTest(UnconfinedTestDispatcher()) { + val lifecycleClient = SessionLifecycleClient(backgroundDispatcher(coroutineContext)) + + // Set lifecycle client before any foreground happened. + lifecycleClient.bindToService() + fakeService.serviceConnected() + SessionsActivityLifecycleCallbacks.lifecycleClient = lifecycleClient + + // Assert lifecycleClient.foregrounded did not get called. + waitForMessages() + assertThat(fakeService.receivedMessageCodes).hasSize(0) + + // Activity comes to foreground. + SessionsActivityLifecycleCallbacks.onActivityResumed(fakeActivity) + + // Assert lifecycleClient.foregrounded did get called. + waitForMessages() + assertThat(fakeService.receivedMessageCodes).hasSize(1) + } + + private fun waitForMessages() = Shadows.shadowOf(Looper.getMainLooper()).idle() + + private fun backgroundDispatcher(coroutineContext: CoroutineContext) = + TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext +} From b78b7ea25d766ce4640b65224f67a85a6a96a5ba Mon Sep 17 00:00:00 2001 From: Visu Date: Fri, 27 Oct 2023 11:14:43 -0700 Subject: [PATCH 36/38] Remove AQS based session ID dependency from Fireperf. (#5454) --- firebase-perf/CHANGELOG.md | 10 +- .../firebase/perf/FirebasePerfEarly.java | 30 +---- .../firebase/perf/FirebasePerfRegistrar.java | 8 -- .../firebase/perf/session/SessionManager.java | 40 +++++- .../perf/FirebasePerfRegistrarTest.java | 2 - .../perf/session/SessionManagerTest.java | 122 ++++++++++++++++++ 6 files changed, 162 insertions(+), 50 deletions(-) diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index 841db1ee053..6ed2fb1827a 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,23 +1,18 @@ # Unreleased - +* [changed] Make Fireperf generate its own session Id. # 20.5.0 * [changed] Added Kotlin extensions (KTX) APIs from `com.google.firebase:firebase-perf-ktx` to `com.google.firebase:firebase-perf` under the `com.google.firebase.perf` package. For details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) + * [deprecated] All the APIs from `com.google.firebase:firebase-perf-ktx` have been added to `com.google.firebase:firebase-perf` under the `com.google.firebase.perf` package, and all the Kotlin extensions (KTX) APIs in `com.google.firebase:firebase-perf-ktx` are now deprecated. As early as April 2024, we'll no longer release KTX modules. For details, see the [FAQ about this initiative](https://firebase.google.com/docs/android/kotlin-migration) - -## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-performance` library. The Kotlin extensions library has no additional -updates. - # 20.4.1 * [changed] Updated `firebase-sessions` dependency to v1.0.2 * [fixed] Make fireperf data collection state is reliable for Firebase Sessions library. @@ -369,4 +364,3 @@ updates. # 16.1.0 * [fixed] Fixed a `SecurityException` crash on certain devices that do not have Google Play Services on them. - diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java index 5e5d05ee7a4..5b89deaad82 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java @@ -15,17 +15,13 @@ package com.google.firebase.perf; import android.content.Context; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.firebase.FirebaseApp; import com.google.firebase.StartupTime; import com.google.firebase.perf.application.AppStateMonitor; import com.google.firebase.perf.config.ConfigResolver; import com.google.firebase.perf.metrics.AppStartTrace; -import com.google.firebase.perf.session.PerfSession; import com.google.firebase.perf.session.SessionManager; -import com.google.firebase.sessions.api.FirebaseSessionsDependencies; -import com.google.firebase.sessions.api.SessionSubscriber; import java.util.concurrent.Executor; /** @@ -55,31 +51,7 @@ public FirebasePerfEarly( uiExecutor.execute(new AppStartTrace.StartFromBackgroundRunnable(appStartTrace)); } - // Register with Firebase sessions to receive updates about session changes. - FirebaseSessionsDependencies.register( - new SessionSubscriber() { - @Override - public void onSessionChanged(@NonNull SessionDetails sessionDetails) { - PerfSession perfSession = PerfSession.createWithId(sessionDetails.getSessionId()); - SessionManager.getInstance().updatePerfSession(perfSession); - } - - @Override - public boolean isDataCollectionEnabled() { - // If there is no cached config data available for data collection, be conservative. - // Return false. - if (!configResolver.isCollectionEnabledConfigValueAvailable()) { - return false; - } - return ConfigResolver.getInstance().isPerformanceMonitoringEnabled(); - } - - @NonNull - @Override - public Name getSessionSubscriberName() { - return SessionSubscriber.Name.PERFORMANCE; - } - }); + // TODO: Bring back Firebase Sessions dependency to watch for updates to sessions. // In the case of cold start, we create a session and start collecting gauges as early as // possible. diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java index 7e1500447a3..c01f035af1f 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java @@ -30,9 +30,6 @@ import com.google.firebase.perf.injection.modules.FirebasePerformanceModule; import com.google.firebase.platforminfo.LibraryVersionComponent; import com.google.firebase.remoteconfig.RemoteConfigComponent; -import com.google.firebase.sessions.FirebaseSessions; -import com.google.firebase.sessions.api.FirebaseSessionsDependencies; -import com.google.firebase.sessions.api.SessionSubscriber; import java.util.Arrays; import java.util.List; import java.util.concurrent.Executor; @@ -50,10 +47,6 @@ public class FirebasePerfRegistrar implements ComponentRegistrar { private static final String LIBRARY_NAME = "fire-perf"; private static final String EARLY_LIBRARY_NAME = "fire-perf-early"; - static { - FirebaseSessionsDependencies.INSTANCE.addDependency(SessionSubscriber.Name.PERFORMANCE); - } - @Override @Keep public List> getComponents() { @@ -71,7 +64,6 @@ public List> getComponents() { Component.builder(FirebasePerfEarly.class) .name(EARLY_LIBRARY_NAME) .add(Dependency.required(FirebaseApp.class)) - .add(Dependency.required(FirebaseSessions.class)) .add(Dependency.optionalProvider(StartupTime.class)) .add(Dependency.required(uiExecutor)) .eagerInDefaultApp() diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java index 73c505a8b47..c7eb4ca53f8 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java @@ -19,6 +19,7 @@ import androidx.annotation.Keep; import com.google.android.gms.common.util.VisibleForTesting; import com.google.firebase.perf.application.AppStateMonitor; +import com.google.firebase.perf.application.AppStateUpdateHandler; import com.google.firebase.perf.session.gauges.GaugeManager; import com.google.firebase.perf.v1.ApplicationProcessState; import com.google.firebase.perf.v1.GaugeMetadata; @@ -27,13 +28,14 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** Session manager to generate sessionIDs and broadcast to the application. */ @Keep // Needed because of b/117526359. -public class SessionManager { +public class SessionManager extends AppStateUpdateHandler { @SuppressLint("StaticFieldLeak") private static final SessionManager instance = new SessionManager(); @@ -56,8 +58,11 @@ public final PerfSession perfSession() { } private SessionManager() { - // Start with an empty session ID as the firebase sessions will override with real Id. - this(GaugeManager.getInstance(), PerfSession.createWithId(""), AppStateMonitor.getInstance()); + // Generate a new sessionID for every cold start. + this( + GaugeManager.getInstance(), + PerfSession.createWithId(UUID.randomUUID().toString()), + AppStateMonitor.getInstance()); } @VisibleForTesting @@ -66,6 +71,7 @@ public SessionManager( this.gaugeManager = gaugeManager; this.perfSession = perfSession; this.appStateMonitor = appStateMonitor; + registerForAppState(); } /** @@ -90,6 +96,34 @@ public void setApplicationContext(final Context appContext) { }); } + @Override + public void onUpdateAppState(ApplicationProcessState newAppState) { + super.onUpdateAppState(newAppState); + + if (appStateMonitor.isColdStart()) { + // We want the Session to remain unchanged if this is a cold start of the app since we already + // update the PerfSession in FirebasePerfProvider#onAttachInfo(). + return; + } + + if (newAppState == ApplicationProcessState.FOREGROUND) { + // A new foregrounding of app will force a new sessionID generation. + PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString()); + updatePerfSession(session); + } else { + // If the session is running for too long, generate a new session and collect gauges as + // necessary. + if (perfSession.isSessionRunningTooLong()) { + PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString()); + updatePerfSession(session); + } else { + // For any other state change of the application, modify gauge collection state as + // necessary. + startOrStopCollectingGauges(newAppState); + } + } + } + /** * Checks if the current {@link PerfSession} is expired/timed out. If so, stop collecting gauges. * diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerfRegistrarTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerfRegistrarTest.java index 524949cd124..7df39fe6a1e 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerfRegistrarTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerfRegistrarTest.java @@ -25,7 +25,6 @@ import com.google.firebase.components.Qualified; import com.google.firebase.installations.FirebaseInstallationsApi; import com.google.firebase.remoteconfig.RemoteConfigComponent; -import com.google.firebase.sessions.FirebaseSessions; import java.util.List; import java.util.concurrent.Executor; import org.junit.Test; @@ -60,7 +59,6 @@ public void testGetComponents() { .containsExactly( Dependency.required(Qualified.qualified(UiThread.class, Executor.class)), Dependency.required(FirebaseApp.class), - Dependency.required(FirebaseSessions.class), Dependency.optionalProvider(StartupTime.class)); assertThat(firebasePerfEarlyComponent.isLazy()).isFalse(); diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java index f55b89dd001..f3e3795f3f8 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java @@ -16,7 +16,9 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -31,12 +33,14 @@ import com.google.firebase.perf.session.gauges.GaugeManager; import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Timer; +import com.google.firebase.perf.v1.ApplicationProcessState; import java.lang.ref.WeakReference; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.AdditionalMatchers; import org.mockito.ArgumentMatchers; import org.mockito.InOrder; import org.mockito.Mock; @@ -83,6 +87,124 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn inOrder.verify(mockGaugeManager).logGaugeMetadata(any(), any()); } + @Test + public void testOnUpdateAppStateDoesNothingDuringAppStart() { + String oldSessionId = SessionManager.getInstance().perfSession().sessionId(); + + assertThat(oldSessionId).isNotNull(); + assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); + + AppStateMonitor.getInstance().setIsColdStart(true); + + SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); + assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); + } + + @Test + public void testOnUpdateAppStateGeneratesNewSessionIdOnForegroundState() { + String oldSessionId = SessionManager.getInstance().perfSession().sessionId(); + + assertThat(oldSessionId).isNotNull(); + assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); + + SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); + assertThat(oldSessionId).isNotEqualTo(SessionManager.getInstance().perfSession().sessionId()); + } + + @Test + public void testOnUpdateAppStateDoesntGenerateNewSessionIdOnBackgroundState() { + String oldSessionId = SessionManager.getInstance().perfSession().sessionId(); + + assertThat(oldSessionId).isNotNull(); + assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); + + SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND); + assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); + } + + @Test + public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSessionExpires() { + when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true); + SessionManager testSessionManager = + new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + String oldSessionId = testSessionManager.perfSession().sessionId(); + + assertThat(oldSessionId).isNotNull(); + assertThat(oldSessionId).isEqualTo(testSessionManager.perfSession().sessionId()); + + testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); + assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId()); + } + + @Test + public void + testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsVerbose() { + forceVerboseSession(); + + SessionManager testSessionManager = + new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); + + verify(mockGaugeManager) + .logGaugeMetadata( + anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class)); + } + + @Test + public void + testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsNonVerbose() { + forceNonVerboseSession(); + + SessionManager testSessionManager = + new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); + + verify(mockGaugeManager, never()) + .logGaugeMetadata( + anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class)); + } + + @Test + public void + testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnBackgroundStateEvenIfSessionIsVerbose() { + forceVerboseSession(); + + SessionManager testSessionManager = + new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); + + verify(mockGaugeManager, never()) + .logGaugeMetadata( + anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class)); + } + + @Test + public void + testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnBackgroundAppStateIfSessionIsVerboseAndTimedOut() { + when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true); + forceVerboseSession(); + + SessionManager testSessionManager = + new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); + + verify(mockGaugeManager) + .logGaugeMetadata( + anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class)); + } + + @Test + public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionIsVerbose() { + forceVerboseSession(); + + SessionManager testSessionManager = + new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); + + verify(mockGaugeManager) + .startCollectingGauges(AdditionalMatchers.not(eq(mockPerfSession)), any()); + } + // LogGaugeData on new perf session when Verbose // NotLogGaugeData on new perf session when not Verbose // Mark Session as expired after time limit. From d9bdc42d59a22416eff643e8c1babda61cffd39b Mon Sep 17 00:00:00 2001 From: Bryan Atkinson Date: Mon, 30 Oct 2023 09:22:53 -0400 Subject: [PATCH 37/38] =?UTF-8?q?Updates=20test=20app=20to=20have=20more?= =?UTF-8?q?=20functionality=20so=20we=20can=20verify=20multiple=E2=80=A6?= =?UTF-8?q?=20(#5481)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … processes and crash behaviour with the SDK. --- .../testing/sessions/FirebaseSessionsTest.kt | 15 ++- .../test-app/src/main/AndroidManifest.xml | 94 ++++++++++---- .../firebase/testing/sessions/BaseActivity.kt | 72 +++++++++++ .../sessions/CrashBroadcastReceiver.kt | 49 ++++++++ .../testing/sessions/CrashWidgetProvider.kt | 78 ++++++++++++ .../testing/sessions/FirstFragment.kt | 103 +++++++++++++++ .../testing/sessions/ForegroundService.kt | 118 ++++++++++++++++++ .../firebase/testing/sessions/MainActivity.kt | 12 +- .../testing/sessions/SecondActivity.kt | 48 +++++++ .../testing/sessions/TestApplication.kt | 32 +++++ .../src/main/res/layout/activity_main.xml | 25 ++-- .../src/main/res/layout/activity_second.xml | 39 ++++++ .../src/main/res/layout/content_main.xml | 34 +++++ .../src/main/res/layout/crash_widget.xml | 39 ++++++ .../main/res/layout/crash_widget_preview.xml | 31 +++++ .../src/main/res/layout/fragment_first.xml | 76 +++++++++++ .../test-app/src/main/res/menu/menu_main.xml | 26 ++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 19 +++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 19 +++ .../res/mipmap-anydpi-v33/ic_launcher.xml | 20 +++ .../src/main/res/navigation/nav_graph.xml | 28 +++++ .../src/main/res/values-night/themes.xml | 30 ++--- .../test-app/src/main/res/values/strings.xml | 18 ++- .../test-app/src/main/res/values/themes.xml | 42 +++---- .../src/main/res/xml/homescreen_widget.xml | 31 +++++ .../test-app/test-app.gradle.kts | 8 +- 26 files changed, 1021 insertions(+), 85 deletions(-) create mode 100644 firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt create mode 100644 firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashBroadcastReceiver.kt create mode 100644 firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashWidgetProvider.kt create mode 100644 firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt create mode 100644 firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/ForegroundService.kt create mode 100644 firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt create mode 100644 firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/TestApplication.kt create mode 100644 firebase-sessions/test-app/src/main/res/layout/activity_second.xml create mode 100644 firebase-sessions/test-app/src/main/res/layout/content_main.xml create mode 100644 firebase-sessions/test-app/src/main/res/layout/crash_widget.xml create mode 100644 firebase-sessions/test-app/src/main/res/layout/crash_widget_preview.xml create mode 100644 firebase-sessions/test-app/src/main/res/layout/fragment_first.xml create mode 100644 firebase-sessions/test-app/src/main/res/menu/menu_main.xml create mode 100644 firebase-sessions/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 firebase-sessions/test-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 firebase-sessions/test-app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml create mode 100644 firebase-sessions/test-app/src/main/res/navigation/nav_graph.xml create mode 100644 firebase-sessions/test-app/src/main/res/xml/homescreen_widget.xml diff --git a/firebase-sessions/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsTest.kt b/firebase-sessions/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsTest.kt index 9631b9776b4..192084aad6e 100644 --- a/firebase-sessions/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsTest.kt +++ b/firebase-sessions/test-app/src/androidTest/kotlin/com/google/firebase/testing/sessions/FirebaseSessionsTest.kt @@ -16,6 +16,7 @@ package com.google.firebase.testing.sessions +import androidx.lifecycle.Lifecycle.State import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -49,10 +50,19 @@ class FirebaseSessionsTest { FirebaseSessionsDependencies.register(fakeSessionSubscriber) ActivityScenario.launch(MainActivity::class.java).use { scenario -> + scenario.onActivity { + // Wait for the settings to be fetched from the server. + Thread.sleep(TIME_TO_READ_SETTINGS) + } + // Move the activity to the background and then foreground + // This is necessary because the initial app launch does not yet know whether the sdk is + // enabled, and so the first session isnt' created until a lifecycle event happens after the + // settings are read. + scenario.moveToState(State.CREATED) + scenario.moveToState(State.RESUMED) scenario.onActivity { // Wait for the session start event to send. Thread.sleep(TIME_TO_LOG_SESSION) - // Assert that some session was generated and sent to the subscriber. assertThat(fakeSessionSubscriber.sessionDetails).isNotNull() } @@ -60,7 +70,8 @@ class FirebaseSessionsTest { } companion object { - private const val TIME_TO_LOG_SESSION = 60_000L + private const val TIME_TO_READ_SETTINGS = 60_000L + private const val TIME_TO_LOG_SESSION = 10_000L init { FirebaseSessionsDependencies.addDependency(SessionSubscriber.Name.MATT_SAYS_HI) diff --git a/firebase-sessions/test-app/src/main/AndroidManifest.xml b/firebase-sessions/test-app/src/main/AndroidManifest.xml index 2cda3e2bc0d..a07d1b913d7 100644 --- a/firebase-sessions/test-app/src/main/AndroidManifest.xml +++ b/firebase-sessions/test-app/src/main/AndroidManifest.xml @@ -1,23 +1,75 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt new file mode 100644 index 00000000000..30ded36512f --- /dev/null +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/BaseActivity.kt @@ -0,0 +1,72 @@ +/* + * 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.testing.sessions + +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo +import android.app.Application +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import com.google.firebase.FirebaseApp + +/** */ +open class BaseActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + FirebaseApp.initializeApp(this) + Log.i(TAG, "onCreate - ${getProcessName()} - ${getImportance()}") + } + + override fun onPause() { + super.onPause() + Log.i(TAG, "onPause - ${getProcessName()} - ${getImportance()}") + } + + override fun onStop() { + super.onStop() + Log.i(TAG, "onStop - ${getProcessName()} - ${getImportance()}") + } + + override fun onResume() { + super.onResume() + Log.i(TAG, "onResume - ${getProcessName()} - ${getImportance()}") + } + + override fun onStart() { + super.onStart() + Log.i(TAG, "onStart - ${getProcessName()} - ${getImportance()}") + } + + override fun onDestroy() { + super.onDestroy() + Log.i(TAG, "onDestroy - ${getProcessName()} - ${getImportance()}") + } + + private fun getImportance(): Int { + val processInfo = RunningAppProcessInfo() + ActivityManager.getMyMemoryState(processInfo) + return processInfo.importance + } + + private fun getProcessName(): String = Application.getProcessName() + + companion object { + val TAG = "BaseActivity" + } +} diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashBroadcastReceiver.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashBroadcastReceiver.kt new file mode 100644 index 00000000000..89d2f03f1ce --- /dev/null +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashBroadcastReceiver.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.testing.sessions + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import android.widget.Toast + +class CrashBroadcastReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + Log.i(TAG, "Received intent: $intent") + when (intent.action) { + CRASH_ACTION -> crash(context) + TOAST_ACTION -> toast(context) + } + } + + fun crash(context: Context) { + Toast.makeText(context, "KABOOM!", Toast.LENGTH_LONG).show() + throw RuntimeException("CRASH_BROADCAST") + } + + fun toast(context: Context) { + Toast.makeText(context, "Cheers!", Toast.LENGTH_LONG).show() + } + + companion object { + val TAG = "CrashBroadcastReceiver" + val CRASH_ACTION = "com.google.firebase.testing.sessions.CrashBroadcastReceiver.CRASH_ACTION" + val TOAST_ACTION = "com.google.firebase.testing.sessions.CrashBroadcastReceiver.TOAST_ACTION" + } +} diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashWidgetProvider.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashWidgetProvider.kt new file mode 100644 index 00000000000..0661c9e5164 --- /dev/null +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/CrashWidgetProvider.kt @@ -0,0 +1,78 @@ +/* + * 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.testing.sessions + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.icu.text.SimpleDateFormat +import android.widget.RemoteViews +import com.google.firebase.FirebaseApp +import java.util.Date +import java.util.Locale + +/** Provides homescreen widget for the test app. */ +class CrashWidgetProvider : AppWidgetProvider() { + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + FirebaseApp.initializeApp(context) + + appWidgetIds.forEach { appWidgetId -> + // Get the layout for the widget and attach an on-click listener + // to the button. + val views: RemoteViews = + RemoteViews(context.packageName, R.layout.crash_widget).apply { + setOnClickPendingIntent(R.id.widgetCrashButton, getPendingCrashIntent(context)) + setTextViewText(R.id.widgetTimeText, DATE_FMT.format(Date())) + } + + // Tell the AppWidgetManager to perform an update on the current + // widget. + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } + + override fun onReceive(context: Context, intent: Intent): Unit { + super.onReceive(context, intent) + + if (CRASH_BUTTON_CLICK == intent.getAction()) { + throw RuntimeException("CRASHED FROM WIDGET") + } + } + + fun getPendingCrashIntent(context: Context): PendingIntent { + val intent = Intent(context, CrashWidgetProvider::class.java) + intent.setAction(CRASH_BUTTON_CLICK) + return PendingIntent.getBroadcast( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + companion object { + val CRASH_BUTTON_CLICK = "widgetCrashButtonClick" + val DATE_FMT = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + } +} diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt new file mode 100644 index 00000000000..a190a39cd2b --- /dev/null +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/FirstFragment.kt @@ -0,0 +1,103 @@ +/* + * 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.testing.sessions + +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.icu.text.SimpleDateFormat +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.testing.sessions.databinding.FragmentFirstBinding +import java.util.Date +import java.util.Locale + +/** A simple [Fragment] subclass as the default destination in the navigation. */ +class FirstFragment : Fragment() { + val crashlytics = FirebaseCrashlytics.getInstance() + + private var _binding: FragmentFirstBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding + get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + _binding = FragmentFirstBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buttonCrash.setOnClickListener { throw RuntimeException("CRASHED") } + binding.buttonNonFatal.setOnClickListener { + crashlytics.recordException(IllegalStateException()) + } + binding.buttonAnr.setOnClickListener { + while (true) { + Thread.sleep(10_000) + } + } + binding.buttonForegroundProcess.setOnClickListener { + if (binding.buttonForegroundProcess.getText().startsWith("Start")) { + ForegroundService.startService( + getContext()!!, + "Starting service at ${DATE_FMT.format(Date())}" + ) + binding.buttonForegroundProcess.setText("Stop foreground service") + } else { + ForegroundService.stopService(getContext()!!) + binding.buttonForegroundProcess.setText("Start foreground service") + } + } + binding.startSplitscreen.setOnClickListener { + val intent = Intent(getContext()!!, SecondActivity::class.java) + intent.addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_LAUNCH_ADJACENT) + startActivity(intent) + } + binding.startSplitscreenSame.setOnClickListener { + val intent = Intent(getContext()!!, MainActivity::class.java) + intent.addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_LAUNCH_ADJACENT) + startActivity(intent) + } + binding.nextActivityButton.setOnClickListener { + val intent = Intent(getContext()!!, SecondActivity::class.java) + intent.addFlags(FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + val DATE_FMT = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + } +} diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/ForegroundService.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/ForegroundService.kt new file mode 100644 index 00000000000..21f99e70d91 --- /dev/null +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/ForegroundService.kt @@ -0,0 +1,118 @@ +/* + * 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.testing.sessions + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import com.google.firebase.FirebaseApp + +/** */ +class ForegroundService : Service() { + private val CHANNEL_ID = "CrashForegroundService" + val receiver = CrashBroadcastReceiver() + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.i(TAG, "Initializing app From ForegroundSErvice") + FirebaseApp.initializeApp(this) + createNotificationChannel() + val pending = + PendingIntent.getActivity( + this, + 0, + Intent(this, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + val crashIntent = Intent(CrashBroadcastReceiver.CRASH_ACTION) + val toastIntent = Intent(CrashBroadcastReceiver.TOAST_ACTION) + + val pendingCrash = + PendingIntent.getBroadcast(this, 0, crashIntent, PendingIntent.FLAG_IMMUTABLE) + val pendingToast = + PendingIntent.getBroadcast(this, 0, toastIntent, PendingIntent.FLAG_IMMUTABLE) + val pendingMsg = + PendingIntent.getActivity( + this, + 0, + Intent(this, SecondActivity::class.java).setAction("MESSAGE"), + PendingIntent.FLAG_IMMUTABLE + ) + + val notification = + NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Crash Test Notification Widget") + .setContentText(intent?.getStringExtra("inputExtra")) + .setContentIntent(pending) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setTicker("Crash Notification Widget Ticker") + .addAction(R.drawable.ic_launcher_foreground, "CRASH!", pendingCrash) + .addAction(R.drawable.ic_launcher_foreground, "TOAST!", pendingToast) + .addAction(R.drawable.ic_launcher_foreground, "Send Message", pendingMsg) + .build() + + startForeground(1, notification) + return START_STICKY + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onDestroy() { + super.onDestroy() + Log.i(TAG, "OnDestroy for ForegroundService") + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val serviceChannel = + NotificationChannel( + CHANNEL_ID, + "Foreground Service Channel", + NotificationManager.IMPORTANCE_DEFAULT + ) + val manager = getSystemService(NotificationManager::class.java) + manager!!.createNotificationChannel(serviceChannel) + } + } + + companion object { + val TAG = "CrashWidgetForegroundService" + + fun startService(context: Context, message: String) { + Log.i(TAG, "Starting foreground serice") + ContextCompat.startForegroundService( + context, + Intent(context, ForegroundService::class.java).putExtra("inputExtra", message) + ) + } + + fun stopService(context: Context) { + Log.i(TAG, "Stopping serice") + context.stopService(Intent(context, ForegroundService::class.java)) + } + } +} diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/MainActivity.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/MainActivity.kt index 9db0be0ef7c..ac41d11d73e 100644 --- a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/MainActivity.kt +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/MainActivity.kt @@ -17,14 +17,16 @@ package com.google.firebase.testing.sessions import android.os.Bundle -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity +import com.google.firebase.testing.sessions.databinding.ActivityMainBinding + +class MainActivity : BaseActivity() { + + private lateinit var binding: ActivityMainBinding -class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - findViewById(R.id.greeting_text).text = getText(R.string.firebase_greetings) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) } } diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt new file mode 100644 index 00000000000..aff1bbacd91 --- /dev/null +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/SecondActivity.kt @@ -0,0 +1,48 @@ +/* + * 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.testing.sessions + +import android.app.ActivityManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.content.ServiceConnection +import android.os.Bundle +import android.os.IBinder +import android.os.Message +import android.os.Messenger +import android.os.RemoteException +import android.util.Log +import android.widget.Button + +/** Second activity from the MainActivity that runs on a different process. */ +class SecondActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_second) + findViewById