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

Commit 1ccc8d44 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Update show App-to-web education requirements" into main

parents bfc4ee45 3f3065ad
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -145,6 +145,9 @@ public enum DesktopExperienceFlags {
    ENABLE_DESKTOP_TASK_LIMIT_SEPARATE_TRANSITION(
            Flags::enableDesktopTaskLimitSeparateTransition, true,
            Flags.FLAG_ENABLE_DESKTOP_TASK_LIMIT_SEPARATE_TRANSITION),
    ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION_INTEGRATION(
            Flags::enableDesktopWindowingAppToWebEducationIntegration, true,
            Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION_INTEGRATION),
    ENABLE_DESKTOP_WINDOWING_ENTERPRISE_BUGFIX(
            Flags::enableDesktopWindowingEnterpriseBugfix,
            false, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_ENTERPRISE_BUGFIX),
+25 −116
Original line number Diff line number Diff line
@@ -17,134 +17,43 @@
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

/** Interface for storing and retrieving data for app-to-web. */
interface AppToWebRepository {

    /**
 * 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.
     * Updates the most recent show education request timestamp and returns [true] when new request
     * is received.
     */
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)
    }
    fun updateAppToWebEducationRequestTimestamp(
        taskId: Int, latestOpenInBrowserEducationTimestamp: Long
    ): Boolean

    /**
     * Checks if [capturedLink] is available (non-null and has not been used) to use for switching
     * to browser session.
     * Returns true if browser app and valid URI is available to switch to viewing app content
     * on browser.
     */
    fun isCapturedLinkAvailable(): Boolean {
        val link = capturedLink ?: return false
        return !link.used
    }
    suspend fun isBrowserSessionAvailable(taskInfo: RunningTaskInfo): Boolean

    /** Sets the captured link as used. */
    fun onCapturedLinkUsed() {
        capturedLink?.setUsed()
    }
    /** Returns [true] if repository has a saved and unused captured link. */
    fun isCapturedLinkAvailable(taskId: Int): Boolean

    /** Sets the captured link for the given task if a new link is provided. */
    fun setCapturedLink(taskId: Int, link: Uri, timeStamp: Long)

    /** Sets the captured link as used for the given task. */
    fun onCapturedLinkUsed(taskId: Int)

    /**
     * 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.
     * Retrieves the latest webUri and genericLink  for the given task. 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
        }
    }
    suspend fun getAppToWebIntent(taskInfo: RunningTaskInfo, isBrowserApp: Boolean): Intent?
}
+228 −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 android.util.SparseArray
import androidx.core.net.toUri
import androidx.core.util.forEach
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTaskOrganizer.TaskVanishedListener
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.sysui.ShellInit
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 all tasks and
 * creates the intents used to open switch between an app or browser session.
 */
class AppToWebRepositoryImpl(
    private val context: Context,
    private val assistContentRequester: AssistContentRequester,
    private val genericLinksParser: AppToWebGenericLinksParser,
    shellTaskOrganizer: ShellTaskOrganizer,
    shellInit: ShellInit,
) : TaskVanishedListener, AppToWebRepository {
    private var appToWebDataByTask = SparseArray<TaskAppToWebData>()

    init {
        shellInit.addInitCallback(
            { shellTaskOrganizer.addTaskVanishedListener(this) }, this
        )
    }

    override fun onTaskVanished(taskInfo: RunningTaskInfo) {
        logD("Task %d is vanishing. Removing task data from repository", taskInfo.taskId)
        appToWebDataByTask.remove(taskInfo.taskId)
    }

    /** Sets the captured link for the given task if a new link is provided. */
    override fun setCapturedLink(taskId: Int, link: Uri, timeStamp: Long) {
        val taskData = getOrCreateTaskData(taskId)
        if (taskData.capturedLink?.timeStamp == timeStamp) return
        taskData.capturedLink = CapturedLink(link, timeStamp)
    }

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

    /** Sets the captured link as used for the given task. */
    override fun onCapturedLinkUsed(taskId: Int) {
        val taskData = getOrCreateTaskData(taskId)
        taskData.capturedLink?.setUsed()
    }

    /**
     * Records the timestamp of the most recent request to show the App-to-Web education  for the
     * given task and returns [true] if new request is received.
     */
    override fun updateAppToWebEducationRequestTimestamp(
        taskId: Int,
        latestOpenInBrowserEducationTimestamp: Long
    ): Boolean {
        val taskData = getOrCreateTaskData(taskId)
        if (latestOpenInBrowserEducationTimestamp == 0L
            || (latestOpenInBrowserEducationTimestamp == taskData.educationRequestTimestamp)
        ) {
            return false
        }
        logD(
            "Updating education request timestamp with timestamp %d for task %d",
            latestOpenInBrowserEducationTimestamp,
            taskId
        )
        taskData.educationRequestTimestamp = latestOpenInBrowserEducationTimestamp
        return true
    }

    /** Returns true if browser application and [Uri] are available for the given task. */
    override suspend fun isBrowserSessionAvailable(taskInfo: RunningTaskInfo): Boolean {
        logD("Checking for valid browser session for task %d", taskInfo.taskId)
        // If no browser application is available, return false
        context.packageManager.getDefaultBrowserPackageNameAsUser(taskInfo.userId)
            ?: return false

        if (isCapturedLinkAvailable(taskInfo.taskId) || getGenericLink(taskInfo) != null) {
            return true
        }
        val assistContent = assistContentRequester.requestAssistContent(taskInfo.taskId)
        return assistContent?.getSessionWebUri() != null
    }

    /**
     * Retrieves the latest webUri and genericLink  for the given task. 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]
     *
     */
    override suspend fun getAppToWebIntent(
        taskInfo: RunningTaskInfo,
        isBrowserApp: Boolean
    ): Intent? {
        logD("Updating browser links for task %d", taskInfo.taskId)
        val assistContent = assistContentRequester.requestAssistContent(taskInfo.taskId)
        val webUri = assistContent?.getSessionWebUri()
        return if (isBrowserApp) {
            getAppIntent(taskInfo, webUri)
        } else {
            getBrowserIntent(taskInfo, 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(
        taskInfo: RunningTaskInfo,
        webUri: Uri?,
        genericLink: Uri?
    ): Intent? {
        val taskData = getOrCreateTaskData(taskInfo.taskId)
        val browserLink = webUri ?: if (isCapturedLinkAvailable(taskInfo.taskId)) {
            taskData.capturedLink?.uri
        } else {
            genericLink
        } ?: return null
        return getBrowserIntent(browserLink, context.packageManager, taskInfo.userId)
    }

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

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

    private fun getOrCreateTaskData(taskId: Int) =
        appToWebDataByTask[taskId] ?: TaskAppToWebData().also { appToWebDataByTask[taskId] = it }

    /** Dumps the repository's current state. */
    fun dump(originalWriter: PrintWriter, prefix: String) {
        val pw = IndentingPrintWriter(originalWriter, " ", prefix)
        pw.increaseIndent()
        appToWebDataByTask.forEach { key, value ->
            pw.println("AppToWebRepository for task#$key")
            pw.increaseIndent()
            pw.println("CapturedLink=${value.capturedLink}")
            pw.println("EducationRequestTimestamp=${value.educationRequestTimestamp}")
            pw.decreaseIndent()
        }
    }

    private data class TaskAppToWebData(
        var capturedLink: CapturedLink? = null,
        var educationRequestTimestamp: Long = 0L,
    )

    private fun logD(msg: String, vararg arguments: Any?) {
        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
    }

    /** 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
        }
    }

    companion object {
        private const val TAG = "AppToWebRepository"
    }
}
+38 −6
Original line number Diff line number Diff line
@@ -57,6 +57,8 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
import com.android.wm.shell.apptoweb.AppToWebRepository;
import com.android.wm.shell.apptoweb.AppToWebRepositoryImpl;
import com.android.wm.shell.apptoweb.AssistContentRequester;
import com.android.wm.shell.appzoomout.AppZoomOutController;
import com.android.wm.shell.back.BackAnimationController;
@@ -463,6 +465,30 @@ public abstract class WMShellModule {
        return new AppToWebGenericLinksParser(context, mainExecutor, desktopConfig);
    }

    @WMSingleton
    @Provides
    static AppToWebRepositoryImpl provideAppToWebRepositoryImpl(
            Context context, AssistContentRequester assistContentRequester,
            AppToWebGenericLinksParser appToWebGenericLinksParser,
            ShellTaskOrganizer shellTaskOrganizer,
            ShellInit shellInit) {
        return new AppToWebRepositoryImpl(context, assistContentRequester,
                appToWebGenericLinksParser, shellTaskOrganizer, shellInit);
    }

    @WMSingleton
    @Provides
    static AppToWebRepository provideAppToWebRepository(
            AppToWebRepositoryImpl appToWebRepositoryImpl,
            Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel
    ) {
        if (DesktopExperienceFlags.ENABLE_WINDOW_DECORATION_REFACTOR.isTrue()
                || desktopModeWindowDecorViewModel.isEmpty()) {
            return appToWebRepositoryImpl;
        }
        return desktopModeWindowDecorViewModel.get();
    }

    @Provides
    static AssistContentRequester provideAssistContentRequester(
            Context context,
@@ -1232,12 +1258,12 @@ public abstract class WMShellModule {
            RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
            InteractionJankMonitor interactionJankMonitor,
            AppToWebGenericLinksParser genericLinksParser,
            AppToWebRepositoryImpl appToWebRepository,
            AssistContentRequester assistContentRequester,
            WindowDecorViewHostSupplier<WindowDecorViewHost> windowDecorViewHostSupplier,
            MultiInstanceHelper multiInstanceHelper,
            Optional<DesktopTasksLimiter> desktopTasksLimiter,
            AppHandleEducationController appHandleEducationController,
            AppToWebEducationController appToWebEducationController,
            AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper,
            WindowDecorCaptionRepository windowDecorCaptionRepository,
            Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
@@ -1266,8 +1292,8 @@ public abstract class WMShellModule {
                displayInsetsController, syncQueue, transitions, desktopTasksController,
                desktopImmersiveController.get(),
                rootTaskDisplayAreaOrganizer, interactionJankMonitor, genericLinksParser,
                assistContentRequester, windowDecorViewHostSupplier, multiInstanceHelper,
                desktopTasksLimiter, appHandleEducationController, appToWebEducationController,
                appToWebRepository, assistContentRequester, windowDecorViewHostSupplier,
                multiInstanceHelper, desktopTasksLimiter, appHandleEducationController,
                appHandleAndHeaderVisibilityHelper, windowDecorCaptionRepository,
                activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger,
                desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler,
@@ -1705,12 +1731,14 @@ public abstract class WMShellModule {
            Context context,
            AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory,
            DisplayController displayController,
            ShellController shellController,
            @ShellBackgroundThread MainCoroutineDispatcher bgDispatcher
    ) {
        return new DesktopWindowingEducationPromoController(
                context,
                additionalSystemViewContainerFactory,
                displayController,
                shellController,
                bgDispatcher
        );
    }
@@ -1751,8 +1779,11 @@ public abstract class WMShellModule {
    @Provides
    static AppToWebEducationFilter provideAppToWebEducationFilter(
            Context context,
            AppToWebEducationDatastoreRepository appToWebEducationDatastoreRepository) {
        return new AppToWebEducationFilter(context, appToWebEducationDatastoreRepository);
            AppToWebEducationDatastoreRepository appToWebEducationDatastoreRepository,
            AppToWebRepository appToWebRepository
    ) {
        return new AppToWebEducationFilter(
                context, appToWebEducationDatastoreRepository, appToWebRepository);
    }

    @OptIn(markerClass = ExperimentalCoroutinesApi.class)
@@ -1963,7 +1994,8 @@ public abstract class WMShellModule {
            Optional<SystemModalsTransitionHandler> systemModalsTransitionHandler,
            Optional<DisplayDisconnectTransitionHandler> displayDisconnectTransitionHandler,
            Optional<DesktopImeHandler> desktopImeHandler,
            ShellCrashHandler shellCrashHandler) {
            ShellCrashHandler shellCrashHandler,
            AppToWebEducationController appToWebEducationController) {
        return new Object();
    }

+8 −2
Original line number Diff line number Diff line
@@ -65,7 +65,6 @@ sealed class CaptionState {
        val runningTaskInfo: RunningTaskInfo,
        val isHandleMenuExpanded: Boolean,
        val globalAppHandleBounds: Rect,
        val isCapturedLinkAvailable: Boolean,
        val appHandleIdentifier: AppHandleIdentifier,
        override val isFocused: Boolean,
    ) : CaptionState()
@@ -74,7 +73,6 @@ sealed class CaptionState {
        val runningTaskInfo: RunningTaskInfo,
        val isHeaderMenuExpanded: Boolean,
        val globalAppChipBounds: Rect,
        val isCapturedLinkAvailable: Boolean,
        override val isFocused: Boolean,
    ) : CaptionState()

@@ -82,6 +80,14 @@ sealed class CaptionState {
        override val isFocused = false
    }

    /** Returns the [RunningTaskInfo] of the [CaptionState] or null if unavailable. */
    fun getTaskInfo(): RunningTaskInfo? =
        when (this) {
            is AppHandle -> runningTaskInfo
            is AppHeader -> runningTaskInfo
            is NoCaption -> null
        }

    private companion object {
        private const val INVALID_TASK_ID = -1
    }
Loading