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

Commit 1a53ff91 authored by Maryam Dehaini's avatar Maryam Dehaini
Browse files

[7/N] WindowDecor refactor: Create App-to-Web repository

App-to-web info (i.e. captured link, generic link) needs to be shared
across multiple controllers. Creating a repo allows us to do this and
prevents us from needing to duplicate the logic.

Bug: 409648813
Flag: com.android.window.flags.enable_window_decoration_refactor
Test: m
Change-Id: I4c3341a102d9f9b21e2f759f67a3f2e867c90b69
parent 7b2c493f
Loading
Loading
Loading
Loading
+150 −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.wm.shell.apptoweb

import android.app.ActivityManager.RunningTaskInfo
import android.app.assist.AssistContent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.IndentingPrintWriter
import androidx.core.net.toUri
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import java.io.PrintWriter
import kotlin.coroutines.suspendCoroutine

/**
 * App-to-Web has the following features: transferring an app session to the web and transferring
 * a web session to the relevant app. To transfer an app session to the web, we utilize
 * three different [Uri]s:
 * 1. webUri: The web URI provided by the app using [AssistContent]
 * 2. capturedLink: The link used to open the app if app was opened by clicking on a link
 * 3. genericLink: The system provided link for the app
 * In order to create the [Intent] to transfer the user from app to the web, the [Uri]s listed above
 * are checked in the given order and the first non-null link is used. When transferring from the
 * web to an app, the [Uri] must be provided by the browser application through [AssistContent].
 *
 * This Repository encapsulates the data stored for the App-to-Web feature for a single task and
 * creates the intents used to open switch between an app or browser session.
 */
class AppToWebRepository(
    private val userContext: Context,
    private val taskId: Int,
    private val assistContentRequester: AssistContentRequester,
    private val genericLinksParser: AppToWebGenericLinksParser,
) {
    private var capturedLink: CapturedLink? = null

    /** Sets the captured link if a new link is provided. */
    fun setCapturedLink(link: Uri, timeStamp: Long) {
        if (capturedLink?.timeStamp == timeStamp) return
        capturedLink = CapturedLink(link, timeStamp)
    }

    /**
     * Checks if [capturedLink] is available (non-null and has not been used) to use for switching
     * to browser session.
     */
    fun isCapturedLinkAvailable(): Boolean {
        val link = capturedLink ?: return false
        return !link.used
    }

    /** Sets the captured link as used. */
    fun onCapturedLinkUsed() {
        capturedLink?.setUsed()
    }

    /**
     * Retrieves the latest webUri and genericLink. If the task requesting the intent
     * [isBrowserApp], intent is created to switch to application if link was provided by browser
     * app and a relevant application exists to host the app. Otherwise, returns intent to switch
     * to browser if webUri, capturedLink, or genericLink is available.
     *
     * Note that the capturedLink should be updated separately using [setCapturedLink]
     *
     */
    suspend fun getAppToWebIntent(taskInfo: RunningTaskInfo, isBrowserApp: Boolean): Intent? {
        ProtoLog.d(
            WM_SHELL_DESKTOP_MODE,
            "AppToWebRepository: Updating browser links for task $taskId"
        )
        val assistContent = assistContentRequester.requestAssistContent(taskInfo.taskId)
        val webUri = assistContent?.getSessionWebUri()
        return if (isBrowserApp) {
            getAppIntent(webUri)
        } else {
            getBrowserIntent(webUri, getGenericLink(taskInfo))
        }
    }

    private suspend fun AssistContentRequester.requestAssistContent(taskId: Int): AssistContent? =
        suspendCoroutine { continuation ->
            requestAssistContent(taskId) { continuation.resumeWith(Result.success(it)) }
        }

    /** Returns the browser link associated with the given application if available. */
    private fun getBrowserIntent(webUri: Uri?, genericLink: Uri?): Intent? {
        val browserLink = webUri ?: if (isCapturedLinkAvailable()) {
            capturedLink?.uri
        } else {
            genericLink
        } ?: return null
        return getBrowserIntent(browserLink, userContext.packageManager, userContext.userId)
    }

    private fun getAppIntent(webUri: Uri?): Intent? {
        webUri ?: return null
        return getAppIntent(
            uri = webUri,
            packageManager = userContext.packageManager,
            userId = userContext.userId
        )
    }


    private fun getGenericLink(taskInfo: RunningTaskInfo): Uri? {
        ProtoLog.d(
            WM_SHELL_DESKTOP_MODE,
            "AppToWebRepository: Updating generic link for task %d",
            taskId
        )
        val baseActivity = taskInfo.baseActivity ?: return null
        return genericLinksParser.getGenericLink(baseActivity.packageName)?.toUri()
    }

    /** Dumps the repository's current state. */
    fun dump(originalWriter: PrintWriter, prefix: String) {
        val pw = IndentingPrintWriter(originalWriter, " ", prefix)
        pw.println("AppToWebRepository for task#$taskId")
        pw.increaseIndent()
        pw.println("CapturedLink=$capturedLink")
    }

    /** Encapsulates data associated with a captured link. */
    private data class CapturedLink(val uri: Uri, val timeStamp: Long) {

        /** Signifies if captured link has already been used, making it invalid. */
        var used = false

        /** Sets the captured link as used. */
        fun setUsed() {
            used = true
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -39,7 +39,7 @@ class AssistContentRequester(
    private val callBackExecutor: Executor,
    private val systemInteractionExecutor: Executor
) {
    interface Callback {
    fun interface Callback {
        // Called when the [AssistContent] of the requested task is available.
        fun onAssistContentAvailable(assistContent: AssistContent?)
    }
+184 −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.wm.shell.apptoweb

import android.app.assist.AssistContent
import android.content.ComponentName
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.test.mock.MockContext
import android.testing.AndroidTestingRunner
import androidx.core.net.toUri
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.util.createTaskInfo
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.whenever
import kotlin.test.assertEquals
import kotlin.test.assertFalse

/**
 * Tests for [AppToWebRepository].
 *
 * Build/Install/Run: atest WMShellUnitTests:AppToWebRepositoryTests
 */
@SmallTest
@RunWith(AndroidTestingRunner::class)
class AppToWebRepositoryTests : ShellTestCase() {

    @Mock private lateinit var mockAssistContentRequester: AssistContentRequester
    @Mock private lateinit var mockGenericLinksParser: AppToWebGenericLinksParser
    @Mock private lateinit var mockAssistContent: AssistContent
    @Mock private lateinit var mockContext: MockContext
    @Mock private lateinit var mockPackageManager: PackageManager

    private lateinit var mockInit: AutoCloseable
    private lateinit var appToWebRepository: AppToWebRepository
    private val taskInfo = createTaskInfo().apply {
        taskId = TEST_TASK_ID
        baseActivity = ComponentName("appToWeb", "testActivity")
    }
    private val resolveInfo = ResolveInfo().apply {
        activityInfo = ActivityInfo()
        activityInfo.packageName = "appToWeb"
        activityInfo.name = "testActivity"
    }

    @Before
    fun setUp() {
        mockInit = MockitoAnnotations.openMocks(this)
        whenever(mockContext.packageManager).thenReturn(mockPackageManager)
        whenever(mockPackageManager.resolveActivityAsUser(any(), anyInt(), anyInt()))
            .thenReturn(resolveInfo)
        appToWebRepository = AppToWebRepository(
            mockContext,
            TEST_TASK_ID,
            mockAssistContentRequester,
            mockGenericLinksParser
        )
    }

    @After
    fun tearDown() {
        mockInit.close()
    }

    @Test
    fun capturedLink_unavailableAfterUse() {
        appToWebRepository.setCapturedLink(
            link = TEST_CAPTURED_URI,
            timeStamp = 100
        )
        appToWebRepository.onCapturedLinkUsed()
        assertFalse(appToWebRepository.isCapturedLinkAvailable())
    }

    @Test
    fun capturedLink_sameCapturedLinkNotSavedAgain() {
        val timeStamp = 100L
        appToWebRepository.setCapturedLink(TEST_CAPTURED_URI, timeStamp)
        appToWebRepository.onCapturedLinkUsed()
        // Set the same captured link (same timeStamp)
        appToWebRepository.setCapturedLink(TEST_CAPTURED_URI, timeStamp)
        assertFalse(appToWebRepository.isCapturedLinkAvailable())
    }

    @Test
    fun webUri_prioritizedOverCapturedAndGenericLinks() = runTest {
        // Set captured link
        appToWebRepository.setCapturedLink(
            link = TEST_CAPTURED_URI,
            timeStamp = 100
        )
        // Set web Uri
        whenever(mockAssistContent.getSessionWebUri()).thenReturn(TEST_WEB_URI)
        mockAssistContent.webUri = TEST_WEB_URI
        // Set generic link
        whenever(mockGenericLinksParser.getGenericLink(any())).thenReturn(TEST_GENERIC_LINK)

        assertAppToWebIntent(TEST_WEB_URI)
    }

    @Test
    fun capturedLink_prioritizedOverGenericLink() = runTest {
        // Set captured link
        appToWebRepository.setCapturedLink(
            link = TEST_CAPTURED_URI,
            timeStamp = 100
        )
        // Set generic link
        whenever(mockGenericLinksParser.getGenericLink(any())).thenReturn(TEST_GENERIC_LINK)

        assertAppToWebIntent(TEST_CAPTURED_URI)
    }

    @Test
    fun genericLink_utilizedWhenAllOtherLinksAreUnavailable() = runTest {
        // Set generic link
        whenever(mockGenericLinksParser.getGenericLink(any())).thenReturn(TEST_GENERIC_LINK)

        assertAppToWebIntent(TEST_GENERIC_LINK.toUri())
    }

    @Test
    fun webUri_usedWhenIsBrowserApp() = runTest {
        // Set web Uri
        whenever(mockAssistContent.getSessionWebUri()).thenReturn(TEST_WEB_URI)

        assertAppToWebIntent(
            expectedUri = TEST_WEB_URI,
            isBrowserApp = true
        )
    }

    private suspend fun assertAppToWebIntent(expectedUri: Uri, isBrowserApp: Boolean = false) {
        whenever(
            mockAssistContentRequester.requestAssistContent(
                anyInt(),
                any()
            )
        ).thenAnswer { invocation ->
            val callback = invocation.arguments[1] as AssistContentRequester.Callback
            callback.onAssistContentAvailable(mockAssistContent)
        }

        assertEquals(
            expectedUri,
            appToWebRepository.getAppToWebIntent(
                taskInfo = taskInfo,
                isBrowserApp = isBrowserApp,
            )?.data
        )
    }

    private companion object {
        private const val TEST_TASK_ID = 1
        private val TEST_WEB_URI = Uri.parse("http://www.youtube.com")
        private val TEST_CAPTURED_URI = Uri.parse("www.google.com")
        private const val TEST_GENERIC_LINK = "http://www.gmail.com"
    }
}