1 /*
2  * 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.graphics.drawable.LayerDrawable
21 import android.os.Bundle
22 import android.text.TextUtils
23 import android.view.Gravity
24 import android.view.LayoutInflater
25 import android.view.View
26 import android.view.ViewGroup
27 import android.view.WindowInsets
28 import android.widget.ImageView
29 import android.widget.TextView
30 import com.android.settingslib.Utils
31 import com.android.systemui.R
32 import com.android.systemui.statusbar.phone.SystemUIDialog
33 import java.lang.ref.WeakReference
34 import java.util.concurrent.atomic.AtomicBoolean
35 
36 /**
37  * Dialog to show ongoing and recent app ops usage.
38  *
39  * @see PrivacyDialogController
40  * @param context A context to create the dialog
41  * @param list list of elements to show in the dialog. The elements will show in the same order they
42  *             appear in the list
43  * @param activityStarter a callback to start an activity for a given package name and user id
44  */
45 class PrivacyDialog(
46     context: Context,
47     private val list: List<PrivacyElement>,
48     activityStarter: (String, Int) -> Unit
49 ) : SystemUIDialog(context, R.style.PrivacyDialog) {
50 
51     private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>()
52     private val dismissed = AtomicBoolean(false)
53 
54     private val iconColorSolid = Utils.getColorAttrDefaultColor(
55             this.context, com.android.internal.R.attr.colorPrimary
56     )
57     private val enterpriseText = " ${context.getString(R.string.ongoing_privacy_dialog_enterprise)}"
58     private val phonecall = context.getString(R.string.ongoing_privacy_dialog_phonecall)
59 
60     private lateinit var rootView: ViewGroup
61 
62     override fun onCreate(savedInstanceState: Bundle?) {
63         super.onCreate(savedInstanceState)
64         window?.apply {
65             attributes.fitInsetsTypes = attributes.fitInsetsTypes or WindowInsets.Type.statusBars()
66             attributes.receiveInsetsIgnoringZOrder = true
67             setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL)
68         }
69 
70         setContentView(R.layout.privacy_dialog)
71         rootView = requireViewById<ViewGroup>(R.id.root)
72 
73         list.forEach {
74             rootView.addView(createView(it))
75         }
76     }
77 
78     /**
79      * Add a listener that will be called when the dialog is dismissed.
80      *
81      * If the dialog has already been dismissed, the listener will be called immediately, in the
82      * same thread.
83      */
84     fun addOnDismissListener(listener: OnDialogDismissed) {
85         if (dismissed.get()) {
86             listener.onDialogDismissed()
87         } else {
88             dismissListeners.add(WeakReference(listener))
89         }
90     }
91 
92     override fun onStop() {
93         super.onStop()
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 = element.attribution?.let {
122             TextUtils.concat(
123                     firstLine,
124                     " ",
125                     context.getString(R.string.ongoing_privacy_dialog_attribution_text, it)
126             )
127         } ?: firstLine
128         newView.requireViewById<TextView>(R.id.text).text = finalText
129         if (element.phoneCall) {
130             newView.requireViewById<View>(R.id.chevron).visibility = View.GONE
131         }
132         newView.apply {
133             setTag(element)
134             if (!element.phoneCall) {
135                 setOnClickListener(clickListener)
136             }
137         }
138         return newView
139     }
140 
141     private fun getStringIdForState(active: Boolean): Int {
142         return if (active) {
143             R.string.ongoing_privacy_dialog_using_op
144         } else {
145             R.string.ongoing_privacy_dialog_recent_op
146         }
147     }
148 
149     private fun getDrawableForType(type: PrivacyType): LayerDrawable {
150         return context.getDrawable(when (type) {
151             PrivacyType.TYPE_LOCATION -> R.drawable.privacy_item_circle_location
152             PrivacyType.TYPE_CAMERA -> R.drawable.privacy_item_circle_camera
153             PrivacyType.TYPE_MICROPHONE -> R.drawable.privacy_item_circle_microphone
154         }) as LayerDrawable
155     }
156 
157     private val clickListener = View.OnClickListener { v ->
158         v.tag?.let {
159             val element = it as PrivacyElement
160             activityStarter(element.packageName, element.userId)
161         }
162     }
163 
164     /** */
165     data class PrivacyElement(
166         val type: PrivacyType,
167         val packageName: String,
168         val userId: Int,
169         val applicationName: CharSequence,
170         val attribution: CharSequence?,
171         val lastActiveTimestamp: Long,
172         val active: Boolean,
173         val enterprise: Boolean,
174         val phoneCall: Boolean
175     ) {
176         private val builder = StringBuilder("PrivacyElement(")
177 
178         init {
179             builder.append("type=${type.logName}")
180             builder.append(", packageName=$packageName")
181             builder.append(", userId=$userId")
182             builder.append(", appName=$applicationName")
183             if (attribution != null) {
184                 builder.append(", attribution=$attribution")
185             }
186             builder.append(", lastActive=$lastActiveTimestamp")
187             if (active) {
188                 builder.append(", active")
189             }
190             if (enterprise) {
191                 builder.append(", enterprise")
192             }
193             if (phoneCall) {
194                 builder.append(", phoneCall")
195             }
196             builder.append(")")
197         }
198 
199         override fun toString(): String = builder.toString()
200     }
201 
202     /** */
203     interface OnDialogDismissed {
204         fun onDialogDismissed()
205     }
206 }