1 /*
<lambda>null2  * Copyright (C) 2021 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.privacy
18 
19 import android.content.Context
20 import android.content.Intent
21 import android.graphics.drawable.LayerDrawable
22 import android.os.Bundle
23 import android.text.TextUtils
24 import android.view.Gravity
25 import android.view.LayoutInflater
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.WindowInsets
29 import android.widget.ImageView
30 import android.widget.TextView
31 import com.android.settingslib.Utils
32 import com.android.systemui.res.R
33 import com.android.systemui.statusbar.phone.SystemUIDialog
34 import java.lang.ref.WeakReference
35 import java.util.concurrent.atomic.AtomicBoolean
36 
37 /**
38  * Dialog to show ongoing and recent app ops usage.
39  *
40  * @see PrivacyDialogController
41  * @param context A context to create the dialog
42  * @param list list of elements to show in the dialog. The elements will show in the same order they
43  *             appear in the list
44  * @param activityStarter a callback to start an activity for a given package name, user id, attributionTag and intent
45  */
46 class PrivacyDialog(
47     context: Context,
48     private val list: List<PrivacyElement>,
49     activityStarter: (String, Int, CharSequence?, Intent?) -> Unit
50 ) : SystemUIDialog(context, R.style.PrivacyDialog) {
51 
52     private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
53     private val dismissed = AtomicBoolean(false)
54 
55     private val iconColorSolid = Utils.getColorAttrDefaultColor(
56             this.context, com.android.internal.R.attr.colorPrimary
57     )
58     private val enterpriseText = " ${context.getString(R.string.ongoing_privacy_dialog_enterprise)}"
59     private val phonecall = context.getString(R.string.ongoing_privacy_dialog_phonecall)
60 
61     private lateinit var rootView: ViewGroup
62 
63     override fun onCreate(savedInstanceState: Bundle?) {
64         super.onCreate(savedInstanceState)
65         window?.apply {
66             attributes.fitInsetsTypes = attributes.fitInsetsTypes or WindowInsets.Type.statusBars()
67             attributes.receiveInsetsIgnoringZOrder = true
68             setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL)
69         }
70         setTitle(R.string.ongoing_privacy_dialog_a11y_title)
71         setContentView(R.layout.privacy_dialog)
72         rootView = requireViewById<ViewGroup>(R.id.root)
73 
74         list.forEach {
75             rootView.addView(createView(it))
76         }
77     }
78 
79     /**
80      * Add a listener that will be called when the dialog is dismissed.
81      *
82      * If the dialog has already been dismissed, the listener will be called immediately, in the
83      * same thread.
84      */
85     fun addOnDismissListener(listener: OnDialogDismissed) {
86         if (dismissed.get()) {
87             listener.onDialogDismissed()
88         } else {
89             dismissListeners.add(WeakReference(listener))
90         }
91     }
92 
93     override fun stop() {
94         dismissed.set(true)
95         val iterator = dismissListeners.iterator()
96         while (iterator.hasNext()) {
97             val el = iterator.next()
98             iterator.remove()
99             el.get()?.onDialogDismissed()
100         }
101     }
102 
103     private fun createView(element: PrivacyElement): View {
104         val newView = LayoutInflater.from(context).inflate(
105                 R.layout.privacy_dialog_item, rootView, false
106         ) as ViewGroup
107         val d = getDrawableForType(element.type)
108         d.findDrawableByLayerId(R.id.icon).setTint(iconColorSolid)
109         newView.requireViewById<ImageView>(R.id.icon).apply {
110             setImageDrawable(d)
111             contentDescription = element.type.getName(context)
112         }
113         val stringId = getStringIdForState(element.active)
114         val app = if (element.phoneCall) phonecall else element.applicationName
115         val appName = if (element.enterprise) {
116             TextUtils.concat(app, enterpriseText)
117         } else {
118             app
119         }
120         val firstLine = context.getString(stringId, appName)
121         val finalText = getFinalText(firstLine, element.attributionLabel, element.proxyLabel)
122         newView.requireViewById<TextView>(R.id.text).text = finalText
123         if (element.phoneCall) {
124             newView.requireViewById<View>(R.id.chevron).visibility = View.GONE
125         }
126         newView.apply {
127             setTag(element)
128             if (!element.phoneCall) {
129                 setOnClickListener(clickListener)
130             }
131         }
132         return newView
133     }
134 
135     private fun getFinalText(
136         firstLine: CharSequence,
137         attributionLabel: CharSequence?,
138         proxyLabel: CharSequence?
139     ): CharSequence {
140         var dialogText: CharSequence? = null
141         if (attributionLabel != null && proxyLabel != null) {
142             dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_proxy_label,
143                 attributionLabel, proxyLabel)
144         } else if (attributionLabel != null) {
145             dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_label,
146                 attributionLabel)
147         } else if (proxyLabel != null) {
148             dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_text,
149                 proxyLabel)
150         }
151         return if (dialogText != null) TextUtils.concat(firstLine, " ", dialogText) else firstLine
152     }
153 
154     private fun getStringIdForState(active: Boolean): Int {
155         return if (active) {
156             R.string.ongoing_privacy_dialog_using_op
157         } else {
158             R.string.ongoing_privacy_dialog_recent_op
159         }
160     }
161 
162     private fun getDrawableForType(type: PrivacyType): LayerDrawable {
163         return context.getDrawable(when (type) {
164             PrivacyType.TYPE_LOCATION -> R.drawable.privacy_item_circle_location
165             PrivacyType.TYPE_CAMERA -> R.drawable.privacy_item_circle_camera
166             PrivacyType.TYPE_MICROPHONE -> R.drawable.privacy_item_circle_microphone
167             PrivacyType.TYPE_MEDIA_PROJECTION -> R.drawable.privacy_item_circle_media_projection
168         }) as LayerDrawable
169     }
170 
171     private val clickListener = View.OnClickListener { v ->
172         v.tag?.let {
173             val element = it as PrivacyElement
174             activityStarter(element.packageName, element.userId,
175                 element.attributionTag, element.navigationIntent)
176         }
177     }
178 
179     /** */
180     data class PrivacyElement(
181         val type: PrivacyType,
182         val packageName: String,
183         val userId: Int,
184         val applicationName: CharSequence,
185         val attributionTag: CharSequence?,
186         val attributionLabel: CharSequence?,
187         val proxyLabel: CharSequence?,
188         val lastActiveTimestamp: Long,
189         val active: Boolean,
190         val enterprise: Boolean,
191         val phoneCall: Boolean,
192         val permGroupName: CharSequence,
193         val navigationIntent: Intent?
194     ) {
195         private val builder = StringBuilder("PrivacyElement(")
196 
197         init {
198             builder.append("type=${type.logName}")
199             builder.append(", packageName=$packageName")
200             builder.append(", userId=$userId")
201             builder.append(", appName=$applicationName")
202             if (attributionTag != null) {
203                 builder.append(", attributionTag=$attributionTag")
204             }
205             if (attributionLabel != null) {
206                 builder.append(", attributionLabel=$attributionLabel")
207             }
208             if (proxyLabel != null) {
209                 builder.append(", proxyLabel=$proxyLabel")
210             }
211             builder.append(", lastActive=$lastActiveTimestamp")
212             if (active) {
213                 builder.append(", active")
214             }
215             if (enterprise) {
216                 builder.append(", enterprise")
217             }
218             if (phoneCall) {
219                 builder.append(", phoneCall")
220             }
221             builder.append(", permGroupName=$permGroupName)")
222             if (navigationIntent != null) {
223                 builder.append(", navigationIntent=$navigationIntent")
224             }
225         }
226 
227         override fun toString(): String = builder.toString()
228     }
229 
230     /** */
231     interface OnDialogDismissed {
232         fun onDialogDismissed()
233     }
234 }