Migrate CustomTile to the new infra
The change pull together couple of interactors to migrate a CustomTile.
Flag: LEGACY QS_PIPELINE_NEW_TILES DISABLED
Test: atest QSTileViewModelImplTest
Test: atest cts/tests/quicksettings
Test: atest cts/hostsidetests/systemui
Bug: 301055700
Change-Id: Ie33f8a63011d33811a549cdd7f146ca1332bcda8
diff --git a/packages/SystemUI/aconfig/Android.bp b/packages/SystemUI/aconfig/Android.bp
index 7f16ca5..03f9d74 100644
--- a/packages/SystemUI/aconfig/Android.bp
+++ b/packages/SystemUI/aconfig/Android.bp
@@ -25,6 +25,7 @@
"//frameworks/base/packages/SystemUI:__subpackages__",
"//frameworks/libs/systemui/tracinglib:__subpackages__",
"//platform_testing:__subpackages__",
+ "//cts:__subpackages__",
],
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
index 90779cb..20653ca 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt
@@ -55,6 +55,7 @@
private val underTest: CustomTileInteractor =
with(kosmos) {
CustomTileInteractor(
+ tileSpec,
customTileDefaultsRepository,
customTileRepository,
testScope.backgroundScope,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index 47b0624..a45d6f6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -259,7 +259,11 @@
private State getState(Collection<QSTile> tiles, String spec) {
for (QSTile tile : tiles) {
if (spec.equals(tile.getTileSpec())) {
- return tile.getState().copy();
+ if (tile.isTileReady()) {
+ return tile.getState().copy();
+ } else {
+ return null;
+ }
}
}
return null;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 5d28c8c..957cb1e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -319,7 +319,7 @@
override fun dumpProto(systemUIProtoDump: SystemUIProtoDump, args: Array<String>) {
val data =
- currentTiles.value.map { it.tile.state }.mapNotNull { it.toProto() }.toTypedArray()
+ currentTiles.value.map { it.tile.state }.mapNotNull { it?.toProto() }.toTypedArray()
systemUIProtoDump.tiles = data
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
index 840db26..fc06090 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/actions/QSTileIntentUserInputHandler.kt
@@ -63,6 +63,10 @@
InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE,
)
}
- activityStarter.postStartActivityDismissingKeyguard(pendingIntent, animationController)
+ activityStarter.startPendingIntentMaybeDismissingKeyguard(
+ pendingIntent,
+ null,
+ animationController
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
index 0a9a6d3..bc016bd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
@@ -158,6 +158,33 @@
)
}
+ fun logError(
+ tileSpec: TileSpec,
+ message: String,
+ error: Throwable,
+ ) {
+ tileSpec
+ .getLogBuffer()
+ .log(
+ tileSpec.getLogTag(),
+ LogLevel.ERROR,
+ {},
+ { message },
+ error,
+ )
+ }
+
+ fun logCustomTileUserActionDelivered(tileSpec: TileSpec) {
+ tileSpec
+ .getLogBuffer()
+ .log(
+ tileSpec.getLogTag(),
+ LogLevel.DEBUG,
+ {},
+ { "user action delivered to the service" },
+ )
+ }
+
private fun TileSpec.getLogTag(): String = "${TAG_FORMAT_PREFIX}_${this.spec}"
private fun TileSpec.getLogBuffer(): LogBuffer =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
index 382cfe2..6c9a8a4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/NewQSTileFactory.kt
@@ -56,7 +56,7 @@
override fun createTile(tileSpec: String): QSTile? {
val viewModel: QSTileViewModel =
when (val spec = TileSpec.create(tileSpec)) {
- is TileSpec.CustomTileSpec -> null
+ is TileSpec.CustomTileSpec -> createCustomTileViewModel(spec)
is TileSpec.PlatformTileSpec -> tileMap[tileSpec]?.get()
is TileSpec.Invalid -> null
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt
deleted file mode 100644
index 14bf25d..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileInteractor.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles.impl.custom
-
-import android.os.UserHandle
-import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
-import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
-import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
-import com.android.systemui.qs.tiles.impl.di.QSTileScope
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-
-@QSTileScope
-class CustomTileInteractor @Inject constructor() : QSTileDataInteractor<CustomTileDataModel> {
-
- override fun tileData(
- user: UserHandle,
- triggers: Flow<DataUpdateTrigger>
- ): Flow<CustomTileDataModel> {
- TODO("Not yet implemented")
- }
-
- override fun availability(user: UserHandle): Flow<Boolean> {
- TODO("Not yet implemented")
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt
deleted file mode 100644
index e23a5c2..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileMapper.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles.impl.custom
-
-import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
-import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
-import com.android.systemui.qs.tiles.impl.di.QSTileScope
-import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
-import com.android.systemui.qs.tiles.viewmodel.QSTileState
-import javax.inject.Inject
-
-@QSTileScope
-class CustomTileMapper @Inject constructor() : QSTileDataToStateMapper<CustomTileDataModel> {
-
- override fun map(config: QSTileConfig, data: CustomTileDataModel): QSTileState {
- TODO("Not yet implemented")
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt
deleted file mode 100644
index f34704b..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/CustomTileUserActionInteractor.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles.impl.custom
-
-import com.android.systemui.qs.tiles.base.interactor.QSTileInput
-import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
-import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
-import com.android.systemui.qs.tiles.impl.di.QSTileScope
-import javax.inject.Inject
-
-@QSTileScope
-class CustomTileUserActionInteractor @Inject constructor() :
- QSTileUserActionInteractor<CustomTileDataModel> {
-
- override suspend fun handleInput(input: QSTileInput<CustomTileDataModel>) {
- TODO("Not yet implemented")
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt
index 88bc8fa..7b099c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileComponent.kt
@@ -16,7 +16,10 @@
package com.android.systemui.qs.tiles.impl.custom.di
+import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository
import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
+import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileInteractor
+import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileServiceInteractor
import com.android.systemui.qs.tiles.impl.di.QSTileComponent
import com.android.systemui.qs.tiles.impl.di.QSTileScope
import dagger.Subcomponent
@@ -25,6 +28,12 @@
@Subcomponent(modules = [QSTileConfigModule::class, CustomTileModule::class])
interface CustomTileComponent : QSTileComponent<CustomTileDataModel> {
+ fun customTileInterfaceInteractor(): CustomTileServiceInteractor
+
+ fun customTileInteractor(): CustomTileInteractor
+
+ fun customTilePackageUpdatesRepository(): CustomTilePackageUpdatesRepository
+
@Subcomponent.Builder
interface Builder {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
index ba8b23a..196fa12 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/CustomTileModule.kt
@@ -20,14 +20,16 @@
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
import com.android.systemui.qs.tiles.base.viewmodel.QSTileCoroutineScopeFactory
-import com.android.systemui.qs.tiles.impl.custom.CustomTileInteractor
-import com.android.systemui.qs.tiles.impl.custom.CustomTileMapper
-import com.android.systemui.qs.tiles.impl.custom.CustomTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepositoryImpl
+import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepositoryImpl
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepository
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepositoryImpl
+import com.android.systemui.qs.tiles.impl.custom.domain.CustomTileMapper
import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
+import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileDataInteractor
+import com.android.systemui.qs.tiles.impl.custom.domain.interactor.CustomTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.di.QSTileScope
import dagger.Binds
import dagger.Module
@@ -40,7 +42,7 @@
@Binds
fun bindDataInteractor(
- dataInteractor: CustomTileInteractor
+ dataInteractor: CustomTileDataInteractor
): QSTileDataInteractor<CustomTileDataModel>
@Binds
@@ -58,6 +60,11 @@
@Binds fun bindCustomTileRepository(impl: CustomTileRepositoryImpl): CustomTileRepository
+ @Binds
+ abstract fun bindCustomTilePackageUpdatesRepository(
+ impl: CustomTilePackageUpdatesRepositoryImpl
+ ): CustomTilePackageUpdatesRepository
+
companion object {
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt
deleted file mode 100644
index d382d20..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundComponent.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles.impl.custom.di.bound
-
-import android.os.UserHandle
-import dagger.BindsInstance
-import dagger.Subcomponent
-import kotlinx.coroutines.CoroutineScope
-
-/** @see CustomTileBoundScope */
-@CustomTileBoundScope
-@Subcomponent(modules = [CustomTileBoundModule::class])
-interface CustomTileBoundComponent {
-
- @Subcomponent.Builder
- interface Builder {
- @BindsInstance fun user(@CustomTileUser user: UserHandle): Builder
- @BindsInstance fun coroutineScope(@CustomTileBoundScope scope: CoroutineScope): Builder
-
- fun build(): CustomTileBoundComponent
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundModule.kt
deleted file mode 100644
index 889424a..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundModule.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles.impl.custom.di.bound
-
-import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository
-import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepositoryImpl
-import dagger.Binds
-import dagger.Module
-
-@Module
-interface CustomTileBoundModule {
-
- @Binds
- fun bindCustomTilePackageUpdatesRepository(
- impl: CustomTilePackageUpdatesRepositoryImpl
- ): CustomTilePackageUpdatesRepository
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt
deleted file mode 100644
index 4a4ba2b..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileBoundScope.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles.impl.custom.di.bound
-
-import javax.inject.Scope
-
-/**
- * Scope annotation for bound custom tile scope. This scope lives when a particular
- * [com.android.systemui.qs.external.CustomTile] is listening and bound to the
- * [android.service.quicksettings.TileService].
- */
-@MustBeDocumented
-@Retention(AnnotationRetention.RUNTIME)
-@Scope
-annotation class CustomTileBoundScope
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt
deleted file mode 100644
index efc7431..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/di/bound/CustomTileUser.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.tiles.impl.custom.di.bound
-
-import javax.inject.Qualifier
-
-/** User associated with current custom tile binding. */
-@Qualifier
-@MustBeDocumented
-@Retention(AnnotationRetention.RUNTIME)
-annotation class CustomTileUser
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt
new file mode 100644
index 0000000..875079c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.custom.domain
+
+import android.annotation.SuppressLint
+import android.app.IUriGrantsManager
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.service.quicksettings.Tile
+import android.widget.Button
+import android.widget.Switch
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import javax.inject.Inject
+
+@SysUISingleton
+class CustomTileMapper
+@Inject
+constructor(
+ private val context: Context,
+ private val uriGrantsManager: IUriGrantsManager,
+) : QSTileDataToStateMapper<CustomTileDataModel> {
+
+ override fun map(config: QSTileConfig, data: CustomTileDataModel): QSTileState {
+ val userContext = context.createContextAsUser(UserHandle(data.user.identifier), 0)
+
+ val iconResult =
+ getIconProvider(
+ userContext = userContext,
+ icon = data.tile.icon,
+ callingAppUid = data.callingAppUid,
+ packageName = data.componentName.packageName,
+ defaultIcon = data.defaultTileIcon,
+ )
+
+ return QSTileState.build(iconResult.iconProvider, data.tile.label) {
+ var tileState: Int = data.tile.state
+ if (data.hasPendingBind) {
+ tileState = Tile.STATE_UNAVAILABLE
+ }
+
+ icon = iconResult.iconProvider
+ activationState =
+ if (iconResult.failedToLoad) {
+ QSTileState.ActivationState.INACTIVE
+ } else {
+ QSTileState.ActivationState.valueOf(tileState)
+ }
+
+ if (!data.tile.subtitle.isNullOrEmpty()) {
+ secondaryLabel = data.tile.subtitle
+ }
+
+ contentDescription = data.tile.contentDescription
+ stateDescription = data.tile.stateDescription
+
+ if (!data.isToggleable) {
+ sideViewIcon = QSTileState.SideViewIcon.Chevron
+ }
+
+ supportedActions =
+ if (tileState == Tile.STATE_UNAVAILABLE) {
+ setOf(QSTileState.UserAction.LONG_CLICK)
+ } else {
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ }
+ expandedAccessibilityClass =
+ if (data.isToggleable) {
+ Switch::class
+ } else {
+ Button::class
+ }
+ }
+ }
+
+ @SuppressLint("MissingPermission") // android.permission.INTERACT_ACROSS_USERS_FULL
+ private fun getIconProvider(
+ userContext: Context,
+ icon: android.graphics.drawable.Icon?,
+ callingAppUid: Int,
+ packageName: String,
+ defaultIcon: android.graphics.drawable.Icon?,
+ ): IconResult {
+ var failedToLoad = false
+ val drawable: Drawable? =
+ try {
+ icon?.loadDrawableCheckingUriGrant(
+ userContext,
+ uriGrantsManager,
+ callingAppUid,
+ packageName,
+ )
+ } catch (e: Exception) {
+ failedToLoad = true
+ null
+ } ?: defaultIcon?.loadDrawable(userContext)
+ return IconResult(
+ {
+ drawable?.constantState?.newDrawable()?.let {
+ Icon.Loaded(it, contentDescription = null)
+ }
+ },
+ failedToLoad,
+ )
+ }
+
+ class IconResult(
+ val iconProvider: () -> Icon?,
+ val failedToLoad: Boolean,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt
index f095c01..5b6ff1e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/entity/CustomTileDataModel.kt
@@ -20,16 +20,14 @@
import android.graphics.drawable.Icon
import android.os.UserHandle
import android.service.quicksettings.Tile
-import com.android.systemui.qs.tiles.impl.custom.di.bound.CustomTileBoundComponent
data class CustomTileDataModel(
val user: UserHandle,
val componentName: ComponentName,
val tile: Tile,
+ val isToggleable: Boolean,
val callingAppUid: Int,
val hasPendingBind: Boolean,
- val shouldShowChevron: Boolean,
- val defaultTileLabel: CharSequence?,
- val defaultTileIcon: Icon?,
- val component: CustomTileBoundComponent,
+ val defaultTileLabel: CharSequence,
+ val defaultTileIcon: Icon,
)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt
new file mode 100644
index 0000000..cff95d8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileDataInteractor.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.custom.domain.interactor
+
+import android.os.UserHandle
+import android.service.quicksettings.Tile
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
+import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository
+import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTilePackageUpdatesRepository
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.launch
+
+@QSTileScope
+@OptIn(ExperimentalCoroutinesApi::class)
+class CustomTileDataInteractor
+@Inject
+constructor(
+ private val tileSpec: TileSpec.CustomTileSpec,
+ private val defaultsRepository: CustomTileDefaultsRepository,
+ private val serviceInteractor: CustomTileServiceInteractor,
+ private val customTileInteractor: CustomTileInteractor,
+ private val packageUpdatesRepository: CustomTilePackageUpdatesRepository,
+ userRepository: UserRepository,
+ @QSTileScope private val tileScope: CoroutineScope,
+) : QSTileDataInteractor<CustomTileDataModel> {
+
+ private val mutableUserFlow = MutableStateFlow(userRepository.getSelectedUserInfo().userHandle)
+ private val bindingFlow =
+ mutableUserFlow
+ .flatMapLatest { user ->
+ ConflatedCallbackFlow.conflatedCallbackFlow {
+ serviceInteractor.setUser(user)
+
+ // Wait for the CustomTileInteractor to become initialized first, because
+ // binding
+ // the service might access it
+ customTileInteractor.initForUser(user)
+ // Bind the TileService for not active tile
+ serviceInteractor.bindOnStart()
+
+ packageUpdatesRepository
+ .getPackageChangesForUser(user)
+ .onEach {
+ defaultsRepository.requestNewDefaults(
+ user,
+ tileSpec.componentName,
+ true
+ )
+ }
+ .launchIn(this)
+
+ send(Unit)
+ awaitClose { serviceInteractor.unbind() }
+ }
+ }
+ .shareIn(tileScope, SharingStarted.WhileSubscribed())
+
+ init {
+ // Initialize binding once to flush all the pending messages inside
+ // CustomTileServiceInteractor and then unbind if the tile data isn't observed. This ensures
+ // that all the interactors are loaded and warmed up before binding.
+ tileScope.launch { bindingFlow.first() }
+ }
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<CustomTileDataModel> {
+ tileScope.launch { mutableUserFlow.emit(user) }
+ return bindingFlow.combine(triggers) { _, _ -> }.flatMapLatest { dataFlow(user) }
+ }
+
+ private fun dataFlow(user: UserHandle): Flow<CustomTileDataModel> =
+ combine(
+ serviceInteractor.refreshEvents.onStart { emit(Unit) },
+ serviceInteractor.callingAppIds,
+ customTileInteractor.getTiles(user),
+ defaultsRepository.defaults(user).mapNotNull { it as? CustomTileDefaults.Result },
+ ) { _: Unit, callingAppId: Int, tile: Tile, defaults: CustomTileDefaults.Result ->
+ CustomTileDataModel(
+ user = user,
+ componentName = tileSpec.componentName,
+ tile = tile,
+ callingAppUid = callingAppId,
+ hasPendingBind = serviceInteractor.hasPendingBind(),
+ defaultTileLabel = defaults.label,
+ defaultTileIcon = defaults.icon,
+ isToggleable = customTileInteractor.isTileToggleable(),
+ )
+ }
+
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ with(defaultsRepository) {
+ requestNewDefaults(user, tileSpec.componentName)
+ return defaults(user).map { it is CustomTileDefaults.Result }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
index 10b012d..fd96fc5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractor.kt
@@ -19,12 +19,14 @@
import android.os.UserHandle
import android.service.quicksettings.Tile
import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileDefaultsRepository
import com.android.systemui.qs.tiles.impl.custom.data.repository.CustomTileRepository
import com.android.systemui.qs.tiles.impl.di.QSTileScope
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -32,21 +34,29 @@
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
/** Manages updates of the [Tile] assigned for the current custom tile. */
@QSTileScope
class CustomTileInteractor
@Inject
constructor(
+ private val tileSpec: TileSpec.CustomTileSpec,
private val defaultsRepository: CustomTileDefaultsRepository,
private val customTileRepository: CustomTileRepository,
@QSTileScope private val tileScope: CoroutineScope,
@Background private val backgroundContext: CoroutineContext,
) {
+ private val userMutex = Mutex()
private val tileUpdates =
MutableSharedFlow<Tile>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
+ private var currentUser: UserHandle? = null
+ private var updatesJob: Job? = null
+
/** [Tile] updates. [updateTile] to emit a new one. */
fun getTiles(user: UserHandle): Flow<Tile> = customTileRepository.getTiles(user)
@@ -55,7 +65,7 @@
*
* @throws IllegalStateException when the repository stores a tile for another user. This means
* the tile hasn't been updated for the current user. Can happen when this is accessed before
- * [init] returns.
+ * [initForUser] returns.
*/
fun getTile(user: UserHandle): Tile =
customTileRepository.getTile(user)
@@ -67,45 +77,60 @@
suspend fun isTileToggleable(): Boolean = customTileRepository.isTileToggleable()
/**
- * Initializes the repository for the current user. Suspends until it's safe to call [tile]
+ * Initializes the repository for the current user. Suspends until it's safe to call [getTile]
* which needs at least one of the following:
* - defaults are loaded;
* - receive tile update in [updateTile];
* - restoration happened for a persisted tile.
*/
suspend fun initForUser(user: UserHandle) {
- launchUpdates(user)
- customTileRepository.restoreForTheUserIfNeeded(user, customTileRepository.isTileActive())
- // Suspend to make sure it gets the tile from one of the sources: restoration, defaults, or
- // tile update.
- customTileRepository.getTiles(user).firstOrNull()
+ userMutex.withLock {
+ if (currentUser == user) {
+ return
+ }
+ updatesJob?.cancel()
+ defaultsRepository.requestNewDefaults(user, tileSpec.componentName)
+ launchUpdates(user)
+ customTileRepository.restoreForTheUserIfNeeded(
+ user,
+ customTileRepository.isTileActive()
+ )
+ // Suspend to make sure it gets the tile from one of the sources: restoration, defaults,
+ // or
+ // tile update.
+ customTileRepository.getTiles(user).firstOrNull()
+ currentUser = user
+ }
}
private fun launchUpdates(user: UserHandle) {
- tileUpdates
- .onEach {
- customTileRepository.updateWithTile(
- user,
- it,
- customTileRepository.isTileActive(),
- )
+ updatesJob =
+ tileScope.launch {
+ tileUpdates
+ .onEach {
+ customTileRepository.updateWithTile(
+ user,
+ it,
+ customTileRepository.isTileActive(),
+ )
+ }
+ .flowOn(backgroundContext)
+ .launchIn(this)
+ defaultsRepository
+ .defaults(user)
+ .onEach {
+ customTileRepository.updateWithDefaults(
+ user,
+ it,
+ customTileRepository.isTileActive(),
+ )
+ }
+ .flowOn(backgroundContext)
+ .launchIn(this)
}
- .flowOn(backgroundContext)
- .launchIn(tileScope)
- defaultsRepository
- .defaults(user)
- .onEach {
- customTileRepository.updateWithDefaults(
- user,
- it,
- customTileRepository.isTileActive(),
- )
- }
- .flowOn(backgroundContext)
- .launchIn(tileScope)
}
- /** Updates current [Tile]. Emits a new event in [tiles]. */
+ /** Updates current [Tile]. Emits a new event in [getTiles]. */
fun updateTile(newTile: Tile) {
tileUpdates.tryEmit(newTile)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt
new file mode 100644
index 0000000..acff40f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileServiceInteractor.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.custom.domain.interactor
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.os.IBinder
+import android.os.Process
+import android.os.RemoteException
+import android.os.UserHandle
+import android.service.quicksettings.IQSTileService
+import android.service.quicksettings.Tile
+import android.service.quicksettings.TileService
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.external.CustomTileInterface
+import com.android.systemui.qs.external.TileServiceManager
+import com.android.systemui.qs.external.TileServices
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
+import com.android.systemui.user.data.repository.UserRepository
+import dagger.Lazy
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.produce
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+
+/**
+ * Communicates with [TileService] via [TileServiceManager] and [IQSTileService]. This interactor is
+ * also responsible for the binding to the [TileService].
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@QSTileScope
+class CustomTileServiceInteractor
+@Inject
+constructor(
+ private val tileSpec: TileSpec.CustomTileSpec,
+ private val activityStarter: ActivityStarter,
+ private val userActionInteractor: Lazy<CustomTileUserActionInteractor>,
+ private val customTileInteractor: CustomTileInteractor,
+ private val userRepository: UserRepository,
+ private val qsTileLogger: QSTileLogger,
+ private val tileServices: TileServices,
+ @QSTileScope private val tileScope: CoroutineScope,
+) {
+
+ private val tileReceivingInterface = ReceivingInterface()
+ private var tileServiceManager: TileServiceManager? = null
+ private val tileServiceInterface: IQSTileService
+ get() = getTileServiceManager().tileService
+
+ private var currentUser: UserHandle = userRepository.getSelectedUserInfo().userHandle
+ private var destructionJob: Job? = null
+
+ val callingAppIds: Flow<Int>
+ get() = tileReceivingInterface.mutableCallingAppIds
+ val refreshEvents: Flow<Unit>
+ get() = tileReceivingInterface.mutableRefreshEvents
+
+ /** Clears all pending binding for an active tile and binds not active one. */
+ fun bindOnStart() {
+ try {
+ with(getTileServiceManager()) {
+ if (isActiveTile) {
+ clearPendingBind()
+ } else {
+ setBindRequested(true)
+ tileServiceInterface.onStartListening()
+ }
+ }
+ } catch (e: RemoteException) {
+ qsTileLogger.logError(tileSpec, "Binding to the service failed", e)
+ }
+ }
+
+ /** Binds active tile WITHOUT CLEARING pending binds. */
+ fun bindOnClick() {
+ try {
+ with(getTileServiceManager()) {
+ if (isActiveTile) {
+ setBindRequested(true)
+ tileServiceInterface.onStartListening()
+ }
+ }
+ } catch (e: RemoteException) {
+ qsTileLogger.logError(tileSpec, "Binding to the service on click failed", e)
+ }
+ }
+
+ /** Releases resources held by the binding and prepares the interactor to be collected */
+ fun unbind() {
+ try {
+ with(userActionInteractor.get()) {
+ clearLastClickedView()
+ tileServiceInterface.onStopListening()
+ revokeToken(false)
+ setShowingDialog(false)
+ }
+ getTileServiceManager().setBindRequested(false)
+ } catch (e: RemoteException) {
+ qsTileLogger.logError(tileSpec, "Unbinding failed", e)
+ }
+ }
+
+ /**
+ * Checks if [TileServiceManager] has a pending [android.service.quicksettings.TileService]
+ * bind.
+ */
+ fun hasPendingBind(): Boolean = getTileServiceManager().hasPendingBind()
+
+ /** Sets a [user] for the custom tile to use. User change triggers service rebinding. */
+ fun setUser(user: UserHandle) {
+ if (user == currentUser) {
+ return
+ }
+ currentUser = user
+ destructionJob?.cancel()
+
+ tileServiceManager = null
+ }
+
+ /** Sends click event to [TileService] using [IQSTileService.onClick]. */
+ fun onClick(token: IBinder) {
+ tileServiceInterface.onClick(token)
+ }
+
+ private fun getTileServiceManager(): TileServiceManager =
+ synchronized(tileServices) {
+ if (tileServiceManager == null) {
+ tileServices
+ .getTileWrapper(tileReceivingInterface)
+ .also { destructionJob = createDestructionJob() }
+ .also { tileServiceManager = it }
+ } else {
+ tileServiceManager!!
+ }
+ }
+
+ /**
+ * This job used to free the resources when the [QSTileScope] coroutine scope gets cancelled by
+ * the View Model.
+ */
+ private fun createDestructionJob(): Job =
+ tileScope.launch {
+ produce<Unit> {
+ awaitClose {
+ userActionInteractor.get().revokeToken(true)
+ tileServices.freeService(tileReceivingInterface, getTileServiceManager())
+ destructionJob = null
+ }
+ }
+ }
+
+ private inner class ReceivingInterface : CustomTileInterface {
+
+ override val user: Int
+ get() = currentUser.identifier
+ override val qsTile: Tile
+ get() = customTileInteractor.getTile(currentUser)
+ override val component: ComponentName = tileSpec.componentName
+
+ val mutableCallingAppIds = MutableStateFlow(Process.INVALID_UID)
+ val mutableRefreshEvents = MutableSharedFlow<Unit>()
+
+ override fun getTileSpec(): String = tileSpec.spec
+
+ override fun refreshState() {
+ tileScope.launch { mutableRefreshEvents.emit(Unit) }
+ }
+
+ override fun updateTileState(tile: Tile, uid: Int) {
+ customTileInteractor.updateTile(tile)
+ mutableCallingAppIds.tryEmit(uid)
+ }
+
+ override fun onDialogShown() {
+ userActionInteractor.get().setShowingDialog(true)
+ }
+
+ override fun onDialogHidden() =
+ with(userActionInteractor.get()) {
+ setShowingDialog(false)
+ revokeToken(true)
+ }
+
+ override fun startActivityAndCollapse(pendingIntent: PendingIntent) {
+ userActionInteractor.get().startActivityAndCollapse(pendingIntent)
+ }
+
+ override fun startUnlockAndRun() {
+ activityStarter.postQSRunnableDismissingKeyguard {
+ tileServiceInterface.onUnlockComplete()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
new file mode 100644
index 0000000..c3e1fea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.custom.domain.interactor
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.Uri
+import android.os.Binder
+import android.os.IBinder
+import android.os.RemoteException
+import android.os.UserHandle
+import android.provider.Settings
+import android.service.quicksettings.TileService
+import android.view.IWindowManager
+import android.view.View
+import android.view.WindowManager
+import androidx.annotation.GuardedBy
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel
+import com.android.systemui.qs.tiles.impl.di.QSTileScope
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.settings.DisplayTracker
+import java.util.concurrent.atomic.AtomicReference
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+@QSTileScope
+class CustomTileUserActionInteractor
+@Inject
+constructor(
+ private val context: Context,
+ private val tileSpec: TileSpec,
+ private val qsTileLogger: QSTileLogger,
+ private val windowManager: IWindowManager,
+ private val displayTracker: DisplayTracker,
+ private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler,
+ @Background private val backgroundContext: CoroutineContext,
+ private val serviceInteractor: CustomTileServiceInteractor,
+) : QSTileUserActionInteractor<CustomTileDataModel> {
+
+ private val token: IBinder = Binder()
+
+ @GuardedBy("token") private var isTokenGranted: Boolean = false
+ @GuardedBy("token") private var isShowingDialog: Boolean = false
+ private val lastClickedView: AtomicReference<View> = AtomicReference<View>()
+
+ override suspend fun handleInput(input: QSTileInput<CustomTileDataModel>) =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> click(action.view, data.tile.activityLaunchForClick)
+ is QSTileUserAction.LongClick ->
+ longClick(user, action.view, data.componentName, data.tile.state)
+ }
+ qsTileLogger.logCustomTileUserActionDelivered(tileSpec)
+ }
+
+ private fun click(
+ view: View?,
+ activityLaunchForClick: PendingIntent?,
+ ) {
+ grantToken()
+ try {
+ // Bind active tile to deliver user action
+ serviceInteractor.bindOnClick()
+ if (activityLaunchForClick == null) {
+ lastClickedView.set(view)
+ serviceInteractor.onClick(token)
+ } else {
+ qsTileIntentUserInputHandler.handle(view, activityLaunchForClick)
+ }
+ } catch (e: RemoteException) {
+ qsTileLogger.logError(tileSpec, "Failed to deliver click", e)
+ }
+ }
+
+ fun revokeToken(ignoreShownDialog: Boolean) {
+ synchronized(token) {
+ if (isTokenGranted && (ignoreShownDialog || !isShowingDialog)) {
+ try {
+ windowManager.removeWindowToken(token, displayTracker.defaultDisplayId)
+ } catch (e: RemoteException) {
+ qsTileLogger.logError(tileSpec, "Failed to remove a window token", e)
+ }
+ isTokenGranted = false
+ }
+ }
+ }
+
+ fun setShowingDialog(isShowingDialog: Boolean) {
+ synchronized(token) { this.isShowingDialog = isShowingDialog }
+ }
+
+ fun startActivityAndCollapse(pendingIntent: PendingIntent) {
+ if (!pendingIntent.isActivity) {
+ return
+ }
+ if (!isTokenGranted) {
+ return
+ }
+ qsTileIntentUserInputHandler.handle(lastClickedView.getAndSet(null), pendingIntent)
+ }
+
+ fun clearLastClickedView() = lastClickedView.set(null)
+
+ private fun grantToken() {
+ synchronized(token) {
+ if (!isTokenGranted) {
+ try {
+ windowManager.addWindowToken(
+ token,
+ WindowManager.LayoutParams.TYPE_QS_DIALOG,
+ displayTracker.defaultDisplayId,
+ null /* options */
+ )
+ } catch (e: RemoteException) {
+ qsTileLogger.logError(tileSpec, "Failed to grant a window token", e)
+ }
+ isTokenGranted = true
+ }
+ }
+ }
+
+ private suspend fun longClick(
+ user: UserHandle,
+ view: View?,
+ componentName: ComponentName,
+ state: Int
+ ) {
+ val resolvedIntent: Intent? =
+ resolveIntent(
+ Intent(TileService.ACTION_QS_TILE_PREFERENCES).apply {
+ setPackage(componentName.packageName)
+ },
+ user,
+ )
+ ?.apply {
+ putExtra(Intent.EXTRA_COMPONENT_NAME, componentName)
+ putExtra(TileService.EXTRA_STATE, state)
+ }
+ if (resolvedIntent == null) {
+ qsTileIntentUserInputHandler.handle(
+ view,
+ Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(
+ Uri.fromParts(IntentFilter.SCHEME_PACKAGE, componentName.packageName, null)
+ )
+ )
+ } else {
+ qsTileIntentUserInputHandler.handle(view, resolvedIntent)
+ }
+ }
+
+ /**
+ * Returns an intent resolved by [android.content.pm.PackageManager.resolveActivityAsUser] or
+ * null.
+ */
+ private suspend fun resolveIntent(intent: Intent, user: UserHandle): Intent? =
+ withContext(backgroundContext) {
+ val activityInfo =
+ context.packageManager
+ .resolveActivityAsUser(intent, 0, user.identifier)
+ ?.activityInfo
+ activityInfo ?: return@withContext null
+ with(activityInfo) {
+ Intent(TileService.ACTION_QS_TILE_PREFERENCES).apply {
+ setClassName(packageName, name)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
index be1b740..b927e41 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
@@ -32,7 +32,7 @@
* // TODO(b/http://b/299909989): Clean up legacy mappings after the transition
*/
data class QSTileState(
- val icon: () -> Icon,
+ val icon: () -> Icon?,
val label: CharSequence,
val activationState: ActivationState,
val secondaryLabel: CharSequence?,
@@ -60,7 +60,7 @@
)
}
- fun build(icon: () -> Icon, label: CharSequence, build: Builder.() -> Unit): QSTileState =
+ fun build(icon: () -> Icon?, label: CharSequence, build: Builder.() -> Unit): QSTileState =
Builder(icon, label).apply(build).build()
}
@@ -108,7 +108,7 @@
}
class Builder(
- var icon: () -> Icon,
+ var icon: () -> Icon?,
var label: CharSequence,
) {
var activationState: ActivationState = ActivationState.INACTIVE
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
index ef3df48..226e2fa0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
@@ -45,7 +45,10 @@
*/
fun onUserChanged(user: UserHandle)
- /** Triggers the emission of the new [QSTileState] in a [state]. */
+ /**
+ * Triggers the emission of the new [QSTileState] in a [state]. The new value can still be
+ * skipped if there is no change.
+ */
fun forceUpdate()
/** Notifies underlying logic about user input. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 977df81..4780a2e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -37,6 +37,7 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectIndexed
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -60,27 +61,34 @@
private val listeningClients: MutableCollection<Any> = mutableSetOf()
// Cancels the jobs when the adapter is no longer alive
- private var availabilityJob: Job? = null
+ private var tileAdapterJob: Job? = null
// Cancels the jobs when clients stop listening
private var stateJob: Job? = null
init {
- availabilityJob =
+ tileAdapterJob =
applicationScope.launch {
- qsTileViewModel.isAvailable.collectIndexed { index, isAvailable ->
- if (!isAvailable) {
- qsHost.removeTile(tileSpec)
- }
- // qsTileViewModel.isAvailable flow often starts with isAvailable == true.
- // That's
- // why we only allow isAvailable == true once and throw an exception afterwards.
- if (index > 0 && isAvailable) {
- // See com.android.systemui.qs.pipeline.domain.model.AutoAddable for
- // additional
- // guidance on how to auto add your tile
- throw UnsupportedOperationException("Turning on tile is not supported now")
+ launch {
+ qsTileViewModel.isAvailable.collectIndexed { index, isAvailable ->
+ if (!isAvailable) {
+ qsHost.removeTile(tileSpec)
+ }
+ // qsTileViewModel.isAvailable flow often starts with isAvailable == true.
+ // That's
+ // why we only allow isAvailable == true once and throw an exception
+ // afterwards.
+ if (index > 0 && isAvailable) {
+ // See com.android.systemui.qs.pipeline.domain.model.AutoAddable for
+ // additional
+ // guidance on how to auto add your tile
+ throw UnsupportedOperationException(
+ "Turning on tile is not supported now"
+ )
+ }
}
}
+ // Warm up tile with some initial state
+ launch { qsTileViewModel.state.first() }
}
// QSTileHost doesn't call this when userId is initialized
@@ -185,7 +193,7 @@
override fun destroy() {
stateJob?.cancel()
- availabilityJob?.cancel()
+ tileAdapterJob?.cancel()
qsTileViewModel.destroy()
}
@@ -222,8 +230,9 @@
QSTile.BooleanState().apply {
spec = config.tileSpec.spec
label = viewModelState.label
- // This value is synthetic and doesn't have any meaning
- value = false
+ // This value is synthetic and doesn't have any meaning. It's only needed to satisfy
+ // CTS tests.
+ value = viewModelState.activationState == QSTileState.ActivationState.ACTIVE
secondaryLabel = viewModelState.secondaryLabel
handlesLongClick =
@@ -233,6 +242,7 @@
when (val stateIcon = viewModelState.icon()) {
is Icon.Loaded -> DrawableIcon(stateIcon.drawable)
is Icon.Resource -> ResourceIcon.get(stateIcon.res)
+ null -> null
}
}
state = viewModelState.activationState.legacyState
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
index 02d40da..ea2b22c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
@@ -115,7 +115,7 @@
.isEqualTo(
"test_spec:\n" +
" QSTileState(" +
- "icon=() -> com.android.systemui.common.shared.model.Icon, " +
+ "icon=() -> com.android.systemui.common.shared.model.Icon?, " +
"label=test_data, " +
"activationState=INACTIVE, " +
"secondaryLabel=null, " +
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt
index d705248..14f28fe 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/CustomTileKosmos.kt
@@ -33,6 +33,7 @@
val Kosmos.customTileRepository: FakeCustomTileRepository by
Kosmos.Fixture {
FakeCustomTileRepository(
+ tileSpec,
customTileStatePersister,
packageManagerAdapterFacade,
testScope.testScheduler,
@@ -46,4 +47,4 @@
Kosmos.Fixture { FakeCustomTilePackageUpdatesRepository() }
val Kosmos.packageManagerAdapterFacade: FakePackageManagerAdapterFacade by
- Kosmos.Fixture { FakePackageManagerAdapterFacade(tileSpec) }
+ Kosmos.Fixture { FakePackageManagerAdapterFacade(tileSpec.componentName) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
index ba803d8..c110da0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakeCustomTileRepository.kt
@@ -19,11 +19,13 @@
import android.os.UserHandle
import android.service.quicksettings.Tile
import com.android.systemui.qs.external.FakeCustomTileStatePersister
+import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.Flow
class FakeCustomTileRepository(
+ tileSpec: TileSpec.CustomTileSpec,
customTileStatePersister: FakeCustomTileStatePersister,
private val packageManagerAdapterFacade: FakePackageManagerAdapterFacade,
testBackgroundContext: CoroutineContext,
@@ -31,12 +33,16 @@
private val realDelegate: CustomTileRepository =
CustomTileRepositoryImpl(
- packageManagerAdapterFacade.tileSpec,
+ tileSpec,
customTileStatePersister,
packageManagerAdapterFacade.packageManagerAdapter,
testBackgroundContext,
)
+ init {
+ require(tileSpec.componentName == packageManagerAdapterFacade.componentName)
+ }
+
override suspend fun restoreForTheUserIfNeeded(user: UserHandle, isPersistable: Boolean) =
realDelegate.restoreForTheUserIfNeeded(user, isPersistable)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
index c9a7655..634d121 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/data/repository/FakePackageManagerAdapterFacade.kt
@@ -16,17 +16,27 @@
package com.android.systemui.qs.tiles.impl.custom.data.repository
+import android.content.ComponentName
import android.content.pm.ServiceInfo
import android.os.Bundle
import com.android.systemui.qs.external.PackageManagerAdapter
-import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+/**
+ * Facade for [PackageManagerAdapter] to provide a fake-like behaviour. You can create this class
+ * and then get [packageManagerAdapter] to use in your test code.
+ *
+ * This allows to mock [PackageManagerAdapter] to provide a custom behaviour for
+ * [CustomTileRepository.isTileActive], [CustomTileRepository.isTileToggleable],
+ * [com.android.systemui.qs.external.TileServiceManager.isToggleableTile] or
+ * [com.android.systemui.qs.external.TileServiceManager.isActiveTile] when the real objects are
+ * used.
+ */
class FakePackageManagerAdapterFacade(
- val tileSpec: TileSpec.CustomTileSpec,
+ val componentName: ComponentName,
val packageManagerAdapter: PackageManagerAdapter = mock {},
) {
@@ -34,22 +44,21 @@
private var isActive: Boolean = false
init {
- whenever(packageManagerAdapter.getServiceInfo(eq(tileSpec.componentName), any()))
- .thenAnswer {
- ServiceInfo().apply {
- metaData =
- Bundle().apply {
- putBoolean(
- android.service.quicksettings.TileService.META_DATA_TOGGLEABLE_TILE,
- isToggleable
- )
- putBoolean(
- android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE,
- isActive
- )
- }
- }
+ whenever(packageManagerAdapter.getServiceInfo(eq(componentName), any())).thenAnswer {
+ ServiceInfo().apply {
+ metaData =
+ Bundle().apply {
+ putBoolean(
+ android.service.quicksettings.TileService.META_DATA_TOGGLEABLE_TILE,
+ isToggleable
+ )
+ putBoolean(
+ android.service.quicksettings.TileService.META_DATA_ACTIVE_TILE,
+ isActive
+ )
+ }
}
+ }
}
fun setIsActive(isActive: Boolean) {