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

Commit f80df120 authored by Beverly's avatar Beverly Committed by Beverly Tai
Browse files

Add ConfigurationRepository and BurnInInteractor

Bug: 278719514
Test: atest BurnInInteractorTest ConfigurationRepositoryImplTest
DozeServiceHostTest

Change-Id: I265f528923b205333cf935ae97f4c204b6cb30dd
parent 322a9ee2
Loading
Loading
Loading
Loading
+25 −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.common.ui.data.repository

import dagger.Binds
import dagger.Module

@Module
interface CommonRepositoryModule {
    @Binds fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository
}
+118 −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.common.ui.data.repository

import android.content.Context
import android.content.res.Configuration
import android.view.DisplayInfo
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.wrapper.DisplayUtilsWrapper
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.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn

interface ConfigurationRepository {
    /** Called whenever ui mode, theme or configuration has changed. */
    val onAnyConfigurationChange: Flow<Unit>
    val scaleForResolution: Flow<Float>

    fun getResolutionScale(): Float
}

@ExperimentalCoroutinesApi
@SysUISingleton
class ConfigurationRepositoryImpl
@Inject
constructor(
    private val configurationController: ConfigurationController,
    private val context: Context,
    @Application private val scope: CoroutineScope,
    private val displayUtils: DisplayUtilsWrapper,
) : ConfigurationRepository {
    private val displayInfo = MutableStateFlow(DisplayInfo())

    override val onAnyConfigurationChange: Flow<Unit> =
        ConflatedCallbackFlow.conflatedCallbackFlow {
            val callback =
                object : ConfigurationController.ConfigurationListener {
                    override fun onUiModeChanged() {
                        sendUpdate("ConfigurationRepository#onUiModeChanged")
                    }

                    override fun onThemeChanged() {
                        sendUpdate("ConfigurationRepository#onThemeChanged")
                    }

                    override fun onConfigChanged(newConfig: Configuration) {
                        sendUpdate("ConfigurationRepository#onConfigChanged")
                    }

                    fun sendUpdate(reason: String) {
                        trySendWithFailureLogging(Unit, reason)
                    }
                }
            configurationController.addCallback(callback)
            awaitClose { configurationController.removeCallback(callback) }
        }

    private val configurationChange: Flow<Unit> =
        ConflatedCallbackFlow.conflatedCallbackFlow {
            val callback =
                object : ConfigurationController.ConfigurationListener {
                    override fun onConfigChanged(newConfig: Configuration) {
                        trySendWithFailureLogging(Unit, "ConfigurationRepository#onConfigChanged")
                    }
                }
            configurationController.addCallback(callback)
            awaitClose { configurationController.removeCallback(callback) }
        }

    override val scaleForResolution: StateFlow<Float> =
        configurationChange
            .mapLatest { getResolutionScale() }
            .distinctUntilChanged()
            .stateIn(scope, SharingStarted.WhileSubscribed(), getResolutionScale())

    override fun getResolutionScale(): Float {
        context.display.getDisplayInfo(displayInfo.value)
        val maxDisplayMode =
            displayUtils.getMaximumResolutionDisplayMode(displayInfo.value.supportedModes)
        maxDisplayMode?.let {
            val scaleFactor =
                displayUtils.getPhysicalPixelDisplaySizeRatio(
                    maxDisplayMode.physicalWidth,
                    maxDisplayMode.physicalHeight,
                    displayInfo.value.naturalWidth,
                    displayInfo.value.naturalHeight
                )
            return if (scaleFactor == Float.POSITIVE_INFINITY) 1f else scaleFactor
        }
        return 1f
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import com.android.systemui.biometrics.dagger.BiometricsModule;
import com.android.systemui.biometrics.dagger.UdfpsModule;
import com.android.systemui.classifier.FalsingModule;
import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
import com.android.systemui.common.ui.data.repository.CommonRepositoryModule;
import com.android.systemui.complication.dagger.ComplicationComponent;
import com.android.systemui.controls.dagger.ControlsModule;
import com.android.systemui.dagger.qualifiers.Main;
@@ -151,6 +152,7 @@ import javax.inject.Named;
            ClipboardOverlayModule.class,
            ClockInfoModule.class,
            ClockRegistryModule.class,
            CommonRepositoryModule.class,
            ConnectivityModule.class,
            CoroutinesModule.class,
            DreamModule.class,
+4 −0
Original line number Diff line number Diff line
@@ -24,4 +24,8 @@ class BurnInHelperWrapper @Inject constructor() {
    fun burnInOffset(amplitude: Int, xAxis: Boolean): Int {
        return getBurnInOffset(amplitude, xAxis)
    }

    fun burnInProgressOffset(): Float {
        return getBurnInProgressOffset()
    }
}
+125 −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.keyguard.domain.interactor

import android.content.Context
import androidx.annotation.DimenRes
import com.android.systemui.R
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.doze.util.BurnInHelperWrapper
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn

/** Encapsulates business-logic related to Ambient Display burn-in offsets. */
@ExperimentalCoroutinesApi
@SysUISingleton
class BurnInInteractor
@Inject
constructor(
    private val context: Context,
    private val burnInHelperWrapper: BurnInHelperWrapper,
    @Application private val scope: CoroutineScope,
    private val configurationRepository: ConfigurationRepository,
    private val systemClock: SystemClock,
) {
    private val _dozeTimeTick = MutableStateFlow<Long>(0)
    val dozeTimeTick: StateFlow<Long> = _dozeTimeTick.asStateFlow()

    val udfpsBurnInXOffset: StateFlow<Int> =
        burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_x, isXAxis = true)
    val udfpsBurnInYOffset: StateFlow<Int> =
        burnInOffsetDefinedInPixels(R.dimen.udfps_burn_in_offset_y, isXAxis = false)
    val udfpsBurnInProgress: StateFlow<Float> =
        dozeTimeTick
            .mapLatest { burnInHelperWrapper.burnInProgressOffset() }
            .stateIn(scope, SharingStarted.Lazily, burnInHelperWrapper.burnInProgressOffset())

    fun dozeTimeTick() {
        _dozeTimeTick.value = systemClock.uptimeMillis()
    }

    /**
     * Use for max burn-in offsets that are NOT specified in pixels. This flow will recalculate the
     * max burn-in offset on any configuration changes. If the max burn-in offset is specified in
     * pixels, use [burnInOffsetDefinedInPixels].
     */
    private fun burnInOffset(
        @DimenRes maxBurnInOffsetResourceId: Int,
        isXAxis: Boolean,
    ): StateFlow<Int> {
        return configurationRepository.onAnyConfigurationChange
            .flatMapLatest {
                val maxBurnInOffsetPixels =
                    context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId)
                dozeTimeTick.mapLatest { calculateOffset(maxBurnInOffsetPixels, isXAxis) }
            }
            .stateIn(
                scope,
                SharingStarted.Lazily,
                calculateOffset(
                    context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId),
                    isXAxis,
                )
            )
    }

    /**
     * Use for max burn-in offBurn-in offsets that ARE specified in pixels. This flow will apply the
     * a scale for any resolution changes. If the max burn-in offset is specified in dp, use
     * [burnInOffset].
     */
    private fun burnInOffsetDefinedInPixels(
        @DimenRes maxBurnInOffsetResourceId: Int,
        isXAxis: Boolean,
    ): StateFlow<Int> {
        return configurationRepository.scaleForResolution
            .flatMapLatest { scale ->
                val maxBurnInOffsetPixels =
                    context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId)
                dozeTimeTick.mapLatest { calculateOffset(maxBurnInOffsetPixels, isXAxis, scale) }
            }
            .stateIn(
                scope,
                SharingStarted.WhileSubscribed(),
                calculateOffset(
                    context.resources.getDimensionPixelSize(maxBurnInOffsetResourceId),
                    isXAxis,
                    configurationRepository.getResolutionScale(),
                )
            )
    }

    private fun calculateOffset(
        maxBurnInOffsetPixels: Int,
        isXAxis: Boolean,
        scale: Float = 1f
    ): Int {
        return (burnInHelperWrapper.burnInOffset(maxBurnInOffsetPixels, isXAxis) * scale).toInt()
    }
}
Loading