1 /*
2  * Copyright (C) 2021 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.content.ComponentName
20 import android.content.Intent
21 import android.os.Handler
22 import android.os.Looper
23 import android.service.quicksettings.Tile
24 import android.view.View
25 import androidx.annotation.VisibleForTesting
26 import com.android.internal.jank.InteractionJankMonitor
27 import com.android.internal.logging.MetricsLogger
28 import com.android.systemui.R
29 import com.android.systemui.animation.ActivityLaunchAnimator
30 import com.android.systemui.controls.ControlsServiceInfo
31 import com.android.systemui.controls.dagger.ControlsComponent
32 import com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE
33 import com.android.systemui.controls.management.ControlsListingController
34 import com.android.systemui.controls.ui.ControlsUiController
35 import com.android.systemui.controls.ui.SelectedItem
36 import com.android.systemui.dagger.qualifiers.Background
37 import com.android.systemui.dagger.qualifiers.Main
38 import com.android.systemui.plugins.ActivityStarter
39 import com.android.systemui.plugins.FalsingManager
40 import com.android.systemui.plugins.qs.QSTile
41 import com.android.systemui.plugins.statusbar.StatusBarStateController
42 import com.android.systemui.qs.QSHost
43 import com.android.systemui.qs.QsEventLogger
44 import com.android.systemui.qs.logging.QSLogger
45 import com.android.systemui.qs.tileimpl.QSTileImpl
46 import java.util.concurrent.atomic.AtomicBoolean
47 import javax.inject.Inject
48 
49 class DeviceControlsTile @Inject constructor(
50     host: QSHost,
51     uiEventLogger: QsEventLogger,
52     @Background backgroundLooper: Looper,
53     @Main mainHandler: Handler,
54     falsingManager: FalsingManager,
55     metricsLogger: MetricsLogger,
56     statusBarStateController: StatusBarStateController,
57     activityStarter: ActivityStarter,
58     qsLogger: QSLogger,
59     private val controlsComponent: ControlsComponent
60 ) : QSTileImpl<QSTile.State>(
61     host,
62     uiEventLogger,
63     backgroundLooper,
64     mainHandler,
65     falsingManager,
66     metricsLogger,
67     statusBarStateController,
68     activityStarter,
69     qsLogger
70 ) {
71 
72     private var hasControlsApps = AtomicBoolean(false)
73 
74     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
75     val icon: QSTile.Icon
76         get() = ResourceIcon.get(controlsComponent.getTileImageId())
77 
78     private val listingCallback = object : ControlsListingController.ControlsListingCallback {
79         override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
80             if (hasControlsApps.compareAndSet(serviceInfos.isEmpty(), serviceInfos.isNotEmpty())) {
81                 refreshState()
82             }
83         }
84     }
85 
86     init {
87         controlsComponent.getControlsListingController().ifPresent {
88             it.observe(this, listingCallback)
89         }
90     }
91 
92     override fun isAvailable(): Boolean {
93         return controlsComponent.getControlsController().isPresent
94     }
95 
96     override fun newTileState(): QSTile.State {
97         return QSTile.State().also {
98             it.state = Tile.STATE_UNAVAILABLE // Start unavailable matching `hasControlsApps`
99             it.handlesLongClick = false
100         }
101     }
102 
103     override fun handleClick(view: View?) {
104         if (state.state == Tile.STATE_UNAVAILABLE) {
105             return
106         }
107 
108         val intent = Intent().apply {
109             component = ComponentName(mContext, controlsComponent.getControlsUiController().get()
110                 .resolveActivity())
111             addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
112             putExtra(ControlsUiController.EXTRA_ANIMATE, true)
113         }
114         val animationController = view?.let {
115             ActivityLaunchAnimator.Controller.fromView(
116                     it, InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE)
117         }
118 
119         mUiHandler.post {
120             val showOverLockscreenWhenLocked = state.state == Tile.STATE_ACTIVE
121             mActivityStarter.startActivity(
122                 intent, true /* dismissShade */, animationController, showOverLockscreenWhenLocked)
123         }
124     }
125 
126     override fun handleUpdateState(state: QSTile.State, arg: Any?) {
127         state.label = tileLabel
128         state.contentDescription = state.label
129         state.icon = icon
130         if (controlsComponent.isEnabled() && hasControlsApps.get()) {
131             if (controlsComponent.getVisibility() == AVAILABLE) {
132                 val selection = controlsComponent
133                     .getControlsController().get().getPreferredSelection()
134                 state.state = if (selection is SelectedItem.StructureItem &&
135                         selection.structure.controls.isEmpty()) {
136                     Tile.STATE_INACTIVE
137                 } else {
138                     Tile.STATE_ACTIVE
139                 }
140                 val label = selection.name
141                 state.secondaryLabel = if (label == tileLabel) null else label
142             } else {
143                 state.state = Tile.STATE_INACTIVE
144                 state.secondaryLabel = mContext.getText(R.string.controls_tile_locked)
145             }
146             state.stateDescription = state.secondaryLabel
147         } else {
148             state.state = Tile.STATE_UNAVAILABLE
149         }
150     }
151 
152     override fun getMetricsCategory(): Int {
153         return 0
154     }
155 
156     override fun getLongClickIntent(): Intent? {
157         return null
158     }
159 
160     override fun handleLongClick(view: View?) {}
161 
162     override fun getTileLabel(): CharSequence {
163         return mContext.getText(controlsComponent.getTileTitleId())
164     }
165 
166     companion object {
167         const val TILE_SPEC = "controls"
168     }
169 }
170