Skip to content

Commit 0920dd3

Browse files
authored
Merge 9e67118 into c61d5c5
2 parents c61d5c5 + 9e67118 commit 0920dd3

File tree

4 files changed

+285
-0
lines changed

4 files changed

+285
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 android.util.Log
20+
import androidx.annotation.VisibleForTesting
21+
import java.util.Collections.synchronizedMap
22+
import kotlinx.coroutines.sync.Mutex
23+
import kotlinx.coroutines.sync.withLock
24+
25+
/**
26+
* [FirebaseSessionsDependencies] determines when a dependent SDK is installed in the app. The
27+
* Sessions SDK uses this to figure out which dependencies to wait for to getting the data
28+
* collection state. This is thread safe.
29+
*
30+
* This is important because the Sessions SDK starts up before dependent SDKs.
31+
*/
32+
object FirebaseSessionsDependencies {
33+
private const val TAG = "SessionsDependencies"
34+
35+
private val dependencies = synchronizedMap(mutableMapOf<SessionSubscriber.Name, Dependency>())
36+
37+
/**
38+
* Add a subscriber as a dependency to the Sessions SDK. Every dependency must register itself, or
39+
* the Sessions SDK will never generate a session.
40+
*/
41+
fun addDependency(subscriberName: SessionSubscriber.Name) {
42+
if (dependencies.containsKey(subscriberName)) {
43+
Log.d(TAG, "Dependency $subscriberName already added.")
44+
return
45+
}
46+
47+
// The dependency is locked until the subscriber registers itself.
48+
dependencies[subscriberName] = Dependency(Mutex(locked = true))
49+
}
50+
51+
/**
52+
* Register and unlock the subscriber. This must be called before [getRegisteredSubscribers] can
53+
* return.
54+
*/
55+
internal fun register(subscriber: SessionSubscriber) {
56+
val subscriberName = subscriber.sessionSubscriberName
57+
val dependency = getDependency(subscriberName)
58+
59+
dependency.subscriber?.run {
60+
Log.d(TAG, "Subscriber $subscriberName already registered.")
61+
return
62+
}
63+
dependency.subscriber = subscriber
64+
65+
// Unlock to show the subscriber has been registered, it is possible to get it now.
66+
dependency.mutex.unlock()
67+
}
68+
69+
/** Gets the subscribers safely, blocks until all the subscribers are registered. */
70+
internal suspend fun getRegisteredSubscribers(): Map<SessionSubscriber.Name, SessionSubscriber> {
71+
// The call to getSubscriber will never throw because the mutex guarantees it's been registered.
72+
return dependencies.mapValues { (subscriberName, dependency) ->
73+
dependency.mutex.withLock { getSubscriber(subscriberName) }
74+
}
75+
}
76+
77+
/** Gets the subscriber, regardless of being registered. This is exposed for testing. */
78+
@VisibleForTesting
79+
internal fun getSubscriber(subscriberName: SessionSubscriber.Name): SessionSubscriber {
80+
return getDependency(subscriberName).subscriber
81+
?: throw IllegalStateException("Subscriber $subscriberName has not been registered.")
82+
}
83+
84+
/** Resets all the dependencies for testing purposes. */
85+
@VisibleForTesting internal fun reset() = dependencies.clear()
86+
87+
private fun getDependency(subscriberName: SessionSubscriber.Name): Dependency {
88+
return dependencies.getOrElse(subscriberName) {
89+
throw IllegalStateException(
90+
"Cannot get dependency $subscriberName. Dependencies should be added at class load time."
91+
)
92+
}
93+
}
94+
95+
private data class Dependency(val mutex: Mutex, var subscriber: SessionSubscriber? = null)
96+
}
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: SessionSubscriber.Name
35+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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 all dependencies after each test.
43+
FirebaseSessionsDependencies.reset()
44+
}
45+
46+
@Test
47+
fun register_dependencyAdded_canGet() {
48+
val crashlyticsSubscriber = FakeSessionSubscriber(sessionSubscriberName = CRASHLYTICS)
49+
FirebaseSessionsDependencies.addDependency(CRASHLYTICS)
50+
FirebaseSessionsDependencies.register(crashlyticsSubscriber)
51+
52+
assertThat(FirebaseSessionsDependencies.getSubscriber(CRASHLYTICS))
53+
.isEqualTo(crashlyticsSubscriber)
54+
}
55+
56+
@Test
57+
fun register_alreadyRegisteredSameName_ignoresSecondSubscriber() {
58+
val firstSubscriber = FakeSessionSubscriber(sessionSubscriberName = CRASHLYTICS)
59+
val secondSubscriber = FakeSessionSubscriber(sessionSubscriberName = CRASHLYTICS)
60+
61+
FirebaseSessionsDependencies.addDependency(CRASHLYTICS)
62+
63+
// Register the first time, no problem.
64+
FirebaseSessionsDependencies.register(firstSubscriber)
65+
66+
// Attempt to register a second subscriber with the same name.
67+
FirebaseSessionsDependencies.register(secondSubscriber)
68+
69+
assertThat(FirebaseSessionsDependencies.getSubscriber(CRASHLYTICS)).isEqualTo(firstSubscriber)
70+
}
71+
72+
@Test
73+
fun getSubscriber_dependencyAdded_notRegistered_throws() {
74+
FirebaseSessionsDependencies.addDependency(PERFORMANCE)
75+
76+
val thrown =
77+
assertThrows(IllegalStateException::class.java) {
78+
FirebaseSessionsDependencies.getSubscriber(PERFORMANCE)
79+
}
80+
81+
assertThat(thrown).hasMessageThat().contains("Subscriber PERFORMANCE has not been registered")
82+
}
83+
84+
@Test
85+
fun getSubscriber_notDepended_throws() {
86+
val thrown =
87+
assertThrows(IllegalStateException::class.java) {
88+
// Crashlytics was never added as a dependency.
89+
FirebaseSessionsDependencies.getSubscriber(CRASHLYTICS)
90+
}
91+
92+
assertThat(thrown).hasMessageThat().contains("Cannot get dependency CRASHLYTICS")
93+
}
94+
95+
@Test
96+
fun getSubscribers_waitsForRegister(): Unit = runBlocking {
97+
val crashlyticsSubscriber = FakeSessionSubscriber(sessionSubscriberName = CRASHLYTICS)
98+
FirebaseSessionsDependencies.addDependency(CRASHLYTICS)
99+
100+
// Wait a few seconds and then register.
101+
launch {
102+
delay(2.seconds)
103+
FirebaseSessionsDependencies.register(crashlyticsSubscriber)
104+
}
105+
106+
// Block until the register happens.
107+
val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers()
108+
109+
assertThat(subscribers).containsExactly(CRASHLYTICS, crashlyticsSubscriber)
110+
}
111+
112+
@Test
113+
fun getSubscribers_noDependencies() = runTest {
114+
val subscribers = FirebaseSessionsDependencies.getRegisteredSubscribers()
115+
116+
assertThat(subscribers).isEmpty()
117+
}
118+
119+
@Test(expected = TimeoutCancellationException::class)
120+
fun getSubscribers_neverRegister_waitsForever() = runTest {
121+
FirebaseSessionsDependencies.addDependency(CRASHLYTICS)
122+
123+
// The register never happens, wait until the timeout.
124+
withTimeout(2.seconds) { FirebaseSessionsDependencies.getRegisteredSubscribers() }
125+
}
126+
}
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)