1 /*
<lambda>null2  * 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 package com.android.systemui.mediaprojection.permission
17 
18 import android.app.AlertDialog
19 import android.content.Context
20 import android.os.Bundle
21 import android.view.Gravity
22 import android.view.LayoutInflater
23 import android.view.View
24 import android.view.ViewGroup
25 import android.view.ViewStub
26 import android.view.WindowManager
27 import android.view.accessibility.AccessibilityNodeInfo
28 import android.widget.AdapterView
29 import android.widget.ArrayAdapter
30 import android.widget.ImageView
31 import android.widget.Spinner
32 import android.widget.TextView
33 import androidx.annotation.CallSuper
34 import androidx.annotation.ColorRes
35 import androidx.annotation.DrawableRes
36 import androidx.annotation.LayoutRes
37 import androidx.annotation.StringRes
38 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
39 import com.android.systemui.res.R
40 import com.android.systemui.statusbar.phone.DialogDelegate
41 
42 /** Base permission dialog for screen share and recording */
43 abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>(
44     private val screenShareOptions: List<ScreenShareOption>,
45     private val appName: String?,
46     private val hostUid: Int,
47     private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
48     @DrawableRes private val dialogIconDrawable: Int? = null,
49     @ColorRes private val dialogIconTint: Int? = null,
50     @ScreenShareMode val defaultSelectedMode: Int = screenShareOptions.first().mode,
51 ) : DialogDelegate<T>, AdapterView.OnItemSelectedListener {
52     private lateinit var dialogTitle: TextView
53     private lateinit var startButton: TextView
54     private lateinit var cancelButton: TextView
55     private lateinit var warning: TextView
56     private lateinit var screenShareModeSpinner: Spinner
57     protected lateinit var dialog: AlertDialog
58     private var shouldLogCancel: Boolean = true
59     var selectedScreenShareOption: ScreenShareOption =
60         screenShareOptions.first { it.mode == defaultSelectedMode }
61 
62     @CallSuper
63     override fun onStop(dialog: T) {
64         // onStop can be called multiple times and we only want to log once.
65         if (shouldLogCancel) {
66             mediaProjectionMetricsLogger.notifyProjectionRequestCancelled(hostUid)
67             shouldLogCancel = false
68         }
69     }
70 
71     @CallSuper
72     override fun onCreate(dialog: T, savedInstanceState: Bundle?) {
73         this.dialog = dialog
74         dialog.window?.addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS)
75         dialog.window?.setGravity(Gravity.CENTER)
76         dialog.setContentView(R.layout.screen_share_dialog)
77         dialogTitle = dialog.requireViewById(R.id.screen_share_dialog_title)
78         warning = dialog.requireViewById(R.id.text_warning)
79         startButton = dialog.requireViewById(android.R.id.button1)
80         cancelButton = dialog.requireViewById(android.R.id.button2)
81         updateIcon()
82         initScreenShareOptions()
83         createOptionsView(getOptionsViewLayoutId())
84     }
85 
86     private fun updateIcon() {
87         val icon = dialog.requireViewById<ImageView>(R.id.screen_share_dialog_icon)
88         if (dialogIconTint != null) {
89             icon.setColorFilter(dialog.context.getColor(dialogIconTint))
90         }
91         if (dialogIconDrawable != null) {
92             icon.setImageDrawable(dialog.context.getDrawable(dialogIconDrawable))
93         }
94     }
95 
96     private fun initScreenShareOptions() {
97         selectedScreenShareOption = screenShareOptions.first { it.mode == defaultSelectedMode }
98         warning.text = warningText
99         initScreenShareSpinner()
100     }
101 
102     private val warningText: String
103         get() = dialog.context.getString(selectedScreenShareOption.warningText, appName)
104 
105     private fun initScreenShareSpinner() {
106         val adapter = OptionsAdapter(dialog.context.applicationContext, screenShareOptions)
107         screenShareModeSpinner = dialog.requireViewById(R.id.screen_share_mode_spinner)
108         screenShareModeSpinner.adapter = adapter
109         screenShareModeSpinner.onItemSelectedListener = this
110 
111         // disable redundant Touch & Hold accessibility action for Switch Access
112         screenShareModeSpinner.accessibilityDelegate =
113             object : View.AccessibilityDelegate() {
114                 override fun onInitializeAccessibilityNodeInfo(
115                     host: View,
116                     info: AccessibilityNodeInfo
117                 ) {
118                     info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK)
119                     super.onInitializeAccessibilityNodeInfo(host, info)
120                 }
121             }
122         screenShareModeSpinner.isLongClickable = false
123         val defaultModePosition = screenShareOptions.indexOfFirst { it.mode == defaultSelectedMode }
124         screenShareModeSpinner.setSelection(defaultModePosition, /* animate= */ false)
125     }
126 
127     override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) {
128         selectedScreenShareOption = screenShareOptions[pos]
129         warning.text = warningText
130     }
131 
132     override fun onNothingSelected(parent: AdapterView<*>?) {}
133 
134     /** Protected methods for the text updates & functionality */
135     protected fun setDialogTitle(@StringRes stringId: Int) {
136         val title = dialog.context.getString(stringId, appName)
137         dialogTitle.text = title
138     }
139 
140     protected fun setStartButtonText(@StringRes stringId: Int) {
141         startButton.setText(stringId)
142     }
143 
144     protected fun setStartButtonOnClickListener(listener: View.OnClickListener?) {
145         startButton.setOnClickListener { view ->
146             shouldLogCancel = false
147             listener?.onClick(view)
148         }
149     }
150 
151     protected fun setCancelButtonOnClickListener(listener: View.OnClickListener?) {
152         cancelButton.setOnClickListener(listener)
153     }
154 
155     // Create additional options that is shown under the share mode spinner
156     // Eg. the audio and tap toggles in SysUI Recorder
157     @LayoutRes protected open fun getOptionsViewLayoutId(): Int? = null
158 
159     private fun createOptionsView(@LayoutRes layoutId: Int?) {
160         if (layoutId == null) return
161         val stub = dialog.requireViewById<View>(R.id.options_stub) as ViewStub
162         stub.layoutResource = layoutId
163         stub.inflate()
164     }
165 }
166 
167 private class OptionsAdapter(
168     context: Context,
169     private val options: List<ScreenShareOption>,
170 ) :
171     ArrayAdapter<String>(
172         context,
173         R.layout.screen_share_dialog_spinner_text,
<lambda>null174         options.map { context.getString(it.spinnerText) }
175     ) {
176 
isEnablednull177     override fun isEnabled(position: Int): Boolean {
178         return options[position].spinnerDisabledText == null
179     }
180 
getDropDownViewnull181     override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
182         val inflater = LayoutInflater.from(parent.context)
183         val view = inflater.inflate(R.layout.screen_share_dialog_spinner_item_text, parent, false)
184         val titleTextView = view.requireViewById<TextView>(android.R.id.text1)
185         val errorTextView = view.requireViewById<TextView>(android.R.id.text2)
186         titleTextView.text = getItem(position)
187         errorTextView.text = options[position].spinnerDisabledText
188         if (isEnabled(position)) {
189             errorTextView.visibility = View.GONE
190             titleTextView.isEnabled = true
191         } else {
192             errorTextView.visibility = View.VISIBLE
193             titleTextView.isEnabled = false
194         }
195         return view
196     }
197 }
198