Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c065605d authored by Tao Wu's avatar Tao Wu Committed by Android (Google) Code Review
Browse files

Merge "[Growth]: Broadcast when the device has entered directly" into main

parents 59be4cc7 bf53c295
Loading
Loading
Loading
Loading
+161 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * 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.android.systemui.growth.domain.interactor

import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.broadcast.mockBroadcastSender
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.capture
import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.times

@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
@SmallTest
class GrowthInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val broadcastSender by lazy { kosmos.mockBroadcastSender }
    private lateinit var underTest: GrowthInteractor

    @Captor
    private lateinit var intentArgumentCaptor: ArgumentCaptor<Intent>

    @Before
    fun setUp() {
        MockitoAnnotations.openMocks(this)
        kosmos.sceneContainerStartable.start()
    }

    @Test
    fun onDeviceEnteredDirectly_sendBroadcast_withAllConfigs() =
        kosmos.runTest {
            overrideResources()
            underTest = kosmos.growthInteractor
            underTest.activateIn(kosmos.testScope)
            setDeviceEntered()

            verify(broadcastSender, times(1))
                .sendBroadcast(capture(intentArgumentCaptor), eq(GROWTH_RECEIVER_PERMISSION))
            assertThat(intentArgumentCaptor.value.action)
                .isEqualTo(GrowthInteractor.ACTION_DEVICE_ENTERED_DIRECTLY)
            assertThat(intentArgumentCaptor.value.`package`).isEqualTo(GROWTH_APP_PACKAGE_NAME)
            assertThat(intentArgumentCaptor.value.component?.className)
                .isEqualTo(GROWTH_RECEIVER_CLASS_NAME)
        }

    @Test
    fun onDeviceEnteredDirectly_sendBroadcast_withEmptyConfigs() =
        kosmos.runTest {
            overrideResourcesWithEmptyValues()
            underTest = kosmos.growthInteractor
            underTest.activateIn(kosmos.testScope)
            setDeviceEntered()

            verify(broadcastSender, times(1))
                .sendBroadcast(capture(intentArgumentCaptor))
            assertThat(intentArgumentCaptor.value.action)
                .isEqualTo(GrowthInteractor.ACTION_DEVICE_ENTERED_DIRECTLY)
            assertThat(intentArgumentCaptor.value.`package`).isNull()
            assertThat(intentArgumentCaptor.value.component)
                .isNull()
        }

    @Test
    fun onDeviceNotEnteredDirectly_doNotSendBroadcast() =
        kosmos.runTest {
            setDeviceEntered()
            clearInvocations(broadcastSender)

            setScene(Scenes.Lockscreen)
            verify(broadcastSender, never()).sendBroadcast(any(), any())
        }

    private fun overrideResources() {
        overrideResource(R.string.config_growthAppPackageName, GROWTH_APP_PACKAGE_NAME)
        overrideResource(R.string.config_growthReceiverClassName, GROWTH_RECEIVER_CLASS_NAME)
        overrideResource(R.string.config_growthReceiverPermission, GROWTH_RECEIVER_PERMISSION)
    }

    private fun overrideResourcesWithEmptyValues() {
        overrideResource(R.string.config_growthAppPackageName, "")
        overrideResource(R.string.config_growthReceiverClassName, "")
        overrideResource(R.string.config_growthReceiverPermission, "")
    }

    private suspend fun Kosmos.setDeviceEntered() {
        val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
        val isDeviceEnteredDirectly by
        collectLastValue(kosmos.deviceEntryInteractor.isDeviceEnteredDirectly)
        assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
        assertThat(isDeviceEnteredDirectly).isFalse()

        // Authenticate with PIN to unlock and dismiss the lockscreen.
        kosmos.authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
        kosmos.runCurrent()

        assertThat(currentScene).isEqualTo(Scenes.Gone)
        assertThat(isDeviceEnteredDirectly).isTrue()
    }

    private fun Kosmos.setScene(sceneKey: SceneKey) {
        kosmos.sceneInteractor.changeScene(sceneKey, "test")
        kosmos.sceneInteractor.setTransitionState(
            flowOf<ObservableTransitionState>(ObservableTransitionState.Idle(sceneKey))
        )
        kosmos.runCurrent()
    }

    companion object {
        private const val GROWTH_APP_PACKAGE_NAME = "com.android.systemui.growth"
        private const val GROWTH_RECEIVER_CLASS_NAME = "com.android.systemui.growth.GrowthReceiver"
        private const val GROWTH_RECEIVER_PERMISSION =
            "com.android.systemui.growth.permission.SEND_BROADCAST"
    }
}
 No newline at end of file
+7 −0
Original line number Diff line number Diff line
@@ -1140,4 +1140,11 @@

    <!-- Whether to enable features improving large screen interaction -->
    <bool name="config_improveLargeScreenInteractionOnLockscreen">false</bool>

    <!-- Package name for the growth app. -->
    <string name="config_growthAppPackageName" translatable="false" />
    <!-- Class name for the growth app's receiver. -->
    <string name="config_growthReceiverClassName" translatable="false" />
    <!-- Permission to send a broadcast to the growth app receiver. -->
    <string name="config_growthReceiverPermission" translatable="false" />
</resources>
+39 −6
Original line number Diff line number Diff line
@@ -16,24 +16,57 @@

package com.android.systemui.growth.domain.interactor

import android.content.ComponentName
import android.content.Intent
import android.content.res.Resources
import androidx.annotation.VisibleForTesting
import com.android.systemui.broadcast.BroadcastSender
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import com.android.systemui.res.R
import dagger.Lazy
import javax.inject.Inject

/** Interactor to communicate with the growth app. */
@SysUISingleton
class GrowthInteractor
@Inject
constructor(private val deviceEntryInteractor: Lazy<DeviceEntryInteractor>) :
    ExclusiveActivatable() {
constructor(
    @Main private val resources: Resources,
    private val deviceEntryInteractor: Lazy<DeviceEntryInteractor>,
    private val broadcastSender: BroadcastSender,
) : ExclusiveActivatable() {
    private val growthAppPackageName =
        resources.getString(R.string.config_growthAppPackageName)
    private val growthReceiverClassName =
        resources.getString(R.string.config_growthReceiverClassName)
    private val growthReceiverPermission =
        resources.getString(R.string.config_growthReceiverPermission)

    override suspend fun onActivated(): Nothing {
        deviceEntryInteractor.get().isDeviceEnteredDirectly.collect {
            if (it) {
                // TODO: b/410857267 - Notify the growth app.
                // Broadcast the device entered event to the growth app.
                val intent = Intent().apply { setAction(ACTION_DEVICE_ENTERED_DIRECTLY) }
                if (growthAppPackageName.isNotEmpty() && growthReceiverClassName.isNotEmpty()) {
                    intent.setPackage(growthAppPackageName)
                    intent.setComponent(ComponentName(growthAppPackageName, growthReceiverClassName))
                }

                if (growthReceiverPermission.isNotEmpty()) {
                    broadcastSender.sendBroadcast(intent, growthReceiverPermission)
                } else {
                    broadcastSender.sendBroadcast(intent)
                }
            }
        }
    }

    companion object {
        @VisibleForTesting
        const val ACTION_DEVICE_ENTERED_DIRECTLY =
            "com.android.systemui.growth.action.DEVICE_ENTERED_DIRECTLY"
    }
}
 No newline at end of file
+32 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * 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.android.systemui.growth.domain.interactor

import android.content.res.mainResources
import com.android.systemui.broadcast.mockBroadcastSender
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture

var Kosmos.growthInteractor: GrowthInteractor by
Kosmos.Fixture {
    GrowthInteractor(
        resources = mainResources,
        deviceEntryInteractor = { deviceEntryInteractor },
        broadcastSender = mockBroadcastSender,
    )
}
 No newline at end of file