Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +5 −1 Original line number Diff line number Diff line Loading @@ -148,7 +148,11 @@ import java.util.function.IntPredicate; * dependencies that are device/form factor SystemUI implementation specific should go into their * respective modules (ie. {@link WMShellModule} for handheld, {@link TvWMShellModule} for tv, etc.) */ @Module(includes = WMShellConcurrencyModule.class) @Module( includes = { WMShellConcurrencyModule.class, WMShellCoroutinesModule.class }) public abstract class WMShellBaseModule { // Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +16 −1 Original line number Diff line number Diff line Loading @@ -73,6 +73,7 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator; import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.draganddrop.DragAndDropController; Loading Loading @@ -118,6 +119,8 @@ import dagger.Lazy; import dagger.Module; import dagger.Provides; import kotlinx.coroutines.CoroutineScope; import java.util.ArrayList; import java.util.List; import java.util.Optional; Loading Loading @@ -743,6 +746,17 @@ public abstract class WMShellModule { return new AppHandleEducationFilter(context, appHandleEducationDatastoreRepository); } @WMSingleton @Provides static AppHandleEducationController provideAppHandleEducationController( AppHandleEducationFilter appHandleEducationFilter, ShellTaskOrganizer shellTaskOrganizer, AppHandleEducationDatastoreRepository appHandleEducationDatastoreRepository, @ShellMainThread CoroutineScope applicationScope) { return new AppHandleEducationController(appHandleEducationFilter, shellTaskOrganizer, appHandleEducationDatastoreRepository, applicationScope); } // // Drag and drop // Loading Loading @@ -784,7 +798,8 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional, AppHandleEducationController appHandleEducationController ) { return new Object(); } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt 0 → 100644 +113 −0 Original line number Diff line number Diff line /* * 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.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.os.SystemProperties import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository import com.android.wm.shell.shared.annotations.ShellMainThread import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow 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.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** * Controls app handle education end to end. * * Listen to the user trigger for app handle education, calls an api to check if the education * should be shown and calls an api to show education. */ @OptIn(kotlinx.coroutines.FlowPreview::class) @kotlinx.coroutines.ExperimentalCoroutinesApi class AppHandleEducationController( private val appHandleEducationFilter: AppHandleEducationFilter, shellTaskOrganizer: ShellTaskOrganizer, private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository, @ShellMainThread private val applicationCoroutineScope: CoroutineScope ) { init { runIfEducationFeatureEnabled { // TODO: b/361038716 - Use app handle state flow instead of focus task change flow val focusTaskChangeFlow = focusTaskChangeFlow(shellTaskOrganizer) applicationCoroutineScope.launch { // Central block handling the app's educational flow end-to-end. // This flow listens to the changes to the result of // [WindowingEducationProto#hasEducationViewedTimestampMillis()] in datastore proto object isEducationViewedFlow() .flatMapLatest { isEducationViewed -> if (isEducationViewed) { // If the education is viewed then return emptyFlow() that completes immediately. // This will help us to not listen to focus task changes after the education has // been viewed already. emptyFlow() } else { // This flow listens for focus task changes, which trigger the app handle education. focusTaskChangeFlow .filter { runningTaskInfo -> runningTaskInfo.topActivityInfo?.packageName?.let { appHandleEducationFilter.shouldShowAppHandleEducation(it) } ?: false && runningTaskInfo.windowingMode != WINDOWING_MODE_FREEFORM } .distinctUntilChanged() } } .debounce( APP_HANDLE_EDUCATION_DELAY) // Wait for few seconds, if the focus task changes. // During the delay then current emission will be cancelled. .flowOn(Dispatchers.IO) .collectLatest { // Fire and forget show education suspend function, manage entire lifecycle of // tooltip in UI class. } } } } private inline fun runIfEducationFeatureEnabled(block: () -> Unit) { if (Flags.enableDesktopWindowingAppHandleEducation()) block() } private fun isEducationViewedFlow(): Flow<Boolean> = appHandleEducationDatastoreRepository.dataStoreFlow .map { preferences -> preferences.hasEducationViewedTimestampMillis() } .distinctUntilChanged() private fun focusTaskChangeFlow(shellTaskOrganizer: ShellTaskOrganizer): Flow<RunningTaskInfo> = callbackFlow { val focusTaskChange = ShellTaskOrganizer.FocusListener { taskInfo -> trySend(taskInfo) } shellTaskOrganizer.addFocusListener(focusTaskChange) awaitClose { shellTaskOrganizer.removeFocusListener(focusTaskChange) } } private companion object { val APP_HANDLE_EDUCATION_DELAY: Long get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L) } } libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt +19 −7 Original line number Diff line number Diff line Loading @@ -25,9 +25,12 @@ import androidx.datastore.core.Serializer import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.internal.annotations.VisibleForTesting import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first /** Loading @@ -46,17 +49,26 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { serializer = WindowingEducationProtoSerializer, produceFile = { context.dataStoreFile(APP_HANDLE_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) { Log.e( TAG, "Error in reading app handle education related data from datastore, data is " + "stored in a file named $APP_HANDLE_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 = try { dataStore.data.first() } catch (e: Exception) { Log.e(TAG, "Unable to read from datastore") WindowingEducationProto.getDefaultInstance() } suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() /** * Updates [AppHandleEducation.appUsageStats] and Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +5 −1 Original line number Diff line number Diff line Loading @@ -148,7 +148,11 @@ import java.util.function.IntPredicate; * dependencies that are device/form factor SystemUI implementation specific should go into their * respective modules (ie. {@link WMShellModule} for handheld, {@link TvWMShellModule} for tv, etc.) */ @Module(includes = WMShellConcurrencyModule.class) @Module( includes = { WMShellConcurrencyModule.class, WMShellCoroutinesModule.class }) public abstract class WMShellBaseModule { // Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +16 −1 Original line number Diff line number Diff line Loading @@ -73,6 +73,7 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator; import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.draganddrop.DragAndDropController; Loading Loading @@ -118,6 +119,8 @@ import dagger.Lazy; import dagger.Module; import dagger.Provides; import kotlinx.coroutines.CoroutineScope; import java.util.ArrayList; import java.util.List; import java.util.Optional; Loading Loading @@ -743,6 +746,17 @@ public abstract class WMShellModule { return new AppHandleEducationFilter(context, appHandleEducationDatastoreRepository); } @WMSingleton @Provides static AppHandleEducationController provideAppHandleEducationController( AppHandleEducationFilter appHandleEducationFilter, ShellTaskOrganizer shellTaskOrganizer, AppHandleEducationDatastoreRepository appHandleEducationDatastoreRepository, @ShellMainThread CoroutineScope applicationScope) { return new AppHandleEducationController(appHandleEducationFilter, shellTaskOrganizer, appHandleEducationDatastoreRepository, applicationScope); } // // Drag and drop // Loading Loading @@ -784,7 +798,8 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional, AppHandleEducationController appHandleEducationController ) { return new Object(); } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt 0 → 100644 +113 −0 Original line number Diff line number Diff line /* * 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.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.os.SystemProperties import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository import com.android.wm.shell.shared.annotations.ShellMainThread import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow 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.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch /** * Controls app handle education end to end. * * Listen to the user trigger for app handle education, calls an api to check if the education * should be shown and calls an api to show education. */ @OptIn(kotlinx.coroutines.FlowPreview::class) @kotlinx.coroutines.ExperimentalCoroutinesApi class AppHandleEducationController( private val appHandleEducationFilter: AppHandleEducationFilter, shellTaskOrganizer: ShellTaskOrganizer, private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository, @ShellMainThread private val applicationCoroutineScope: CoroutineScope ) { init { runIfEducationFeatureEnabled { // TODO: b/361038716 - Use app handle state flow instead of focus task change flow val focusTaskChangeFlow = focusTaskChangeFlow(shellTaskOrganizer) applicationCoroutineScope.launch { // Central block handling the app's educational flow end-to-end. // This flow listens to the changes to the result of // [WindowingEducationProto#hasEducationViewedTimestampMillis()] in datastore proto object isEducationViewedFlow() .flatMapLatest { isEducationViewed -> if (isEducationViewed) { // If the education is viewed then return emptyFlow() that completes immediately. // This will help us to not listen to focus task changes after the education has // been viewed already. emptyFlow() } else { // This flow listens for focus task changes, which trigger the app handle education. focusTaskChangeFlow .filter { runningTaskInfo -> runningTaskInfo.topActivityInfo?.packageName?.let { appHandleEducationFilter.shouldShowAppHandleEducation(it) } ?: false && runningTaskInfo.windowingMode != WINDOWING_MODE_FREEFORM } .distinctUntilChanged() } } .debounce( APP_HANDLE_EDUCATION_DELAY) // Wait for few seconds, if the focus task changes. // During the delay then current emission will be cancelled. .flowOn(Dispatchers.IO) .collectLatest { // Fire and forget show education suspend function, manage entire lifecycle of // tooltip in UI class. } } } } private inline fun runIfEducationFeatureEnabled(block: () -> Unit) { if (Flags.enableDesktopWindowingAppHandleEducation()) block() } private fun isEducationViewedFlow(): Flow<Boolean> = appHandleEducationDatastoreRepository.dataStoreFlow .map { preferences -> preferences.hasEducationViewedTimestampMillis() } .distinctUntilChanged() private fun focusTaskChangeFlow(shellTaskOrganizer: ShellTaskOrganizer): Flow<RunningTaskInfo> = callbackFlow { val focusTaskChange = ShellTaskOrganizer.FocusListener { taskInfo -> trySend(taskInfo) } shellTaskOrganizer.addFocusListener(focusTaskChange) awaitClose { shellTaskOrganizer.removeFocusListener(focusTaskChange) } } private companion object { val APP_HANDLE_EDUCATION_DELAY: Long get() = SystemProperties.getLong("persist.windowing_app_handle_education_delay", 3000L) } }
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt +19 −7 Original line number Diff line number Diff line Loading @@ -25,9 +25,12 @@ import androidx.datastore.core.Serializer import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.internal.annotations.VisibleForTesting import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first /** Loading @@ -46,17 +49,26 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { serializer = WindowingEducationProtoSerializer, produceFile = { context.dataStoreFile(APP_HANDLE_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) { Log.e( TAG, "Error in reading app handle education related data from datastore, data is " + "stored in a file named $APP_HANDLE_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 = try { dataStore.data.first() } catch (e: Exception) { Log.e(TAG, "Unable to read from datastore") WindowingEducationProto.getDefaultInstance() } suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() /** * Updates [AppHandleEducation.appUsageStats] and Loading