diff --git a/app/src/internal/AndroidManifest.xml b/app/src/internal/AndroidManifest.xml index b89dff89ff6f..82091edb2c82 100644 --- a/app/src/internal/AndroidManifest.xml +++ b/app/src/internal/AndroidManifest.xml @@ -7,6 +7,10 @@ android:name="com.duckduckgo.app.dev.settings.DevSettingsActivity" android:label="@string/devSettingsTitle" android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" /> + + when (command) { + is TriggerNotification -> addNotification(id = command.notificationItem.id, notification = command.notificationItem.notification) + } + }.launchIn(lifecycleScope) + } + + private fun render(viewState: ViewState) { + viewState.notificationItems.forEach { notificationItem -> + TwoLineListItem(this).apply { + + setPrimaryText(notificationItem.title) + setSecondaryText(notificationItem.subtitle) + setOnClickListener { + viewModel.onNotificationItemClick(notificationItem) + } + }.also { + binding.notificationsContainer.addView(it) + } + } + } + + private fun addNotification( + id: Int, + notification: Notification + ) { + NotificationManagerCompat.from(this) + .checkPermissionAndNotify(context = this, id = id, notification = notification) + } + + companion object { + + fun intent(context: Context): Intent { + return Intent(context, NotificationsActivity::class.java) + } + } +} diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsViewModel.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsViewModel.kt new file mode 100644 index 000000000000..f5ce059426b1 --- /dev/null +++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsViewModel.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024 DuckDuckGo + * + * 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.duckduckgo.app.dev.settings.notifications + +import android.app.Notification +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.duckduckgo.anvil.annotations.ContributesViewModel +import com.duckduckgo.app.dev.settings.notifications.NotificationViewModel.ViewState.NotificationItem +import com.duckduckgo.app.notification.NotificationFactory +import com.duckduckgo.app.notification.model.SchedulableNotificationPlugin +import com.duckduckgo.app.survey.api.SurveyRepository +import com.duckduckgo.app.survey.model.Survey +import com.duckduckgo.app.survey.model.Survey.Status.SCHEDULED +import com.duckduckgo.app.survey.notification.SurveyAvailableNotification +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.common.utils.plugins.PluginPoint +import com.duckduckgo.di.scopes.ActivityScope +import javax.inject.Inject +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@ContributesViewModel(ActivityScope::class) +class NotificationViewModel @Inject constructor( + private val dispatcher: DispatcherProvider, + private val schedulableNotificationPluginPoint: PluginPoint, + private val factory: NotificationFactory, + private val surveyRepository: SurveyRepository, +) : ViewModel() { + + data class ViewState( + val notificationItems: List = emptyList(), + ) { + + data class NotificationItem( + val id: Int, + val title: String, + val subtitle: String, + val notification: Notification + ) + } + + sealed class Command { + data class TriggerNotification(val notificationItem: NotificationItem) : Command() + } + + private val _viewState = MutableStateFlow(ViewState()) + val viewState = _viewState.asStateFlow() + + private val _command = Channel(1, BufferOverflow.DROP_OLDEST) + val command = _command.receiveAsFlow() + + init { + viewModelScope.launch { + val notificationItems = schedulableNotificationPluginPoint.getPlugins().map { plugin -> + + // The survey notification will crash if we do not have a survey in the database + if (plugin.getSchedulableNotification().javaClass == SurveyAvailableNotification::class.java) { + withContext(dispatcher.io()) { + addTestSurvey() + } + } + + // the survey intent hits the DB, so we need to do this on IO + val launchIntent = withContext(dispatcher.io()) { plugin.getLaunchIntent() } + + NotificationItem( + id = plugin.getSpecification().systemId, + title = plugin.getSpecification().title, + subtitle = plugin.getSpecification().description, + notification = factory.createNotification(plugin.getSpecification(), launchIntent, null), + ) + } + + _viewState.update { it.copy(notificationItems = notificationItems) } + } + } + + private fun addTestSurvey() { + surveyRepository.persistSurvey( + Survey( + "testSurveyId", + "https://youtu.be/dQw4w9WgXcQ?si=iztopgFbXoWUnoOE", + daysInstalled = 1, + status = SCHEDULED, + ), + ) + } + + fun onNotificationItemClick(notificationItem: NotificationItem) { + viewModelScope.launch { + _command.send(Command.TriggerNotification(notificationItem)) + } + } +} diff --git a/app/src/internal/res/layout/activity_notifications.xml b/app/src/internal/res/layout/activity_notifications.xml new file mode 100644 index 000000000000..2de682d5dd28 --- /dev/null +++ b/app/src/internal/res/layout/activity_notifications.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file