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

Commit 5c7153aa authored by Shivangi Dubey's avatar Shivangi Dubey Committed by Android (Google) Code Review
Browse files

Merge "Create AppHandleEducationController class to control education flow" into main

parents 10bf2e16 97d8ac67
Loading
Loading
Loading
Loading
+5 −1
Original line number Original line Diff line number Diff line
@@ -148,7 +148,11 @@ import java.util.function.IntPredicate;
 * dependencies that are device/form factor SystemUI implementation specific should go into their
 * 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.)
 * 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 {
public abstract class WMShellBaseModule {


    //
    //
+16 −1
Original line number Original line Diff line number Diff line
@@ -73,6 +73,7 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
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.AppHandleEducationFilter;
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.DragAndDropController;
@@ -118,6 +119,8 @@ import dagger.Lazy;
import dagger.Module;
import dagger.Module;
import dagger.Provides;
import dagger.Provides;


import kotlinx.coroutines.CoroutineScope;

import java.util.ArrayList;
import java.util.ArrayList;
import java.util.List;
import java.util.List;
import java.util.Optional;
import java.util.Optional;
@@ -743,6 +746,17 @@ public abstract class WMShellModule {
        return new AppHandleEducationFilter(context, appHandleEducationDatastoreRepository);
        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
    // Drag and drop
    //
    //
@@ -784,7 +798,8 @@ public abstract class WMShellModule {
    @Provides
    @Provides
    static Object provideIndependentShellComponentsToCreate(
    static Object provideIndependentShellComponentsToCreate(
            DragAndDropController dragAndDropController,
            DragAndDropController dragAndDropController,
            Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional
            Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional,
            AppHandleEducationController appHandleEducationController
    ) {
    ) {
        return new Object();
        return new Object();
    }
    }
+113 −0
Original line number Original line 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)
  }
}
+19 −7
Original line number Original line Diff line number Diff line
@@ -25,9 +25,12 @@ import androidx.datastore.core.Serializer
import androidx.datastore.dataStoreFile
import androidx.datastore.dataStoreFile
import com.android.framework.protobuf.InvalidProtocolBufferException
import com.android.framework.protobuf.InvalidProtocolBufferException
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.annotations.VisibleForTesting
import java.io.IOException
import java.io.InputStream
import java.io.InputStream
import java.io.OutputStream
import java.io.OutputStream
import java.time.Duration
import java.time.Duration
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.first


/**
/**
@@ -46,17 +49,26 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) {
          serializer = WindowingEducationProtoSerializer,
          serializer = WindowingEducationProtoSerializer,
          produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) }))
          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
   * 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.
   * DataStore is empty or there's an error reading, it returns the default value of Proto.
   */
   */
  suspend fun windowingEducationProto(): WindowingEducationProto =
  suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first()
      try {
        dataStore.data.first()
      } catch (e: Exception) {
        Log.e(TAG, "Unable to read from datastore")
        WindowingEducationProto.getDefaultInstance()
      }


  /**
  /**
   * Updates [AppHandleEducation.appUsageStats] and
   * Updates [AppHandleEducation.appUsageStats] and