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 }