Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ef87d087 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge "Introduce SystemUIDialogFactory to create (Compose) SystemUIDialogs" into main

parents 86045abd 35276450
Loading
Loading
Loading
Loading
+11 −7
Original line number Diff line number Diff line
@@ -78,6 +78,7 @@ fun PlatformTextButton(
        modifier = modifier,
        enabled = enabled,
        content = content,
        colors = textButtonColors(),
    )
}

@@ -85,26 +86,29 @@ private val ButtonPaddings = PaddingValues(horizontal = 16.dp, vertical = 8.dp)

@Composable
private fun filledButtonColors(): ButtonColors {
    val colors = LocalAndroidColorScheme.current.deprecated
    val colors = LocalAndroidColorScheme.current
    return ButtonDefaults.buttonColors(
        containerColor = colors.colorAccentPrimary,
        contentColor = colors.textColorOnAccent,
        containerColor = colors.primary,
        contentColor = colors.onPrimary,
    )
}

@Composable
private fun outlineButtonColors(): ButtonColors {
    val colors = LocalAndroidColorScheme.current.deprecated
    return ButtonDefaults.outlinedButtonColors(
        contentColor = colors.textColorPrimary,
        contentColor = LocalAndroidColorScheme.current.onSurface,
    )
}

@Composable
private fun outlineButtonBorder(): BorderStroke {
    val colors = LocalAndroidColorScheme.current.deprecated
    return BorderStroke(
        width = 1.dp,
        color = colors.colorAccentPrimaryVariant,
        color = LocalAndroidColorScheme.current.primary,
    )
}

@Composable
private fun textButtonColors(): ButtonColors {
    return ButtonDefaults.textButtonColors(contentColor = LocalAndroidColorScheme.current.primary)
}
+129 −0
Original line number Diff line number Diff line
/*
 * 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.dialog.ui.composable

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.android.compose.theme.LocalAndroidColorScheme

/**
 * The content of an AlertDialog which can be used together with
 * [SystemUIDialogFactory.create][com.android.systemui.statusbar.phone.create] to create an alert
 * dialog in Compose.
 *
 * @see com.android.systemui.statusbar.phone.create
 */
@Composable
fun AlertDialogContent(
    title: @Composable () -> Unit,
    content: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    icon: (@Composable () -> Unit)? = null,
    positiveButton: (@Composable () -> Unit)? = null,
    negativeButton: (@Composable () -> Unit)? = null,
    neutralButton: (@Composable () -> Unit)? = null,
) {
    Column(
        modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(DialogPaddings),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        // Icon.
        if (icon != null) {
            val defaultSize = 32.dp
            Box(
                Modifier.defaultMinSize(minWidth = defaultSize, minHeight = defaultSize),
                propagateMinConstraints = true,
            ) {
                val iconColor = LocalAndroidColorScheme.current.primary
                CompositionLocalProvider(LocalContentColor provides iconColor) { icon() }
            }

            Spacer(Modifier.height(16.dp))
        }

        // Title.
        val titleColor = LocalAndroidColorScheme.current.onSurface
        CompositionLocalProvider(LocalContentColor provides titleColor) {
            ProvideTextStyle(
                MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center)
            ) {
                title()
            }
        }
        Spacer(Modifier.height(16.dp))

        // Content.
        val contentColor = LocalAndroidColorScheme.current.onSurfaceVariant
        Box(Modifier.defaultMinSize(minHeight = 48.dp)) {
            CompositionLocalProvider(LocalContentColor provides contentColor) {
                ProvideTextStyle(
                    MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.Center)
                ) {
                    content()
                }
            }
        }
        Spacer(Modifier.height(32.dp))

        // Buttons.
        // TODO(b/283817398): If there is not enough space, the buttons should automatically stack
        // as shown in go/sysui-dialog-styling.
        if (positiveButton != null || negativeButton != null || neutralButton != null) {
            Row(Modifier.fillMaxWidth()) {
                if (neutralButton != null) {
                    neutralButton()
                    Spacer(Modifier.width(8.dp))
                }

                Spacer(Modifier.weight(1f))

                if (negativeButton != null) {
                    negativeButton()
                }

                if (positiveButton != null) {
                    if (negativeButton != null) {
                        Spacer(Modifier.width(8.dp))
                    }

                    positiveButton()
                }
            }
        }
    }
}

private val DialogPaddings = PaddingValues(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 18.dp)
+76 −0
Original line number Diff line number Diff line
/*
 * 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.statusbar.phone

import android.content.Context
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.ComposeView
import com.android.compose.theme.PlatformTheme

/**
 * Create a [SystemUIDialog] with the given [content].
 *
 * Note that the returned dialog will already have a background so the content should not draw an
 * additional background.
 *
 * Example:
 * ```
 * val dialog = systemUiDialogFactory.create {
 *   AlertDialogContent(
 *     title = { Text("My title") },
 *     content = { Text("My content") },
 *   )
 * }
 *
 * dialogLaunchAnimator.showFromView(dialog, viewThatWasClicked)
 * ```
 *
 * @param context the [Context] in which the dialog will be constructed.
 * @param dismissOnDeviceLock whether the dialog should be automatically dismissed when the device
 *   is locked (true by default).
 */
fun SystemUIDialogFactory.create(
    context: Context = this.applicationContext,
    dismissOnDeviceLock: Boolean = SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
    content: @Composable (SystemUIDialog) -> Unit,
): ComponentSystemUIDialog {
    val dialog = create(context, dismissOnDeviceLock)

    // Create the dialog so that it is properly constructed before we set the Compose content.
    // Otherwise, the ComposeView won't render properly.
    dialog.create()

    // Set the content. Note that the background of the dialog is drawn on the DecorView of the
    // dialog directly, which makes it automatically work nicely with DialogLaunchAnimator.
    dialog.setContentView(
        ComposeView(context).apply {
            setContent {
                PlatformTheme {
                    val defaultContentColor = MaterialTheme.colorScheme.onSurfaceVariant
                    CompositionLocalProvider(LocalContentColor provides defaultContentColor) {
                        content(dialog)
                    }
                }
            }
        }
    )

    return dialog
}
+154 −0
Original line number Diff line number Diff line
/*
 * 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.statusbar.phone

import android.annotation.CallSuper
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.model.SysUiState

/**
 * A [SystemUIDialog] that implements [LifecycleOwner], [SavedStateRegistryOwner] and
 * [OnBackPressedDispatcherOwner].
 *
 * This class was forked from [androidx.activity.ComponentDialog] and can be used to easily create
 * SystemUI dialogs without the need to subclass [SystemUIDialog]. You should call
 * [SystemUIDialogFactory.create] to easily instantiate this class.
 *
 * Important: [ComponentSystemUIDialog] should be created and shown on the main thread.
 *
 * @see SystemUIDialogFactory.create
 */
class ComponentSystemUIDialog(
    context: Context,
    theme: Int,
    dismissOnDeviceLock: Boolean,
    featureFlags: FeatureFlags,
    dialogManager: SystemUIDialogManager,
    sysUiState: SysUiState,
    broadcastDispatcher: BroadcastDispatcher,
    dialogLaunchAnimator: DialogLaunchAnimator,
) :
    SystemUIDialog(
        context,
        theme,
        dismissOnDeviceLock,
        featureFlags,
        dialogManager,
        sysUiState,
        broadcastDispatcher,
        dialogLaunchAnimator
    ),
    LifecycleOwner,
    SavedStateRegistryOwner,
    OnBackPressedDispatcherOwner {
    private var _lifecycleRegistry: LifecycleRegistry? = null
    private val lifecycleRegistry: LifecycleRegistry
        get() = _lifecycleRegistry ?: LifecycleRegistry(this).also { _lifecycleRegistry = it }

    private val savedStateRegistryController: SavedStateRegistryController =
        SavedStateRegistryController.create(this)
    override val savedStateRegistry: SavedStateRegistry
        get() = savedStateRegistryController.savedStateRegistry

    override val lifecycle: Lifecycle
        get() = lifecycleRegistry

    override fun onSaveInstanceState(): Bundle {
        val bundle = super.onSaveInstanceState()
        savedStateRegistryController.performSave(bundle)
        return bundle
    }

    @CallSuper
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        onBackPressedDispatcher.setOnBackInvokedDispatcher(onBackInvokedDispatcher)
        savedStateRegistryController.performRestore(savedInstanceState)
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
    }

    @CallSuper
    override fun start() {
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
    }

    @CallSuper
    override fun stop() {
        // This is the closest thing to onDestroy that a Dialog has
        // TODO(b/296180426): Make SystemUIDialog.onStop() and onStart() open again (annotated with
        // @CallSuper) and do this *before* calling super.onStop(), like AndroidX ComponentDialog
        // does.
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
        _lifecycleRegistry = null
    }

    @Suppress("DEPRECATION")
    override val onBackPressedDispatcher = OnBackPressedDispatcher { super.onBackPressed() }

    @CallSuper
    override fun onBackPressed() {
        onBackPressedDispatcher.onBackPressed()
    }

    override fun setContentView(layoutResID: Int) {
        initializeViewTreeOwners()
        super.setContentView(layoutResID)
    }

    override fun setContentView(view: View) {
        initializeViewTreeOwners()
        super.setContentView(view)
    }

    override fun setContentView(view: View, params: ViewGroup.LayoutParams?) {
        initializeViewTreeOwners()
        super.setContentView(view, params)
    }

    override fun addContentView(view: View, params: ViewGroup.LayoutParams?) {
        initializeViewTreeOwners()
        super.addContentView(view, params)
    }

    /**
     * Sets the view tree owners before setting the content view so that the inflation process and
     * attach listeners will see them already present.
     */
    @CallSuper
    open fun initializeViewTreeOwners() {
        window!!.decorView.setViewTreeLifecycleOwner(this)
        window!!.decorView.setViewTreeOnBackPressedDispatcherOwner(this)
        window!!.decorView.setViewTreeSavedStateRegistryOwner(this)
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -68,7 +68,7 @@ public class SystemUIDialog extends AlertDialog implements ViewRootImpl.ConfigCh
    // TODO(b/203389579): Remove this once the dialog width on large screens has been agreed on.
    private static final String FLAG_TABLET_DIALOG_WIDTH =
            "persist.systemui.flag_tablet_dialog_width";
    private static final boolean DEFAULT_DISMISS_ON_DEVICE_LOCK = true;
    public static final boolean DEFAULT_DISMISS_ON_DEVICE_LOCK = true;

    private final Context mContext;
    private final FeatureFlags mFeatureFlags;
Loading