Skip to content

Commit 85afdf5

Browse files
authored
State datasource for PowerMode (#2919)
Datasource for Low Power mode. This is disabled in production because of the feature flag and base InstrumentationProvider
1 parent 41ef4fa commit 85afdf5

File tree

7 files changed

+156
-2
lines changed

7 files changed

+156
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.embrace.android.embracesdk.internal.instrumentation.powersave
2+
3+
import android.content.Context
4+
import android.os.PowerManager
5+
import io.embrace.android.embracesdk.internal.arch.InstrumentationArgs
6+
import io.embrace.android.embracesdk.internal.arch.datasource.StateDataSource
7+
import io.embrace.android.embracesdk.internal.arch.schema.SchemaType.PowerState
8+
import io.embrace.android.embracesdk.internal.arch.schema.SchemaType.PowerState.PowerMode
9+
import io.embrace.android.embracesdk.internal.utils.Provider
10+
import io.embrace.android.embracesdk.internal.worker.Worker
11+
12+
class PowerStateDataSource(
13+
private val args: InstrumentationArgs,
14+
) : StateDataSource<PowerMode>(
15+
args = args,
16+
stateValueFactory = ::PowerState,
17+
defaultValue = PowerMode.UNKNOWN,
18+
) {
19+
private val powerManagerProvider: Provider<PowerManager?> = { args.systemService(Context.POWER_SERVICE) }
20+
private val receiver = PowerSaveModeReceiver(powerManagerProvider, ::onPowerSaveModeChanged)
21+
22+
override fun onDataCaptureEnabled() {
23+
super.onDataCaptureEnabled()
24+
args.backgroundWorker(Worker.Background.NonIoRegWorker).run {
25+
receiver.register(args.context, this)
26+
submit {
27+
powerManagerProvider()?.let {
28+
onPowerSaveModeChanged(it.isPowerSaveMode)
29+
}
30+
}
31+
}
32+
}
33+
34+
override fun onDataCaptureDisabled(): Unit = receiver.unregister(args.context)
35+
36+
private fun onPowerSaveModeChanged(powerSaveMode: Boolean) {
37+
val timestamp = clock.now()
38+
val newState = if (powerSaveMode) {
39+
PowerMode.LOW
40+
} else {
41+
PowerMode.NORMAL
42+
}
43+
onStateChange(timestamp, newState)
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.embrace.android.embracesdk.internal.instrumentation.powersave
2+
3+
import io.embrace.android.embracesdk.internal.arch.InstrumentationArgs
4+
import io.embrace.android.embracesdk.internal.arch.datasource.StateInstrumentationProvider
5+
import io.embrace.android.embracesdk.internal.arch.schema.SchemaType
6+
7+
class PowerStateInstrumentationProvider :
8+
StateInstrumentationProvider<PowerStateDataSource, SchemaType.PowerState.PowerMode>(
9+
configGate = { configService.autoDataCaptureBehavior.isPowerSaveModeCaptureEnabled() }
10+
) {
11+
override fun factoryProvider(args: InstrumentationArgs): () -> PowerStateDataSource {
12+
return { PowerStateDataSource(args) }
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
io.embrace.android.embracesdk.internal.instrumentation.powersave.PowerSaveInstrumentationProvider
2+
io.embrace.android.embracesdk.internal.instrumentation.powersave.PowerStateInstrumentationProvider

embrace-android-instrumentation-schema/src/main/kotlin/io/embrace/android/embracesdk/internal/arch/schema/SchemaType.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,17 @@ sealed class SchemaType(
307307
}
308308
}
309309

310+
class PowerState(initialValue: PowerMode) :
311+
State<PowerState.PowerMode>(initialValue, "power") {
312+
enum class PowerMode(private val value: String) {
313+
NORMAL("normal"),
314+
LOW("low"),
315+
UNKNOWN("unknown");
316+
317+
override fun toString(): String = value
318+
}
319+
}
320+
310321
/**
311322
* A custom telemetry type. This allows the hybrid SDKs (and others) to pass in custom
312323
* telemetry schemas if required.

embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/SessionApiTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
44
import io.embrace.android.embracesdk.assertions.toMap
55
import io.embrace.android.embracesdk.fakes.config.FakeEnabledFeatureConfig
66
import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig
7-
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
87
import io.embrace.android.embracesdk.internal.arch.attrs.embFreeDiskBytes
8+
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
99
import io.embrace.android.embracesdk.internal.otel.sdk.findAttributeValue
1010
import io.embrace.android.embracesdk.testframework.SdkIntegrationTestRule
1111
import io.embrace.opentelemetry.kotlin.semconv.IncubatingApi

embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/features/LowPowerFeatureTest.kt

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@ import android.os.PowerManager
88
import androidx.test.core.app.ApplicationProvider
99
import androidx.test.ext.junit.runners.AndroidJUnit4
1010
import io.embrace.android.embracesdk.assertions.assertMatches
11+
import io.embrace.android.embracesdk.assertions.assertStateTransition
1112
import io.embrace.android.embracesdk.assertions.findSpanOfType
1213
import io.embrace.android.embracesdk.fakes.config.FakeEnabledFeatureConfig
1314
import io.embrace.android.embracesdk.fakes.config.FakeInstrumentedConfig
1415
import io.embrace.android.embracesdk.internal.arch.schema.EmbType
16+
import io.embrace.android.embracesdk.internal.arch.schema.SchemaType
1517
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
18+
import io.embrace.android.embracesdk.internal.instrumentation.powersave.PowerStateDataSource
19+
import io.embrace.android.embracesdk.internal.session.getSessionSpan
20+
import io.embrace.android.embracesdk.internal.session.getStateSpan
1621
import io.embrace.android.embracesdk.testframework.SdkIntegrationTestRule
22+
import io.embrace.android.embracesdk.testframework.actions.EmbraceActionInterface.Companion.LIFECYCLE_EVENT_GAP
1723
import org.junit.Assert.assertEquals
24+
import org.junit.Assert.assertNull
25+
import org.junit.Assert.assertTrue
1826
import org.junit.Before
1927
import org.junit.Rule
2028
import org.junit.Test
@@ -74,6 +82,81 @@ internal class LowPowerFeatureTest {
7482
)
7583
}
7684

85+
@Test
86+
fun `power state feature`() {
87+
val transitions: MutableList<Pair<Long, SchemaType.PowerState.PowerMode>> = mutableListOf()
88+
testRule.runTest(
89+
instrumentedConfig = FakeInstrumentedConfig(
90+
enabledFeatures = FakeEnabledFeatureConfig(
91+
stateCaptureEnabled = true,
92+
bgActivityCapture = false,
93+
powerSaveCapture = true
94+
)
95+
),
96+
testCaseAction = {
97+
recordSession {
98+
setPowerSaveMode(true)
99+
transitions.add(Pair(clock.now(), SchemaType.PowerState.PowerMode.LOW))
100+
clock.tick(10000L)
101+
setPowerSaveMode(false)
102+
transitions.add(Pair(clock.now(), SchemaType.PowerState.PowerMode.NORMAL))
103+
}
104+
},
105+
assertAction = {
106+
val message = getSingleSessionEnvelope()
107+
val sessionSpan = checkNotNull(message.getSessionSpan())
108+
with(checkNotNull(message.getStateSpan("emb-state-power"))) {
109+
assertEquals(
110+
checkNotNull(sessionSpan.startTimeNanos).nanosToMillis() + LIFECYCLE_EVENT_GAP,
111+
checkNotNull(startTimeNanos).nanosToMillis()
112+
)
113+
assertEquals(sessionSpan.endTimeNanos, endTimeNanos)
114+
with(checkNotNull(events)) {
115+
assertEquals(2, size)
116+
repeat(size) { i ->
117+
this[i].assertStateTransition(
118+
timestampMs = transitions[i].first,
119+
newStateValue = transitions[i].second,
120+
notInSession = if (i == 0) {
121+
1
122+
} else {
123+
0
124+
}
125+
)
126+
}
127+
}
128+
}
129+
}
130+
)
131+
}
132+
133+
@Test
134+
fun `power state disabled by feature flag`() {
135+
var throwable: Throwable? = null
136+
testRule.runTest(
137+
instrumentedConfig = FakeInstrumentedConfig(
138+
enabledFeatures = FakeEnabledFeatureConfig(
139+
stateCaptureEnabled = true,
140+
powerSaveCapture = false
141+
)
142+
),
143+
testCaseAction = {
144+
try {
145+
findDataSource<PowerStateDataSource>()
146+
} catch (e: IllegalStateException) {
147+
throwable = e
148+
}
149+
recordSession {
150+
setPowerSaveMode(true)
151+
}
152+
},
153+
assertAction = {
154+
assertNull(getSingleSessionEnvelope().getStateSpan("emb-state-power"))
155+
assertTrue(throwable is IllegalStateException)
156+
}
157+
)
158+
}
159+
77160
private fun setPowerSaveMode(powerSaveModeEnabled: Boolean) {
78161
shadowPowerManager.setIsPowerSaveMode(powerSaveModeEnabled)
79162
application.sendBroadcast(Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED))

embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/session/BackgroundActivityDisabledTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import io.embrace.android.embracesdk.internal.arch.attrs.embSessionStartType
1616
import io.embrace.android.embracesdk.internal.arch.attrs.embState
1717
import io.embrace.android.embracesdk.internal.arch.attrs.embTerminated
1818
import io.embrace.android.embracesdk.internal.arch.schema.EmbType
19+
import io.embrace.android.embracesdk.internal.arch.state.AppState
1920
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
2021
import io.embrace.android.embracesdk.internal.otel.sdk.findAttributeValue
2122
import io.embrace.android.embracesdk.internal.payload.Span
22-
import io.embrace.android.embracesdk.internal.arch.state.AppState
2323
import io.embrace.android.embracesdk.spans.EmbraceSpan
2424
import io.embrace.android.embracesdk.testframework.SdkIntegrationTestRule
2525
import io.embrace.opentelemetry.kotlin.semconv.IncubatingApi

0 commit comments

Comments
 (0)