1 /* 2 * 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.qs.tiles 18 19 import android.app.AlertDialog 20 import android.app.BroadcastOptions 21 import android.app.PendingIntent 22 import android.content.Intent 23 import android.os.Handler 24 import android.os.Looper 25 import android.service.quicksettings.Tile 26 import android.text.TextUtils 27 import android.widget.Switch 28 import androidx.annotation.VisibleForTesting 29 import com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN 30 import com.android.internal.logging.MetricsLogger 31 import com.android.systemui.Flags.recordIssueQsTile 32 import com.android.systemui.animation.DialogCuj 33 import com.android.systemui.animation.DialogTransitionAnimator 34 import com.android.systemui.animation.Expandable 35 import com.android.systemui.dagger.qualifiers.Background 36 import com.android.systemui.dagger.qualifiers.Main 37 import com.android.systemui.plugins.ActivityStarter 38 import com.android.systemui.plugins.FalsingManager 39 import com.android.systemui.plugins.qs.QSTile 40 import com.android.systemui.plugins.statusbar.StatusBarStateController 41 import com.android.systemui.qs.QSHost 42 import com.android.systemui.qs.QsEventLogger 43 import com.android.systemui.qs.logging.QSLogger 44 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor 45 import com.android.systemui.qs.tileimpl.QSTileImpl 46 import com.android.systemui.recordissue.IssueRecordingService.Companion.getStartIntent 47 import com.android.systemui.recordissue.IssueRecordingService.Companion.getStopIntent 48 import com.android.systemui.recordissue.IssueRecordingServiceConnection 49 import com.android.systemui.recordissue.IssueRecordingState 50 import com.android.systemui.recordissue.RecordIssueDialogDelegate 51 import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC 52 import com.android.systemui.recordissue.TraceurConnection 53 import com.android.systemui.res.R 54 import com.android.systemui.screenrecord.RecordingController 55 import com.android.systemui.screenrecord.RecordingService 56 import com.android.systemui.settings.UserContextProvider 57 import com.android.systemui.statusbar.phone.KeyguardDismissUtil 58 import com.android.systemui.statusbar.policy.KeyguardStateController 59 import java.util.concurrent.Executor 60 import javax.inject.Inject 61 62 const val DELAY_MS: Long = 0 63 const val INTERVAL_MS: Long = 1000 64 65 class RecordIssueTile 66 @Inject 67 constructor( 68 host: QSHost, 69 uiEventLogger: QsEventLogger, 70 @Background private val backgroundLooper: Looper, 71 @Main mainHandler: Handler, 72 falsingManager: FalsingManager, 73 metricsLogger: MetricsLogger, 74 statusBarStateController: StatusBarStateController, 75 activityStarter: ActivityStarter, 76 qsLogger: QSLogger, 77 private val keyguardDismissUtil: KeyguardDismissUtil, 78 private val keyguardStateController: KeyguardStateController, 79 private val dialogTransitionAnimator: DialogTransitionAnimator, 80 private val panelInteractor: PanelInteractor, 81 private val userContextProvider: UserContextProvider, 82 irsConnProvider: IssueRecordingServiceConnection.Provider, 83 traceurConnProvider: TraceurConnection.Provider, 84 @Background private val bgExecutor: Executor, 85 private val issueRecordingState: IssueRecordingState, 86 private val delegateFactory: RecordIssueDialogDelegate.Factory, 87 private val recordingController: RecordingController, 88 ) : 89 QSTileImpl<QSTile.BooleanState>( 90 host, 91 uiEventLogger, 92 backgroundLooper, 93 mainHandler, 94 falsingManager, 95 metricsLogger, 96 statusBarStateController, 97 activityStarter, 98 qsLogger, 99 ) { 100 <lambda>null101 private val onRecordingChangeListener = Runnable { refreshState() } 102 103 private val irsConnection: IssueRecordingServiceConnection = irsConnProvider.create() 104 private val traceurConnection = <lambda>null105 traceurConnProvider.create().apply { 106 onBound.add { 107 getTags(issueRecordingState) 108 doUnBind() 109 } 110 } 111 handleSetListeningnull112 override fun handleSetListening(listening: Boolean) { 113 super.handleSetListening(listening) 114 bgExecutor.execute { 115 if (listening) { 116 issueRecordingState.addListener(onRecordingChangeListener) 117 } else { 118 issueRecordingState.removeListener(onRecordingChangeListener) 119 } 120 } 121 } 122 handleDestroynull123 override fun handleDestroy() { 124 super.handleDestroy() 125 bgExecutor.execute { irsConnection.doUnBind() } 126 } 127 getTileLabelnull128 override fun getTileLabel(): CharSequence = mContext.getString(R.string.qs_record_issue_label) 129 130 /** 131 * There are SELinux constraints that are stopping this tile from reaching production builds. 132 * Once those are resolved, this condition will be removed, but the solution (of properly 133 * creating a distince SELinux context for com.android.systemui) is complex and will take time 134 * to implement. 135 */ 136 override fun isAvailable(): Boolean = android.os.Build.IS_DEBUGGABLE && recordIssueQsTile() 137 138 override fun newTileState(): QSTile.BooleanState = 139 QSTile.BooleanState().apply { 140 label = tileLabel 141 handlesLongClick = false 142 } 143 144 @VisibleForTesting handleClicknull145 public override fun handleClick(expandable: Expandable?) { 146 if (issueRecordingState.isRecording) { 147 stopIssueRecordingService() 148 } else { 149 mUiHandler.post { showPrompt(expandable) } 150 } 151 } 152 startIssueRecordingServicenull153 private fun startIssueRecordingService() = 154 recordingController.startCountdown( 155 DELAY_MS, 156 INTERVAL_MS, 157 pendingServiceIntent( 158 getStartIntent( 159 userContextProvider.userContext, 160 issueRecordingState.traceConfig, 161 issueRecordingState.recordScreen, 162 issueRecordingState.takeBugreport, 163 ) 164 ), 165 pendingServiceIntent(getStopIntent(userContextProvider.userContext)), 166 ) 167 168 private fun stopIssueRecordingService() = 169 pendingServiceIntent(getStopIntent(userContextProvider.userContext)) 170 .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) 171 pendingServiceIntentnull172 private fun pendingServiceIntent(action: Intent) = 173 PendingIntent.getService( 174 userContextProvider.userContext, 175 RecordingService.REQUEST_CODE, 176 action, 177 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, 178 ) 179 180 private fun showPrompt(expandable: Expandable?) { 181 bgExecutor.execute { 182 // We only want to get the tags once per session, as this is not likely to change, if at 183 // all on a month to month basis. Using onBound's size is a way to verify if the tag 184 // retrieval has already happened or not. 185 if (traceurConnection.onBound.isNotEmpty()) { 186 traceurConnection.doBind() 187 } 188 irsConnection.doBind() 189 } 190 val dialog: AlertDialog = 191 delegateFactory 192 .create { 193 startIssueRecordingService() 194 dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() 195 panelInteractor.collapsePanels() 196 } 197 .createDialog() 198 val dismissAction = 199 ActivityStarter.OnDismissAction { 200 // We animate from the touched view only if we are not on the keyguard, given 201 // that if we are we will dismiss it which will also collapse the shade. 202 if (expandable != null && !keyguardStateController.isShowing) { 203 expandable 204 .dialogTransitionController(DialogCuj(CUJ_SHADE_DIALOG_OPEN, TILE_SPEC)) 205 ?.let { dialogTransitionAnimator.show(dialog, it) } ?: dialog.show() 206 } else { 207 dialog.show() 208 } 209 false 210 } 211 keyguardDismissUtil.executeWhenUnlocked(dismissAction, false, true) 212 } 213 getLongClickIntentnull214 override fun getLongClickIntent(): Intent? = null 215 216 @VisibleForTesting 217 public override fun handleUpdateState(qsTileState: QSTile.BooleanState, arg: Any?) { 218 qsTileState.apply { 219 if (issueRecordingState.isRecording) { 220 value = true 221 state = Tile.STATE_ACTIVE 222 forceExpandIcon = false 223 secondaryLabel = mContext.getString(R.string.qs_record_issue_stop) 224 icon = maybeLoadResourceIcon(R.drawable.qs_record_issue_icon_on) 225 } else { 226 value = false 227 state = Tile.STATE_INACTIVE 228 forceExpandIcon = true 229 secondaryLabel = mContext.getString(R.string.qs_record_issue_start) 230 icon = maybeLoadResourceIcon(R.drawable.qs_record_issue_icon_off) 231 } 232 label = tileLabel 233 contentDescription = 234 if (TextUtils.isEmpty(secondaryLabel)) label else "$label, $secondaryLabel" 235 expandedAccessibilityClassName = Switch::class.java.name 236 } 237 } 238 } 239