Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +14 −14 Original line number Diff line number Diff line Loading @@ -82,9 +82,9 @@ class AppHandleEducationController( runIfEducationFeatureEnabled { applicationCoroutineScope.launch { // Central block handling the app handle's educational flow end-to-end. isEducationViewedFlow() .flatMapLatest { isEducationViewed -> if (isEducationViewed) { isAppHandleHintViewedFlow() .flatMapLatest { isAppHandleHintViewed -> if (isAppHandleHintViewed) { // If the education is viewed then return emptyFlow() that completes immediately. // This will help us to not listen to [captionHandleStateFlow] after the education // has been viewed already. Loading @@ -106,12 +106,12 @@ class AppHandleEducationController( showEducation(captionState, tooltipColorScheme) // After showing first tooltip, mark education as viewed appHandleEducationDatastoreRepository.updateEducationViewedTimestampMillis(true) appHandleEducationDatastoreRepository.updateAppHandleHintViewedTimestampMillis(true) } } applicationCoroutineScope.launch { if (isFeatureUsed()) return@launch if (isAppHandleHintUsed()) return@launch windowDecorCaptionHandleRepository.captionStateFlow .filter { captionState -> captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded Loading @@ -119,8 +119,8 @@ class AppHandleEducationController( .take(1) .flowOn(backgroundDispatcher) .collect { // If user expands app handle, mark user has used the feature appHandleEducationDatastoreRepository.updateFeatureUsedTimestampMillis(true) // If user expands app handle, mark user has used the app handle hint appHandleEducationDatastoreRepository.updateAppHandleHintUsedTimestampMillis(true) } } } Loading Loading @@ -323,25 +323,25 @@ class AppHandleEducationController( } /** * Listens to the changes to [WindowingEducationProto#hasEducationViewedTimestampMillis()] in * Listens to the changes to [WindowingEducationProto#hasAppHandleHintViewedTimestampMillis()] in * datastore proto object. * * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this flow will always emit false. That means * it will emit education has not been viewed yet always. * it will always emit app handle hint has not been viewed yet. */ private fun isEducationViewedFlow(): Flow<Boolean> = private fun isAppHandleHintViewedFlow(): Flow<Boolean> = appHandleEducationDatastoreRepository.dataStoreFlow .map { preferences -> preferences.hasEducationViewedTimestampMillis() && !SHOULD_OVERRIDE_EDUCATION_CONDITIONS preferences.hasAppHandleHintViewedTimestampMillis() && !SHOULD_OVERRIDE_EDUCATION_CONDITIONS } .distinctUntilChanged() /** * Listens to the changes to [WindowingEducationProto#hasFeatureUsedTimestampMillis()] in * Listens to the changes to [WindowingEducationProto#hasAppHandleHintUsedTimestampMillis()] in * datastore proto object. */ private suspend fun isFeatureUsed(): Boolean = appHandleEducationDatastoreRepository.dataStoreFlow.first().hasFeatureUsedTimestampMillis() private suspend fun isAppHandleHintUsed(): Boolean = appHandleEducationDatastoreRepository.dataStoreFlow.first().hasAppHandleHintUsedTimestampMillis() private fun getSize(@DimenRes resourceId: Int): Int { if (resourceId == Resources.ID_NULL) return 0 Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt +6 −6 Original line number Diff line number Diff line Loading @@ -54,8 +54,8 @@ class AppHandleEducationFilter( return isFocusAppInAllowlist(focusAppPackageName) && !isOtherEducationShowing() && hasSufficientTimeSinceSetup() && !isEducationViewedBefore(windowingEducationProto) && !isFeatureUsedBefore(windowingEducationProto) && !isAppHandleHintViewedBefore(windowingEducationProto) && !isAppHandleHintUsedBefore(windowingEducationProto) && hasMinAppUsage(windowingEducationProto, focusAppPackageName) } Loading @@ -76,11 +76,11 @@ class AppHandleEducationFilter( convertIntegerResourceToDuration( R.integer.desktop_windowing_education_required_time_since_setup_seconds) private fun isEducationViewedBefore(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.hasEducationViewedTimestampMillis() private fun isAppHandleHintViewedBefore(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.hasAppHandleHintViewedTimestampMillis() private fun isFeatureUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.hasFeatureUsedTimestampMillis() private fun isAppHandleHintUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.hasAppHandleHintUsedTimestampMillis() private suspend fun hasMinAppUsage( windowingEducationProto: WindowingEducationProto, Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt +12 −10 Original line number Diff line number Diff line Loading @@ -71,32 +71,34 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() /** * Updates [WindowingEducationProto.educationViewedTimestampMillis_] field in datastore with * current timestamp if [isViewed] is true, if not then clears the field. * Updates [WindowingEducationProto.appHandleHintViewedTimestampMillis_] field * in datastore with current timestamp if [isViewed] is true, if not then * clears the field. */ suspend fun updateEducationViewedTimestampMillis(isViewed: Boolean) { suspend fun updateAppHandleHintViewedTimestampMillis(isViewed: Boolean) { dataStore.updateData { preferences -> if (isViewed) { preferences .toBuilder() .setEducationViewedTimestampMillis(System.currentTimeMillis()) .setAppHandleHintViewedTimestampMillis(System.currentTimeMillis()) .build() } else { preferences.toBuilder().clearEducationViewedTimestampMillis().build() preferences.toBuilder().clearAppHandleHintViewedTimestampMillis().build() } } } /** * Updates [WindowingEducationProto.featureUsedTimestampMillis_] field in datastore with current * timestamp if [isViewed] is true, if not then clears the field. * Updates [WindowingEducationProto.appHandleHintUsedTimestampMillis_] field * in datastore with current timestamp if [isViewed] is true, if not then * clears the field. */ suspend fun updateFeatureUsedTimestampMillis(isViewed: Boolean) { suspend fun updateAppHandleHintUsedTimestampMillis(isViewed: Boolean) { dataStore.updateData { preferences -> if (isViewed) { preferences.toBuilder().setFeatureUsedTimestampMillis(System.currentTimeMillis()).build() preferences.toBuilder().setAppHandleHintUsedTimestampMillis(System.currentTimeMillis()).build() } else { preferences.toBuilder().clearFeatureUsedTimestampMillis().build() preferences.toBuilder().clearAppHandleHintUsedTimestampMillis().build() } } } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto +12 −2 Original line number Diff line number Diff line Loading @@ -22,9 +22,19 @@ option java_multiple_files = true; // Desktop Windowing education data message WindowingEducationProto { // Timestamp in milliseconds of when the education was last viewed. optional int64 education_viewed_timestamp_millis = 1; optional int64 education_viewed_timestamp_millis = 1 [deprecated=true]; // Timestamp in milliseconds of when the feature was last used. optional int64 feature_used_timestamp_millis = 2; optional int64 feature_used_timestamp_millis = 2 [deprecated=true]; // Timestamp in milliseconds of when the app handle hint was last viewed. optional int64 app_handle_hint_viewed_timestamp_millis = 5; // Timestamp in milliseconds of when the app handle hint was last used. optional int64 app_handle_hint_used_timestamp_millis = 6; // Timestamp in milliseconds of when the enter desktop mode hint was last viewed. optional int64 enter_desktop_mode_hint_viewed_timestamp_millis = 7; // Timestamp in milliseconds of when the exit desktop mode hint was last viewed. optional int64 exit_desktop_mode_hint_viewed_timestamp_millis = 8; oneof education_data { // Fields specific to app handle education AppHandleEducation app_handle_education = 3; Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +15 −14 Original line number Diff line number Diff line Loading @@ -175,13 +175,13 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun init_educationViewedAlready_shouldNotCallShowEducationTooltip() = fun init_appHandleHintViewedAlready_shouldNotCallShowEducationTooltip() = testScope.runTest { // App handle is visible but education has been viewed before. Should not show education // tooltip. // Mark education viewed. // App handle is visible but app handle hint has been viewed before, // should not show education tooltip. // Mark app handle hint viewed. testDataStoreFlow.value = createWindowingEducationProto(educationViewedTimestampMillis = 123L) createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) setShouldShowAppHandleEducation(true) // Simulate app handle visible. Loading @@ -194,13 +194,14 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun overridePrerequisite_educationViewedAlready_shouldCallShowEducationTooltip() = fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() = testScope.runTest { // App handle is visible but education has been viewed before. But as we are overriding // prerequisite conditions, we should show education tooltip. // Mark education viewed. // App handle is visible but app handle hint has been viewed before. // But as we are overriding prerequisite conditions, we should show app // handle tooltip. // Mark app handle hint viewed. testDataStoreFlow.value = createWindowingEducationProto(educationViewedTimestampMillis = 123L) createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) val systemPropertiesKey = "persist.desktop_windowing_app_handle_education_override_conditions" whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) Loading @@ -217,7 +218,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun init_appHandleExpanded_shouldMarkFeatureViewed() = fun init_appHandleExpanded_shouldMarkAppHandleHintUsed() = testScope.runTest { setShouldShowAppHandleEducation(false) Loading @@ -226,12 +227,12 @@ class AppHandleEducationControllerTest : ShellTestCase() { // Wait for some time before verifying waitForBufferDelay() verify(mockDataStoreRepository, times(1)).updateFeatureUsedTimestampMillis(eq(true)) verify(mockDataStoreRepository, times(1)).updateAppHandleHintUsedTimestampMillis(eq(true)) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun init_showFirstTooltip_shouldMarkEducationViewed() = fun init_showFirstTooltip_shouldMarkAppHandleHintViewed() = testScope.runTest { // App handle is visible. Should show education tooltip. setShouldShowAppHandleEducation(true) Loading @@ -241,7 +242,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { // Wait for first tooltip to showup. waitForBufferDelay() verify(mockDataStoreRepository, times(1)).updateEducationViewedTimestampMillis(eq(true)) verify(mockDataStoreRepository, times(1)).updateAppHandleHintViewedTimestampMillis(eq(true)) } @Test Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +14 −14 Original line number Diff line number Diff line Loading @@ -82,9 +82,9 @@ class AppHandleEducationController( runIfEducationFeatureEnabled { applicationCoroutineScope.launch { // Central block handling the app handle's educational flow end-to-end. isEducationViewedFlow() .flatMapLatest { isEducationViewed -> if (isEducationViewed) { isAppHandleHintViewedFlow() .flatMapLatest { isAppHandleHintViewed -> if (isAppHandleHintViewed) { // If the education is viewed then return emptyFlow() that completes immediately. // This will help us to not listen to [captionHandleStateFlow] after the education // has been viewed already. Loading @@ -106,12 +106,12 @@ class AppHandleEducationController( showEducation(captionState, tooltipColorScheme) // After showing first tooltip, mark education as viewed appHandleEducationDatastoreRepository.updateEducationViewedTimestampMillis(true) appHandleEducationDatastoreRepository.updateAppHandleHintViewedTimestampMillis(true) } } applicationCoroutineScope.launch { if (isFeatureUsed()) return@launch if (isAppHandleHintUsed()) return@launch windowDecorCaptionHandleRepository.captionStateFlow .filter { captionState -> captionState is CaptionState.AppHandle && captionState.isHandleMenuExpanded Loading @@ -119,8 +119,8 @@ class AppHandleEducationController( .take(1) .flowOn(backgroundDispatcher) .collect { // If user expands app handle, mark user has used the feature appHandleEducationDatastoreRepository.updateFeatureUsedTimestampMillis(true) // If user expands app handle, mark user has used the app handle hint appHandleEducationDatastoreRepository.updateAppHandleHintUsedTimestampMillis(true) } } } Loading Loading @@ -323,25 +323,25 @@ class AppHandleEducationController( } /** * Listens to the changes to [WindowingEducationProto#hasEducationViewedTimestampMillis()] in * Listens to the changes to [WindowingEducationProto#hasAppHandleHintViewedTimestampMillis()] in * datastore proto object. * * If [SHOULD_OVERRIDE_EDUCATION_CONDITIONS] is true, this flow will always emit false. That means * it will emit education has not been viewed yet always. * it will always emit app handle hint has not been viewed yet. */ private fun isEducationViewedFlow(): Flow<Boolean> = private fun isAppHandleHintViewedFlow(): Flow<Boolean> = appHandleEducationDatastoreRepository.dataStoreFlow .map { preferences -> preferences.hasEducationViewedTimestampMillis() && !SHOULD_OVERRIDE_EDUCATION_CONDITIONS preferences.hasAppHandleHintViewedTimestampMillis() && !SHOULD_OVERRIDE_EDUCATION_CONDITIONS } .distinctUntilChanged() /** * Listens to the changes to [WindowingEducationProto#hasFeatureUsedTimestampMillis()] in * Listens to the changes to [WindowingEducationProto#hasAppHandleHintUsedTimestampMillis()] in * datastore proto object. */ private suspend fun isFeatureUsed(): Boolean = appHandleEducationDatastoreRepository.dataStoreFlow.first().hasFeatureUsedTimestampMillis() private suspend fun isAppHandleHintUsed(): Boolean = appHandleEducationDatastoreRepository.dataStoreFlow.first().hasAppHandleHintUsedTimestampMillis() private fun getSize(@DimenRes resourceId: Int): Int { if (resourceId == Resources.ID_NULL) return 0 Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilter.kt +6 −6 Original line number Diff line number Diff line Loading @@ -54,8 +54,8 @@ class AppHandleEducationFilter( return isFocusAppInAllowlist(focusAppPackageName) && !isOtherEducationShowing() && hasSufficientTimeSinceSetup() && !isEducationViewedBefore(windowingEducationProto) && !isFeatureUsedBefore(windowingEducationProto) && !isAppHandleHintViewedBefore(windowingEducationProto) && !isAppHandleHintUsedBefore(windowingEducationProto) && hasMinAppUsage(windowingEducationProto, focusAppPackageName) } Loading @@ -76,11 +76,11 @@ class AppHandleEducationFilter( convertIntegerResourceToDuration( R.integer.desktop_windowing_education_required_time_since_setup_seconds) private fun isEducationViewedBefore(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.hasEducationViewedTimestampMillis() private fun isAppHandleHintViewedBefore(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.hasAppHandleHintViewedTimestampMillis() private fun isFeatureUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.hasFeatureUsedTimestampMillis() private fun isAppHandleHintUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean = windowingEducationProto.hasAppHandleHintUsedTimestampMillis() private suspend fun hasMinAppUsage( windowingEducationProto: WindowingEducationProto, Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt +12 −10 Original line number Diff line number Diff line Loading @@ -71,32 +71,34 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() /** * Updates [WindowingEducationProto.educationViewedTimestampMillis_] field in datastore with * current timestamp if [isViewed] is true, if not then clears the field. * Updates [WindowingEducationProto.appHandleHintViewedTimestampMillis_] field * in datastore with current timestamp if [isViewed] is true, if not then * clears the field. */ suspend fun updateEducationViewedTimestampMillis(isViewed: Boolean) { suspend fun updateAppHandleHintViewedTimestampMillis(isViewed: Boolean) { dataStore.updateData { preferences -> if (isViewed) { preferences .toBuilder() .setEducationViewedTimestampMillis(System.currentTimeMillis()) .setAppHandleHintViewedTimestampMillis(System.currentTimeMillis()) .build() } else { preferences.toBuilder().clearEducationViewedTimestampMillis().build() preferences.toBuilder().clearAppHandleHintViewedTimestampMillis().build() } } } /** * Updates [WindowingEducationProto.featureUsedTimestampMillis_] field in datastore with current * timestamp if [isViewed] is true, if not then clears the field. * Updates [WindowingEducationProto.appHandleHintUsedTimestampMillis_] field * in datastore with current timestamp if [isViewed] is true, if not then * clears the field. */ suspend fun updateFeatureUsedTimestampMillis(isViewed: Boolean) { suspend fun updateAppHandleHintUsedTimestampMillis(isViewed: Boolean) { dataStore.updateData { preferences -> if (isViewed) { preferences.toBuilder().setFeatureUsedTimestampMillis(System.currentTimeMillis()).build() preferences.toBuilder().setAppHandleHintUsedTimestampMillis(System.currentTimeMillis()).build() } else { preferences.toBuilder().clearFeatureUsedTimestampMillis().build() preferences.toBuilder().clearAppHandleHintUsedTimestampMillis().build() } } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto +12 −2 Original line number Diff line number Diff line Loading @@ -22,9 +22,19 @@ option java_multiple_files = true; // Desktop Windowing education data message WindowingEducationProto { // Timestamp in milliseconds of when the education was last viewed. optional int64 education_viewed_timestamp_millis = 1; optional int64 education_viewed_timestamp_millis = 1 [deprecated=true]; // Timestamp in milliseconds of when the feature was last used. optional int64 feature_used_timestamp_millis = 2; optional int64 feature_used_timestamp_millis = 2 [deprecated=true]; // Timestamp in milliseconds of when the app handle hint was last viewed. optional int64 app_handle_hint_viewed_timestamp_millis = 5; // Timestamp in milliseconds of when the app handle hint was last used. optional int64 app_handle_hint_used_timestamp_millis = 6; // Timestamp in milliseconds of when the enter desktop mode hint was last viewed. optional int64 enter_desktop_mode_hint_viewed_timestamp_millis = 7; // Timestamp in milliseconds of when the exit desktop mode hint was last viewed. optional int64 exit_desktop_mode_hint_viewed_timestamp_millis = 8; oneof education_data { // Fields specific to app handle education AppHandleEducation app_handle_education = 3; Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +15 −14 Original line number Diff line number Diff line Loading @@ -175,13 +175,13 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun init_educationViewedAlready_shouldNotCallShowEducationTooltip() = fun init_appHandleHintViewedAlready_shouldNotCallShowEducationTooltip() = testScope.runTest { // App handle is visible but education has been viewed before. Should not show education // tooltip. // Mark education viewed. // App handle is visible but app handle hint has been viewed before, // should not show education tooltip. // Mark app handle hint viewed. testDataStoreFlow.value = createWindowingEducationProto(educationViewedTimestampMillis = 123L) createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) setShouldShowAppHandleEducation(true) // Simulate app handle visible. Loading @@ -194,13 +194,14 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun overridePrerequisite_educationViewedAlready_shouldCallShowEducationTooltip() = fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() = testScope.runTest { // App handle is visible but education has been viewed before. But as we are overriding // prerequisite conditions, we should show education tooltip. // Mark education viewed. // App handle is visible but app handle hint has been viewed before. // But as we are overriding prerequisite conditions, we should show app // handle tooltip. // Mark app handle hint viewed. testDataStoreFlow.value = createWindowingEducationProto(educationViewedTimestampMillis = 123L) createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) val systemPropertiesKey = "persist.desktop_windowing_app_handle_education_override_conditions" whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) Loading @@ -217,7 +218,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun init_appHandleExpanded_shouldMarkFeatureViewed() = fun init_appHandleExpanded_shouldMarkAppHandleHintUsed() = testScope.runTest { setShouldShowAppHandleEducation(false) Loading @@ -226,12 +227,12 @@ class AppHandleEducationControllerTest : ShellTestCase() { // Wait for some time before verifying waitForBufferDelay() verify(mockDataStoreRepository, times(1)).updateFeatureUsedTimestampMillis(eq(true)) verify(mockDataStoreRepository, times(1)).updateAppHandleHintUsedTimestampMillis(eq(true)) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) fun init_showFirstTooltip_shouldMarkEducationViewed() = fun init_showFirstTooltip_shouldMarkAppHandleHintViewed() = testScope.runTest { // App handle is visible. Should show education tooltip. setShouldShowAppHandleEducation(true) Loading @@ -241,7 +242,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { // Wait for first tooltip to showup. waitForBufferDelay() verify(mockDataStoreRepository, times(1)).updateEducationViewedTimestampMillis(eq(true)) verify(mockDataStoreRepository, times(1)).updateAppHandleHintViewedTimestampMillis(eq(true)) } @Test Loading