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

Commit 3f3065ad authored by Maryam Dehaini's avatar Maryam Dehaini
Browse files

Update show App-to-web education requirements

If the education is requested via the app, show the education if:
1. Education view limit is not reached
2. No other education is shown
3. Is not a browser app
4. Browser app and uri are available

If not requested, show if:
1. Education view limit is not reached
2. No other education is shown
3. Is not a browser app
4. Browser app and uri are available
5. Enough time has been passed since setup
6. Feature has not been used before
7. Is in allowlist of apps that allow education to show
8. App was opened via captured link

Test: request edu using test app + adb shell setprop persist.wm.debug.remove_app_to_web_education_limit_for_testing true
Bug: 359226240
Flag: com.android.window.flags.enable_desktop_windowing_app_to_web_education_integration

Change-Id: I533bc4a0ea8cc3b8d338eff17b2c092b771ebed7
parent 6a1789fb
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