1 /* 2 * Copyright (C) 2022 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.permissioncontroller.safetycenter.ui.view 18 19 import android.content.Context 20 import android.os.Build 21 import android.safetycenter.SafetyCenterEntry 22 import android.safetycenter.SafetyCenterEntry.IconAction.ICON_ACTION_TYPE_GEAR 23 import android.util.AttributeSet 24 import android.util.Log 25 import android.view.ViewGroup 26 import android.widget.ImageView 27 import android.widget.LinearLayout 28 import android.widget.TextView 29 import androidx.annotation.RequiresApi 30 import com.android.permissioncontroller.R 31 import com.android.permissioncontroller.safetycenter.ui.Action 32 import com.android.permissioncontroller.safetycenter.ui.PendingIntentSender 33 import com.android.permissioncontroller.safetycenter.ui.PositionInCardList 34 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterViewModel 35 import com.android.permissioncontroller.safetycenter.ui.view.SafetyEntryCommonViewsManager.Companion.changeEnabledState 36 37 @RequiresApi(Build.VERSION_CODES.TIRAMISU) 38 internal class SafetyEntryView 39 @JvmOverloads 40 constructor( 41 context: Context?, 42 attrs: AttributeSet? = null, 43 defStyleAttr: Int = 0, 44 defStyleRes: Int = 0 45 ) : LinearLayout(context, attrs, defStyleAttr, defStyleRes) { 46 47 private companion object { 48 val TAG = SafetyEntryView::class.java.simpleName 49 } 50 51 init { 52 inflate(context, R.layout.view_entry, this) 53 } 54 <lambda>null55 private val commonEntryView: SafetyEntryCommonViewsManager? by lazyView { 56 SafetyEntryCommonViewsManager(this) 57 } 58 private val widgetFrame: ViewGroup? by lazyView(R.id.widget_frame) 59 showEntrynull60 fun showEntry( 61 entry: SafetyCenterEntry, 62 position: PositionInCardList, 63 launchTaskId: Int?, 64 viewModel: SafetyCenterViewModel 65 ) { 66 setBackgroundResource(position.backgroundDrawableResId) 67 val topMargin: Int = position.getTopMargin(context) 68 69 val params = layoutParams as MarginLayoutParams 70 if (params.topMargin != topMargin) { 71 params.topMargin = topMargin 72 layoutParams = params 73 } 74 75 showEntryDetails(entry) 76 setupEntryClickListener(entry, launchTaskId, viewModel) 77 enableOrDisableEntry(entry) 78 setupIconActionButton(entry, launchTaskId, viewModel) 79 setContentDescription(entry, position == PositionInCardList.INSIDE_GROUP) 80 } 81 showEntryDetailsnull82 private fun showEntryDetails(entry: SafetyCenterEntry) { 83 commonEntryView?.showDetails( 84 entry.id, 85 entry.title, 86 entry.summary, 87 entry.severityLevel, 88 entry.severityUnspecifiedIconType 89 ) 90 } 91 showTextnull92 private fun TextView.showText(text: CharSequence?) { 93 if (text?.isNotEmpty() != true) { 94 visibility = GONE 95 } else { 96 visibility = VISIBLE 97 this.text = text 98 } 99 } 100 setupEntryClickListenernull101 private fun setupEntryClickListener( 102 entry: SafetyCenterEntry, 103 launchTaskId: Int?, 104 viewModel: SafetyCenterViewModel 105 ) { 106 val pendingIntent = entry.pendingIntent 107 if (pendingIntent != null) { 108 setOnClickListener { 109 try { 110 PendingIntentSender.send(entry.pendingIntent, launchTaskId) 111 viewModel.interactionLogger.recordForEntry(Action.ENTRY_CLICKED, entry) 112 } catch (ex: java.lang.Exception) { 113 Log.e(TAG, "Failed to execute pending intent for entry: $entry", ex) 114 } 115 } 116 } else { 117 // Ensure that views without listeners can still be focused by accessibility services 118 // TODO b/243713158: Set the proper accessibility focus in style, rather than in code 119 isFocusable = true 120 } 121 } 122 setupIconActionButtonnull123 private fun setupIconActionButton( 124 entry: SafetyCenterEntry, 125 launchTaskId: Int?, 126 viewModel: SafetyCenterViewModel 127 ) { 128 val iconAction = entry.iconAction 129 if (iconAction != null) { 130 val iconActionButton = 131 widgetFrame?.findViewById(R.id.icon_action_button) 132 ?: kotlin.run { 133 val widgetLayout = 134 if (iconAction.type == ICON_ACTION_TYPE_GEAR) { 135 R.layout.preference_entry_icon_action_gear_widget 136 } else { 137 R.layout.preference_entry_icon_action_info_widget 138 } 139 inflate(context, widgetLayout, widgetFrame) 140 widgetFrame?.findViewById<ImageView>(R.id.icon_action_button) 141 } 142 widgetFrame?.visibility = VISIBLE 143 iconActionButton?.setOnClickListener { 144 sendIconActionIntent(iconAction, launchTaskId, entry) 145 viewModel.interactionLogger.recordForEntry(Action.ENTRY_ICON_ACTION_CLICKED, entry) 146 } 147 setPaddingRelative(paddingStart, paddingTop, /* end = */ 0, paddingBottom) 148 } else { 149 widgetFrame?.visibility = GONE 150 setPaddingRelative( 151 paddingStart, 152 paddingTop, 153 context.resources.getDimensionPixelSize(R.dimen.sc_entry_padding_end), 154 paddingBottom 155 ) 156 } 157 } 158 sendIconActionIntentnull159 private fun sendIconActionIntent( 160 iconAction: SafetyCenterEntry.IconAction, 161 launchTaskId: Int?, 162 entry: SafetyCenterEntry 163 ) { 164 try { 165 PendingIntentSender.send(iconAction.pendingIntent, launchTaskId) 166 } catch (ex: Exception) { 167 Log.e(TAG, "Failed to execute icon action intent for entry: $entry", ex) 168 } 169 } 170 171 /** We are doing this because we need some entries to look disabled but still be clickable. */ enableOrDisableEntrynull172 private fun enableOrDisableEntry(entry: SafetyCenterEntry) { 173 // Making it clickable allows a disabled Entry View to consume its click which would 174 // otherwise be sent to the parent and cause the entry group to collapse. 175 isClickable = true 176 isEnabled = entry.pendingIntent != null 177 changeEnabledState( 178 context, 179 entry.isEnabled, 180 isEnabled, 181 commonEntryView?.titleView, 182 commonEntryView?.summaryView 183 ) 184 } 185 setContentDescriptionnull186 private fun setContentDescription(entry: SafetyCenterEntry, isGroupEntry: Boolean) { 187 // Setting a customized description for entries that are part of an expandable group. 188 // Whereas for non-expandable entries, the default description of title and summary is used. 189 val resourceId = 190 if (isGroupEntry) { 191 R.string.safety_center_entry_group_item_content_description 192 } else { 193 R.string.safety_center_entry_content_description 194 } 195 contentDescription = context.getString(resourceId, entry.title, entry.summary) 196 } 197 } 198