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 47 import com.android.systemui.recordissue.IssueRecordingState 48 import com.android.systemui.recordissue.RecordIssueDialogDelegate 49 import com.android.systemui.recordissue.TraceurMessageSender 50 import com.android.systemui.res.R 51 import com.android.systemui.screenrecord.RecordingService 52 import com.android.systemui.settings.UserContextProvider 53 import com.android.systemui.statusbar.phone.KeyguardDismissUtil 54 import com.android.systemui.statusbar.policy.KeyguardStateController 55 import java.util.concurrent.Executor 56 import javax.inject.Inject 57 58 class RecordIssueTile 59 @Inject 60 constructor( 61 host: QSHost, 62 uiEventLogger: QsEventLogger, 63 @Background backgroundLooper: Looper, 64 @Main mainHandler: Handler, 65 falsingManager: FalsingManager, 66 metricsLogger: MetricsLogger, 67 statusBarStateController: StatusBarStateController, 68 activityStarter: ActivityStarter, 69 qsLogger: QSLogger, 70 private val keyguardDismissUtil: KeyguardDismissUtil, 71 private val keyguardStateController: KeyguardStateController, 72 private val dialogTransitionAnimator: DialogTransitionAnimator, 73 private val panelInteractor: PanelInteractor, 74 private val userContextProvider: UserContextProvider, 75 private val traceurMessageSender: TraceurMessageSender, 76 @Background private val bgExecutor: Executor, 77 private val issueRecordingState: IssueRecordingState, 78 private val delegateFactory: RecordIssueDialogDelegate.Factory, 79 ) : 80 QSTileImpl<QSTile.BooleanState>( 81 host, 82 uiEventLogger, 83 backgroundLooper, 84 mainHandler, 85 falsingManager, 86 metricsLogger, 87 statusBarStateController, 88 activityStarter, 89 qsLogger 90 ) { 91 <lambda>null92 private val onRecordingChangeListener = Runnable { refreshState() } 93 handleSetListeningnull94 override fun handleSetListening(listening: Boolean) { 95 super.handleSetListening(listening) 96 if (listening) { 97 issueRecordingState.addListener(onRecordingChangeListener) 98 } else { 99 issueRecordingState.removeListener(onRecordingChangeListener) 100 } 101 } 102 handleDestroynull103 override fun handleDestroy() { 104 super.handleDestroy() 105 bgExecutor.execute { traceurMessageSender.unbindFromTraceur(mContext) } 106 } 107 getTileLabelnull108 override fun getTileLabel(): CharSequence = mContext.getString(R.string.qs_record_issue_label) 109 110 /** 111 * There are SELinux constraints that are stopping this tile from reaching production builds. 112 * Once those are resolved, this condition will be removed, but the solution (of properly 113 * creating a distince SELinux context for com.android.systemui) is complex and will take time 114 * to implement. 115 */ 116 override fun isAvailable(): Boolean = android.os.Build.IS_DEBUGGABLE && recordIssueQsTile() 117 118 override fun newTileState(): QSTile.BooleanState = 119 QSTile.BooleanState().apply { 120 label = tileLabel 121 handlesLongClick = false 122 } 123 124 @VisibleForTesting handleClicknull125 public override fun handleClick(expandable: Expandable?) { 126 if (issueRecordingState.isRecording) { 127 stopIssueRecordingService() 128 } else { 129 mUiHandler.post { showPrompt(expandable) } 130 } 131 } 132 startIssueRecordingServicenull133 private fun startIssueRecordingService() = 134 PendingIntent.getForegroundService( 135 userContextProvider.userContext, 136 RecordingService.REQUEST_CODE, 137 IssueRecordingService.getStartIntent(userContextProvider.userContext), 138 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 139 ) 140 .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) 141 stopIssueRecordingServicenull142 private fun stopIssueRecordingService() = 143 PendingIntent.getService( 144 userContextProvider.userContext, 145 RecordingService.REQUEST_CODE, 146 IssueRecordingService.getStopIntent(userContextProvider.userContext), 147 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 148 ) 149 .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) 150 showPromptnull151 private fun showPrompt(expandable: Expandable?) { 152 val dialog: AlertDialog = 153 delegateFactory 154 .create { 155 startIssueRecordingService() 156 dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() 157 panelInteractor.collapsePanels() 158 } 159 .createDialog() 160 val dismissAction = 161 ActivityStarter.OnDismissAction { 162 // We animate from the touched view only if we are not on the keyguard, given 163 // that if we are we will dismiss it which will also collapse the shade. 164 if (expandable != null && !keyguardStateController.isShowing) { 165 expandable 166 .dialogTransitionController(DialogCuj(CUJ_SHADE_DIALOG_OPEN, TILE_SPEC)) 167 ?.let { dialogTransitionAnimator.show(dialog, it) } ?: dialog.show() 168 } else { 169 dialog.show() 170 } 171 false 172 } 173 keyguardDismissUtil.executeWhenUnlocked(dismissAction, false, true) 174 } 175 getLongClickIntentnull176 override fun getLongClickIntent(): Intent? = null 177 178 @VisibleForTesting 179 public override fun handleUpdateState(qsTileState: QSTile.BooleanState, arg: Any?) { 180 qsTileState.apply { 181 if (issueRecordingState.isRecording) { 182 value = true 183 state = Tile.STATE_ACTIVE 184 forceExpandIcon = false 185 secondaryLabel = mContext.getString(R.string.qs_record_issue_stop) 186 icon = ResourceIcon.get(R.drawable.qs_record_issue_icon_on) 187 } else { 188 value = false 189 state = Tile.STATE_INACTIVE 190 forceExpandIcon = true 191 secondaryLabel = mContext.getString(R.string.qs_record_issue_start) 192 icon = ResourceIcon.get(R.drawable.qs_record_issue_icon_off) 193 } 194 label = tileLabel 195 contentDescription = 196 if (TextUtils.isEmpty(secondaryLabel)) label else "$label, $secondaryLabel" 197 expandedAccessibilityClassName = Switch::class.java.name 198 } 199 } 200 201 companion object { 202 const val TILE_SPEC = "record_issue" 203 } 204 } 205