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