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.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 onStop() { 94 super.onStop() 95 dismissed.set(true) 96 val iterator = dismissListeners.iterator() 97 while (iterator.hasNext()) { 98 val el = iterator.next() 99 iterator.remove() 100 el.get()?.onDialogDismissed() 101 } 102 } 103 104 private fun createView(element: PrivacyElement): View { 105 val newView = LayoutInflater.from(context).inflate( 106 R.layout.privacy_dialog_item, rootView, false 107 ) as ViewGroup 108 val d = getDrawableForType(element.type) 109 d.findDrawableByLayerId(R.id.icon).setTint(iconColorSolid) 110 newView.requireViewById<ImageView>(R.id.icon).apply { 111 setImageDrawable(d) 112 contentDescription = element.type.getName(context) 113 } 114 val stringId = getStringIdForState(element.active) 115 val app = if (element.phoneCall) phonecall else element.applicationName 116 val appName = if (element.enterprise) { 117 TextUtils.concat(app, enterpriseText) 118 } else { 119 app 120 } 121 val firstLine = context.getString(stringId, appName) 122 val finalText = getFinalText(firstLine, element.attributionLabel, element.proxyLabel) 123 newView.requireViewById<TextView>(R.id.text).text = finalText 124 if (element.phoneCall) { 125 newView.requireViewById<View>(R.id.chevron).visibility = View.GONE 126 } 127 newView.apply { 128 setTag(element) 129 if (!element.phoneCall) { 130 setOnClickListener(clickListener) 131 } 132 } 133 return newView 134 } 135 136 private fun getFinalText( 137 firstLine: CharSequence, 138 attributionLabel: CharSequence?, 139 proxyLabel: CharSequence? 140 ): CharSequence { 141 var dialogText: CharSequence? = null 142 if (attributionLabel != null && proxyLabel != null) { 143 dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_proxy_label, 144 attributionLabel, proxyLabel) 145 } else if (attributionLabel != null) { 146 dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_label, 147 attributionLabel) 148 } else if (proxyLabel != null) { 149 dialogText = context.getString(R.string.ongoing_privacy_dialog_attribution_text, 150 proxyLabel) 151 } 152 return if (dialogText != null) TextUtils.concat(firstLine, " ", dialogText) else firstLine 153 } 154 155 private fun getStringIdForState(active: Boolean): Int { 156 return if (active) { 157 R.string.ongoing_privacy_dialog_using_op 158 } else { 159 R.string.ongoing_privacy_dialog_recent_op 160 } 161 } 162 163 private fun getDrawableForType(type: PrivacyType): LayerDrawable { 164 return context.getDrawable(when (type) { 165 PrivacyType.TYPE_LOCATION -> R.drawable.privacy_item_circle_location 166 PrivacyType.TYPE_CAMERA -> R.drawable.privacy_item_circle_camera 167 PrivacyType.TYPE_MICROPHONE -> R.drawable.privacy_item_circle_microphone 168 PrivacyType.TYPE_MEDIA_PROJECTION -> R.drawable.privacy_item_circle_media_projection 169 }) as LayerDrawable 170 } 171 172 private val clickListener = View.OnClickListener { v -> 173 v.tag?.let { 174 val element = it as PrivacyElement 175 activityStarter(element.packageName, element.userId, 176 element.attributionTag, element.navigationIntent) 177 } 178 } 179 180 /** */ 181 data class PrivacyElement( 182 val type: PrivacyType, 183 val packageName: String, 184 val userId: Int, 185 val applicationName: CharSequence, 186 val attributionTag: CharSequence?, 187 val attributionLabel: CharSequence?, 188 val proxyLabel: CharSequence?, 189 val lastActiveTimestamp: Long, 190 val active: Boolean, 191 val enterprise: Boolean, 192 val phoneCall: Boolean, 193 val permGroupName: CharSequence, 194 val navigationIntent: Intent? 195 ) { 196 private val builder = StringBuilder("PrivacyElement(") 197 198 init { 199 builder.append("type=${type.logName}") 200 builder.append(", packageName=$packageName") 201 builder.append(", userId=$userId") 202 builder.append(", appName=$applicationName") 203 if (attributionTag != null) { 204 builder.append(", attributionTag=$attributionTag") 205 } 206 if (attributionLabel != null) { 207 builder.append(", attributionLabel=$attributionLabel") 208 } 209 if (proxyLabel != null) { 210 builder.append(", proxyLabel=$proxyLabel") 211 } 212 builder.append(", lastActive=$lastActiveTimestamp") 213 if (active) { 214 builder.append(", active") 215 } 216 if (enterprise) { 217 builder.append(", enterprise") 218 } 219 if (phoneCall) { 220 builder.append(", phoneCall") 221 } 222 builder.append(", permGroupName=$permGroupName)") 223 if (navigationIntent != null) { 224 builder.append(", navigationIntent=$navigationIntent") 225 } 226 } 227 228 override fun toString(): String = builder.toString() 229 } 230 231 /** */ 232 interface OnDialogDismissed { 233 fun onDialogDismissed() 234 } 235 }