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