1 /*
2  * Copyright (C) 2019 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.controls
18 
19 import android.Manifest
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.Intent
23 import android.content.pm.ActivityInfo
24 import android.content.pm.PackageManager
25 import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE
26 import android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE
27 import android.content.pm.ResolveInfo
28 import android.content.pm.ServiceInfo
29 import android.graphics.drawable.Drawable
30 import android.os.UserHandle
31 import android.service.controls.ControlsProviderService
32 import android.util.IconDrawableFactory
33 import androidx.annotation.WorkerThread
34 import com.android.settingslib.applications.DefaultAppInfo
35 import com.android.systemui.R
36 import java.util.Objects
37 
38 open class ControlsServiceInfo(
39     private val context: Context,
40     val serviceInfo: ServiceInfo
41 ) : DefaultAppInfo(
42     context,
43     context.packageManager,
44     context.userId,
45     serviceInfo.componentName
46 ) {
47     private val _panelActivity: ComponentName?
48 
49     init {
50         val metadata = serviceInfo.metaData
51             ?.getString(ControlsProviderService.META_DATA_PANEL_ACTIVITY) ?: ""
52         val unflatenned = ComponentName.unflattenFromString(metadata)
53         if (unflatenned != null && unflatenned.packageName == componentName.packageName) {
54             _panelActivity = unflatenned
55         } else {
56             _panelActivity = null
57         }
58     }
59 
60     /**
61      * Component name of an activity that will be shown embedded in the device controls space
62      * instead of using the controls rendered by SystemUI.
63      *
64      * The activity must be in the same package, exported, enabled and protected by the
65      * [Manifest.permission.BIND_CONTROLS] permission. Additionally, only packages declared in
66      * [R.array.config_controlsPreferredPackages] can declare activities for use as a panel.
67      */
68     var panelActivity: ComponentName? = null
69         protected set
70 
71     private var resolved: Boolean = false
72 
73     @WorkerThread
74     fun resolvePanelActivity(
75         allowAllApps: Boolean = false
76     ) {
77         if (resolved) return
78         resolved = true
79         val validPackages = context.resources
80             .getStringArray(R.array.config_controlsPreferredPackages)
81         if (componentName.packageName !in validPackages && !allowAllApps) return
82         panelActivity = _panelActivity?.let {
83             val resolveInfos = mPm.queryIntentActivitiesAsUser(
84                 Intent().setComponent(it),
85                 PackageManager.ResolveInfoFlags.of(
86                     MATCH_DIRECT_BOOT_AWARE.toLong() or
87                             MATCH_DIRECT_BOOT_UNAWARE.toLong()
88                 ),
89                 UserHandle.of(userId)
90             )
91             if (resolveInfos.isNotEmpty() && verifyResolveInfo(resolveInfos[0])) {
92                 it
93             } else {
94                 null
95             }
96         }
97     }
98 
99     /**
100      * Verifies that the panel activity is enabled, exported and protected by the correct
101      * permission. This last check is to prevent apps from forgetting to protect the activity, as
102      * they won't be able to see the panel until they do.
103      */
104     @WorkerThread
105     private fun verifyResolveInfo(resolveInfo: ResolveInfo): Boolean {
106         return resolveInfo.activityInfo?.let {
107             it.permission == Manifest.permission.BIND_CONTROLS &&
108                     it.exported && isComponentActuallyEnabled(it)
109         } ?: false
110     }
111 
112     @WorkerThread
113     private fun isComponentActuallyEnabled(activityInfo: ActivityInfo): Boolean {
114         return when (mPm.getComponentEnabledSetting(activityInfo.componentName)) {
115             PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true
116             PackageManager.COMPONENT_ENABLED_STATE_DISABLED -> false
117             PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> activityInfo.enabled
118             else -> false
119         }
120     }
121 
122     @WorkerThread
123     override fun loadLabel(): CharSequence {
124         return componentName?.let {
125             val appInfo = mPm.getApplicationInfoAsUser(componentName.packageName, 0, userId)
126             appInfo.loadLabel(mPm)
127         }
128             ?: packageItemInfo?.loadLabel(mPm)
129             ?: throw IllegalArgumentException("Package info is missing")
130     }
131 
132     @WorkerThread
133     override fun loadIcon(): Drawable {
134         val packageName =
135             componentName?.packageName
136                 ?: packageItemInfo?.packageName
137                 ?: throw IllegalArgumentException("Package info is missing")
138         val factory = IconDrawableFactory.newInstance(context)
139         val appInfo = mPm.getApplicationInfoAsUser(packageName, 0, userId)
140         return factory.getBadgedIcon(appInfo)
141     }
142 
143     override fun equals(other: Any?): Boolean {
144         return other is ControlsServiceInfo &&
145                 userId == other.userId &&
146                 componentName == other.componentName &&
147                 panelActivity == other.panelActivity
148     }
149 
150     override fun hashCode(): Int {
151         return Objects.hash(userId, componentName, panelActivity)
152     }
153 
154     fun copy(): ControlsServiceInfo {
155         return ControlsServiceInfo(context, serviceInfo).also {
156             it.panelActivity = this.panelActivity
157         }
158     }
159 
160     override fun toString(): String {
161         return """
162             ControlsServiceInfo(serviceInfo=$serviceInfo, panelActivity=$panelActivity, resolved=$resolved)
163         """.trimIndent()
164     }
165 }