1 /* <lambda>null2 * Copyright (C) 2023 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.recordissue 18 19 import android.annotation.SuppressLint 20 import android.app.AlertDialog.BUTTON_POSITIVE 21 import android.content.Context 22 import android.content.Intent 23 import android.content.res.ColorStateList 24 import android.graphics.Color 25 import android.os.Bundle 26 import android.os.UserHandle 27 import android.view.Gravity 28 import android.view.LayoutInflater 29 import android.view.WindowManager 30 import android.widget.Button 31 import android.widget.PopupMenu 32 import android.widget.Switch 33 import androidx.annotation.MainThread 34 import androidx.annotation.WorkerThread 35 import com.android.systemui.dagger.qualifiers.Background 36 import com.android.systemui.dagger.qualifiers.Main 37 import com.android.systemui.flags.FeatureFlagsClassic 38 import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger 39 import com.android.systemui.mediaprojection.SessionCreationSource 40 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver 41 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialogDelegate 42 import com.android.systemui.recordissue.IssueRecordingState.Companion.ALL_ISSUE_TYPES 43 import com.android.systemui.recordissue.IssueRecordingState.Companion.ISSUE_TYPE_NOT_SET 44 import com.android.systemui.res.R 45 import com.android.systemui.settings.UserTracker 46 import com.android.systemui.statusbar.phone.SystemUIDialog 47 import dagger.assisted.Assisted 48 import dagger.assisted.AssistedFactory 49 import dagger.assisted.AssistedInject 50 import java.util.concurrent.Executor 51 52 private const val EXTRA_ISSUE_TYPE_RES = "extra_issueTypeRes" 53 54 class RecordIssueDialogDelegate 55 @AssistedInject 56 constructor( 57 private val factory: SystemUIDialog.Factory, 58 private val userTracker: UserTracker, 59 private val flags: FeatureFlagsClassic, 60 @Background private val bgExecutor: Executor, 61 @Main private val mainExecutor: Executor, 62 private val devicePolicyResolver: dagger.Lazy<ScreenCaptureDevicePolicyResolver>, 63 private val mediaProjectionMetricsLogger: MediaProjectionMetricsLogger, 64 private val screenCaptureDisabledDialogDelegate: ScreenCaptureDisabledDialogDelegate, 65 private val state: IssueRecordingState, 66 @Assisted private val onStarted: Runnable, 67 ) : SystemUIDialog.Delegate { 68 69 /** To inject dependencies and allow for easier testing */ 70 @AssistedFactory 71 interface Factory { 72 /** Create a dialog object */ 73 fun create(onStarted: Runnable): RecordIssueDialogDelegate 74 } 75 76 @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch 77 private lateinit var issueTypeButton: Button 78 79 @MainThread 80 override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { 81 dialog.apply { 82 setView(LayoutInflater.from(context).inflate(R.layout.record_issue_dialog, null)) 83 setTitle(context.getString(R.string.qs_record_issue_label)) 84 setIcon(R.drawable.qs_record_issue_icon_off) 85 setNegativeButton(R.string.cancel) { _, _ -> } 86 setPositiveButton(R.string.qs_record_issue_start) { _, _ -> onStarted.run() } 87 } 88 } 89 90 override fun createDialog(): SystemUIDialog = factory.create(this) 91 92 @MainThread 93 override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { 94 dialog.apply { 95 window?.apply { 96 addPrivateFlags(WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS) 97 setGravity(Gravity.CENTER) 98 } 99 100 screenRecordSwitch = 101 requireViewById<Switch>(R.id.screenrecord_switch).apply { 102 isChecked = state.recordScreen 103 setOnCheckedChangeListener { _, isChecked -> 104 state.recordScreen = isChecked 105 if (isChecked) { 106 bgExecutor.execute { onScreenRecordSwitchClicked() } 107 } 108 } 109 } 110 111 requireViewById<Switch>(R.id.bugreport_switch).apply { 112 isChecked = state.takeBugreport 113 setOnCheckedChangeListener { _, isChecked -> state.takeBugreport = isChecked } 114 } 115 116 issueTypeButton = 117 requireViewById<Button>(R.id.issue_type_button).apply { 118 val startButton = dialog.getButton(BUTTON_POSITIVE) 119 if (state.issueTypeRes != ISSUE_TYPE_NOT_SET) { 120 setText(state.issueTypeRes) 121 } else { 122 startButton.isEnabled = false 123 } 124 setOnClickListener { 125 onIssueTypeClicked(context) { startButton.isEnabled = true } 126 } 127 } 128 } 129 } 130 131 @WorkerThread 132 private fun onScreenRecordSwitchClicked() { 133 if ( 134 devicePolicyResolver 135 .get() 136 .isScreenCaptureCompletelyDisabled(UserHandle.of(userTracker.userId)) 137 ) { 138 mainExecutor.execute { 139 screenCaptureDisabledDialogDelegate.createSysUIDialog().show() 140 screenRecordSwitch.isChecked = false 141 } 142 return 143 } 144 145 mediaProjectionMetricsLogger.notifyProjectionInitiated( 146 userTracker.userId, 147 SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER, 148 ) 149 150 if (!state.hasUserApprovedScreenRecording) { 151 mainExecutor.execute { 152 ScreenCapturePermissionDialogDelegate(factory, state).createDialog().apply { 153 setOnCancelListener { screenRecordSwitch.isChecked = false } 154 show() 155 } 156 } 157 } 158 } 159 160 @MainThread 161 private fun onIssueTypeClicked(context: Context, onIssueTypeSelected: Runnable) { 162 val popupMenu = PopupMenu(context, issueTypeButton) 163 val onMenuItemClickListener = 164 PopupMenu.OnMenuItemClickListener { 165 issueTypeButton.text = it.title 166 state.issueTypeRes = 167 it.intent?.getIntExtra(EXTRA_ISSUE_TYPE_RES, ISSUE_TYPE_NOT_SET) 168 ?: ISSUE_TYPE_NOT_SET 169 onIssueTypeSelected.run() 170 true 171 } 172 ALL_ISSUE_TYPES.keys.forEach { 173 popupMenu.menu.add(it).apply { 174 // Set this for every item in the list to ensure equal spacing. Set it to 175 // transparent for non-selected items so icon is only visible for selected element. 176 setIcon(R.drawable.arrow_pointing_down) 177 if (it != state.issueTypeRes) { 178 iconTintList = ColorStateList.valueOf(Color.TRANSPARENT) 179 } else { 180 contentDescription = 181 context.getString(com.android.internal.R.string.selected) + 182 " " + 183 context.getString(it) 184 } 185 intent = Intent().putExtra(EXTRA_ISSUE_TYPE_RES, it) 186 187 if (it == R.string.custom) { 188 setOnMenuItemClickListener { 189 CustomTraceSettingsDialogDelegate( 190 factory, 191 state.customTraceState, 192 state.tagTitles, 193 ) { 194 onMenuItemClickListener.onMenuItemClick(it) 195 } 196 .createDialog() 197 .show() 198 true 199 } 200 } 201 } 202 } 203 popupMenu.apply { 204 setOnMenuItemClickListener(onMenuItemClickListener) 205 setForceShowIcon(true) 206 show() 207 } 208 } 209 } 210