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 }