diff options
7 files changed, 217 insertions, 103 deletions
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 1c09f84b6fd0..94b0b5f67934 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -540,6 +540,7 @@ <!-- Overridden by values-television/styles.xml with tv-specific settings --> <style name="volume_dialog_theme" parent="Theme.SystemUI"> <item name="android:windowIsFloating">true</item> + <item name="android:showWhenLocked">true</item> </style> <style name="Theme.SystemUI.DayNightDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"/> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt new file mode 100644 index 000000000000..441cbb33f821 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DevicePostureControllerExt.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 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.statusbar.policy + +import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onStart + +/** [DevicePostureController.getDevicePosture] as a [Flow]. */ +@DevicePostureInt +fun DevicePostureController.devicePosture(): Flow<Int> = + conflatedCallbackFlow { + val callback = DevicePostureController.Callback { posture -> trySend(posture) } + addCallback(callback) + awaitClose { removeCallback(callback) } + } + .onStart { emit(devicePosture) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt deleted file mode 100644 index 869b3c65d70e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialog.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2024 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.volume.dialog - -import android.app.Dialog -import android.content.Context -import android.os.Bundle -import android.view.ContextThemeWrapper -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.res.R -import javax.inject.Inject - -class NewVolumeDialog @Inject constructor(@Application context: Context) : - Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.volume_dialog) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt deleted file mode 100644 index b93714ae4fd4..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/NewVolumeDialogPlugin.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2024 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.volume.dialog - -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.plugins.VolumeDialog -import com.android.systemui.volume.dialog.dagger.VolumeDialogComponent -import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch - -class NewVolumeDialogPlugin -@Inject -constructor( - @Application private val applicationCoroutineScope: CoroutineScope, - private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory, -) : VolumeDialog { - - private var volumeDialogPluginComponent: VolumeDialogPluginComponent? = null - private var job: Job? = null - - override fun init(windowType: Int, callback: VolumeDialog.Callback?) { - job = - applicationCoroutineScope.launch { - coroutineScope { - volumeDialogPluginComponent = volumeDialogPluginComponentFactory.create(this) - } - } - } - - private fun showDialog() { - val volumeDialogPluginComponent = - volumeDialogPluginComponent ?: error("Creating dialog before init was called") - volumeDialogPluginComponent.coroutineScope().launch { - coroutineScope { - val volumeDialogComponent: VolumeDialogComponent = - volumeDialogPluginComponent.volumeDialogComponentFactory().create(this) - with(volumeDialogComponent.volumeDialog()) { - setOnDismissListener { volumeDialogComponent.coroutineScope().cancel() } - show() - } - } - } - } - - override fun destroy() { - job?.cancel() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt index 74e823e9f592..aa73dbd555d7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt @@ -22,13 +22,16 @@ import android.os.Bundle import android.view.ContextThemeWrapper import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.res.R +import com.android.systemui.volume.dialog.ui.binder.VolumeDialogBinder import javax.inject.Inject -class VolumeDialog @Inject constructor(@Application context: Context) : +class VolumeDialog +@Inject +constructor(@Application context: Context, private val dialogBinder: VolumeDialogBinder) : Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.volume_dialog) + dialogBinder.bind(this) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt new file mode 100644 index 000000000000..3f2c39bba6e7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 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.volume.dialog.ui.binder + +import android.app.Dialog +import android.graphics.Color +import android.graphics.PixelFormat +import android.graphics.drawable.ColorDrawable +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.view.WindowManager +import androidx.lifecycle.lifecycleScope +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope +import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogGravityViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +@VolumeDialogScope +class VolumeDialogBinder +@Inject +constructor( + @VolumeDialog private val coroutineScope: CoroutineScope, + private val volumeDialogViewBinder: VolumeDialogViewBinder, + private val gravityViewModel: VolumeDialogGravityViewModel, +) { + + fun bind(dialog: Dialog) { + with(dialog) { + setupWindow(window!!) + dialog.setContentView(R.layout.volume_dialog) + + val volumeDialogView: View = dialog.requireViewById(R.id.volume_dialog_container) + volumeDialogView.repeatWhenAttached { + lifecycleScope.launch { volumeDialogViewBinder.bind(volumeDialogView) } + } + } + } + + /** Configures [Window] for the [Dialog]. */ + private fun setupWindow(window: Window) = + with(window) { + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + addFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED + ) + addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) + + requestFeature(Window.FEATURE_NO_TITLE) + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY) + setWindowAnimations(-1) + setFormat(PixelFormat.TRANSLUCENT) + + attributes = + attributes.apply { + title = "VolumeDialog" // Not the same as Window#setTitle + } + setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + gravityViewModel.dialogGravity.onEach { window.setGravity(it) }.launchIn(coroutineScope) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt new file mode 100644 index 000000000000..df6523c9d750 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogGravityViewModel.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 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.volume.dialog.ui.viewmodel + +import android.content.Context +import android.content.res.Configuration +import android.view.Gravity +import androidx.annotation.GravityInt +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.UiBackground +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.DevicePostureController +import com.android.systemui.statusbar.policy.devicePosture +import com.android.systemui.statusbar.policy.onConfigChanged +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +@VolumeDialogScope +class VolumeDialogGravityViewModel +@Inject +constructor( + @Application private val context: Context, + @VolumeDialog private val coroutineScope: CoroutineScope, + @UiBackground private val uiBackgroundCoroutineContext: CoroutineContext, + configurationController: ConfigurationController, + private val devicePostureController: DevicePostureController, +) { + + @GravityInt private var originalGravity: Int = context.getAbsoluteGravity() + + val dialogGravity: Flow<Int> = + combine( + devicePostureController.devicePosture(), + configurationController.onConfigChanged.onEach { onConfigurationChanged() }, + ) { devicePosture, configuration -> + context.calculateGravity(devicePosture, configuration) + } + .stateIn( + scope = coroutineScope, + started = SharingStarted.Eagerly, + context.calculateGravity(), + ) + + private suspend fun onConfigurationChanged() { + withContext(uiBackgroundCoroutineContext) { originalGravity = context.getAbsoluteGravity() } + } + + @GravityInt + private fun Context.calculateGravity( + devicePosture: Int = devicePostureController.devicePosture, + config: Configuration = resources.configuration, + ): Int { + val isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE + val isHalfOpen = devicePosture == DevicePostureController.DEVICE_POSTURE_HALF_OPENED + val gravity = + if (isLandscape && isHalfOpen) { + originalGravity or Gravity.TOP + } else { + originalGravity + } + return getAbsoluteGravity(gravity) + } +} + +@GravityInt +private fun Context.getAbsoluteGravity( + gravity: Int = resources.getInteger(R.integer.volume_dialog_gravity) +): Int = with(resources) { Gravity.getAbsoluteGravity(gravity, configuration.layoutDirection) } |