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

Commit ac2fd9fa authored by Android Build Coastguard Worker's avatar Android Build Coastguard Worker
Browse files

Snap for 12306720 from 5c7153aa to 24Q4-release

Change-Id: I2be6cd49f4191a35f61d8adb0b2f10e5a3fbfa43
parents 7517cb89 5c7153aa
Loading
Loading
Loading
Loading
+5 −1
Original line number 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
 * 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 {

    //
+16 −1
Original line number 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.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;
@@ -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;
@@ -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
    //
@@ -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();
    }
+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)
  }
}
+19 −7
Original line number Diff line number Diff line
@@ -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

/**
@@ -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