1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.desktopmode.education 18 19 import android.annotation.IntegerRes 20 import android.app.ActivityManager.RunningTaskInfo 21 import android.app.usage.UsageStatsManager 22 import android.content.Context 23 import android.os.SystemClock 24 import android.provider.Settings.Secure 25 import com.android.wm.shell.R 26 import com.android.wm.shell.desktopmode.CaptionState 27 import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.FORCE_SHOW_DESKTOP_MODE_EDUCATION 28 import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository 29 import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto 30 import java.time.Duration 31 32 @kotlinx.coroutines.ExperimentalCoroutinesApi 33 /** Filters incoming app handle education triggers based on set conditions. */ 34 class AppHandleEducationFilter( 35 private val context: Context, 36 private val appHandleEducationDatastoreRepository: AppHandleEducationDatastoreRepository, 37 ) { 38 private val usageStatsManager = 39 context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager 40 shouldShowDesktopModeEducationnull41 suspend fun shouldShowDesktopModeEducation(captionState: CaptionState.AppHeader): Boolean = 42 shouldShowDesktopModeEducation(captionState.runningTaskInfo) 43 44 suspend fun shouldShowDesktopModeEducation(captionState: CaptionState.AppHandle): Boolean = 45 shouldShowDesktopModeEducation(captionState.runningTaskInfo) 46 47 /** 48 * Returns true if conditions to show app handle, enter desktop mode and exit desktop mode 49 * education are met based on the app info and usage, returns false otherwise. 50 * 51 * If [FORCE_SHOW_DESKTOP_MODE_EDUCATION] is true, this method will always return true. 52 */ 53 private suspend fun shouldShowDesktopModeEducation(taskInfo: RunningTaskInfo): Boolean { 54 if (FORCE_SHOW_DESKTOP_MODE_EDUCATION) return true 55 56 val focusAppPackageName = taskInfo.topActivityInfo?.packageName ?: return false 57 val windowingEducationProto = 58 appHandleEducationDatastoreRepository.windowingEducationProto() 59 60 return isFocusAppInAllowlist(focusAppPackageName) && 61 !isOtherEducationShowing() && 62 hasSufficientTimeSinceSetup() && 63 hasMinAppUsage(windowingEducationProto, focusAppPackageName) 64 } 65 isFocusAppInAllowlistnull66 private fun isFocusAppInAllowlist(focusAppPackageName: String): Boolean = 67 focusAppPackageName in 68 context.resources.getStringArray( 69 R.array.desktop_windowing_app_handle_education_allowlist_apps 70 ) 71 72 // TODO: b/350953004 - Add checks based on App compat 73 // TODO: b/350951797 - Add checks based on PKT tips education 74 private fun isOtherEducationShowing(): Boolean = isTaskbarEducationShowing() 75 76 private fun isTaskbarEducationShowing(): Boolean = 77 Secure.getInt(context.contentResolver, Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) == 1 78 79 private fun hasSufficientTimeSinceSetup(): Boolean = 80 Duration.ofMillis(SystemClock.elapsedRealtime()) > 81 convertIntegerResourceToDuration( 82 R.integer.desktop_windowing_education_required_time_since_setup_seconds 83 ) 84 85 private suspend fun hasMinAppUsage( 86 windowingEducationProto: WindowingEducationProto, 87 focusAppPackageName: String, 88 ): Boolean = 89 (launchCountByPackageName(windowingEducationProto)[focusAppPackageName] ?: 0) >= 90 context.resources.getInteger(R.integer.desktop_windowing_education_min_app_launch_count) 91 92 private suspend fun launchCountByPackageName( 93 windowingEducationProto: WindowingEducationProto 94 ): Map<String, Int> = 95 if (isAppUsageCacheStale(windowingEducationProto)) { 96 // Query and return user stats, update cache in datastore 97 getAndCacheAppUsageStats() 98 } else { 99 // Return cached usage stats 100 windowingEducationProto.appHandleEducation.appUsageStatsMap 101 } 102 isAppUsageCacheStalenull103 private fun isAppUsageCacheStale(windowingEducationProto: WindowingEducationProto): Boolean { 104 val currentTime = currentTimeInDuration() 105 val lastUpdateTime = 106 Duration.ofMillis( 107 windowingEducationProto.appHandleEducation.appUsageStatsLastUpdateTimestampMillis 108 ) 109 val appUsageStatsCachingInterval = 110 convertIntegerResourceToDuration( 111 R.integer.desktop_windowing_education_app_usage_cache_interval_seconds 112 ) 113 return (currentTime - lastUpdateTime) > appUsageStatsCachingInterval 114 } 115 getAndCacheAppUsageStatsnull116 private suspend fun getAndCacheAppUsageStats(): Map<String, Int> { 117 val currentTime = currentTimeInDuration() 118 val appUsageStats = queryAppUsageStats() 119 appHandleEducationDatastoreRepository.updateAppUsageStats(appUsageStats, currentTime) 120 return appUsageStats 121 } 122 queryAppUsageStatsnull123 private fun queryAppUsageStats(): Map<String, Int> { 124 val endTime = currentTimeInDuration() 125 val appLaunchInterval = 126 convertIntegerResourceToDuration( 127 R.integer.desktop_windowing_education_app_launch_interval_seconds 128 ) 129 val startTime = endTime - appLaunchInterval 130 131 return usageStatsManager 132 .queryAndAggregateUsageStats(startTime.toMillis(), endTime.toMillis()) 133 .mapValues { it.value.appLaunchCount } 134 } 135 convertIntegerResourceToDurationnull136 private fun convertIntegerResourceToDuration(@IntegerRes resourceId: Int): Duration = 137 Duration.ofSeconds(context.resources.getInteger(resourceId).toLong()) 138 139 private fun currentTimeInDuration(): Duration = Duration.ofMillis(System.currentTimeMillis()) 140 } 141