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

Commit 0b552647 authored by Govinda Wasserman's avatar Govinda Wasserman Committed by Android (Google) Code Review
Browse files

Merge "Add ScreenCaptureAppContentInteractor" into main

parents 9b1db66a ce09c1bb
Loading
Loading
Loading
Loading
+193 −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.screencapture.common.domain.interactor

import android.media.projection.MediaProjectionAppContent
import android.os.UserHandle
import androidx.core.graphics.createBitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.screencapture.common.data.repository.fakeScreenCaptureAppContentRepository
import com.android.systemui.screencapture.common.domain.model.ScreenCaptureAppContent
import com.android.systemui.screencapture.common.shared.model.castScreenCaptureUiParameters
import com.android.systemui.testKosmosNew
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.launch
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class ScreenCaptureAppContentInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmosNew()

    private val fakeUserHandle = UserHandle.of(123)
    private val fakeMediaProjectionAppContent1 =
        MediaProjectionAppContent(
            /* thumbnail= */ createBitmap(100, 100),
            /* title= */ "FakeTitle1",
            /* id= */ 456,
        )
    private val fakeMediaProjectionAppContent2 =
        MediaProjectionAppContent(
            /* thumbnail= */ createBitmap(200, 200),
            /* title= */ "FakeTitle2",
            /* id= */ 789,
        )
    private val fakeThrowable = IllegalStateException("FakeMessage")

    @Test
    fun appContentsFor_singlePackage_propagatesSuccess() =
        kosmos.runTest {
            // Arrange
            val interactor =
                ScreenCaptureAppContentInteractor(
                    repository = fakeScreenCaptureAppContentRepository,
                    parameters =
                        castScreenCaptureUiParameters.copy(hostAppUserHandle = fakeUserHandle),
                )
            var result: Result<List<ScreenCaptureAppContent>>? = null

            // Act
            val appContents = interactor.appContentsFor("FakePackage", 200, 150)
            val job = testScope.launch { appContents.collect { result = it } }
            assertThat(result).isNull()
            fakeScreenCaptureAppContentRepository.setAppContentSuccess(
                packageName = "FakePackage",
                user = fakeUserHandle,
                fakeMediaProjectionAppContent1,
            )

            // Assert
            assertThat(fakeScreenCaptureAppContentRepository.appContentsForCalls).hasSize(1)
            with(fakeScreenCaptureAppContentRepository.appContentsForCalls.last()) {
                assertThat(packageName).isEqualTo("FakePackage")
                assertThat(user).isEqualTo(fakeUserHandle)
                assertThat(thumbnailWidthPx).isEqualTo(200)
                assertThat(thumbnailHeightPx).isEqualTo(150)
            }
            assertThat(result?.isSuccess).isTrue()
            assertThat(result?.getOrNull())
                .containsExactly(
                    ScreenCaptureAppContent("FakePackage", fakeMediaProjectionAppContent1)
                )

            // Cleanup
            job.cancel()
        }

    @Test
    fun appContentsFor_singlePackage_propagatesFailure() =
        kosmos.runTest {
            // Arrange
            val interactor =
                ScreenCaptureAppContentInteractor(
                    repository = fakeScreenCaptureAppContentRepository,
                    parameters =
                        castScreenCaptureUiParameters.copy(hostAppUserHandle = fakeUserHandle),
                )
            var result: Result<List<ScreenCaptureAppContent>>? = null

            // Act
            val appContents = interactor.appContentsFor("FakePackage", 200, 150)
            val job = testScope.launch { appContents.collect { result = it } }
            assertThat(result).isNull()
            fakeScreenCaptureAppContentRepository.setAppContentFailure(
                packageName = "FakePackage",
                user = fakeUserHandle,
                fakeThrowable,
            )

            // Assert
            assertThat(fakeScreenCaptureAppContentRepository.appContentsForCalls).hasSize(1)
            with(fakeScreenCaptureAppContentRepository.appContentsForCalls.last()) {
                assertThat(packageName).isEqualTo("FakePackage")
                assertThat(user).isEqualTo(fakeUserHandle)
                assertThat(thumbnailWidthPx).isEqualTo(200)
                assertThat(thumbnailHeightPx).isEqualTo(150)
            }
            assertThat(result?.isFailure).isTrue()
            assertThat(result?.exceptionOrNull()).isSameInstanceAs(fakeThrowable)

            // Cleanup
            job.cancel()
        }

    @Test
    fun appContentsFor_multiplePackages_propagatesOnlySuccessfulFetches() =
        kosmos.runTest {
            // Arrange
            val interactor =
                ScreenCaptureAppContentInteractor(
                    repository = fakeScreenCaptureAppContentRepository,
                    parameters =
                        castScreenCaptureUiParameters.copy(hostAppUserHandle = fakeUserHandle),
                )
            var result: List<ScreenCaptureAppContent>? = null

            // Act
            val appContents =
                interactor.appContentsFor(
                    listOf("FakePackage1", "FakePackage2", "FakePackage3"),
                    200,
                    150,
                )
            val job = testScope.launch { appContents.collect { result = it } }
            assertThat(result).isNull()
            with(fakeScreenCaptureAppContentRepository) {
                setAppContentSuccess(
                    packageName = "FakePackage1",
                    user = fakeUserHandle,
                    fakeMediaProjectionAppContent1,
                )
                setAppContentFailure(
                    packageName = "FakePackage2",
                    user = fakeUserHandle,
                    throwable = fakeThrowable,
                )
                setAppContentSuccess(
                    packageName = "FakePackage3",
                    user = fakeUserHandle,
                    fakeMediaProjectionAppContent2,
                )
            }

            // Assert
            assertThat(fakeScreenCaptureAppContentRepository.appContentsForCalls).hasSize(3)
            fakeScreenCaptureAppContentRepository.appContentsForCalls.forEachIndexed { index, call
                ->
                with(call) {
                    assertThat(packageName).isEqualTo("FakePackage${index + 1}")
                    assertThat(user).isEqualTo(fakeUserHandle)
                    assertThat(thumbnailWidthPx).isEqualTo(200)
                    assertThat(thumbnailHeightPx).isEqualTo(150)
                }
            }
            assertThat(result)
                .containsExactly(
                    ScreenCaptureAppContent("FakePackage1", fakeMediaProjectionAppContent1),
                    ScreenCaptureAppContent("FakePackage3", fakeMediaProjectionAppContent2),
                )

            // Cleanup
            job.cancel()
        }
}
+54 −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.screencapture.common.domain.model

import android.media.projection.MediaProjectionAppContent
import androidx.core.graphics.createBitmap
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class ScreenCaptureAppContentTest : SysuiTestCase() {

    @Test
    fun constructor_usesMediaProjectionAppContentFields() {
        // Arrange
        val fakeThumbnail = createBitmap(100, 100)
        val fakeMediaProjectionAppContent =
            MediaProjectionAppContent(
                /* thumbnail= */ fakeThumbnail,
                /* title= */ "FakeTitle",
                /* id= */ 123,
            )

        // Act
        val result = ScreenCaptureAppContent("FakePackage", fakeMediaProjectionAppContent)

        // Assert
        with(result) {
            assertThat(packageName).isEqualTo("FakePackage")
            assertThat(contentId).isEqualTo(123)
            assertThat(label).isEqualTo("FakeTitle")
            assertThat(thumbnail.sameAs(fakeThumbnail)).isTrue()
        }
    }
}
+86 −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.screencapture.common.domain.interactor

import com.android.systemui.screencapture.common.ScreenCapture
import com.android.systemui.screencapture.common.ScreenCaptureScope
import com.android.systemui.screencapture.common.data.repository.ScreenCaptureAppContentRepository
import com.android.systemui.screencapture.common.domain.model.ScreenCaptureAppContent
import com.android.systemui.screencapture.common.shared.model.ScreenCaptureUiParameters
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map

/** Interactor for fetching app content info. */
@ScreenCaptureScope
class ScreenCaptureAppContentInteractor
@Inject
constructor(
    private val repository: ScreenCaptureAppContentRepository,
    @ScreenCapture private val parameters: ScreenCaptureUiParameters,
) {
    /**
     * Fetch app content info for the given [packageName].
     *
     * Thumbnails will be fetched at the given [thumbnailWidthPx] and [thumbnailHeightPx].
     */
    fun appContentsFor(
        packageName: String,
        thumbnailWidthPx: Int,
        thumbnailHeightPx: Int,
    ): Flow<Result<List<ScreenCaptureAppContent>>> =
        repository
            .appContentsFor(
                packageName = packageName,
                user = parameters.hostAppUserHandle,
                thumbnailWidthPx = thumbnailWidthPx,
                thumbnailHeightPx = thumbnailHeightPx,
            )
            .map { appContent ->
                if (appContent.isFailure) {
                    Result.failure(appContent.exceptionOrNull()!!)
                } else {
                    Result.success(
                        appContent.getOrNull()!!.map { ScreenCaptureAppContent(packageName, it) }
                    )
                }
            }

    /**
     * Fetch app content info for all the given [packageNames].
     *
     * Thumbnails will be fetched at the given [thumbnailWidthPx] and [thumbnailHeightPx]. Only
     * includes entries for packages that have app content that was successfully fetched.
     */
    fun appContentsFor(
        packageNames: List<String>,
        thumbnailWidthPx: Int,
        thumbnailHeightPx: Int,
    ): Flow<List<ScreenCaptureAppContent>> =
        combine(
            packageNames.distinct().map {
                appContentsFor(
                    packageName = it,
                    thumbnailWidthPx = thumbnailWidthPx,
                    thumbnailHeightPx = thumbnailHeightPx,
                )
            }
        ) { appContents ->
            appContents.mapNotNull { it.getOrNull() }.flatMap { it }
        }
}
+38 −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.screencapture.common.domain.model

import android.graphics.Bitmap
import android.media.projection.MediaProjectionAppContent

/** Collection of information about currently available app content. */
data class ScreenCaptureAppContent(
    val packageName: String,
    val contentId: Int,
    val label: CharSequence,
    val thumbnail: Bitmap,
) {
    constructor(
        packageName: String,
        appContent: MediaProjectionAppContent,
    ) : this(
        packageName = packageName,
        contentId = appContent.id,
        label = appContent.title,
        thumbnail = appContent.thumbnail,
    )
}
+29 −11
Original line number Diff line number Diff line
@@ -24,8 +24,8 @@ import kotlinx.coroutines.flow.receiveAsFlow

class FakeScreenCaptureAppContentRepository : ScreenCaptureAppContentRepository {

    private val appContentChannel =
        Channel<Result<List<MediaProjectionAppContent>>>(Channel.CONFLATED)
    private val appContentChannels =
        mutableMapOf<Pair<String, UserHandle>, Channel<Result<List<MediaProjectionAppContent>>>>()

    val appContentsForCalls = mutableListOf<AppContentsForCall>()

@@ -38,25 +38,43 @@ class FakeScreenCaptureAppContentRepository : ScreenCaptureAppContentRepository
        appContentsForCalls.add(
            AppContentsForCall(packageName, user, thumbnailWidthPx, thumbnailHeightPx)
        )
        return appContentChannel.receiveAsFlow()
        return channelFor(packageName, user).receiveAsFlow()
    }

    fun setAppContent(appContent: Result<List<MediaProjectionAppContent>>) {
        appContentChannel.trySend(appContent)
    fun setAppContent(
        packageName: String,
        user: UserHandle,
        appContent: Result<List<MediaProjectionAppContent>>,
    ) {
        channelFor(packageName, user).trySend(appContent)
    }

    fun setAppContentSuccess(appContent: List<MediaProjectionAppContent>) {
        setAppContent(Result.success(appContent))
    fun setAppContentSuccess(
        packageName: String,
        user: UserHandle,
        appContent: List<MediaProjectionAppContent>,
    ) {
        setAppContent(packageName, user, Result.success(appContent))
    }

    fun setAppContentSuccess(vararg appContent: MediaProjectionAppContent) {
        setAppContentSuccess(appContent.toList())
    fun setAppContentSuccess(
        packageName: String,
        user: UserHandle,
        vararg appContent: MediaProjectionAppContent,
    ) {
        setAppContentSuccess(packageName, user, appContent.toList())
    }

    fun setAppContentFailure(throwable: Throwable) {
        setAppContent(Result.failure(throwable))
    fun setAppContentFailure(packageName: String, user: UserHandle, throwable: Throwable) {
        setAppContent(packageName, user, Result.failure(throwable))
    }

    private fun channelFor(
        packageName: String,
        user: UserHandle,
    ): Channel<Result<List<MediaProjectionAppContent>>> =
        appContentChannels.computeIfAbsent(packageName to user) { Channel(Channel.CONFLATED) }

    data class AppContentsForCall(
        val packageName: String,
        val user: UserHandle,
Loading