Loading core/java/android/window/DesktopExperienceFlags.java +3 −0 Original line number Diff line number Diff line Loading @@ -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), Loading libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebRepository.kt +25 −116 Original line number Diff line number Diff line Loading @@ -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? } libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebRepositoryImpl.kt 0 → 100644 +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" } } libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +38 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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 ); } Loading Loading @@ -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) Loading Loading @@ -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(); } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionRepository.kt +8 −2 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -74,7 +73,6 @@ sealed class CaptionState { val runningTaskInfo: RunningTaskInfo, val isHeaderMenuExpanded: Boolean, val globalAppChipBounds: Rect, val isCapturedLinkAvailable: Boolean, override val isFocused: Boolean, ) : CaptionState() Loading @@ -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 Loading
core/java/android/window/DesktopExperienceFlags.java +3 −0 Original line number Diff line number Diff line Loading @@ -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), Loading
libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebRepository.kt +25 −116 Original line number Diff line number Diff line Loading @@ -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? }
libs/WindowManager/Shell/src/com/android/wm/shell/apptoweb/AppToWebRepositoryImpl.kt 0 → 100644 +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" } }
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +38 −6 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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, Loading Loading @@ -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 ); } Loading Loading @@ -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) Loading Loading @@ -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(); } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionRepository.kt +8 −2 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -74,7 +73,6 @@ sealed class CaptionState { val runningTaskInfo: RunningTaskInfo, val isHeaderMenuExpanded: Boolean, val globalAppChipBounds: Rect, val isCapturedLinkAvailable: Boolean, override val isFocused: Boolean, ) : CaptionState() Loading @@ -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