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

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

Merge "Filter trigger for desktop windowing education" into main

parents f743c078 e3c5a7b3
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