• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.systemui.keyboard.shortcut.data.repository
18 
19 import android.annotation.SuppressLint
20 import android.app.role.RoleManager
21 import android.content.ComponentName
22 import android.content.Context
23 import android.content.Intent
24 import android.content.Intent.ACTION_MAIN
25 import android.content.Intent.CATEGORY_LAUNCHER
26 import android.content.pm.ActivityInfo
27 import android.content.pm.PackageManager.MATCH_DEFAULT_ONLY
28 import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE
29 import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE
30 import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
31 import android.content.pm.PackageManager.NameNotFoundException
32 import android.graphics.drawable.Icon
33 import android.hardware.input.AppLaunchData
34 import android.hardware.input.AppLaunchData.CategoryData
35 import android.hardware.input.AppLaunchData.ComponentData
36 import android.hardware.input.AppLaunchData.RoleData
37 import android.hardware.input.InputGestureData
38 import android.hardware.input.InputGestureData.KeyTrigger
39 import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION
40 import android.hardware.input.KeyGestureEvent.KeyGestureType
41 import android.util.Log
42 import com.android.internal.app.ResolverActivity
43 import com.android.systemui.keyboard.shortcut.data.model.InternalGroupsSource
44 import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup
45 import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo
46 import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType
47 import com.android.systemui.res.R
48 import com.android.systemui.settings.UserTracker
49 import javax.inject.Inject
50 
51 /**
52  * Serves as a bridge for converting InputGestureData API Models to Shortcut Helper Data Layer
53  * Models and vice versa.
54  */
55 class InputGestureDataAdapter
56 @Inject
57 constructor(
58     private val userTracker: UserTracker,
59     private val inputGestureMaps: InputGestureMaps,
60     private val context: Context,
61 ) {
62     private val userContext: Context
63         get() = userTracker.createCurrentUserContext(userTracker.userContext)
64 
65     fun toInternalGroupSources(inputGestures: List<InputGestureData>): List<InternalGroupsSource> {
66         val ungroupedInternalGroupSources =
67             inputGestures.mapNotNull { gestureData ->
68                 val keyTrigger = gestureData.trigger as KeyTrigger
69                 val keyGestureType = gestureData.action.keyGestureType()
70                 val appLaunchData: AppLaunchData? = gestureData.action.appLaunchData()
71                 fetchGroupLabelByGestureType(keyGestureType)?.let { groupLabel ->
72                     toInternalKeyboardShortcutInfo(keyGestureType, keyTrigger, appLaunchData)
73                         ?.let { internalKeyboardShortcutInfo ->
74                             val group =
75                                 InternalKeyboardShortcutGroup(
76                                     label = groupLabel,
77                                     items = listOf(internalKeyboardShortcutInfo),
78                                 )
79 
80                             fetchShortcutCategoryTypeByGestureType(keyGestureType)?.let {
81                                 InternalGroupsSource(groups = listOf(group), type = it)
82                             }
83                         }
84                 }
85             }
86 
87         return ungroupedInternalGroupSources
88     }
89 
90     fun getKeyGestureTypeForShortcut(
91         shortcutLabel: String,
92         shortcutCategoryType: ShortcutCategoryType,
93     ): Int? {
94         if (shortcutCategoryType == ShortcutCategoryType.AppCategories) {
95             return KEY_GESTURE_TYPE_LAUNCH_APPLICATION
96         }
97         val result = inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutLabel]
98         return result
99     }
100 
101     private fun toInternalKeyboardShortcutInfo(
102         keyGestureType: Int,
103         keyTrigger: KeyTrigger,
104         appLaunchData: AppLaunchData?,
105     ): InternalKeyboardShortcutInfo? {
106         fetchShortcutLabelByGestureType(keyGestureType, appLaunchData)?.let {
107             return InternalKeyboardShortcutInfo(
108                 label = it,
109                 keycode = keyTrigger.keycode,
110                 modifiers = keyTrigger.modifierState,
111                 isCustomShortcut = true,
112                 icon = appLaunchData?.let { fetchShortcutIconByAppLaunchData(appLaunchData) },
113             )
114         }
115         return null
116     }
117 
118     @SuppressLint("QueryPermissionsNeeded")
119     private fun fetchShortcutIconByAppLaunchData(appLaunchData: AppLaunchData): Icon? {
120         val intent = fetchIntentFromAppLaunchData(appLaunchData) ?: return null
121         val resolvedActivity = resolveSingleMatchingActivityFrom(intent)
122 
123         return if (resolvedActivity == null) {
124             null
125         } else {
126             Icon.createWithResource(context, resolvedActivity.iconResource)
127         }
128     }
129 
130     private fun fetchGroupLabelByGestureType(@KeyGestureType keyGestureType: Int): String? {
131         inputGestureMaps.gestureToInternalKeyboardShortcutGroupLabelResIdMap[keyGestureType]?.let {
132             return context.getString(it)
133         } ?: return null
134     }
135 
136     private fun fetchShortcutLabelByGestureType(
137         @KeyGestureType keyGestureType: Int,
138         appLaunchData: AppLaunchData?,
139     ): String? {
140         inputGestureMaps.gestureToInternalKeyboardShortcutInfoLabelResIdMap[keyGestureType]?.let {
141             return context.getString(it)
142         }
143 
144         if (keyGestureType == KEY_GESTURE_TYPE_LAUNCH_APPLICATION) {
145             return fetchShortcutLabelByAppLaunchData(appLaunchData!!)
146         }
147 
148         return null
149     }
150 
151     private fun fetchShortcutLabelByAppLaunchData(appLaunchData: AppLaunchData): String? {
152         val intent = fetchIntentFromAppLaunchData(appLaunchData) ?: return null
153         val resolvedActivity = resolveSingleMatchingActivityFrom(intent)
154 
155         return if (resolvedActivity == null) {
156             getIntentCategoryLabel(intent.selector?.categories?.iterator()?.next())
157         } else resolvedActivity.loadLabel(userContext.packageManager).toString()
158     }
159 
160     @SuppressLint("QueryPermissionsNeeded")
161     private fun resolveSingleMatchingActivityFrom(intent: Intent): ActivityInfo? {
162         val packageManager = userContext.packageManager
163         val resolvedActivity =
164             intent.resolveActivityInfo(packageManager, /* flags= */ MATCH_DEFAULT_ONLY)
165                 ?: return null
166 
167         val matchesMultipleActivities =
168             ResolverActivity::class.qualifiedName.equals(resolvedActivity.name)
169 
170         return if (matchesMultipleActivities) {
171             return null
172         } else resolvedActivity
173     }
174 
175     private fun getIntentCategoryLabel(category: String?): String? {
176         val categoryLabelRes =
177             when (category.toString()) {
178                 Intent.CATEGORY_APP_BROWSER -> R.string.keyboard_shortcut_group_applications_browser
179                 Intent.CATEGORY_APP_CONTACTS ->
180                     R.string.keyboard_shortcut_group_applications_contacts
181                 Intent.CATEGORY_APP_EMAIL -> R.string.keyboard_shortcut_group_applications_email
182                 Intent.CATEGORY_APP_CALENDAR ->
183                     R.string.keyboard_shortcut_group_applications_calendar
184                 Intent.CATEGORY_APP_MAPS -> R.string.keyboard_shortcut_group_applications_maps
185                 Intent.CATEGORY_APP_MUSIC -> R.string.keyboard_shortcut_group_applications_music
186                 Intent.CATEGORY_APP_MESSAGING -> R.string.keyboard_shortcut_group_applications_sms
187                 Intent.CATEGORY_APP_CALCULATOR ->
188                     R.string.keyboard_shortcut_group_applications_calculator
189                 else -> {
190                     Log.w(TAG, ("No label for app category $category"))
191                     null
192                 }
193             }
194 
195         return if (categoryLabelRes == null) {
196             return null
197         } else {
198             context.getString(categoryLabelRes)
199         }
200     }
201 
202     private fun fetchIntentFromAppLaunchData(appLaunchData: AppLaunchData): Intent? {
203         return when (appLaunchData) {
204             is CategoryData ->
205                 Intent.makeMainSelectorActivity(
206                     /* selectorAction= */ ACTION_MAIN,
207                     /* selectorCategory= */ appLaunchData.category,
208                 )
209 
210             is RoleData -> getRoleLaunchIntent(appLaunchData.role)
211             is ComponentData ->
212                 resolveComponentNameIntent(
213                     packageName = appLaunchData.packageName,
214                     className = appLaunchData.className,
215                 )
216 
217             else -> null
218         }
219     }
220 
221     private fun resolveComponentNameIntent(packageName: String, className: String): Intent? {
222         buildIntentFromComponentName(ComponentName(packageName, className))?.let {
223             return it
224         }
225         buildIntentFromComponentName(
226                 ComponentName(
227                     userContext.packageManager
228                         .canonicalToCurrentPackageNames(arrayOf(packageName))[0],
229                     className,
230                 )
231             )
232             ?.let {
233                 return it
234             }
235         return null
236     }
237 
238     private fun buildIntentFromComponentName(componentName: ComponentName): Intent? {
239         try {
240             val flags =
241                 MATCH_DIRECT_BOOT_UNAWARE or MATCH_DIRECT_BOOT_AWARE or MATCH_UNINSTALLED_PACKAGES
242             // attempt to retrieve activity info to see if a NameNotFoundException is thrown.
243             userContext.packageManager.getActivityInfo(componentName, flags)
244         } catch (e: NameNotFoundException) {
245             Log.w(TAG, "Unable to find activity info for componentName: $componentName")
246             return null
247         }
248 
249         return Intent(ACTION_MAIN).apply {
250             addCategory(CATEGORY_LAUNCHER)
251             component = componentName
252         }
253     }
254 
255     @SuppressLint("NonInjectedService")
256     private fun getRoleLaunchIntent(role: String): Intent? {
257         val packageManager = userContext.packageManager
258         val roleManager = userContext.getSystemService(RoleManager::class.java)!!
259         if (roleManager.isRoleAvailable(role)) {
260             roleManager.getDefaultApplication(role)?.let { rolePackage ->
261                 packageManager.getLaunchIntentForPackage(rolePackage)?.let {
262                     return it
263                 } ?: Log.w(TAG, "No launch intent for role $role")
264             } ?: Log.w(TAG, "No default application for role $role, user= ${userContext.user}")
265         } else {
266             Log.w(TAG, "Role $role is not available.")
267         }
268         return null
269     }
270 
271     private fun fetchShortcutCategoryTypeByGestureType(
272         @KeyGestureType keyGestureType: Int
273     ): ShortcutCategoryType? {
274         return inputGestureMaps.gestureToShortcutCategoryTypeMap[keyGestureType]
275     }
276 
277     private companion object {
278         private const val TAG = "InputGestureDataUtils"
279     }
280 }
281