Skip to content

Commit e12458f

Browse files
authored
Implement SessionInitiator and hook it up to lifecycle events (#4723)
* Implement SessionInitiator and hook it up to lifecycle events * Add a warning when failed to register lifecycle callbacks * Fix formatting
1 parent 402c686 commit e12458f

File tree

5 files changed

+234
-34
lines changed

5 files changed

+234
-34
lines changed

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessions.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,35 @@
1414

1515
package com.google.firebase.sessions
1616

17+
import android.app.Application
18+
import android.util.Log
1719
import androidx.annotation.Discouraged
1820
import com.google.firebase.FirebaseApp
1921
import com.google.firebase.ktx.Firebase
2022
import com.google.firebase.ktx.app
2123

22-
class FirebaseSessions internal constructor() {
24+
class FirebaseSessions internal constructor(firebaseApp: FirebaseApp) {
25+
init {
26+
val sessionInitiator = SessionInitiator(System::currentTimeMillis, this::initiateSessionStart)
27+
val context = firebaseApp.applicationContext.applicationContext
28+
if (context is Application) {
29+
context.registerActivityLifecycleCallbacks(sessionInitiator.activityLifecycleCallbacks)
30+
} else {
31+
Log.w(TAG, "Failed to register lifecycle callbacks, unexpected context ${context.javaClass}.")
32+
}
33+
}
34+
2335
@Discouraged(message = "This will be replaced with a real API.")
2436
fun greeting(): String = "Matt says hi!"
2537

38+
private fun initiateSessionStart() {
39+
// TODO(mrober): Generate a session
40+
Log.i(TAG, "Initiate session start")
41+
}
42+
2643
companion object {
44+
private const val TAG = "FirebaseSessions"
45+
2746
@JvmStatic
2847
val instance: FirebaseSessions
2948
get() = getInstance(Firebase.app)

firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
package com.google.firebase.sessions
1616

1717
import androidx.annotation.Keep
18+
import com.google.firebase.FirebaseApp
1819
import com.google.firebase.components.Component
1920
import com.google.firebase.components.ComponentRegistrar
21+
import com.google.firebase.components.Dependency
2022
import com.google.firebase.platforminfo.LibraryVersionComponent
2123

2224
/**
@@ -30,12 +32,14 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
3032
listOf(
3133
Component.builder(FirebaseSessions::class.java)
3234
.name(LIBRARY_NAME)
33-
.factory { FirebaseSessions() }
35+
.add(Dependency.required(FirebaseApp::class.java))
36+
.factory { container -> FirebaseSessions(container.get(FirebaseApp::class.java)) }
37+
.eagerInDefaultApp()
3438
.build(),
3539
LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME)
3640
)
3741

3842
companion object {
39-
private const val LIBRARY_NAME = "fire-ses"
43+
private const val LIBRARY_NAME = "fire-sessions"
4044
}
4145
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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
18+
19+
import android.app.Activity
20+
import android.app.Application.ActivityLifecycleCallbacks
21+
import android.os.Bundle
22+
23+
/**
24+
* The [SessionInitiator] is responsible for calling the [initiateSessionStart] callback whenever a
25+
* session starts. This will happen at a cold start of the app, and when the app has been in the
26+
* background for a period of time (default 30 min) and then comes back to the foreground.
27+
*
28+
* @hide
29+
*/
30+
internal class SessionInitiator(
31+
private val currentTimeMs: () -> Long,
32+
private val initiateSessionStart: () -> Unit
33+
) {
34+
private var backgroundTimeMs = currentTimeMs()
35+
private val sessionTimeoutMs = 30 * 60 * 1000L // TODO(mrober): Get session timeout from settings
36+
37+
init {
38+
initiateSessionStart()
39+
}
40+
41+
fun appBackgrounded() {
42+
backgroundTimeMs = currentTimeMs()
43+
}
44+
45+
fun appForegrounded() {
46+
val intervalMs = currentTimeMs() - backgroundTimeMs
47+
if (intervalMs > sessionTimeoutMs) {
48+
initiateSessionStart()
49+
}
50+
}
51+
52+
internal val activityLifecycleCallbacks =
53+
object : ActivityLifecycleCallbacks {
54+
override fun onActivityResumed(activity: Activity) = appForegrounded()
55+
56+
override fun onActivityPaused(activity: Activity) = appBackgrounded()
57+
58+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
59+
60+
override fun onActivityStarted(activity: Activity) = Unit
61+
62+
override fun onActivityStopped(activity: Activity) = Unit
63+
64+
override fun onActivityDestroyed(activity: Activity) = Unit
65+
66+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
67+
}
68+
}

firebase-sessions/src/test/kotlin/com/google/firebase/sessions/FirebaseSessionsTest.kt

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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
18+
19+
import com.google.common.truth.Truth.assertThat
20+
import org.junit.Test
21+
22+
class SessionInitiatorTest {
23+
class FakeTime {
24+
var currentTimeMs = 0L
25+
private set
26+
27+
fun addTimeMs(intervalMs: Long) {
28+
currentTimeMs += intervalMs
29+
}
30+
}
31+
32+
class SessionStartCounter {
33+
var count = 0
34+
private set
35+
36+
fun initiateSessionStart() {
37+
count++
38+
}
39+
}
40+
41+
@Test
42+
fun coldStart_initiatesSession() {
43+
val sessionStartCounter = SessionStartCounter()
44+
45+
// Simulate a cold start by simply constructing the SessionInitiator object
46+
SessionInitiator({ 0 }, sessionStartCounter::initiateSessionStart)
47+
48+
assertThat(sessionStartCounter.count).isEqualTo(1)
49+
}
50+
51+
@Test
52+
fun appForegrounded_largeInterval_initiatesSession() {
53+
val fakeTime = FakeTime()
54+
val sessionStartCounter = SessionStartCounter()
55+
56+
val sessionInitiator =
57+
SessionInitiator(fakeTime::currentTimeMs, sessionStartCounter::initiateSessionStart)
58+
59+
// First session on cold start
60+
assertThat(sessionStartCounter.count).isEqualTo(1)
61+
62+
// Enough tome to initiate a new session, and then foreground
63+
fakeTime.addTimeMs(LARGE_INTERVAL_MS)
64+
sessionInitiator.appForegrounded()
65+
66+
// Another session initiated
67+
assertThat(sessionStartCounter.count).isEqualTo(2)
68+
}
69+
70+
@Test
71+
fun appForegrounded_smallInterval_doesNotInitiatesSession() {
72+
val fakeTime = FakeTime()
73+
val sessionStartCounter = SessionStartCounter()
74+
75+
val sessionInitiator =
76+
SessionInitiator(fakeTime::currentTimeMs, sessionStartCounter::initiateSessionStart)
77+
78+
// First session on cold start
79+
assertThat(sessionStartCounter.count).isEqualTo(1)
80+
81+
// Not enough time to initiate a new session, and then foreground
82+
fakeTime.addTimeMs(SMALL_INTERVAL_MS)
83+
sessionInitiator.appForegrounded()
84+
85+
// No new session
86+
assertThat(sessionStartCounter.count).isEqualTo(1)
87+
}
88+
89+
@Test
90+
fun appForegrounded_background_foreground_largeIntervals_initiatesSessions() {
91+
val fakeTime = FakeTime()
92+
val sessionStartCounter = SessionStartCounter()
93+
94+
val sessionInitiator =
95+
SessionInitiator(fakeTime::currentTimeMs, sessionStartCounter::initiateSessionStart)
96+
97+
assertThat(sessionStartCounter.count).isEqualTo(1)
98+
99+
fakeTime.addTimeMs(LARGE_INTERVAL_MS)
100+
sessionInitiator.appForegrounded()
101+
102+
assertThat(sessionStartCounter.count).isEqualTo(2)
103+
104+
sessionInitiator.appBackgrounded()
105+
fakeTime.addTimeMs(LARGE_INTERVAL_MS)
106+
sessionInitiator.appForegrounded()
107+
108+
assertThat(sessionStartCounter.count).isEqualTo(3)
109+
}
110+
111+
@Test
112+
fun appForegrounded_background_foreground_smallIntervals_doesNotInitiateNewSessions() {
113+
val fakeTime = FakeTime()
114+
val sessionStartCounter = SessionStartCounter()
115+
116+
val sessionInitiator =
117+
SessionInitiator(fakeTime::currentTimeMs, sessionStartCounter::initiateSessionStart)
118+
119+
// First session on cold start
120+
assertThat(sessionStartCounter.count).isEqualTo(1)
121+
122+
fakeTime.addTimeMs(SMALL_INTERVAL_MS)
123+
sessionInitiator.appForegrounded()
124+
125+
assertThat(sessionStartCounter.count).isEqualTo(1)
126+
127+
sessionInitiator.appBackgrounded()
128+
fakeTime.addTimeMs(SMALL_INTERVAL_MS)
129+
sessionInitiator.appForegrounded()
130+
131+
assertThat(sessionStartCounter.count).isEqualTo(1)
132+
133+
assertThat(sessionStartCounter.count).isEqualTo(1)
134+
}
135+
136+
companion object {
137+
private const val SMALL_INTERVAL_MS = 3 * 1000L // not enough time to initiate a new session
138+
private const val LARGE_INTERVAL_MS = 90 * 60 * 1000L // enough to initiate another session
139+
}
140+
}

0 commit comments

Comments
 (0)