diff options
11 files changed, 407 insertions, 9 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt new file mode 100644 index 000000000000..a310ef44cf35 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelTest.kt @@ -0,0 +1,85 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.row.ui.viewmodel + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.notification.row.data.repository.fakeNotificationRowRepository +import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel +import com.android.systemui.statusbar.notification.row.shared.IconModel +import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +@SmallTest +@EnableFlags(RichOngoingNotificationFlag.FLAG_NAME) +class EnRouteViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val repository = kosmos.fakeNotificationRowRepository + + private var contentModel: EnRouteContentModel? + get() = repository.richOngoingContentModel.value as? EnRouteContentModel + set(value) { + repository.richOngoingContentModel.value = value + } + + private lateinit var underTest: EnRouteViewModel + + @Before + fun setup() { + underTest = kosmos.getEnRouteViewModel(repository) + } + + @Test + fun viewModelShowsContent() = + testScope.runTest { + val title by collectLastValue(underTest.title) + val text by collectLastValue(underTest.text) + contentModel = + exampleEnRouteContent( + title = "Example EnRoute Title", + text = "Example EnRoute Text", + ) + assertThat(title).isEqualTo("Example EnRoute Title") + assertThat(text).isEqualTo("Example EnRoute Text") + } + + private fun exampleEnRouteContent( + icon: IconModel = mock(), + title: CharSequence = "example text", + text: CharSequence = "example title", + ) = + EnRouteContentModel( + smallIcon = icon, + title = title, + text = text, + ) +} diff --git a/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml b/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml new file mode 100644 index 000000000000..59cfeccbeb36 --- /dev/null +++ b/packages/SystemUI/res/layout/notification_template_en_route_contracted.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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. + --> + +<com.android.systemui.statusbar.notification.row.ui.view.EnRouteView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/status_bar_latest_event_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:minHeight="@*android:dimen/notification_headerless_min_height" + android:tag="enroute" + > + + <include layout="@*android:layout/notification_template_material_base" /> + +</com.android.systemui.statusbar.notification.row.ui.view.EnRouteView>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt index bf5b3a34afb6..da29b0fd0dc7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationContentExtractor.kt @@ -22,6 +22,7 @@ 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.EnRouteContentModel 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 @@ -68,12 +69,13 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : builder: Notification.Builder, systemUIContext: Context, packageContext: Context - ): RichOngoingContentModel? = + ): RichOngoingContentModel? { + val sbn = entry.sbn + val notification = sbn.notification + val icon = IconModel(notification.smallIcon) + try { - val sbn = entry.sbn - val notification = sbn.notification - val icon = IconModel(notification.smallIcon) - if (sbn.packageName == "com.google.android.deskclock") { + return if (sbn.packageName == "com.google.android.deskclock") { when (notification.channelId) { "Timers v2" -> { parseTimerNotification(notification, icon) @@ -87,11 +89,14 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : null } } + } else if (builder.style is Notification.EnRouteStyle) { + parseEnRouteNotification(notification, icon) } else null } catch (e: Exception) { Log.e("RONs", "Error parsing RON", e) - null + return null } + } /** * FOR PROTOTYPING ONLY: create a RON TimerContentModel using the time information available @@ -199,4 +204,15 @@ class RichOngoingNotificationContentExtractorImpl @Inject constructor() : .plusMinutes(minute.toLong()) .plusSeconds(second.toLong()) } + + private fun parseEnRouteNotification( + notification: Notification, + icon: IconModel, + ): EnRouteContentModel { + return EnRouteContentModel( + smallIcon = icon, + title = notification.extras.getCharSequence(Notification.EXTRA_TITLE), + text = notification.extras.getCharSequence(Notification.EXTRA_TEXT), + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt index 828fc219e773..2c462b7329b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RichOngoingNotificationViewInflater.kt @@ -27,12 +27,15 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.InflatedContentViewHolder import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.KeepExistingView import com.android.systemui.statusbar.notification.row.ContentViewInflationResult.NullContentView +import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel 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.StopwatchContentModel import com.android.systemui.statusbar.notification.row.shared.TimerContentModel +import com.android.systemui.statusbar.notification.row.ui.view.EnRouteView import com.android.systemui.statusbar.notification.row.ui.view.TimerView +import com.android.systemui.statusbar.notification.row.ui.viewbinder.EnRouteViewBinder import com.android.systemui.statusbar.notification.row.ui.viewbinder.TimerViewBinder +import com.android.systemui.statusbar.notification.row.ui.viewmodel.EnRouteViewModel import com.android.systemui.statusbar.notification.row.ui.viewmodel.RichOngoingViewModelComponent import com.android.systemui.statusbar.notification.row.ui.viewmodel.TimerViewModel import javax.inject.Inject @@ -119,7 +122,15 @@ constructor( parentView, viewType ) - is StopwatchContentModel -> TODO("Not yet implemented") + is EnRouteContentModel -> + inflateEnRouteView( + existingView, + component::createEnRouteViewModel, + systemUiContext, + parentView, + viewType + ) + else -> TODO("Not yet implemented") } } @@ -131,7 +142,8 @@ constructor( if (RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode()) return false return when (contentModel) { is TimerContentModel -> canKeepTimerView(contentModel, existingView, viewType) - is StopwatchContentModel -> TODO("Not yet implemented") + is EnRouteContentModel -> canKeepEnRouteView(contentModel, existingView, viewType) + else -> TODO("Not yet implemented") } } @@ -167,4 +179,38 @@ constructor( existingView: View?, viewType: RichOngoingNotificationViewType ): Boolean = true + + private fun inflateEnRouteView( + existingView: View?, + createViewModel: () -> EnRouteViewModel, + systemUiContext: Context, + parentView: ViewGroup, + viewType: RichOngoingNotificationViewType, + ): ContentViewInflationResult { + if (existingView is EnRouteView && !existingView.isReinflateNeeded()) + return KeepExistingView + return when (viewType) { + RichOngoingNotificationViewType.Contracted -> { + val newView = + LayoutInflater.from(systemUiContext) + .inflate( + R.layout.notification_template_en_route_contracted, + parentView, + /* attachToRoot= */ false + ) as EnRouteView + + InflatedContentViewHolder(newView) { + EnRouteViewBinder.bindWhileAttached(newView, createViewModel()) + } + } + RichOngoingNotificationViewType.Expanded, + RichOngoingNotificationViewType.HeadsUp -> NullContentView + } + } + + private fun canKeepEnRouteView( + contentModel: EnRouteContentModel, + existingView: View?, + viewType: RichOngoingNotificationViewType + ): Boolean = true } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt index 4705ace7fac6..72823a760197 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/domain/interactor/NotificationRowInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.row.domain.interactor import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository +import com.android.systemui.statusbar.notification.row.shared.EnRouteContentModel import com.android.systemui.statusbar.notification.row.shared.TimerContentModel import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -26,4 +27,8 @@ class NotificationRowInteractor @Inject constructor(repository: NotificationRowR /** Content of a rich ongoing timer notification. */ val timerContentModel: Flow<TimerContentModel> = repository.richOngoingContentModel.filterIsInstance<TimerContentModel>() + + /** Content of a rich ongoing timer notification. */ + val enRouteContentModel: Flow<EnRouteContentModel> = + repository.richOngoingContentModel.filterIsInstance<EnRouteContentModel>() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt new file mode 100644 index 000000000000..7e78cca028ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/EnRouteContentModel.kt @@ -0,0 +1,30 @@ +/* + * 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 + +/** + * Represents something en route. + * + * @param smallIcon the main small icon of the EnRoute notification. + * @param title the title of the EnRoute notification. + * @param text the text of the EnRoute notification. + */ +data class EnRouteContentModel( + val smallIcon: IconModel, + val title: CharSequence?, + val text: CharSequence?, +) : RichOngoingContentModel diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt new file mode 100644 index 000000000000..e5c2b5fff5e9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/view/EnRouteView.kt @@ -0,0 +1,68 @@ +/* + * 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.ui.view + +import android.content.Context +import android.graphics.drawable.Icon +import android.util.AttributeSet +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import com.android.internal.R +import com.android.internal.widget.NotificationExpandButton + +class EnRouteView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) { + + private val configTracker = ConfigurationTracker(resources) + + private lateinit var icon: ImageView + private lateinit var title: TextView + private lateinit var text: TextView + private lateinit var expandButton: NotificationExpandButton + + override fun onFinishInflate() { + super.onFinishInflate() + icon = requireViewById(R.id.icon) + title = requireViewById(R.id.title) + text = requireViewById(R.id.text) + + expandButton = requireViewById(R.id.expand_button) + expandButton.setExpanded(false) + } + + /** the resources configuration has changed such that the view needs to be reinflated */ + fun isReinflateNeeded(): Boolean = configTracker.hasUnhandledConfigChange() + + fun setIcon(icon: Icon?) { + this.icon.setImageIcon(icon) + } + + fun setTitle(title: CharSequence?) { + this.title.text = title + } + + fun setText(text: CharSequence?) { + this.text.text = text + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt new file mode 100644 index 000000000000..3b8957c092f2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/EnRouteViewBinder.kt @@ -0,0 +1,44 @@ +/* + * 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.ui.viewbinder + +import androidx.lifecycle.lifecycleScope +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.notification.row.ui.view.EnRouteView +import com.android.systemui.statusbar.notification.row.ui.viewmodel.EnRouteViewModel +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +/** Binds a [EnRouteView] to its [view model][EnRouteViewModel]. */ +object EnRouteViewBinder { + fun bindWhileAttached( + view: EnRouteView, + viewModel: EnRouteViewModel, + ): DisposableHandle { + return view.repeatWhenAttached { lifecycleScope.launch { bind(view, viewModel) } } + } + + suspend fun bind( + view: EnRouteView, + viewModel: EnRouteViewModel, + ) = coroutineScope { + launch { viewModel.icon.collect { view.setIcon(it) } } + launch { viewModel.title.collect { view.setTitle(it) } } + launch { viewModel.text.collect { view.setText(it) } } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt new file mode 100644 index 000000000000..307a9834ccc9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModel.kt @@ -0,0 +1,45 @@ +/* + * 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.ui.viewmodel + +import android.graphics.drawable.Icon +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.row.domain.interactor.NotificationRowInteractor +import com.android.systemui.statusbar.notification.row.shared.RichOngoingNotificationFlag +import com.android.systemui.util.kotlin.FlowDumperImpl +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull + +/** A view model for EnRoute notifications. */ +class EnRouteViewModel +@Inject +constructor( + dumpManager: DumpManager, + rowInteractor: NotificationRowInteractor, +) : FlowDumperImpl(dumpManager) { + init { + /* check if */ RichOngoingNotificationFlag.isUnexpectedlyInLegacyMode() + } + + val icon: Flow<Icon?> = rowInteractor.enRouteContentModel.mapNotNull { it.smallIcon.icon } + + val title: Flow<CharSequence?> = rowInteractor.enRouteContentModel.map { it.title } + + val text: Flow<CharSequence?> = rowInteractor.enRouteContentModel.map { it.text } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt index dad52a3b2c45..5552d89b6f9d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/RichOngoingViewModelComponent.kt @@ -33,4 +33,6 @@ interface RichOngoingViewModelComponent { } fun createTimerViewModel(): TimerViewModel + + fun createEnRouteViewModel(): EnRouteViewModel } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt new file mode 100644 index 000000000000..7e511355372b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/EnRouteViewModelKosmos.kt @@ -0,0 +1,28 @@ +/* + * 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.ui.viewmodel + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.row.data.repository.NotificationRowRepository +import com.android.systemui.statusbar.notification.row.domain.interactor.getNotificationRowInteractor + +fun Kosmos.getEnRouteViewModel(repository: NotificationRowRepository) = + EnRouteViewModel( + dumpManager = dumpManager, + rowInteractor = getNotificationRowInteractor(repository), + ) |