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