• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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