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.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.ViewGroup.LayoutParams.WRAP_CONTENT 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 and user id 45 */ 46 class PrivacyDialog( 47 context: Context, 48 private val list: List<PrivacyElement>, 49 activityStarter: (String, Int) -> 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 setLayout(context.resources.getDimensionPixelSize(R.dimen.qs_panel_width), WRAP_CONTENT) 69 setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL) 70 } 71 72 setContentView(R.layout.privacy_dialog) 73 rootView = requireViewById<ViewGroup>(R.id.root) 74 75 list.forEach { 76 rootView.addView(createView(it)) 77 } 78 } 79 80 /** 81 * Add a listener that will be called when the dialog is dismissed. 82 * 83 * If the dialog has already been dismissed, the listener will be called immediately, in the 84 * same thread. 85 */ 86 fun addOnDismissListener(listener: OnDialogDismissed) { 87 if (dismissed.get()) { 88 listener.onDialogDismissed() 89 } else { 90 dismissListeners.add(WeakReference(listener)) 91 } 92 } 93 94 override fun onStop() { 95 super.onStop() 96 dismissed.set(true) 97 val iterator = dismissListeners.iterator() 98 while (iterator.hasNext()) { 99 val el = iterator.next() 100 iterator.remove() 101 el.get()?.onDialogDismissed() 102 } 103 } 104 105 private fun createView(element: PrivacyElement): View { 106 val newView = LayoutInflater.from(context).inflate( 107 R.layout.privacy_dialog_item, rootView, false 108 ) as ViewGroup 109 val d = getDrawableForType(element.type) 110 d.findDrawableByLayerId(R.id.icon).setTint(iconColorSolid) 111 newView.requireViewById<ImageView>(R.id.icon).apply { 112 setImageDrawable(d) 113 contentDescription = element.type.getName(context) 114 } 115 val stringId = getStringIdForState(element.active) 116 val app = if (element.phoneCall) phonecall else element.applicationName 117 val appName = if (element.enterprise) { 118 TextUtils.concat(app, enterpriseText) 119 } else { 120 app 121 } 122 val firstLine = context.getString(stringId, appName) 123 val finalText = element.attribution?.let { 124 TextUtils.concat( 125 firstLine, 126 " ", 127 context.getString(R.string.ongoing_privacy_dialog_attribution_text, it) 128 ) 129 } ?: firstLine 130 newView.requireViewById<TextView>(R.id.text).text = finalText 131 if (element.phoneCall) { 132 newView.requireViewById<View>(R.id.chevron).visibility = View.GONE 133 } 134 newView.apply { 135 setTag(element) 136 if (!element.phoneCall) { 137 setOnClickListener(clickListener) 138 } 139 } 140 return newView 141 } 142 143 private fun getStringIdForState(active: Boolean): Int { 144 return if (active) { 145 R.string.ongoing_privacy_dialog_using_op 146 } else { 147 R.string.ongoing_privacy_dialog_recent_op 148 } 149 } 150 151 private fun getDrawableForType(type: PrivacyType): LayerDrawable { 152 return context.getDrawable(when (type) { 153 PrivacyType.TYPE_LOCATION -> R.drawable.privacy_item_circle_location 154 PrivacyType.TYPE_CAMERA -> R.drawable.privacy_item_circle_camera 155 PrivacyType.TYPE_MICROPHONE -> R.drawable.privacy_item_circle_microphone 156 }) as LayerDrawable 157 } 158 159 private val clickListener = View.OnClickListener { v -> 160 v.tag?.let { 161 val element = it as PrivacyElement 162 activityStarter(element.packageName, element.userId) 163 } 164 } 165 166 /** */ 167 data class PrivacyElement( 168 val type: PrivacyType, 169 val packageName: String, 170 val userId: Int, 171 val applicationName: CharSequence, 172 val attribution: CharSequence?, 173 val lastActiveTimestamp: Long, 174 val active: Boolean, 175 val enterprise: Boolean, 176 val phoneCall: Boolean 177 ) { 178 private val builder = StringBuilder("PrivacyElement(") 179 180 init { 181 builder.append("type=${type.logName}") 182 builder.append(", packageName=$packageName") 183 builder.append(", userId=$userId") 184 builder.append(", appName=$applicationName") 185 if (attribution != null) { 186 builder.append(", attribution=$attribution") 187 } 188 builder.append(", lastActive=$lastActiveTimestamp") 189 if (active) { 190 builder.append(", active") 191 } 192 if (enterprise) { 193 builder.append(", enterprise") 194 } 195 if (phoneCall) { 196 builder.append(", phoneCall") 197 } 198 builder.append(")") 199 } 200 201 override fun toString(): String = builder.toString() 202 } 203 204 /** */ 205 interface OnDialogDismissed { 206 fun onDialogDismissed() 207 } 208 }