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

Commit 5a8fde05 authored by Jeff DeCew's avatar Jeff DeCew
Browse files

[RON] Extract a RichOngoingContentModel from timer notifications for testing.

Bug: 343942780
Flag: com.android.systemui.notification_row_content_binder_refactor
Flag: android.app.api_rich_ongoing
Test: atest NotificationRowContentBinderImplTest
Change-Id: I09b3ee971400b071abb1d12c5cee109c3711223a
parent 7bbf2fd1
Loading
Loading
Loading
Loading
+111 −81
Original line number Diff line number Diff line
@@ -66,6 +66,7 @@ import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarMo
import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews
import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder
import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
@@ -90,6 +91,7 @@ constructor(
    private val remoteViewCache: NotifRemoteViewCache,
    private val remoteInputManager: NotificationRemoteInputManager,
    private val conversationProcessor: ConversationNotificationProcessor,
    private val ronExtractor: RichOngoingNotificationContentExtractor,
    @NotifInflation private val inflationExecutor: Executor,
    private val smartReplyStateInflater: SmartReplyStateInflater,
    private val notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider,
@@ -137,6 +139,7 @@ constructor(
                remoteViewCache,
                entry,
                conversationProcessor,
                ronExtractor,
                row,
                bindParams.isMinimized,
                bindParams.usesIncreasedHeight,
@@ -181,6 +184,7 @@ constructor(
                notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
                headsUpStyleProvider = headsUpStyleProvider,
                conversationProcessor = conversationProcessor,
                ronExtractor = ronExtractor,
                logger = logger,
            )
        inflateSmartReplyViews(
@@ -337,6 +341,7 @@ constructor(
        private val remoteViewCache: NotifRemoteViewCache,
        private val entry: NotificationEntry,
        private val conversationProcessor: ConversationNotificationProcessor,
        private val ronExtractor: RichOngoingNotificationContentExtractor,
        private val row: ExpandableNotificationRow,
        private val isMinimized: Boolean,
        private val usesIncreasedHeight: Boolean,
@@ -416,6 +421,7 @@ constructor(
                    notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider,
                    headsUpStyleProvider = headsUpStyleProvider,
                    conversationProcessor = conversationProcessor,
                    ronExtractor = ronExtractor,
                    logger = logger
                )
            logger.logAsyncTaskProgress(
@@ -586,6 +592,7 @@ constructor(
            val inflateHeadsUp =
                (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0 &&
                    result.remoteViews.headsUp != null)

            if (inflateContracted || inflateExpanded || inflateHeadsUp) {
                logger.logAsyncTaskProgress(entry, "inflating contracted smart reply state")
                result.inflatedSmartReplyState = inflater.inflateSmartReplyState(entry)
@@ -627,6 +634,7 @@ constructor(
            notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider,
            headsUpStyleProvider: HeadsUpStyleProvider,
            conversationProcessor: ConversationNotificationProcessor,
            ronExtractor: RichOngoingNotificationContentExtractor,
            logger: NotificationRowContentBinderLogger
        ): InflationProgress {
            // process conversations and extract the messaging style
@@ -635,9 +643,21 @@ constructor(
                    conversationProcessor.processNotification(entry, builder, logger)
                } else null

            val richOngoingContentModel =
                if (reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING != 0) {
                    ronExtractor.extractContentModel(
                        entry = entry,
                        builder = builder,
                        systemUIContext = systemUIContext,
                        packageContext = packageContext
                    )
                } else null

            val remoteViewsFlags = getRemoteViewsFlags(reInflateFlags, richOngoingContentModel)

            val remoteViews =
                createRemoteViews(
                    reInflateFlags = reInflateFlags,
                    reInflateFlags = remoteViewsFlags,
                    builder = builder,
                    isMinimized = isMinimized,
                    usesIncreasedHeight = usesIncreasedHeight,
@@ -672,6 +692,7 @@ constructor(
                NotificationContentModel(
                    headsUpStatusBarModel = headsUpStatusBarModel,
                    singleLineViewModel = singleLineViewModel,
                    richOngoingContentModel = richOngoingContentModel,
                )

            return InflationProgress(
@@ -799,7 +820,7 @@ constructor(
            val publicLayout = row.publicLayout
            val runningInflations = HashMap<Int, CancellationSignal>()
            var flag = FLAG_CONTENT_VIEW_CONTRACTED
            if (reInflateFlags and flag != 0) {
            if (reInflateFlags and flag != 0 && result.remoteViews.contracted != null) {
                val isNewView =
                    !canReapplyRemoteView(
                        newView = result.remoteViews.contracted,
@@ -813,7 +834,7 @@ constructor(
                        }

                        override val remoteView: RemoteViews
                            get() = result.remoteViews.contracted!!
                            get() = result.remoteViews.contracted
                    }
                logger.logAsyncTaskProgress(entry, "applying contracted view")
                applyRemoteView(
@@ -838,13 +859,11 @@ constructor(
                )
            }
            flag = FLAG_CONTENT_VIEW_EXPANDED
            if (reInflateFlags and flag != 0) {
                if (result.remoteViews.expanded != null) {
            if (reInflateFlags and flag != 0 && result.remoteViews.expanded != null) {
                val isNewView =
                    !canReapplyRemoteView(
                        newView = result.remoteViews.expanded,
                            oldView =
                                remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
                        oldView = remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
                    )
                val applyCallback: ApplyCallback =
                    object : ApplyCallback() {
@@ -878,15 +897,12 @@ constructor(
                    logger = logger
                )
            }
            }
            flag = FLAG_CONTENT_VIEW_HEADS_UP
            if (reInflateFlags and flag != 0) {
                if (result.remoteViews.headsUp != null) {
            if (reInflateFlags and flag != 0 && result.remoteViews.headsUp != null) {
                val isNewView =
                    !canReapplyRemoteView(
                        newView = result.remoteViews.headsUp,
                            oldView =
                                remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
                        oldView = remoteViewCache.getCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
                    )
                val applyCallback: ApplyCallback =
                    object : ApplyCallback() {
@@ -920,7 +936,6 @@ constructor(
                    logger = logger
                )
            }
            }
            flag = FLAG_CONTENT_VIEW_PUBLIC
            if (reInflateFlags and flag != 0) {
                val isNewView =
@@ -1521,6 +1536,21 @@ constructor(
                    !oldView.hasFlags(RemoteViews.FLAG_REAPPLY_DISALLOWED)
        }

        @InflationFlag
        private fun getRemoteViewsFlags(
            @InflationFlag reInflateFlags: Int,
            richOngoingContentModel: RichOngoingContentModel?
        ): Int =
            if (richOngoingContentModel != null) {
                reInflateFlags and CONTENT_VIEWS_TO_CREATE_RICH_ONGOING.inv()
            } else {
                reInflateFlags
            }

        @InflationFlag
        private const val CONTENT_VIEWS_TO_CREATE_RICH_ONGOING =
            FLAG_CONTENT_VIEW_CONTRACTED or FLAG_CONTENT_VIEW_EXPANDED or FLAG_CONTENT_VIEW_HEADS_UP

        private const val ASYNC_TASK_TRACE_METHOD =
            "NotificationRowContentBinderImpl.AsyncInflationTask"
        private const val APPLY_TRACE_METHOD = "NotificationRowContentBinderImpl#apply"
+14 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row;

import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor;
import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag;

import dagger.Binds;
import dagger.Module;
@@ -47,6 +48,19 @@ public abstract class NotificationRowModule {
        }
    }

    /** Provides ron content model extractor. */
    @Provides
    @SysUISingleton
    public static RichOngoingNotificationContentExtractor provideRonContentExtractor(
            Provider<RichOngoingNotificationContentExtractorImpl> realImpl
    ) {
        if (RichOngoingNotificationFlag.isEnabled()) {
            return realImpl.get();
        } else {
            return new NoOpRichOngoingNotificationContentExtractor();
        }
    }

    /**
     * Provides notification remote view cache instance.
     */
+170 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.util.Log
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.row.shared.IconModel
import com.android.systemui.statusbar.notification.row.shared.RichOngoingContentModel
import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag
import com.android.systemui.statusbar.notification.row.shared.TimerContentModel
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
import javax.inject.Inject

/**
 * Interface which provides a [RichOngoingContentModel] for a given [Notification] when one is
 * applicable to the given style.
 */
interface RichOngoingNotificationContentExtractor {
    fun extractContentModel(
        entry: NotificationEntry,
        builder: Notification.Builder,
        systemUIContext: Context,
        packageContext: Context
    ): RichOngoingContentModel?
}

class NoOpRichOngoingNotificationContentExtractor : RichOngoingNotificationContentExtractor {
    override fun extractContentModel(
        entry: NotificationEntry,
        builder: Notification.Builder,
        systemUIContext: Context,
        packageContext: Context
    ): RichOngoingContentModel? = null
}

@SysUISingleton
class RichOngoingNotificationContentExtractorImpl @Inject constructor() :
    RichOngoingNotificationContentExtractor {

    init {
        /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()
    }

    override fun extractContentModel(
        entry: NotificationEntry,
        builder: Notification.Builder,
        systemUIContext: Context,
        packageContext: Context
    ): RichOngoingContentModel? =
        try {
            val sbn = entry.sbn
            val notification = sbn.notification
            val icon = IconModel(notification.smallIcon)
            if (sbn.packageName == "com.google.android.deskclock") {
                when (notification.channelId) {
                    "Timers v2" -> {
                        parseTimerNotification(notification, icon)
                    }
                    "Stopwatch v2" -> {
                        Log.i("RONs", "Can't process stopwatch yet")
                        null
                    }
                    else -> {
                        Log.i("RONs", "Can't process channel '${notification.channelId}'")
                        null
                    }
                }
            } else null
        } catch (e: Exception) {
            Log.e("RONs", "Error parsing RON", e)
            null
        }

    /**
     * FOR PROTOTYPING ONLY: create a RON TimerContentModel using the time information available
     * inside the sortKey of the clock app's timer notifications.
     */
    private fun parseTimerNotification(
        notification: Notification,
        icon: IconModel
    ): TimerContentModel {
        // sortKey=1 0|↺7|RUNNING|▶16:21:58.523|Σ0:05:00|Δ0:00:03|⏳0:04:57
        // sortKey=1 0|↺7|PAUSED|Σ0:05:00|Δ0:04:54|⏳0:00:06
        // sortKey=1 1|↺7|RUNNING|▶16:30:28.433|Σ0:04:05|Δ0:00:06|⏳0:03:59
        // sortKey=1 0|↺7|RUNNING|▶16:36:18.350|Σ0:05:00|Δ0:01:42|⏳0:03:18
        // sortKey=1 2|↺7|RUNNING|▶16:38:37.816|Σ0:02:00|Δ0:01:09|⏳0:00:51
        // ▶ = "current" time (when updated)
        // Σ = total time
        // Δ = time elapsed
        // ⏳ = time remaining
        val sortKey = notification.sortKey
        val (_, _, state, extra) = sortKey.split("|", limit = 4)
        return when (state) {
            "PAUSED" -> {
                val (total, _, remaining) = extra.split("|")
                val timeRemaining = parseTimeDelta(remaining)
                TimerContentModel(
                    icon = icon,
                    name = total,
                    state =
                        TimerContentModel.TimerState.Paused(
                            timeRemaining = timeRemaining,
                            resumeIntent = notification.findActionWithName("Resume"),
                            resetIntent = notification.findActionWithName("Reset"),
                        )
                )
            }
            "RUNNING" -> {
                val (current, total, _, remaining) = extra.split("|")
                val finishTime = parseCurrentTime(current) + parseTimeDelta(remaining).toMillis()
                TimerContentModel(
                    icon = icon,
                    name = total,
                    state =
                        TimerContentModel.TimerState.Running(
                            finishTime = finishTime,
                            pauseIntent = notification.findActionWithName("Pause"),
                            addOneMinuteIntent = notification.findActionWithName("Add 1 min"),
                        )
                )
            }
            else -> error("unknown state ($state) in sortKey=$sortKey")
        }
    }

    private fun Notification.findActionWithName(name: String): PendingIntent? {
        return actions.firstOrNull { name == it.title?.toString() }?.actionIntent
    }

    private fun parseCurrentTime(current: String): Long {
        val (hour, minute, second, millis) = current.replace("▶", "").split(":", ".")
        // NOTE: this won't work correctly at/around midnight.  It's just for prototyping.
        val localDateTime =
            LocalDateTime.of(
                LocalDate.now(),
                LocalTime.of(hour.toInt(), minute.toInt(), second.toInt(), millis.toInt() * 1000000)
            )
        val offset = ZoneId.systemDefault().rules.getOffset(localDateTime)
        return localDateTime.toInstant(offset).toEpochMilli()
    }

    private fun parseTimeDelta(delta: String): Duration {
        val (hour, minute, second) = delta.replace("Σ", "").replace("⏳", "").split(":")
        return Duration.ofHours(hour.toLong())
            .plusMinutes(minute.toLong())
            .plusSeconds(second.toLong())
    }
}
+35 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.shared

import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon

// TODO: figure out how to support lazy resolution of the drawable, e.g. on unrelated text change
class IconModel(val icon: Icon) {
    var drawable: Drawable? = null

    override fun equals(other: Any?): Boolean =
        when (other) {
            null -> false
            (other === this) -> true
            !is IconModel -> false
            else -> other.icon.sameAs(icon)
        }

    override fun toString(): String = "IconModel(icon=$icon, drawable=$drawable)"
}
+3 −0
Original line number Diff line number Diff line
@@ -21,4 +21,7 @@ import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineVi
data class NotificationContentModel(
    val headsUpStatusBarModel: HeadsUpStatusBarModel,
    val singleLineViewModel: SingleLineViewModel? = null,
    val richOngoingContentModel: RichOngoingContentModel? = null,
)

sealed interface RichOngoingContentModel
Loading