Skip to content

Commit 01b1a4b

Browse files
authored
Merge 8e23f15 into d4a30e1
2 parents d4a30e1 + 8e23f15 commit 01b1a4b

File tree

4 files changed

+281
-0
lines changed

4 files changed

+281
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.sessions.api
18+
19+
import androidx.annotation.VisibleForTesting
20+
import java.util.Collections.synchronizedMap
21+
import kotlinx.coroutines.sync.Mutex
22+
import kotlinx.coroutines.sync.withLock
23+
24+
/**
25+
* [FirebaseSessionsDependencies] determines when a dependent SDK is installed in the app. The
26+
* Sessions SDK uses this to figure out which dependencies to wait for to getting the data
27+
* collection state. This is thread safe.
28+
*
29+
* This is important because the Sessions SDK starts up before dependent SDKs.
30+
*/
31+
object FirebaseSessionsDependencies {
32+
private val dependencies = synchronizedMap(mutableMapOf<SessionSubscriber.Name, Dependency>())
33+
34+
/**
35+
* Add a subscriber as a dependency to the Sessions SDK. Every dependency must register itself, or
36+
* the Sessions SDK will never generate a session.
37+
*/
38+
fun addDependency(subscriberName: SessionSubscriber.Name) {
39+
if (dependencies.containsKey(subscriberName)) {
40+
throw IllegalArgumentException("Dependency $subscriberName already added.")
41+
}
42+
43+
// The dependency is locked until the subscriber registers itself.
44+
dependencies[subscriberName] = Dependency(Mutex(locked = true))
45+
}
46+
47+
/** Register and unlock the subscriber. This must be called before [getSubscribers] can return. */
48+
internal fun register(subscriber: SessionSubscriber) {
49+
val subscriberName = subscriber.sessionSubscriberName
50+
val dependency = getDependency(subscriberName)
51+
52+
dependency.subscriber?.run {
53+
throw IllegalArgumentException("Subscriber $subscriberName already registered.")
54+
}
55+
dependency.subscriber = subscriber
56+
57+
// Unlock to show the subscriber has been registered, it is possible to get it now.
58+
dependency.mutex.unlock()
59+
}
60+
61+
/** Gets the subscribers safely, blocks until all the subscribers are registered. */
62+
internal suspend fun getSubscribers(): Map<SessionSubscriber.Name, SessionSubscriber> {
63+
// The call to get will never throw because the mutex guarantees it's been registered.
64+
return dependencies.mapValues { (subscriberName, dependency) ->
65+
dependency.mutex.withLock { get(subscriberName) }
66+
}
67+
}
68+
69+
/** Gets the subscriber regardless of being registered. This is exposed for testing. */
70+
@VisibleForTesting
71+
internal operator fun get(subscriberName: SessionSubscriber.Name): SessionSubscriber {
72+
return getDependency(subscriberName).subscriber
73+
?: throw IllegalStateException("Subscriber $subscriberName has not been registered.")
74+
}
75+
76+
/** Resets the registered subscribers for testing purposes. */
77+
@VisibleForTesting
78+
internal fun reset() {
79+
dependencies.keys.forEach { dependencies[it] = Dependency(Mutex(locked = true)) }
80+
}
81+
82+
private fun getDependency(subscriberName: SessionSubscriber.Name): Dependency {
83+
return dependencies.getOrElse(subscriberName) {
84+
throw IllegalStateException(
85+
"Cannot get dependency $subscriberName. Dependencies should be added at class load time."
86+
)
87+
}
88+
}
89+
90+
private data class Dependency(val mutex: Mutex, var subscriber: SessionSubscriber? = null)
91+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.sessions.api
18+
19+
/** [SessionSubscriber] is an interface that dependent SDKs must implement. */
20+
interface SessionSubscriber {
21+
/** [SessionSubscriber.Name]s are used for identifying subscribers. */
22+
enum class Name {
23+
CRASHLYTICS,
24+
PERFORMANCE,
25+
}
26+
27+
/** [SessionDetails] contains session data passed to subscribers whenever the session changes */
28+
data class SessionDetails(val sessionId: String)
29+
30+
fun onSessionChanged(sessionDetails: SessionDetails)
31+
32+
val isDataCollectionEnabled: Boolean
33+
34+
val sessionSubscriberName: Name
35+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.sessions.api
18+
19+
import androidx.test.ext.junit.runners.AndroidJUnit4
20+
import com.google.common.truth.Truth.assertThat
21+
import com.google.firebase.sessions.api.SessionSubscriber.Name.CRASHLYTICS
22+
import com.google.firebase.sessions.api.SessionSubscriber.Name.PERFORMANCE
23+
import com.google.firebase.sessions.testing.FakeSessionSubscriber
24+
import kotlin.time.Duration.Companion.seconds
25+
import kotlinx.coroutines.ExperimentalCoroutinesApi
26+
import kotlinx.coroutines.TimeoutCancellationException
27+
import kotlinx.coroutines.delay
28+
import kotlinx.coroutines.launch
29+
import kotlinx.coroutines.runBlocking
30+
import kotlinx.coroutines.test.runTest
31+
import kotlinx.coroutines.withTimeout
32+
import org.junit.After
33+
import org.junit.Assert.assertThrows
34+
import org.junit.Test
35+
import org.junit.runner.RunWith
36+
37+
@OptIn(ExperimentalCoroutinesApi::class)
38+
@RunWith(AndroidJUnit4::class)
39+
class FirebaseSessionsDependenciesTest {
40+
@After
41+
fun cleanUp() {
42+
// Reset any registered subscribers after each test.
43+
FirebaseSessionsDependencies.reset()
44+
}
45+
46+
@Test
47+
fun register_dependencyAdded_canGet() {
48+
FirebaseSessionsDependencies.register(crashlyticsSubscriber)
49+
50+
assertThat(FirebaseSessionsDependencies[CRASHLYTICS]).isEqualTo(crashlyticsSubscriber)
51+
}
52+
53+
@Test
54+
fun register_alreadyRegistered_throws() {
55+
// Register the first time, no problem.
56+
FirebaseSessionsDependencies.register(crashlyticsSubscriber)
57+
58+
val thrown =
59+
assertThrows(IllegalArgumentException::class.java) {
60+
// Attempt to register the same subscriber a second time.
61+
FirebaseSessionsDependencies.register(crashlyticsSubscriber)
62+
}
63+
64+
assertThat(thrown).hasMessageThat().contains("Subscriber CRASHLYTICS already registered")
65+
}
66+
67+
@Test
68+
fun getSubscriber_dependencyAdded_notRegistered_throws() {
69+
val thrown =
70+
assertThrows(IllegalStateException::class.java) { FirebaseSessionsDependencies[CRASHLYTICS] }
71+
72+
assertThat(thrown).hasMessageThat().contains("Subscriber CRASHLYTICS has not been registered")
73+
}
74+
75+
@Test
76+
fun getSubscriber_notDepended_throws() {
77+
val thrown =
78+
assertThrows(IllegalStateException::class.java) {
79+
// Performance was never added as a dependency.
80+
FirebaseSessionsDependencies[PERFORMANCE]
81+
}
82+
83+
assertThat(thrown).hasMessageThat().contains("Cannot get dependency PERFORMANCE")
84+
}
85+
86+
@Test
87+
fun addDependencyTwice_throws() {
88+
val thrown =
89+
assertThrows(IllegalArgumentException::class.java) {
90+
// CRASHLYTICS has already been added. Attempt to add it again.
91+
FirebaseSessionsDependencies.addDependency(CRASHLYTICS)
92+
}
93+
94+
assertThat(thrown).hasMessageThat().contains("Dependency CRASHLYTICS already added")
95+
}
96+
97+
@Test
98+
fun getSubscribers_waitsForRegister(): Unit = runBlocking {
99+
// Wait a few seconds and then register.
100+
launch {
101+
delay(2.seconds)
102+
FirebaseSessionsDependencies.register(crashlyticsSubscriber)
103+
}
104+
105+
// Block until the register happens.
106+
val subscribers = runBlocking { FirebaseSessionsDependencies.getSubscribers() }
107+
108+
assertThat(subscribers).containsExactly(CRASHLYTICS, crashlyticsSubscriber)
109+
}
110+
111+
@Test(expected = TimeoutCancellationException::class)
112+
fun getSubscribers_neverRegister_waitsForever() = runTest {
113+
// The register never happens, wait until the timeout.
114+
withTimeout(2.seconds) { FirebaseSessionsDependencies.getSubscribers() }
115+
}
116+
117+
companion object {
118+
init {
119+
// Add only Crashlytics as a dependency, not Performance.
120+
// This is similar to how 1P SDKs will add themselves as dependencies. Note that this
121+
// dependency is added for all unit tests after this class is loaded into memory.
122+
FirebaseSessionsDependencies.addDependency(CRASHLYTICS)
123+
}
124+
}
125+
126+
private val crashlyticsSubscriber = FakeSessionSubscriber(sessionSubscriberName = CRASHLYTICS)
127+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.sessions.testing
18+
19+
import com.google.firebase.sessions.api.SessionSubscriber
20+
import com.google.firebase.sessions.api.SessionSubscriber.Name.CRASHLYTICS
21+
22+
/** Fake [SessionSubscriber] that can set [isDataCollectionEnabled] and [sessionSubscriberName]. */
23+
internal class FakeSessionSubscriber(
24+
override val isDataCollectionEnabled: Boolean = true,
25+
override val sessionSubscriberName: SessionSubscriber.Name = CRASHLYTICS,
26+
) : SessionSubscriber {
27+
override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) = Unit
28+
}

0 commit comments

Comments
 (0)