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

Commit 8e8c4056 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Implement BundleHeader guts in Compose" into main

parents 319386b9 9c34de6c
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -151,7 +151,13 @@ private fun ContentScope.BundleHeaderContent(
        verticalAlignment = Alignment.CenterVertically,
        modifier = modifier.padding(vertical = 16.dp),
    ) {
        BundleIcon(viewModel.bundleIcon, modifier = Modifier.padding(horizontal = 16.dp))
        BundleIcon(
            viewModel.bundleIcon,
            modifier =
                Modifier.padding(horizontal = 16.dp)
                    // Has to be a shared element because we may have a semi-transparent background
                    .element(NotificationRowPrimitives.Elements.NotificationIconBackground),
        )
        Text(
            text = stringResource(viewModel.titleTextResId),
            style = MaterialTheme.typography.titleMediumEmphasized,
+230 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.
 */

@file:OptIn(ExperimentalMaterial3ExpressiveApi::class)

package com.android.systemui.notifications.ui.composable.row

import android.content.Context
import android.view.View
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.compose.theme.PlatformTheme
import com.android.internal.R
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderGutsViewModel

fun createBundleHeaderGutsComposeView(
    context: Context,
    viewModel: BundleHeaderGutsViewModel,
): ComposeView {
    return ComposeView(context).apply {
        repeatWhenAttached {
            repeatOnLifecycle(Lifecycle.State.CREATED) {
                initOnBackPressureDispatcherOwner(this@repeatWhenAttached.lifecycle)
                setContent {
                    // TODO(b/399588047): Check if we can init PlatformTheme once instead of once
                    //  per ComposeView
                    PlatformTheme { BundleHeaderGuts(viewModel) }
                }
            }
        }
    }
}

private fun View.initOnBackPressureDispatcherOwner(lifecycle: Lifecycle) {
    if (!SceneContainerFlag.isEnabled) {
        setViewTreeOnBackPressedDispatcherOwner(
            object : OnBackPressedDispatcherOwner {
                override val onBackPressedDispatcher =
                    OnBackPressedDispatcher().apply {
                        setOnBackInvokedDispatcher(viewRootImpl.onBackInvokedDispatcher)
                    }

                override val lifecycle: Lifecycle = lifecycle
            }
        )
    }
}

@Composable
fun BundleHeaderGuts(viewModel: BundleHeaderGutsViewModel, modifier: Modifier = Modifier) {
    Column(modifier.padding(horizontal = 16.dp)) {
        TopRow(viewModel)
        ContentRow()
        BottomRow(viewModel)
    }
}

@Composable
private fun TopRow(viewModel: BundleHeaderGutsViewModel, modifier: Modifier = Modifier) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = modifier.padding(vertical = 16.dp),
    ) {
        BundleIcon(viewModel.bundleIcon, modifier = Modifier.padding(end = 16.dp))
        Text(
            text = stringResource(viewModel.titleTextResId),
            style = MaterialTheme.typography.titleMediumEmphasized,
            color = MaterialTheme.colorScheme.primary,
            overflow = TextOverflow.Ellipsis,
            maxLines = 1,
            modifier = Modifier.weight(1f),
        )

        Image(
            painter = painterResource(R.drawable.ic_settings_24dp),
            // TODO(b/409748420): Add correct CD
            contentDescription =
                stringResource(com.android.systemui.res.R.string.notification_more_settings),
            modifier =
                Modifier.size(24.dp)
                    .clickable(
                        onClick = viewModel.onSettingsClicked,
                        indication = null,
                        interactionSource = null,
                    ),
            colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
        )
    }
}

@Composable
private fun ContentRow(modifier: Modifier = Modifier) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier =
            modifier
                .background(
                    color = MaterialTheme.colorScheme.secondaryContainer,
                    shape = RoundedCornerShape(size = 20.dp),
                )
                .padding(horizontal = 16.dp, vertical = 12.dp),
    ) {
        Column(Modifier.weight(1f)) {
            Text(
                text =
                    stringResource(
                        com.android.systemui.res.R.string.notification_guts_bundle_title
                    ),
                style = MaterialTheme.typography.titleMedium,
                color = MaterialTheme.colorScheme.onSecondaryContainer,
                overflow = TextOverflow.Ellipsis,
                maxLines = 1,
            )
            Text(
                // TODO(b/409748420): Implement string based on bundle type
                text =
                    stringResource(
                        com.android.systemui.res.R.string.notification_guts_social_summary
                    ),
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSecondaryContainer,
                overflow = TextOverflow.Ellipsis,
                maxLines = 1,
            )
        }

        var checked by remember { mutableStateOf(true) }

        Switch(
            checked = checked,
            // TODO(b/409748420): Implement proper checked logic
            onCheckedChange = { checked = !checked },
            thumbContent = {
                Icon(
                    // TODO(b/409748420): Add correct icon
                    painter = painterResource(R.drawable.ic_check_circle_24px),
                    contentDescription = null,
                    modifier = Modifier.size(SwitchDefaults.IconSize),
                    tint = MaterialTheme.colorScheme.onSecondaryContainer,
                )
            },
            // TODO(b/409748420): Implement correct switch colors
        )
    }
}

@Composable
private fun BottomRow(viewModel: BundleHeaderGutsViewModel, modifier: Modifier = Modifier) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = modifier.padding(vertical = 16.dp),
    ) {
        Text(
            text = stringResource(R.string.dismiss_action),
            style = MaterialTheme.typography.titleSmallEmphasized,
            color = MaterialTheme.colorScheme.primary,
            modifier =
                modifier
                    .padding(vertical = 13.dp)
                    .clickable(
                        onClick = viewModel.onDoneClicked,
                        indication = null,
                        interactionSource = null,
                    ),
        )

        Spacer(modifier = Modifier.weight(1f))

        // TODO(b/409748420): Implement done/apply switch
        Text(
            text = stringResource(R.string.done_label),
            style = MaterialTheme.typography.titleSmallEmphasized,
            color = MaterialTheme.colorScheme.primary,
            modifier =
                modifier
                    .padding(vertical = 13.dp)
                    .clickable(
                        onClick = viewModel.onDismissClicked,
                        indication = null,
                        interactionSource = null,
                    ),
        )
    }
}
+3 −11
Original line number Diff line number Diff line
@@ -72,20 +72,12 @@ object NotificationRowPrimitives {

/** The Icon displayed at the start of any notification row. */
@Composable
fun ContentScope.BundleIcon(@DrawableRes drawable: Int?, modifier: Modifier = Modifier) {
fun BundleIcon(@DrawableRes drawable: Int?, modifier: Modifier = Modifier) {
    val surfaceColor = notificationElementSurfaceColor()
    Box(
        modifier =
            modifier
                // Has to be a shared element because we may have semi-transparent background color
                .element(NotificationRowPrimitives.Elements.NotificationIconBackground)
                .size(40.dp)
                .background(color = surfaceColor, shape = CircleShape)
    ) {
    Box(modifier = modifier.size(40.dp).background(color = surfaceColor, shape = CircleShape)) {
        if (drawable == null) return@Box
        val painter = painterResource(drawable)
        Image(
            painter = painter,
            painter = painterResource(drawable),
            contentDescription = null,
            modifier = Modifier.padding(10.dp).fillMaxSize(),
            contentScale = ContentScale.Fit,
+95 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.notification.row

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.compose.ui.platform.ComposeView
import com.android.systemui.notifications.ui.composable.row.createBundleHeaderGutsComposeView
import com.android.systemui.statusbar.notification.collection.BundleEntryAdapter
import com.android.systemui.statusbar.notification.row.NotificationGuts.GutsContent
import com.android.systemui.statusbar.notification.row.ui.viewmodel.BundleHeaderGutsViewModel

/**
 * This View is a container for a ComposeView and implements GutsContent. Technically, this should
 * not be a View as GutsContent could just return the ComposeView directly for getContentView().
 * Unfortunately, the legacy design of `NotificationMenuRowPlugin.MenuItem.getGutsView()` forces the
 * GutsContent to be a View itself. Therefore this class is a view that just holds the ComposeView.
 *
 * A redesign of `NotificationMenuRowPlugin.MenuItem.getGutsView()` to return GutsContent instead is
 * desired but it lacks proper module dependencies. As soon as this class does not need to inherit
 * from View it can just return the ComposeView directly instead.
 */
class BundleHeaderGutsContent
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    FrameLayout(context, attrs, defStyleAttr), GutsContent {

    private var composeView: ComposeView? = null
    private var gutsParent: NotificationGuts? = null

    fun bindNotification(
        row: ExpandableNotificationRow,
        onSettingsClicked: () -> Unit = {},
        onDoneClicked: () -> Unit = {},
        onDismissClicked: () -> Unit = {},
    ) {
        if (composeView != null) return

        val repository = (row.entryAdapter as BundleEntryAdapter).entry.bundleRepository
        val viewModel =
            BundleHeaderGutsViewModel(
                titleTextResId = repository.titleTextResId,
                bundleIcon = repository.bundleIcon,
                onSettingsClicked = onSettingsClicked,
                onDoneClicked = onDoneClicked,
                onDismissClicked = onDismissClicked,
            )
        composeView = createBundleHeaderGutsComposeView(context, viewModel)
        addView(composeView)
    }

    override fun setGutsParent(listener: NotificationGuts?) {
        this.gutsParent = listener
    }

    override fun getContentView(): View {
        return this
    }

    override fun getActualHeight(): Int {
        return composeView?.measuredHeight ?: 0
    }

    override fun handleCloseControls(save: Boolean, force: Boolean): Boolean {
        return false
    }

    override fun willBeRemoved(): Boolean {
        return false
    }

    override fun shouldBeSavedOnClose(): Boolean {
        return false
    }

    override fun needsFalsingProtection(): Boolean {
        return true
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -2884,6 +2884,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        }
    }

    public boolean isBundle() {
        return mIsBundle;
    }

    private void updateChildrenVisibility() {
        boolean hideContentWhileLaunching = mExpandAnimationRunning && mGuts != null
                && mGuts.isExposed();
Loading