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

Commit e3c5a7b3 authored by Shivangi Dubey's avatar Shivangi Dubey
Browse files

Filter trigger for desktop windowing education

A trigger is an event, like user interaction or system event that can serve as an entry point to initiate user education.
After a trigger occurs, that trigger is filtered based on various conditions.
Filtering is controlled by the WindowingEducationFilter class. WindowingEducationFilter#shouldShowEducation() is the public api used to get the result of filtering.

Bug: 353744739
Bug: 353987858
Test: AppHandleFilterTest
Flag: com.android.window.flags.enable_desktop_windowing_app_handle_education
Change-Id: I795472bb299b8880876f7c72ac077729d4208d62
parent 48d98a08
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -183,4 +183,7 @@
    <!-- This is to be overridden to define a list of packages mapped to web links which will be
         parsed and utilized for desktop windowing's app-to-web feature. -->
    <string name="generic_links_list" translatable="false"/>

    <!-- Apps that can trigger Desktop Windowing App handle Education -->
    <string-array name="desktop_windowing_app_handle_education_allowlist_apps"></string-array>
</resources>
+12 −0
Original line number Diff line number Diff line
@@ -22,4 +22,16 @@
    <integer name="bubbles_overflow_columns">4</integer>
    <!-- Maximum number of bubbles we allow in overflow before we dismiss the oldest one. -->
    <integer name="bubbles_max_overflow">16</integer>
    <!-- App Handle Education - Minimum number of times an app should have been launched, in order
         to be eligible to show education in it -->
    <integer name="desktop_windowing_education_min_app_launch_count">3</integer>
    <!-- App Handle Education - Interval at which app usage stats should be queried and updated in
         cache periodically -->
    <integer name="desktop_windowing_education_app_usage_cache_interval_seconds">86400</integer>
    <!-- App Handle Education - Time interval in seconds for which we'll analyze app usage
         stats to determine if minimum usage requirements are met.  -->
    <integer name="desktop_windowing_education_app_launch_interval_seconds">2592000</integer>
    <!-- App Handle Education - Required time passed in seconds since device has been setup
         in order to be eligible to show education -->
    <integer name="desktop_windowing_education_required_time_since_setup_seconds">604800</integer>
</resources>
 No newline at end of file
+9 −0
Original line number Diff line number Diff line
@@ -72,6 +72,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.AppHandleEducationFilter;
import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.GlobalDragListener;
@@ -711,6 +712,14 @@ public abstract class WMShellModule {
        return new AppHandleEducationDatastoreRepository(context);
    }

    @WMSingleton
    @Provides
    static AppHandleEducationFilter provideAppHandleEducationFilter(
            Context context,
            AppHandleEducationDatastoreRepository appHandleEducationDatastoreRepository) {
        return new AppHandleEducationFilter(context, appHandleEducationDatastoreRepository);
    }

    //
    // Drag and drop
    //
+123 −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.annotation.IntegerRes
import android.app.usage.UsageStatsManager
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.education.data.AppHandleEducationDatastoreRepository
import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto
import java.time.Duration

/** Filters incoming app handle education triggers based on set conditions. */
class AppHandleEducationFilter(
    private val context: Context,
    private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository
) {
  private val usageStatsManager =
      context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager

  /** Returns true if conditions to show app handle education are met, returns false otherwise. */
  suspend fun shouldShowAppHandleEducation(focusAppPackageName: String): Boolean {
    val windowingEducationProto = appHandleEducationDatastoreRepository.windowingEducationProto()
    return isFocusAppInAllowlist(focusAppPackageName) &&
        !isOtherEducationShowing() &&
        hasSufficientTimeSinceSetup() &&
        !isEducationViewedBefore(windowingEducationProto) &&
        !isFeatureUsedBefore(windowingEducationProto) &&
        hasMinAppUsage(windowingEducationProto, focusAppPackageName)
  }

  private fun isFocusAppInAllowlist(focusAppPackageName: String): Boolean =
      focusAppPackageName in
          context.resources.getStringArray(
              R.array.desktop_windowing_app_handle_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()

  private fun isTaskbarEducationShowing(): Boolean =
      Secure.getInt(context.contentResolver, Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) == 1

  private fun hasSufficientTimeSinceSetup(): Boolean =
      Duration.ofMillis(SystemClock.elapsedRealtime()) >
          convertIntegerResourceToDuration(
              R.integer.desktop_windowing_education_required_time_since_setup_seconds)

  private fun isEducationViewedBefore(windowingEducationProto: WindowingEducationProto): Boolean =
      windowingEducationProto.hasEducationViewedTimestampMillis()

  private fun isFeatureUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean =
      windowingEducationProto.hasFeatureUsedTimestampMillis()

  private suspend fun hasMinAppUsage(
      windowingEducationProto: WindowingEducationProto,
      focusAppPackageName: String
  ): Boolean =
      (launchCountByPackageName(windowingEducationProto)[focusAppPackageName] ?: 0) >=
          context.resources.getInteger(R.integer.desktop_windowing_education_min_app_launch_count)

  private suspend fun launchCountByPackageName(
      windowingEducationProto: WindowingEducationProto
  ): Map<String, Int> =
      if (isAppUsageCacheStale(windowingEducationProto)) {
        // Query and return user stats, update cache in datastore
        getAndCacheAppUsageStats()
      } else {
        // Return cached usage stats
        windowingEducationProto.appHandleEducation.appUsageStatsMap
      }

  private fun isAppUsageCacheStale(windowingEducationProto: WindowingEducationProto): Boolean {
    val currentTime = currentTimeInDuration()
    val lastUpdateTime =
        Duration.ofMillis(
            windowingEducationProto.appHandleEducation.appUsageStatsLastUpdateTimestampMillis)
    val appUsageStatsCachingInterval =
        convertIntegerResourceToDuration(
            R.integer.desktop_windowing_education_app_usage_cache_interval_seconds)
    return (currentTime - lastUpdateTime) > appUsageStatsCachingInterval
  }

  private suspend fun getAndCacheAppUsageStats(): Map<String, Int> {
    val currentTime = currentTimeInDuration()
    val appUsageStats = queryAppUsageStats()
    appHandleEducationDatastoreRepository.updateAppUsageStats(appUsageStats, currentTime)
    return appUsageStats
  }

  private fun queryAppUsageStats(): Map<String, Int> {
    val endTime = currentTimeInDuration()
    val appLaunchInterval =
        convertIntegerResourceToDuration(
            R.integer.desktop_windowing_education_app_launch_interval_seconds)
    val startTime = endTime - appLaunchInterval

    return usageStatsManager
        .queryAndAggregateUsageStats(startTime.toMillis(), endTime.toMillis())
        .mapValues { it.value.appLaunchCount }
  }

  private fun convertIntegerResourceToDuration(@IntegerRes resourceId: Int): Duration =
      Duration.ofSeconds(context.resources.getInteger(resourceId).toLong())

  private fun currentTimeInDuration(): Duration = Duration.ofMillis(System.currentTimeMillis())
}
+19 −1
Original line number Diff line number Diff line
@@ -22,12 +22,12 @@ import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import androidx.datastore.dataStoreFile
import com.android.framework.protobuf.InvalidProtocolBufferException
import com.android.internal.annotations.VisibleForTesting
import java.io.InputStream
import java.io.OutputStream
import java.time.Duration
import kotlinx.coroutines.flow.first

/**
@@ -58,6 +58,24 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) {
        WindowingEducationProto.getDefaultInstance()
      }

  /**
   * Updates [AppHandleEducation.appUsageStats] and
   * [AppHandleEducation.appUsageStatsLastUpdateTimestampMillis] fields in datastore with
   * [appUsageStats] and [appUsageStatsLastUpdateTimestamp].
   */
  suspend fun updateAppUsageStats(
      appUsageStats: Map<String, Int>,
      appUsageStatsLastUpdateTimestamp: Duration
  ) {
    val currentAppHandleProto = windowingEducationProto().appHandleEducation.toBuilder()
    currentAppHandleProto
        .putAllAppUsageStats(appUsageStats)
        .setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestamp.toMillis())
    dataStore.updateData { preferences: WindowingEducationProto ->
      preferences.toBuilder().setAppHandleEducation(currentAppHandleProto).build()
    }
  }

  companion object {
    private const val TAG = "AppHandleEducationDatastoreRepository"
    private const val APP_HANDLE_EDUCATION_DATASTORE_FILEPATH = "app_handle_education.pb"
Loading