summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_windowing_education_promo_background.xml22
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_windowing_education_promo.xml36
-rw-r--r--libs/WindowManager/Shell/res/values/config.xml2
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml7
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java52
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt207
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt92
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt117
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationPromoController.kt231
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt10
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt2
22 files changed, 856 insertions, 32 deletions
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_promo_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_promo_background.xml
new file mode 100644
index 000000000000..645d24df7c26
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_promo_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<shape android:shape="rectangle"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <solid android:color="?androidprv:attr/materialColorSurfaceContainerLow" />
+ <corners android:radius="@dimen/desktop_windowing_education_promo_corner_radius" />
+</shape>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_promo.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_promo.xml
new file mode 100644
index 000000000000..eebfd4bb595e
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_promo.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/education_container"
+ android:layout_width="@dimen/desktop_windowing_education_promo_width"
+ android:layout_height="@dimen/desktop_windowing_education_promo_height"
+ android:elevation="1dp"
+ android:orientation="vertical"
+ android:paddingHorizontal="32dp"
+ android:paddingVertical="24dp"
+ android:background="@drawable/desktop_windowing_education_promo_background">
+ <TextView
+ android:id="@+id/education_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="24sp"
+ android:lineHeight="32dp"
+ android:textFontWeight="400"
+ android:fontFamily="google-sans-text"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center"/>
+</LinearLayout>
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index a14461a57a95..59e6c7786cb8 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -186,4 +186,6 @@
<!-- Apps that can trigger Desktop Windowing App handle Education -->
<string-array name="desktop_windowing_app_handle_education_allowlist_apps"></string-array>
+ <!-- Apps that can trigger Desktop Windowing App-To-Web Education -->
+ <string-array name="desktop_windowing_app_to_web_education_allowlist_apps"></string-array>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index fa1aa193e1e3..34f950c0a326 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -650,4 +650,11 @@
<dimen name="open_by_default_settings_dialog_radio_button_height">64dp</dimen>
<!-- The width of radio buttons in the open by default settings dialog. -->
<dimen name="open_by_default_settings_dialog_radio_button_width">316dp</dimen>
+
+ <!-- The width of the desktop windowing education promo. -->
+ <dimen name="desktop_windowing_education_promo_width">412dp</dimen>
+ <!-- The height of the desktop windowing education promo. -->
+ <dimen name="desktop_windowing_education_promo_height">352dp</dimen>
+ <!-- The corner radius of the desktop windowing education promo. -->
+ <dimen name="desktop_windowing_education_promo_corner_radius">28dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index ef0386a1b4dd..52585d4ead35 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -339,4 +339,7 @@
<string name="open_by_default_dialog_in_browser_text">In your browser</string>
<!-- Text for open by default settings dialog dismiss button. -->
<string name="open_by_default_dialog_dismiss_button_text">OK</string>
+
+ <!-- Text for the App-to-Web education promo. -->
+ <string name="desktop_windowing_app_to_web_education_text">Quickly open apps in your browser here</string>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 817be3b1fe0d..fec4c16992a5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -86,7 +86,10 @@ import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter;
+import com.android.wm.shell.desktopmode.education.AppToWebEducationController;
+import com.android.wm.shell.desktopmode.education.AppToWebEducationFilter;
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
+import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository;
import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository;
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer;
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializerImpl;
@@ -132,6 +135,7 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController;
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController;
import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel;
@@ -288,6 +292,7 @@ public abstract class WMShellModule {
MultiInstanceHelper multiInstanceHelper,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
+ AppToWebEducationController appToWebEducationController,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler,
FocusTransitionObserver focusTransitionObserver,
@@ -317,6 +322,7 @@ public abstract class WMShellModule {
multiInstanceHelper,
desktopTasksLimiter,
appHandleEducationController,
+ appToWebEducationController,
windowDecorCaptionHandleRepository,
desktopActivityOrientationHandler,
focusTransitionObserver,
@@ -1040,6 +1046,20 @@ public abstract class WMShellModule {
context, additionalSystemViewContainerFactory, displayController);
}
+ @WMSingleton
+ @Provides
+ static DesktopWindowingEducationPromoController provideDesktopWindowingEducationPromoController(
+ Context context,
+ AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory,
+ DisplayController displayController
+ ) {
+ return new DesktopWindowingEducationPromoController(
+ context,
+ additionalSystemViewContainerFactory,
+ displayController
+ );
+ }
+
@OptIn(markerClass = ExperimentalCoroutinesApi.class)
@WMSingleton
@Provides
@@ -1063,6 +1083,38 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
+ static AppToWebEducationDatastoreRepository provideAppToWebEducationDatastoreRepository(
+ Context context) {
+ return new AppToWebEducationDatastoreRepository(context);
+ }
+
+ @WMSingleton
+ @Provides
+ static AppToWebEducationFilter provideAppToWebEducationFilter(
+ Context context,
+ AppToWebEducationDatastoreRepository appToWebEducationDatastoreRepository) {
+ return new AppToWebEducationFilter(context, appToWebEducationDatastoreRepository);
+ }
+
+ @OptIn(markerClass = ExperimentalCoroutinesApi.class)
+ @WMSingleton
+ @Provides
+ static AppToWebEducationController provideAppToWebEducationController(
+ Context context,
+ AppToWebEducationFilter appToWebEducationFilter,
+ AppToWebEducationDatastoreRepository appToWebEducationDatastoreRepository,
+ WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
+ DesktopWindowingEducationPromoController desktopWindowingEducationPromoController,
+ @ShellMainThread CoroutineScope applicationScope,
+ @ShellBackgroundThread MainCoroutineDispatcher backgroundDispatcher) {
+ return new AppToWebEducationController(context, appToWebEducationFilter,
+ appToWebEducationDatastoreRepository, windowDecorCaptionHandleRepository,
+ desktopWindowingEducationPromoController, applicationScope,
+ backgroundDispatcher);
+ }
+
+ @WMSingleton
+ @Provides
static DesktopPersistentRepository provideDesktopPersistentRepository(
Context context, @ShellBackgroundThread CoroutineScope bgScope) {
return new DesktopPersistentRepository(context, bgScope);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt
index 7ae537088832..8bfcca093855 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt
@@ -18,6 +18,10 @@ package com.android.wm.shell.desktopmode
import android.app.ActivityManager.RunningTaskInfo
import android.graphics.Rect
+import com.android.wm.shell.desktopmode.CaptionState.AppHandle
+import com.android.wm.shell.desktopmode.CaptionState.AppHeader
+import com.android.wm.shell.desktopmode.CaptionState.NoCaption
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -26,11 +30,20 @@ class WindowDecorCaptionHandleRepository {
private val _captionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption)
/** Observer for app handle state changes. */
val captionStateFlow: StateFlow<CaptionState> = _captionStateFlow
+ private val _appToWebUsageFlow = MutableSharedFlow<Unit>()
+ /** Observer for App-to-Web usage. */
+ val appToWebUsageFlow = _appToWebUsageFlow
+
/** Notifies [captionStateFlow] if there is a change to caption state. */
fun notifyCaptionChanged(captionState: CaptionState) {
_captionStateFlow.value = captionState
}
+
+ /** Notifies [appToWebUsageFlow] if App-to-Web feature is used. */
+ fun onAppToWebUsage() {
+ _appToWebUsageFlow.tryEmit(Unit)
+ }
}
/**
@@ -41,17 +54,19 @@ class WindowDecorCaptionHandleRepository {
* * [AppHeader]: Indicating that there is at least one visible app chip on the screen.
* * [NoCaption]: Signifying that no caption handle is currently visible on the device.
*/
-sealed class CaptionState {
+sealed class CaptionState{
data class AppHandle(
- val runningTaskInfo: RunningTaskInfo,
- val isHandleMenuExpanded: Boolean,
- val globalAppHandleBounds: Rect
+ val runningTaskInfo: RunningTaskInfo,
+ val isHandleMenuExpanded: Boolean,
+ val globalAppHandleBounds: Rect,
+ val isCapturedLinkAvailable: Boolean
) : CaptionState()
data class AppHeader(
- val runningTaskInfo: RunningTaskInfo,
- val isHeaderMenuExpanded: Boolean,
- val globalAppChipBounds: Rect
+ val runningTaskInfo: RunningTaskInfo,
+ val isHeaderMenuExpanded: Boolean,
+ val globalAppChipBounds: Rect,
+ val isCapturedLinkAvailable: Boolean
) : CaptionState()
data object NoCaption : CaptionState()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
index f21a124f0b8b..e01c448be8e5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt
@@ -36,7 +36,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
import com.android.wm.shell.windowdecor.common.DecorThemeUtil
import com.android.wm.shell.windowdecor.common.Theme
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController
-import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.EducationViewConfig
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipEducationViewConfig
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipColorScheme
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
@@ -137,7 +137,7 @@ class AppHandleEducationController(
// TODO: b/370546801 - Differentiate between user dismissing the tooltip vs following the cue.
// Populate information important to inflate app handle education tooltip.
val appHandleTooltipConfig =
- EducationViewConfig(
+ TooltipEducationViewConfig(
tooltipViewLayout = R.layout.desktop_windowing_education_top_arrow_tooltip,
tooltipColorScheme = tooltipColorScheme,
tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
@@ -196,7 +196,7 @@ class AppHandleEducationController(
windowingOptionPillHeight / 2)
// Populate information important to inflate windowing image button education tooltip.
val windowingImageButtonTooltipConfig =
- EducationViewConfig(
+ TooltipEducationViewConfig(
tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
tooltipColorScheme = tooltipColorScheme,
tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
@@ -249,7 +249,7 @@ class AppHandleEducationController(
globalAppChipBounds.top + globalAppChipBounds.height() / 2)
// Populate information important to inflate exit desktop mode education tooltip.
val exitWindowingTooltipConfig =
- EducationViewConfig(
+ TooltipEducationViewConfig(
tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip,
tooltipColorScheme = tooltipColorScheme,
tooltipViewGlobalCoordinates = tooltipGlobalCoordinates,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt
new file mode 100644
index 000000000000..693da81ec4a0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2024 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.desktopmode.education
+
+import android.annotation.DimenRes
+import android.annotation.StringRes
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Point
+import android.os.SystemProperties
+import androidx.compose.ui.graphics.toArgb
+import com.android.window.flags.Flags
+import com.android.wm.shell.R
+import com.android.wm.shell.desktopmode.CaptionState
+import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
+import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode
+import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController.EducationColorScheme
+import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController.EducationViewConfig
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.MainCoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/**
+ * Controls App-to-Web education end to end.
+ *
+ * Listen to usages of App-to-Web, calls an api to check if the education
+ * should be shown and controls education UI.
+ */
+@OptIn(kotlinx.coroutines.FlowPreview::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class AppToWebEducationController(
+ private val context: Context,
+ private val appToWebEducationFilter: AppToWebEducationFilter,
+ private val appToWebEducationDatastoreRepository: AppToWebEducationDatastoreRepository,
+ private val windowDecorCaptionHandleRepository: WindowDecorCaptionHandleRepository,
+ private val windowingEducationViewController: DesktopWindowingEducationPromoController,
+ @ShellMainThread private val applicationCoroutineScope: CoroutineScope,
+ @ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher,
+) {
+ private val decorThemeUtil = DecorThemeUtil(context)
+
+ init {
+ runIfEducationFeatureEnabled {
+ applicationCoroutineScope.launch {
+ // Central block handling the App-to-Web's educational flow end-to-end.
+ isEducationViewLimitReachedFlow()
+ .flatMapLatest { countExceedsMaximum ->
+ if (countExceedsMaximum) {
+ // If the education has been viewed the maximum amount of times then
+ // return emptyFlow() that completes immediately. This will help us to
+ // not listen to [captionHandleStateFlow] after the education should
+ // not be shown.
+ emptyFlow()
+ } else {
+ // Listen for changes to window decor's caption.
+ windowDecorCaptionHandleRepository.captionStateFlow
+ // Wait for few seconds before emitting the latest state.
+ .debounce(APP_TO_WEB_EDUCATION_DELAY_MILLIS)
+ .filter { captionState ->
+ captionState !is CaptionState.NoCaption &&
+ appToWebEducationFilter
+ .shouldShowAppToWebEducation(captionState)
+ }
+ }
+ }
+ .flowOn(backgroundDispatcher)
+ .collectLatest { captionState ->
+ val educationColorScheme = educationColorScheme(captionState)
+ showEducation(captionState, educationColorScheme!!)
+ // After showing first tooltip, increase count of education views
+ appToWebEducationDatastoreRepository.updateEducationShownCount()
+ }
+ }
+
+ applicationCoroutineScope.launch {
+ if (isFeatureUsed()) return@launch
+ windowDecorCaptionHandleRepository.appToWebUsageFlow
+ .collect {
+ // If user utilizes App-to-Web, mark user has used the feature
+ appToWebEducationDatastoreRepository
+ .updateFeatureUsedTimestampMillis(isViewed = true)
+ }
+ }
+ }
+ }
+
+ private inline fun runIfEducationFeatureEnabled(block: () -> Unit) {
+ if (canEnterDesktopMode(context) && Flags.enableDesktopWindowingAppToWebEducation()) block()
+ }
+
+ private fun showEducation(captionState: CaptionState, colorScheme: EducationColorScheme) {
+ val educationGlobalCoordinates: Point
+ val taskId: Int
+ when (captionState) {
+ is CaptionState.AppHandle -> {
+ val appHandleBounds = captionState.globalAppHandleBounds
+ val educationWidth =
+ loadDimensionPixelSize(R.dimen.desktop_windowing_education_promo_width)
+ educationGlobalCoordinates = Point(
+ appHandleBounds.centerX() - educationWidth / 2,
+ appHandleBounds.bottom
+ )
+ taskId = captionState.runningTaskInfo.taskId
+ }
+
+ is CaptionState.AppHeader -> {
+ val taskBounds =
+ captionState.runningTaskInfo.configuration.windowConfiguration.bounds
+ educationGlobalCoordinates =
+ Point(taskBounds.left, captionState.globalAppChipBounds.bottom)
+ taskId = captionState.runningTaskInfo.taskId
+ }
+
+ else -> return
+ }
+
+ // Populate information important to inflate education promo.
+ val educationConfig =
+ EducationViewConfig(
+ viewLayout = R.layout.desktop_windowing_education_promo,
+ educationColorScheme = colorScheme,
+ viewGlobalCoordinates = educationGlobalCoordinates,
+ educationText = getString(R.string.desktop_windowing_app_to_web_education_text),
+ widthId = R.dimen.desktop_windowing_education_promo_width,
+ heightId = R.dimen.desktop_windowing_education_promo_height
+ )
+
+ windowingEducationViewController.showEducation(
+ viewConfig = educationConfig, taskId = taskId)
+ }
+
+ private fun educationColorScheme(captionState: CaptionState): EducationColorScheme? {
+ val taskInfo: RunningTaskInfo = when (captionState) {
+ is CaptionState.AppHandle -> captionState.runningTaskInfo
+ is CaptionState.AppHeader -> captionState.runningTaskInfo
+ else -> return null
+ }
+
+ val colorScheme = decorThemeUtil.getColorScheme(taskInfo)
+ val tooltipContainerColor = colorScheme.surfaceBright.toArgb()
+ val tooltipTextColor = colorScheme.onSurface.toArgb()
+ return EducationColorScheme(tooltipContainerColor, tooltipTextColor)
+ }
+
+ /**
+ * Listens to changes in the number of times the education has been viewed, mapping the count to
+ * true if the education has been viewed the maximum amount of times.
+ */
+ private fun isEducationViewLimitReachedFlow(): Flow<Boolean> =
+ appToWebEducationDatastoreRepository.dataStoreFlow
+ .map { preferences ->
+ appToWebEducationFilter.isEducationViewLimitReached(preferences)}
+ .distinctUntilChanged()
+
+ /**
+ * Listens to the changes to [WindowingEducationProto#hasFeatureUsedTimestampMillis()] in
+ * datastore proto object.
+ */
+ private suspend fun isFeatureUsed(): Boolean =
+ appToWebEducationDatastoreRepository.dataStoreFlow.first().hasFeatureUsedTimestampMillis()
+
+ private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int {
+ if (resourceId == Resources.ID_NULL) return 0
+ return context.resources.getDimensionPixelSize(resourceId)
+ }
+
+ private fun getString(@StringRes resId: Int): String = context.resources.getString(resId)
+
+ companion object {
+ const val TAG = "AppToWebEducationController"
+ val APP_TO_WEB_EDUCATION_DELAY_MILLIS: Long
+ get() = SystemProperties.getLong(
+ "persist.windowing_app_handle_education_delay",
+ 3000L
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt
new file mode 100644
index 000000000000..feee6ed86da1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 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.desktopmode.education
+
+import android.annotation.IntegerRes
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.os.SystemClock
+import android.provider.Settings.Secure
+import com.android.wm.shell.R
+import com.android.wm.shell.desktopmode.CaptionState
+import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository
+import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto
+import java.time.Duration
+
+/** Filters incoming App-to-Web education triggers based on set conditions. */
+class AppToWebEducationFilter(
+ private val context: Context,
+ private val appToWebEducationDatastoreRepository: AppToWebEducationDatastoreRepository
+) {
+
+ /** Returns true if conditions to show App-to-web education are met, returns false otherwise. */
+ suspend fun shouldShowAppToWebEducation(captionState: CaptionState): Boolean {
+ val (taskInfo: RunningTaskInfo, isCapturedLinkAvailable: Boolean) = when (captionState) {
+ is CaptionState.AppHandle ->
+ Pair(captionState.runningTaskInfo, captionState.isCapturedLinkAvailable)
+ is CaptionState.AppHeader ->
+ Pair(captionState.runningTaskInfo, captionState.isCapturedLinkAvailable)
+ else -> return false
+ }
+
+ val focusAppPackageName = taskInfo.topActivityInfo?.packageName ?: return false
+ val windowingEducationProto = appToWebEducationDatastoreRepository.windowingEducationProto()
+
+ return !isOtherEducationShowing() &&
+ !isEducationViewLimitReached(windowingEducationProto) &&
+ hasSufficientTimeSinceSetup() &&
+ !isFeatureUsedBefore(windowingEducationProto) &&
+ isCapturedLinkAvailable &&
+ isFocusAppInAllowlist(focusAppPackageName)
+ }
+
+ private fun isFocusAppInAllowlist(focusAppPackageName: String): Boolean =
+ focusAppPackageName in
+ context.resources.getStringArray(
+ R.array.desktop_windowing_app_to_web_education_allowlist_apps)
+
+ // TODO: b/350953004 - Add checks based on App compat
+ // TODO: b/350951797 - Add checks based on PKT tips education
+ private fun isOtherEducationShowing(): Boolean = isTaskbarEducationShowing() ||
+ isCompatUiEducationShowing()
+
+ private fun isTaskbarEducationShowing(): Boolean =
+ Secure.getInt(context.contentResolver, Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) == 1
+
+ private fun isCompatUiEducationShowing(): Boolean =
+ Secure.getInt(context.contentResolver, Secure.COMPAT_UI_EDUCATION_SHOWING, 0) == 1
+
+ private fun hasSufficientTimeSinceSetup(): Boolean =
+ Duration.ofMillis(SystemClock.elapsedRealtime()) >
+ convertIntegerResourceToDuration(
+ R.integer.desktop_windowing_education_required_time_since_setup_seconds)
+
+ /** Returns true if education is viewed maximum amount of times it should be shown. */
+ fun isEducationViewLimitReached(windowingEducationProto: WindowingEducationProto): Boolean =
+ windowingEducationProto.getAppToWebEducation().getEducationShownCount() >=
+ MAXIMUM_TIMES_EDUCATION_SHOWN
+
+ private fun isFeatureUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean =
+ windowingEducationProto.hasFeatureUsedTimestampMillis()
+
+ private fun convertIntegerResourceToDuration(@IntegerRes resourceId: Int): Duration =
+ Duration.ofSeconds(context.resources.getInteger(resourceId).toLong())
+
+ companion object {
+ private const val MAXIMUM_TIMES_EDUCATION_SHOWN = 100
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt
new file mode 100644
index 000000000000..8be6e6dff6fe
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 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.desktopmode.education.data
+
+import android.content.Context
+import android.util.Slog
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.core.Serializer
+import androidx.datastore.dataStoreFile
+import com.android.framework.protobuf.InvalidProtocolBufferException
+import com.android.internal.annotations.VisibleForTesting
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.first
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+
+/** Updates data in App-to-Web's education datastore. */
+class AppToWebEducationDatastoreRepository
+@VisibleForTesting
+constructor(private val dataStore: DataStore<WindowingEducationProto>) {
+ constructor(
+ context: Context
+ ) : this(
+ DataStoreFactory.create(
+ serializer = WindowingEducationProtoSerializer,
+ produceFile = { context.dataStoreFile(APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH) }))
+
+ /** Provides dataStore.data flow and handles exceptions thrown during collection */
+ val dataStoreFlow: Flow<WindowingEducationProto> =
+ dataStore.data.catch { exception ->
+ // dataStore.data throws an IOException when an error is encountered when reading data
+ if (exception is IOException) {
+ Slog.e(
+ TAG,
+ "Error in reading App-to-Web education related data from datastore," +
+ "data is stored in a file named" +
+ "$APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH", exception)
+ } else {
+ throw exception
+ }
+ }
+
+ /**
+ * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the
+ * DataStore is empty or there's an error reading, it returns the default value of Proto.
+ */
+ suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first()
+
+ /**
+ * Updates [WindowingEducationProto.featureUsedTimestampMillis_] field in datastore with current
+ * timestamp if [isViewed] is true, if not then clears the field.
+ */
+ suspend fun updateFeatureUsedTimestampMillis(isViewed: Boolean) {
+ dataStore.updateData { preferences ->
+ if (isViewed) {
+ preferences
+ .toBuilder().setFeatureUsedTimestampMillis(System.currentTimeMillis()).build()
+ } else {
+ preferences.toBuilder().clearFeatureUsedTimestampMillis().build()
+ }
+ }
+ }
+
+ /**
+ * Increases [AppToWebEducation.educationShownCount] field by one.
+ */
+ suspend fun updateEducationShownCount() {
+ val currentAppHandleProto = windowingEducationProto().appToWebEducation.toBuilder()
+ currentAppHandleProto
+ .setEducationShownCount(currentAppHandleProto.getEducationShownCount() + 1)
+ dataStore.updateData { preferences ->
+ preferences.toBuilder().setAppToWebEducation(currentAppHandleProto).build()
+ }
+ }
+
+
+ companion object {
+ private const val TAG = "AppToWebEducationDatastoreRepository"
+ private const val APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH = "app_to_web_education.pb"
+
+ object WindowingEducationProtoSerializer : Serializer<WindowingEducationProto> {
+
+ override val defaultValue: WindowingEducationProto =
+ WindowingEducationProto.getDefaultInstance()
+
+ override suspend fun readFrom(input: InputStream): WindowingEducationProto =
+ try {
+ WindowingEducationProto.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read proto.", exception)
+ }
+
+ override suspend fun writeTo(
+ windowingProto: WindowingEducationProto,
+ output: OutputStream
+ ) = windowingProto.writeTo(output)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto
index d29ec53d9c61..4cddd01ee96b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto
@@ -28,6 +28,8 @@ message WindowingEducationProto {
oneof education_data {
// Fields specific to app handle education
AppHandleEducation app_handle_education = 3;
+ // Fields specific to App-to-Web education
+ AppToWebEducation app_to_web_education = 4;
}
message AppHandleEducation {
@@ -36,4 +38,9 @@ message WindowingEducationProto {
// Timestamp of when app_usage_stats was last cached
optional int64 app_usage_stats_last_update_timestamp_millis = 2;
}
+
+ message AppToWebEducation {
+ // Number of times education is shown
+ optional int64 education_shown_count = 1;
+ }
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 29b8ddd03970..67775f700be4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -113,6 +113,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.education.AppHandleEducationController;
+import com.android.wm.shell.desktopmode.education.AppToWebEducationController;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
@@ -176,6 +177,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository;
private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter;
private final AppHandleEducationController mAppHandleEducationController;
+ private final AppToWebEducationController mAppToWebEducationController;
private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory;
private boolean mTransitionDragActive;
@@ -249,6 +251,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
MultiInstanceHelper multiInstanceHelper,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
+ AppToWebEducationController appToWebEducationController,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
FocusTransitionObserver focusTransitionObserver,
@@ -282,6 +285,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
interactionJankMonitor,
desktopTasksLimiter,
appHandleEducationController,
+ appToWebEducationController,
windowDecorCaptionHandleRepository,
activityOrientationChangeHandler,
new TaskPositionerFactory(),
@@ -319,6 +323,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
InteractionJankMonitor interactionJankMonitor,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
+ AppToWebEducationController appToWebEducationController,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
TaskPositionerFactory taskPositionerFactory,
@@ -354,6 +359,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mInteractionJankMonitor = interactionJankMonitor;
mDesktopTasksLimiter = desktopTasksLimiter;
mAppHandleEducationController = appHandleEducationController;
+ mAppToWebEducationController = appToWebEducationController;
mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
mActivityOrientationChangeHandler = activityOrientationChangeHandler;
mAssistContentRequester = assistContentRequester;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index c88ac3d19635..af7cd0584c17 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -509,7 +509,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop,
boolean hasGlobalFocus) {
Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces");
-
if (Flags.enableDesktopWindowingAppToWeb()) {
setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp);
}
@@ -551,7 +550,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
if (mResult.mRootView == null) {
// This means something blocks the window decor from showing, e.g. the task is hidden.
// Nothing is set up in this case including the decoration surface.
- if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ if (canEnterDesktopMode(mContext) && isEducationEnabled()) {
notifyNoCaptionHandle();
}
mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId);
@@ -570,7 +569,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
if (isAppHandle(mWindowDecorViewHolder)) {
position.set(determineHandlePosition());
}
- if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ if (canEnterDesktopMode(mContext) && isEducationEnabled()) {
notifyCaptionStateChanged();
}
@@ -710,7 +709,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private void notifyCaptionStateChanged() {
// TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode.
- if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) {
+ if (!canEnterDesktopMode(mContext) || !isEducationEnabled()) {
return;
}
if (!isCaptionVisible()) {
@@ -719,7 +718,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
// App handle is visible since `mWindowDecorViewHolder` is of type
// [AppHandleViewHolder].
final CaptionState captionState = new CaptionState.AppHandle(mTaskInfo,
- isHandleMenuActive(), getCurrentAppHandleBounds());
+ isHandleMenuActive(), getCurrentAppHandleBounds(), isCapturedLinkAvailable());
mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
} else {
// App header is visible since `mWindowDecorViewHolder` is of type
@@ -732,8 +731,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
}
+ private boolean isCapturedLinkAvailable() {
+ return mCapturedLink != null && !mCapturedLink.mExpired;
+ }
+
private void notifyNoCaptionHandle() {
- if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) {
+ if (!canEnterDesktopMode(mContext) || !isEducationEnabled()) {
return;
}
mWindowDecorCaptionHandleRepository.notifyCaptionChanged(
@@ -760,7 +763,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
final CaptionState captionState = new CaptionState.AppHeader(
mTaskInfo,
isHandleMenuActive(),
- appChipGlobalPosition);
+ appChipGlobalPosition,
+ isCapturedLinkAvailable());
mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
}
@@ -1389,6 +1393,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
/* openInBrowserClickListener= */ (intent) -> {
mOpenInBrowserClickListener.accept(intent);
onCapturedLinkExpired();
+ if (Flags.enableDesktopWindowingAppToWebEducation()) {
+ mWindowDecorCaptionHandleRepository.onAppToWebUsage();
+ }
return Unit.INSTANCE;
},
/* onOpenByDefaultClickListener= */ () -> {
@@ -1407,7 +1414,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
},
/* forceShowSystemBars= */ inDesktopImmersive
);
- if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ if (canEnterDesktopMode(mContext) && isEducationEnabled()) {
notifyCaptionStateChanged();
}
mMinimumInstancesFound = false;
@@ -1478,7 +1485,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
mWindowDecorViewHolder.onHandleMenuClosed();
mHandleMenu.close();
mHandleMenu = null;
- if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ if (canEnterDesktopMode(mContext) && isEducationEnabled()) {
notifyCaptionStateChanged();
}
}
@@ -1633,6 +1640,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
&& v.getTop() <= y && v.getBottom() >= y;
}
+ /** Returns true if at least one education flag is enabled. */
+ private boolean isEducationEnabled() {
+ return Flags.enableDesktopWindowingAppHandleEducation()
+ || Flags.enableDesktopWindowingAppToWebEducation();
+ }
+
@Override
public void close() {
closeDragResizeListener();
@@ -1642,7 +1655,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
disposeResizeVeil();
disposeStatusBarInputLayer();
clearCurrentViewHostRunnable();
- if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) {
+ if (canEnterDesktopMode(mContext) && isEducationEnabled()) {
notifyNoCaptionHandle();
}
super.close();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationPromoController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationPromoController.kt
new file mode 100644
index 000000000000..b3489a4b4348
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationPromoController.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2024 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.windowdecor.education
+
+import android.annotation.ColorInt
+import android.annotation.DimenRes
+import android.annotation.LayoutRes
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Point
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.WindowManager
+import android.widget.LinearLayout
+import android.widget.TextView
+import android.window.DisplayAreaInfo
+import android.window.WindowContainerTransaction
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import com.android.wm.shell.R
+import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.shared.animation.PhysicsAnimator
+import com.android.wm.shell.windowdecor.WindowManagerWrapper
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+
+/**
+ * Controls the lifecycle of an education promo, including showing and hiding it.
+ */
+class DesktopWindowingEducationPromoController(
+ private val context: Context,
+ private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory,
+ private val displayController: DisplayController,
+) : OnDisplayChangingListener {
+ private var educationView: View? = null
+ private var animator: PhysicsAnimator<View>? = null
+ private val springConfig by lazy {
+ PhysicsAnimator.SpringConfig(
+ SpringForce.STIFFNESS_MEDIUM,
+ SpringForce.DAMPING_RATIO_LOW_BOUNCY
+ )
+ }
+ private var popupWindow: AdditionalSystemViewContainer? = null
+
+ override fun onDisplayChange(
+ displayId: Int,
+ fromRotation: Int,
+ toRotation: Int,
+ newDisplayAreaInfo: DisplayAreaInfo?,
+ t: WindowContainerTransaction?
+ ) {
+ // Exit if the rotation hasn't changed or is changed by 180 degrees. [fromRotation] and
+ // [toRotation] can be one of the [@Surface.Rotation] values.
+ if ((fromRotation % 2 == toRotation % 2)) return
+ hideEducation()
+ }
+
+ /**
+ * Shows education promo.
+ *
+ * @param viewConfig features of the education.
+ * @param taskId is used in the title of popup window created for the education view.
+ */
+ fun showEducation(
+ viewConfig: EducationViewConfig,
+ taskId: Int
+ ) {
+ hideEducation()
+ educationView = createEducationView(viewConfig, taskId)
+ animator = createAnimator()
+ animateShowEducationTransition()
+ displayController.addDisplayChangingController(this)
+ }
+
+ /** Hide the current education view if visible */
+ private fun hideEducation() = animateHideEducationTransition { cleanUp() }
+
+ /** Create education view by inflating layout provided. */
+ private fun createEducationView(
+ viewConfig: EducationViewConfig,
+ taskId: Int
+ ): View {
+ val educationView =
+ LayoutInflater.from(context)
+ .inflate(
+ viewConfig.viewLayout, /* root= */ null, /* attachToRoot= */ false)
+ .apply {
+ alpha = 0f
+ scaleX = 0f
+ scaleY = 0f
+
+ requireViewById<TextView>(R.id.education_text).apply {
+ text = viewConfig.educationText
+ }
+ setOnTouchListener { _, motionEvent ->
+ if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) {
+ hideEducation()
+ true
+ } else {
+ false
+ }
+ }
+ setOnClickListener {
+ hideEducation()
+ }
+ setEducationColorScheme(viewConfig.educationColorScheme)
+ }
+
+ createEducationPopupWindow(
+ taskId,
+ viewConfig.viewGlobalCoordinates,
+ loadDimensionPixelSize(viewConfig.widthId),
+ loadDimensionPixelSize(viewConfig.heightId),
+ educationView = educationView)
+
+ return educationView
+ }
+
+ /** Create animator for education transitions */
+ private fun createAnimator(): PhysicsAnimator<View>? =
+ educationView?.let {
+ PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) }
+ }
+
+ /** Animate show transition for the education view */
+ private fun animateShowEducationTransition() {
+ animator
+ ?.spring(DynamicAnimation.ALPHA, 1f)
+ ?.spring(DynamicAnimation.SCALE_X, 1f)
+ ?.spring(DynamicAnimation.SCALE_Y, 1f)
+ ?.start()
+ }
+
+ /** Animate hide transition for the education view */
+ private fun animateHideEducationTransition(endActions: () -> Unit) {
+ animator
+ ?.spring(DynamicAnimation.ALPHA, 0f)
+ ?.spring(DynamicAnimation.SCALE_X, 0f)
+ ?.spring(DynamicAnimation.SCALE_Y, 0f)
+ ?.start()
+ endActions()
+ }
+
+ /** Remove education promo and clean up all relative properties */
+ private fun cleanUp() {
+ educationView = null
+ animator = null
+ popupWindow?.releaseView()
+ popupWindow = null
+ displayController.removeDisplayChangingController(this)
+ }
+
+ private fun createEducationPopupWindow(
+ taskId: Int,
+ educationViewGlobalCoordinates: Point,
+ width: Int,
+ height: Int,
+ educationView: View,
+ ) {
+ popupWindow =
+ additionalSystemViewContainerFactory.create(
+ windowManagerWrapper =
+ WindowManagerWrapper(context.getSystemService(WindowManager::class.java)),
+ taskId = taskId,
+ x = educationViewGlobalCoordinates.x,
+ y = educationViewGlobalCoordinates.y,
+ width = width,
+ height = height,
+ flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
+ WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
+ view = educationView)
+ }
+
+ private fun View.setEducationColorScheme(educationColorScheme: EducationColorScheme) {
+ requireViewById<LinearLayout>(R.id.education_container).apply {
+ background.setTint(educationColorScheme.container)
+ }
+ requireViewById<TextView>(R.id.education_text).apply {
+ setTextColor(educationColorScheme.text)
+ }
+ }
+
+ private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int {
+ if (resourceId == Resources.ID_NULL) return 0
+ return context.resources.getDimensionPixelSize(resourceId)
+ }
+
+ /**
+ * The configuration for education view features:
+ *
+ * @property viewLayout Layout resource ID of the view to be used for education promo.
+ * @property viewGlobalCoordinates Global (screen) coordinates of the education.
+ * @property educationText Text to be added to the TextView of the promo.
+ * @property widthId res Id for education width
+ * @property heightId res Id for education height
+ */
+ data class EducationViewConfig(
+ @LayoutRes val viewLayout: Int,
+ val educationColorScheme: EducationColorScheme,
+ val viewGlobalCoordinates: Point,
+ val educationText: String,
+ @DimenRes val widthId: Int,
+ @DimenRes val heightId: Int
+ )
+
+ /**
+ * Color scheme of education view:
+ *
+ * @property container Color of the container of the education.
+ * @property text Text color of the [TextView] of education promo.
+ */
+ data class EducationColorScheme(
+ @ColorInt val container: Int,
+ @ColorInt val text: Int,
+ )
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
index c61b31e7ba01..4fa2744b4c12 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt
@@ -80,7 +80,7 @@ class DesktopWindowingEducationTooltipController(
* @param tooltipViewConfig features of tooltip.
* @param taskId is used in the title of popup window created for the tooltip view.
*/
- fun showEducationTooltip(tooltipViewConfig: EducationViewConfig, taskId: Int) {
+ fun showEducationTooltip(tooltipViewConfig: TooltipEducationViewConfig, taskId: Int) {
hideEducationTooltip()
tooltipView = createEducationTooltipView(tooltipViewConfig, taskId)
animator = createAnimator()
@@ -93,7 +93,7 @@ class DesktopWindowingEducationTooltipController(
/** Create education view by inflating layout provided. */
private fun createEducationTooltipView(
- tooltipViewConfig: EducationViewConfig,
+ tooltipViewConfig: TooltipEducationViewConfig,
taskId: Int,
): View {
val tooltipView =
@@ -271,7 +271,7 @@ class DesktopWindowingEducationTooltipController(
* @property onEducationClickAction Lambda to be executed when the tooltip is clicked.
* @property onDismissAction Lambda to be executed when the tooltip is dismissed.
*/
- data class EducationViewConfig(
+ data class TooltipEducationViewConfig(
@LayoutRes val tooltipViewLayout: Int,
val tooltipColorScheme: TooltipColorScheme,
val tooltipViewGlobalCoordinates: Point,
@@ -299,4 +299,4 @@ class DesktopWindowingEducationTooltipController(
UP,
LEFT,
}
-}
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
index e3caf2ede99d..38c6ed90241c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt
@@ -49,7 +49,10 @@ class WindowDecorCaptionHandleRepositoryTest {
val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME)
val appHandleCaptionState =
CaptionState.AppHandle(
- taskInfo, false, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3))
+ runningTaskInfo = taskInfo,
+ isHandleMenuExpanded = false,
+ globalAppHandleBounds = Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3),
+ isCapturedLinkAvailable = false)
captionHandleRepository.notifyCaptionChanged(appHandleCaptionState)
@@ -61,7 +64,10 @@ class WindowDecorCaptionHandleRepositoryTest {
val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME)
val appHeaderCaptionState =
CaptionState.AppHeader(
- taskInfo, true, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3))
+ runningTaskInfo = taskInfo,
+ isHeaderMenuExpanded = true,
+ globalAppChipBounds = Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3),
+ isCapturedLinkAvailable = false)
captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
index 7dbadc9d9083..d94186c8284e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt
@@ -84,7 +84,7 @@ class AppHandleEducationControllerTest : ShellTestCase() {
private val testDataStoreFlow = MutableStateFlow(createWindowingEducationProto())
private val testCaptionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption)
private val educationConfigCaptor =
- argumentCaptor<DesktopWindowingEducationTooltipController.EducationViewConfig>()
+ argumentCaptor<DesktopWindowingEducationTooltipController.TooltipEducationViewConfig>()
@Mock private lateinit var mockEducationFilter: AppHandleEducationFilter
@Mock private lateinit var mockDataStoreRepository: AppHandleEducationDatastoreRepository
@Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt
index 708fadb7fbe2..99e82959fcd6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt
@@ -32,11 +32,13 @@ fun createAppHandleState(
runningTaskInfo: RunningTaskInfo = createTaskInfo(),
isHandleMenuExpanded: Boolean = false,
globalAppHandleBounds: Rect = Rect(),
+ isCapturedLinkAvailable: Boolean = false
): CaptionState.AppHandle =
CaptionState.AppHandle(
runningTaskInfo = runningTaskInfo,
isHandleMenuExpanded = isHandleMenuExpanded,
- globalAppHandleBounds = globalAppHandleBounds)
+ globalAppHandleBounds = globalAppHandleBounds,
+ isCapturedLinkAvailable = isCapturedLinkAvailable)
/**
* Create an instance of [CaptionState.AppHeader] with parameters as properties.
@@ -47,11 +49,13 @@ fun createAppHeaderState(
runningTaskInfo: RunningTaskInfo = createTaskInfo(),
isHeaderMenuExpanded: Boolean = false,
globalAppChipBounds: Rect = Rect(),
+ isCapturedLinkAvailable: Boolean = false
): CaptionState.AppHeader =
CaptionState.AppHeader(
runningTaskInfo = runningTaskInfo,
isHeaderMenuExpanded = isHeaderMenuExpanded,
- globalAppChipBounds = globalAppChipBounds)
+ globalAppChipBounds = globalAppChipBounds,
+ isCapturedLinkAvailable = isCapturedLinkAvailable)
/**
* Create an instance of [RunningTaskInfo] with parameters as properties.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 36c5be1d7191..03aab18d8d87 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -95,6 +95,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.DesktopTasksLimiter
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository
import com.android.wm.shell.desktopmode.education.AppHandleEducationController
+import com.android.wm.shell.desktopmode.education.AppToWebEducationController
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
@@ -195,6 +196,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
DesktopModeWindowDecorViewModel.TaskPositionerFactory
@Mock private lateinit var mockTaskPositioner: TaskPositioner
@Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController
+ @Mock private lateinit var mockAppToWebEducationController: AppToWebEducationController
@Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver
@Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository
@Mock private lateinit var motionEvent: MotionEvent
@@ -261,6 +263,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
mockInteractionJankMonitor,
Optional.of(mockTasksLimiter),
mockAppHandleEducationController,
+ mockAppToWebEducationController,
mockCaptionHandleRepository,
Optional.of(mockActivityOrientationChangeHandler),
mockTaskPositionerFactory,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index f6fed29bbbe8..86ec6753a1bd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -1299,7 +1299,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION)
+ @DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION})
public void notifyCaptionStateChanged_flagDisabled_doNoNotify() {
when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true);
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
index 741dfb8dd885..15f2c7be38b7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt
@@ -285,7 +285,7 @@ class DesktopWindowingEducationTooltipControllerTest : ShellTestCase() {
onEducationClickAction: () -> Unit = {},
onDismissAction: () -> Unit = {}
) =
- DesktopWindowingEducationTooltipController.EducationViewConfig(
+ DesktopWindowingEducationTooltipController.TooltipEducationViewConfig(
tooltipViewLayout = tooltipViewLayout,
tooltipColorScheme = tooltipColorScheme,
tooltipViewGlobalCoordinates = tooltipViewGlobalCoordinates,