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

Commit cdb6f816 authored by Ewout van Bekkum's avatar Ewout van Bekkum Committed by Automerger Merge Worker
Browse files

Merge "Revert "Revert "FingerprintSensorProperties refactor - ui layer""" into...

Merge "Revert "Revert "FingerprintSensorProperties refactor - ui layer""" into udc-qpr-dev am: cbbde14c am: 2e10d946

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/24149342



Change-Id: I1176852c8744b0bf22e95aab896963570fe1b7ce
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents df37c86b 2e10d946
Loading
Loading
Loading
Loading
+11 −185
Original line number Diff line number Diff line
@@ -17,12 +17,7 @@ package com.android.systemui.biometrics

import android.app.ActivityTaskManager
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
@@ -33,27 +28,23 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.ISidefpsController
import android.os.Handler
import android.util.Log
import android.util.RotationUtils
import android.view.Display
import android.view.DisplayInfo
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
import android.view.View.AccessibilityDelegate
import android.view.ViewPropertyAnimator
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.RawRes
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.model.KeyPath
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder
import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -64,6 +55,7 @@ import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.traceSection
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@@ -86,6 +78,7 @@ constructor(
    @Main private val mainExecutor: DelayableExecutor,
    @Main private val handler: Handler,
    private val alternateBouncerInteractor: AlternateBouncerInteractor,
    private val sideFpsOverlayViewModelFactory: Provider<SideFpsOverlayViewModel>,
    @Application private val scope: CoroutineScope,
    dumpManager: DumpManager
) : Dumpable {
@@ -250,105 +243,15 @@ constructor(
    private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
        val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
        overlayView = view
        val display = context.display!!
        // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation
        display.getDisplayInfo(displayInfo)
        val offsets =
            sensorProps.getLocation(display.uniqueId).let { location ->
                if (location == null) {
                    Log.w(TAG, "No location specified for display: ${display.uniqueId}")
                }
                location ?: sensorProps.location
            }
        overlayOffsets = offsets

        val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
        view.rotation =
            display.asSideFpsAnimationRotation(
                offsets.isYAligned(),
                getRotationFromDefault(displayInfo.rotation)
            )
        lottie.setAnimation(
            display.asSideFpsAnimation(
                offsets.isYAligned(),
                getRotationFromDefault(displayInfo.rotation)
        SideFpsOverlayViewBinder.bind(
            view = view,
            viewModel = sideFpsOverlayViewModelFactory.get(),
            overlayViewParams = overlayViewParams,
            reason = reason,
            context = context,
        )
        )
        lottie.addLottieOnCompositionLoadedListener {
            // Check that view is not stale, and that overlayView has not been hidden/removed
            if (overlayView != null && overlayView == view) {
                updateOverlayParams(display, it.bounds)
            }
        }
        orientationReasonListener.reason = reason
        lottie.addOverlayDynamicColor(context, reason)

        /**
         * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
         * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
         * in focus
         */
        view.setAccessibilityDelegate(
            object : AccessibilityDelegate() {
                override fun dispatchPopulateAccessibilityEvent(
                    host: View,
                    event: AccessibilityEvent
                ): Boolean {
                    return if (
                        event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
                    ) {
                        true
                    } else {
                        super.dispatchPopulateAccessibilityEvent(host, event)
                    }
                }
            }
        )
    }

    @VisibleForTesting
    fun updateOverlayParams(display: Display, bounds: Rect) {
        val isNaturalOrientation = display.isNaturalOrientation()
        val isDefaultOrientation =
            if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
        val size = windowManager.maximumWindowMetrics.bounds

        val displayWidth = if (isDefaultOrientation) size.width() else size.height()
        val displayHeight = if (isDefaultOrientation) size.height() else size.width()
        val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
        val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()

        val sensorBounds =
            if (overlayOffsets.isYAligned()) {
                Rect(
                    displayWidth - boundsWidth,
                    overlayOffsets.sensorLocationY,
                    displayWidth,
                    overlayOffsets.sensorLocationY + boundsHeight
                )
            } else {
                Rect(
                    overlayOffsets.sensorLocationX,
                    0,
                    overlayOffsets.sensorLocationX + boundsWidth,
                    boundsHeight
                )
            }

        RotationUtils.rotateBounds(
            sensorBounds,
            Rect(0, 0, displayWidth, displayHeight),
            getRotationFromDefault(display.rotation)
        )

        overlayViewParams.x = sensorBounds.left
        overlayViewParams.y = sensorBounds.top

        windowManager.updateViewLayout(overlayView, overlayViewParams)
    }

    private fun getRotationFromDefault(rotation: Int): Int =
        if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
}

private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal?
@@ -373,89 +276,12 @@ private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Bo
private fun ActivityTaskManager.topClass(): String =
    getTasks(1).firstOrNull()?.topActivity?.className ?: ""

@RawRes
private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int =
    when (rotationFromDefault) {
        Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
        Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
        else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
    }

private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float =
    when (rotationFromDefault) {
        Surface.ROTATION_90 -> if (yAligned) 0f else 180f
        Surface.ROTATION_180 -> 180f
        Surface.ROTATION_270 -> if (yAligned) 180f else 0f
        else -> 0f
    }

private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0

private fun Display.isNaturalOrientation(): Boolean =
    rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180

private fun LottieAnimationView.addOverlayDynamicColor(
    context: Context,
    @BiometricOverlayConstants.ShowReason reason: Int
) {
    fun update() {
        val isKeyguard = reason == REASON_AUTH_KEYGUARD
        if (isKeyguard) {
            val color =
                com.android.settingslib.Utils.getColorAttrDefaultColor(
                    context,
                    com.android.internal.R.attr.materialColorPrimaryFixed
                )
            val outerRimColor =
                com.android.settingslib.Utils.getColorAttrDefaultColor(
                    context,
                    com.android.internal.R.attr.materialColorPrimaryFixedDim
                )
            val chevronFill =
                com.android.settingslib.Utils.getColorAttrDefaultColor(
                    context,
                    com.android.internal.R.attr.materialColorOnPrimaryFixed
                )
            addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) {
                PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
            }
            addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) {
                PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP)
            }
            addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
                PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
            }
        } else {
            if (!isDarkMode(context)) {
                addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
                    PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
                }
            }
            for (key in listOf(".blue600", ".blue400")) {
                addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
                    PorterDuffColorFilter(
                        context.getColor(R.color.settingslib_color_blue400),
                        PorterDuff.Mode.SRC_ATOP
                    )
                }
            }
        }
    }

    if (composition != null) {
        update()
    } else {
        addLottieOnCompositionLoadedListener { update() }
    }
}

private fun isDarkMode(context: Context): Boolean {
    val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
    return darkMode == Configuration.UI_MODE_NIGHT_YES
}

@VisibleForTesting
class OrientationReasonListener(
public class OrientationReasonListener(
    context: Context,
    displayManager: DisplayManager,
    handler: Handler,
+39 −41
Original line number Diff line number Diff line
@@ -16,8 +16,11 @@

package com.android.systemui.biometrics.data.repository

import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.SensorLocationInternal
import android.hardware.biometrics.SensorProperties
import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
@@ -30,10 +33,8 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
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.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn

/**
@@ -43,22 +44,17 @@ import kotlinx.coroutines.flow.shareIn
 */
interface FingerprintPropertyRepository {

    /**
     * If the repository is initialized or not. Other properties are defaults until this is true.
     */
    val isInitialized: Flow<Boolean>

    /** The id of fingerprint sensor. */
    val sensorId: StateFlow<Int>
    val sensorId: Flow<Int>

    /** The security strength of sensor (convenience, weak, strong). */
    val strength: StateFlow<SensorStrength>
    val strength: Flow<SensorStrength>

    /** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */
    val sensorType: StateFlow<FingerprintSensorType>
    val sensorType: Flow<FingerprintSensorType>

    /** The sensor location relative to each physical display. */
    val sensorLocations: StateFlow<Map<String, SensorLocationInternal>>
    val sensorLocations: Flow<Map<String, SensorLocationInternal>>
}

@SysUISingleton
@@ -66,10 +62,10 @@ class FingerprintPropertyRepositoryImpl
@Inject
constructor(
    @Application private val applicationScope: CoroutineScope,
    private val fingerprintManager: FingerprintManager
    private val fingerprintManager: FingerprintManager?
) : FingerprintPropertyRepository {

    override val isInitialized: Flow<Boolean> =
    private val props: Flow<FingerprintSensorPropertiesInternal> =
        conflatedCallbackFlow {
                val callback =
                    object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -77,45 +73,47 @@ constructor(
                            sensors: List<FingerprintSensorPropertiesInternal>
                        ) {
                            if (sensors.isNotEmpty()) {
                                setProperties(sensors[0])
                                trySendWithFailureLogging(true, TAG, "initialize properties")
                                trySendWithFailureLogging(sensors[0], TAG, "initialize properties")
                            } else {
                                trySendWithFailureLogging(
                                    DEFAULT_PROPS,
                                    TAG,
                                    "initialize with default properties"
                                )
                            }
                        }
                    }
                fingerprintManager.addAuthenticatorsRegisteredCallback(callback)
                trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
                fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
                trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties")
                awaitClose {}
            }
            .shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1)

    private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
    override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()

    private val _strength: MutableStateFlow<SensorStrength> =
        MutableStateFlow(SensorStrength.CONVENIENCE)
    override val strength = _strength.asStateFlow()

    private val _sensorType: MutableStateFlow<FingerprintSensorType> =
        MutableStateFlow(FingerprintSensorType.UNKNOWN)
    override val sensorType = _sensorType.asStateFlow()

    private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
        MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
    override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
        _sensorLocations.asStateFlow()

    private fun setProperties(prop: FingerprintSensorPropertiesInternal) {
        _sensorId.value = prop.sensorId
        _strength.value = sensorStrengthIntToObject(prop.sensorStrength)
        _sensorType.value = sensorTypeIntToObject(prop.sensorType)
        _sensorLocations.value =
            prop.allLocations.associateBy { sensorLocationInternal ->
    override val sensorId: Flow<Int> = props.map { it.sensorId }
    override val strength: Flow<SensorStrength> =
        props.map { sensorStrengthIntToObject(it.sensorStrength) }
    override val sensorType: Flow<FingerprintSensorType> =
        props.map { sensorTypeIntToObject(it.sensorType) }
    override val sensorLocations: Flow<Map<String, SensorLocationInternal>> =
        props.map {
            it.allLocations.associateBy { sensorLocationInternal ->
                sensorLocationInternal.displayId
            }
        }

    companion object {
        private const val TAG = "FingerprintPropertyRepositoryImpl"
        private val DEFAULT_PROPS =
            FingerprintSensorPropertiesInternal(
                -1 /* sensorId */,
                SensorProperties.STRENGTH_CONVENIENCE,
                0 /* maxEnrollmentsPerUser */,
                listOf<ComponentInfoInternal>(),
                FingerprintSensorProperties.TYPE_UNKNOWN,
                false /* halControlsIllumination */,
                true /* resetLockoutRequiresHardwareAuthToken */,
                listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT)
            )
    }
}

+20 −10
Original line number Diff line number Diff line
@@ -17,16 +17,24 @@
package com.android.systemui.biometrics.domain.interactor

import android.hardware.biometrics.SensorLocationInternal
import android.util.Log
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine

/** Business logic for SideFps overlay offsets. */
interface SideFpsOverlayInteractor {
    /** The displayId of the current display. */
    val displayId: Flow<String>

    /** Get the corresponding offsets based on different displayId. */
    fun getOverlayOffsets(displayId: String): SensorLocationInternal
    /** The corresponding offsets based on different displayId. */
    val overlayOffsets: Flow<SensorLocationInternal>

    /** Update the displayId. */
    fun changeDisplay(displayId: String?)
}

@SysUISingleton
@@ -35,14 +43,16 @@ class SideFpsOverlayInteractorImpl
constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) :
    SideFpsOverlayInteractor {

    override fun getOverlayOffsets(displayId: String): SensorLocationInternal {
        val offsets = fingerprintPropertyRepository.sensorLocations.value
        return if (offsets.containsKey(displayId)) {
            offsets[displayId]!!
        } else {
            Log.w(TAG, "No location specified for display: $displayId")
            offsets[""]!!
    private val _displayId: MutableStateFlow<String> = MutableStateFlow("")
    override val displayId: Flow<String> = _displayId.asStateFlow()

    override val overlayOffsets: Flow<SensorLocationInternal> =
        combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets ->
            offsets[displayId] ?: SensorLocationInternal.DEFAULT
        }

    override fun changeDisplay(displayId: String?) {
        _displayId.value = displayId ?: ""
    }

    companion object {
+168 −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.biometrics.ui.binder

import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.hardware.biometrics.BiometricOverlayConstants
import android.view.View
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.model.KeyPath
import com.android.systemui.R
import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.lifecycle.repeatWhenAttached
import kotlinx.coroutines.launch

/** Sub-binder for SideFpsOverlayView. */
object SideFpsOverlayViewBinder {

    /** Bind the view. */
    @JvmStatic
    fun bind(
        view: View,
        viewModel: SideFpsOverlayViewModel,
        overlayViewParams: WindowManager.LayoutParams,
        @BiometricOverlayConstants.ShowReason reason: Int,
        @Application context: Context
    ) {
        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager

        val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView

        viewModel.changeDisplay()

        view.repeatWhenAttached {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    viewModel.sideFpsAnimationRotation.collect { rotation ->
                        view.rotation = rotation
                    }
                }

                launch {
                    // TODO(b/221037350, wenhuiy): Create a separate ViewBinder for sideFpsAnimation
                    // in order to add scuba tests in the future.
                    viewModel.sideFpsAnimation.collect { animation ->
                        lottie.setAnimation(animation)
                    }
                }

                launch {
                    viewModel.sensorBounds.collect { sensorBounds ->
                        overlayViewParams.x = sensorBounds.left
                        overlayViewParams.y = sensorBounds.top

                        windowManager.updateViewLayout(view, overlayViewParams)
                    }
                }

                launch {
                    viewModel.overlayOffsets.collect { overlayOffsets ->
                        lottie.addLottieOnCompositionLoadedListener {
                            viewModel.updateSensorBounds(
                                it.bounds,
                                windowManager.maximumWindowMetrics.bounds,
                                overlayOffsets
                            )
                        }
                    }
                }
            }
        }

        lottie.addOverlayDynamicColor(context, reason)

        /**
         * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
         * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
         * in focus
         */
        view.accessibilityDelegate =
            object : View.AccessibilityDelegate() {
                override fun dispatchPopulateAccessibilityEvent(
                    host: View,
                    event: AccessibilityEvent
                ): Boolean {
                    return if (
                        event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
                    ) {
                        true
                    } else {
                        super.dispatchPopulateAccessibilityEvent(host, event)
                    }
                }
            }
    }
}

private fun LottieAnimationView.addOverlayDynamicColor(
    context: Context,
    @BiometricOverlayConstants.ShowReason reason: Int
) {
    fun update() {
        val isKeyguard = reason == BiometricOverlayConstants.REASON_AUTH_KEYGUARD
        if (isKeyguard) {
            val color = context.getColor(R.color.numpad_key_color_secondary) // match bouncer color
            val chevronFill =
                com.android.settingslib.Utils.getColorAttrDefaultColor(
                    context,
                    android.R.attr.textColorPrimaryInverse
                )
            for (key in listOf(".blue600", ".blue400")) {
                addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
                    PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
                }
            }
            addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
                PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
            }
        } else if (!isDarkMode(context)) {
            addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
                PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
            }
        } else if (isDarkMode(context)) {
            for (key in listOf(".blue600", ".blue400")) {
                addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
                    PorterDuffColorFilter(
                        context.getColor(R.color.settingslib_color_blue400),
                        PorterDuff.Mode.SRC_ATOP
                    )
                }
            }
        }
    }

    if (composition != null) {
        update()
    } else {
        addLottieOnCompositionLoadedListener { update() }
    }
}

private fun isDarkMode(context: Context): Boolean {
    val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
    return darkMode == Configuration.UI_MODE_NIGHT_YES
}
+148 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading