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

Commit 567f2472 authored by Fabian Kozynski's avatar Fabian Kozynski Committed by Android (Google) Code Review
Browse files

Merge changes I1b3bafe4,I9957bd69,I8d764200 into main

* changes:
  Move scrollable to Compose
  Re-inflate on interesting config changes
  Use Compose FooterActions directly in Scene
parents 63320d3f 6b66acfe
Loading
Loading
Loading
Loading
+10 −1
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;

import androidx.annotation.NonNull;

/**
 * A class for applying config changes and determing if doing so resulting in any "interesting"
 * changes.
@@ -48,8 +50,15 @@ public class InterestingConfigChanges {
     */
    @SuppressLint("NewApi")
    public boolean applyNewConfig(Resources res) {
        return applyNewConfig(res.getConfiguration());
    }

    /**
     * Applies the given config change and returns whether an "interesting" change happened.
     */
    public boolean applyNewConfig(@NonNull Configuration configuration) {
        int configChanges = mLastConfiguration.updateFrom(
                Configuration.generateDelta(mLastConfiguration, res.getConfiguration()));
                Configuration.generateDelta(mLastConfiguration, configuration));
        return (configChanges & (mFlags)) != 0;
    }
}
+25 −20
Original line number Diff line number Diff line
@@ -16,17 +16,16 @@

package com.android.systemui.qs.ui.composable

import android.view.ContextThemeWrapper
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
@@ -53,14 +52,6 @@ object QuickSettings {
    }
}

@Composable
private fun QuickSettingsTheme(content: @Composable () -> Unit) {
    val context = LocalContext.current
    val themedContext =
        remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) }
    CompositionLocalProvider(LocalContext provides themedContext) { content() }
}

private fun SceneScope.stateForQuickSettingsContent(): QSSceneAdapter.State {
    return when (val transitionState = layoutState.transitionState) {
        is TransitionState.Idle -> {
@@ -115,6 +106,7 @@ private fun QuickSettingsContent(
    modifier: Modifier = Modifier,
) {
    val qsView by qsSceneAdapter.qsView.collectAsState(null)
    val isCustomizing by qsSceneAdapter.isCustomizing.collectAsState()
    QuickSettingsTheme {
        val context = LocalContext.current

@@ -124,8 +116,20 @@ private fun QuickSettingsContent(
            }
        }
        qsView?.let { view ->
            Box(
                modifier =
                    modifier
                        .fillMaxWidth()
                        .then(
                            if (isCustomizing) {
                                Modifier.fillMaxHeight()
                            } else {
                                Modifier.wrapContentHeight()
                            }
                        )
            ) {
                AndroidView(
                modifier = modifier.fillMaxSize().background(colorAttr(R.attr.underSurface)),
                    modifier = Modifier.fillMaxWidth().background(colorAttr(R.attr.underSurface)),
                    factory = { _ ->
                        qsSceneAdapter.setState(state)
                        view
@@ -135,3 +139,4 @@ private fun QuickSettingsContent(
            }
        }
    }
}
+121 −45
Original line number Diff line number Diff line
@@ -24,31 +24,44 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.TransitionState
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.scene.ui.composable.toTransitionSceneKey
import com.android.systemui.shade.ui.composable.CollapsedShadeHeader
import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
import com.android.systemui.shade.ui.composable.Shade
@@ -105,10 +118,33 @@ private fun SceneScope.QuickSettingsScene(
) {
    // TODO(b/280887232): implement the real UI.
    Box(modifier = modifier.fillMaxSize()) {
        Box(modifier = Modifier.fillMaxSize()) {
        val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
        val collapsedHeaderHeight =
            with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
        val lifecycleOwner = LocalLifecycleOwner.current
        val footerActionsViewModel =
            remember(lifecycleOwner, viewModel) {
                viewModel.getFooterActionsViewModel(lifecycleOwner)
            }
        val scrollState = rememberScrollState()
        // When animating into the scene, we don't want it to be able to scroll, as it could mess
        // up with the expansion animation.
        val isScrollable =
            when (val state = layoutState.transitionState) {
                is TransitionState.Idle -> true
                is TransitionState.Transition -> {
                    state.fromScene == SceneKey.QuickSettings.toTransitionSceneKey()
                }
            }

        LaunchedEffect(isCustomizing, scrollState) {
            if (isCustomizing) {
                scrollState.scrollTo(0)
            }
        }

        // This is the background for the whole scene, as the elements don't necessarily provide
        // a background that extends to the edges.
        Spacer(
            modifier =
                Modifier.element(Shade.Elements.ScrimBackground)
@@ -118,7 +154,28 @@ private fun SceneScope.QuickSettingsScene(
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier =
                    Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp, bottom = 48.dp)
                Modifier.fillMaxSize()
                    // bottom should be tied to insets
                    .padding(bottom = 16.dp)
        ) {
            Box(modifier = Modifier.fillMaxSize().weight(1f)) {
                val shadeHeaderAndQuickSettingsModifier =
                    if (isCustomizing) {
                        Modifier.fillMaxHeight().align(Alignment.TopCenter)
                    } else {
                        Modifier.verticalNestedScrollToScene()
                            .verticalScroll(
                                scrollState,
                                enabled = isScrollable,
                            )
                            .clipScrollableContainer(Orientation.Horizontal)
                            .fillMaxWidth()
                            .wrapContentHeight(unbounded = true)
                            .align(Alignment.TopCenter)
                    }

                Column(
                    modifier = shadeHeaderAndQuickSettingsModifier,
                ) {
                    when (LocalWindowSizeClass.current.widthSizeClass) {
                        WindowWidthSizeClass.Compact ->
@@ -126,21 +183,23 @@ private fun SceneScope.QuickSettingsScene(
                                visible = !isCustomizing,
                                enter =
                                    expandVertically(
                                    animationSpec = tween(1000),
                                        animationSpec = tween(100),
                                        initialHeight = { collapsedHeaderHeight },
                                ) + fadeIn(tween(1000)),
                                    ) + fadeIn(tween(100)),
                                exit =
                                    shrinkVertically(
                                    animationSpec = tween(1000),
                                        animationSpec = tween(100),
                                        targetHeight = { collapsedHeaderHeight },
                                        shrinkTowards = Alignment.Top,
                                ) + fadeOut(tween(1000)),
                                    ) + fadeOut(tween(100)),
                            ) {
                                ExpandedShadeHeader(
                                    viewModel = viewModel.shadeHeaderViewModel,
                                    createTintedIconManager = createTintedIconManager,
                                createBatteryMeterViewController = createBatteryMeterViewController,
                                    createBatteryMeterViewController =
                                        createBatteryMeterViewController,
                                    statusBarIconController = statusBarIconController,
                                    modifier = Modifier.padding(horizontal = 16.dp),
                                )
                            }
                        else ->
@@ -149,15 +208,32 @@ private fun SceneScope.QuickSettingsScene(
                                createTintedIconManager = createTintedIconManager,
                                createBatteryMeterViewController = createBatteryMeterViewController,
                                statusBarIconController = statusBarIconController,
                                modifier = Modifier.padding(horizontal = 16.dp),
                            )
                    }
                    Spacer(modifier = Modifier.height(16.dp))
                    // This view has its own horizontal padding
                    QuickSettings(
                    modifier = Modifier.fillMaxHeight(),
                        modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
                        viewModel.qsSceneAdapter,
                    )
                }
            }
            AnimatedVisibility(
                visible = !isCustomizing,
                modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()
            ) {
                QuickSettingsTheme {
                    // This view has its own horizontal padding
                    // TODO(b/321716470) This should use a lifecycle tied to the scene.
                    FooterActions(
                        viewModel = footerActionsViewModel,
                        qsVisibilityLifecycleOwner = lifecycleOwner,
                        modifier = Modifier.element(QuickSettings.Elements.FooterActions)
                    )
                }
            }
        }
        HeadsUpNotificationSpace(
            viewModel = viewModel.notifications,
            isPeekFromBottom = true,
+32 −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.qs.ui.composable

import android.view.ContextThemeWrapper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.android.systemui.res.R

@Composable
fun QuickSettingsTheme(content: @Composable () -> Unit) {
    val context = LocalContext.current
    val themedContext =
        remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) }
    CompositionLocalProvider(LocalContext provides themedContext) { content() }
}
+92 −0
Original line number Diff line number Diff line
@@ -16,12 +16,16 @@

package com.android.systemui.qs.ui.adapter

import android.content.res.Configuration
import android.os.Bundle
import android.view.Surface
import android.view.View
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.qs.QSImpl
import com.android.systemui.qs.dagger.QSComponent
@@ -34,6 +38,7 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.Locale
import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -81,11 +86,17 @@ class QSSceneAdapterImplTest : SysuiTestCase() {
                    .also { components.add(it) }
            }
        }
    private val configuration = Configuration(context.resources.configuration)

    private val fakeConfigurationRepository =
        FakeConfigurationRepository().apply { onConfigurationChange(configuration) }
    private val configurationInteractor = ConfigurationInteractor(fakeConfigurationRepository)

    private val mockAsyncLayoutInflater =
        mock<AsyncLayoutInflater>() {
            whenever(inflate(anyInt(), nullable(), any())).then { invocation ->
                val mockView = mock<View>()
                whenever(mockView.context).thenReturn(context)
                invocation
                    .getArgument<AsyncLayoutInflater.OnInflateFinishedListener>(2)
                    .onInflateFinished(
@@ -102,6 +113,7 @@ class QSSceneAdapterImplTest : SysuiTestCase() {
            qsImplProvider,
            testDispatcher,
            testScope.backgroundScope,
            configurationInteractor,
            { mockAsyncLayoutInflater },
        )

@@ -297,6 +309,9 @@ class QSSceneAdapterImplTest : SysuiTestCase() {
    @Test
    fun reinflation_previousStateDestroyed() =
        testScope.runTest {
            // Run all flows... In particular, initial configuration propagation that could cause
            // QSImpl to re-inflate.
            runCurrent()
            val qsImpl by collectLastValue(underTest.qsImpl)

            underTest.inflate(context)
@@ -322,4 +337,81 @@ class QSSceneAdapterImplTest : SysuiTestCase() {
                    bundleArgCaptor.value,
                )
        }

    @Test
    fun changeInLocale_reinflation() =
        testScope.runTest {
            val qsImpl by collectLastValue(underTest.qsImpl)

            underTest.inflate(context)
            runCurrent()

            val oldQsImpl = qsImpl!!

            val newLocale =
                if (configuration.locales[0] == Locale("en-US")) {
                    Locale("es-UY")
                } else {
                    Locale("en-US")
                }
            configuration.setLocale(newLocale)
            fakeConfigurationRepository.onConfigurationChange(configuration)
            runCurrent()

            assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
        }

    @Test
    fun changeInFontSize_reinflation() =
        testScope.runTest {
            val qsImpl by collectLastValue(underTest.qsImpl)

            underTest.inflate(context)
            runCurrent()

            val oldQsImpl = qsImpl!!

            configuration.fontScale *= 2
            fakeConfigurationRepository.onConfigurationChange(configuration)
            runCurrent()

            assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
        }

    @Test
    fun changeInAssetPath_reinflation() =
        testScope.runTest {
            val qsImpl by collectLastValue(underTest.qsImpl)

            underTest.inflate(context)
            runCurrent()

            val oldQsImpl = qsImpl!!

            configuration.assetsSeq += 1
            fakeConfigurationRepository.onConfigurationChange(configuration)
            runCurrent()

            assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
        }

    @Test
    fun otherChangesInConfiguration_noReinflation_configurationChangeDispatched() =
        testScope.runTest {
            val qsImpl by collectLastValue(underTest.qsImpl)

            underTest.inflate(context)
            runCurrent()

            val oldQsImpl = qsImpl!!
            configuration.densityDpi *= 2
            configuration.windowConfiguration.maxBounds.scale(2f)
            configuration.windowConfiguration.rotation = Surface.ROTATION_270
            fakeConfigurationRepository.onConfigurationChange(configuration)
            runCurrent()

            assertThat(oldQsImpl).isSameInstanceAs(qsImpl!!)
            verify(qsImpl!!).onConfigurationChanged(configuration)
            verify(qsImpl!!.view).dispatchConfigurationChanged(configuration)
        }
}
Loading