• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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