1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.inputdevice.tutorial.ui 18 19 import android.app.Notification 20 import android.app.NotificationChannel 21 import android.app.NotificationManager 22 import android.app.PendingIntent 23 import android.content.Context 24 import android.content.Intent 25 import android.os.Bundle 26 import androidx.core.app.NotificationCompat 27 import com.android.app.tracing.coroutines.launchTraced as launch 28 import com.android.systemui.dagger.SysUISingleton 29 import com.android.systemui.dagger.qualifiers.Application 30 import com.android.systemui.dagger.qualifiers.Background 31 import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor 32 import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.Companion.TAG 33 import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.TutorialType 34 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity 35 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_KEY 36 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER 37 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_ALL 38 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_KEY 39 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_KEYBOARD 40 import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_SCOPE_TOUCHPAD 41 import com.android.systemui.res.R 42 import com.android.systemui.settings.UserTracker 43 import javax.inject.Inject 44 import kotlinx.coroutines.CoroutineScope 45 import kotlinx.coroutines.Job 46 import kotlinx.coroutines.flow.collectLatest 47 import kotlinx.coroutines.flow.filter 48 import kotlinx.coroutines.flow.merge 49 50 /** When the scheduler is due, show a notification to launch tutorial */ 51 @SysUISingleton 52 class TutorialNotificationCoordinator 53 @Inject 54 constructor( 55 @Background private val backgroundScope: CoroutineScope, 56 @Application private val context: Context, 57 private val tutorialSchedulerInteractor: TutorialSchedulerInteractor, 58 private val notificationManager: NotificationManager, 59 private val userTracker: UserTracker, 60 ) { 61 private var updaterJob: Job? = null 62 startnull63 fun start() { 64 backgroundScope.launch { 65 merge( 66 tutorialSchedulerInteractor.tutorials, 67 tutorialSchedulerInteractor.commandTutorials, 68 ) 69 .filter { it != TutorialType.NONE } 70 .collectLatest { 71 showNotification(it) 72 updaterJob?.cancel() 73 updaterJob = backgroundScope.launch { updateWhenDeviceDisconnects() } 74 } 75 } 76 } 77 updateWhenDeviceDisconnectsnull78 private suspend fun updateWhenDeviceDisconnects() { 79 // Only update the notification when there is an active one (i.e. if the notification has 80 // been dismissed by the user, or if the tutorial has been launched, there's no need to 81 // update) 82 tutorialSchedulerInteractor.tutorialTypeUpdates 83 .filter { hasNotification() } 84 .collect { 85 if (it == TutorialType.NONE) 86 notificationManager.cancelAsUser(TAG, NOTIFICATION_ID, userTracker.userHandle) 87 else showNotification(it) 88 } 89 } 90 hasNotificationnull91 private fun hasNotification() = 92 notificationManager.activeNotifications.any { it.id == NOTIFICATION_ID } 93 94 // By sharing the same tag and id, we update the content of existing notification instead of 95 // creating multiple notifications showNotificationnull96 private fun showNotification(tutorialType: TutorialType) { 97 // Safe guard - but this should never been reached 98 if (tutorialType == TutorialType.NONE) return 99 100 if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) 101 createNotificationChannel() 102 103 // Replace "System UI" app name with "Android System" 104 val extras = Bundle() 105 extras.putString( 106 Notification.EXTRA_SUBSTITUTE_APP_NAME, 107 context.getString(com.android.internal.R.string.android_system_label), 108 ) 109 110 val info = getNotificationInfo(tutorialType)!! 111 val notification = 112 NotificationCompat.Builder(context, CHANNEL_ID) 113 .setSmallIcon(R.drawable.ic_settings) 114 .setContentTitle(info.title) 115 .setContentText(info.text) 116 .setContentIntent(createPendingIntent(info.type)) 117 .setPriority(NotificationCompat.PRIORITY_DEFAULT) 118 .setAutoCancel(true) 119 .addExtras(extras) 120 .build() 121 122 notificationManager.notifyAsUser(TAG, NOTIFICATION_ID, notification, userTracker.userHandle) 123 } 124 createNotificationChannelnull125 private fun createNotificationChannel() { 126 val channel = 127 NotificationChannel( 128 CHANNEL_ID, 129 context.getString(com.android.internal.R.string.android_system_label), 130 NotificationManager.IMPORTANCE_DEFAULT, 131 ) 132 notificationManager.createNotificationChannel(channel) 133 } 134 createPendingIntentnull135 private fun createPendingIntent(tutorialType: String): PendingIntent { 136 val intent = 137 Intent(context, KeyboardTouchpadTutorialActivity::class.java).apply { 138 putExtra(INTENT_TUTORIAL_SCOPE_KEY, tutorialType) 139 putExtra(INTENT_TUTORIAL_ENTRY_POINT_KEY, INTENT_TUTORIAL_ENTRY_POINT_SCHEDULER) 140 flags = Intent.FLAG_ACTIVITY_NEW_TASK 141 } 142 return PendingIntent.getActivity( 143 context, 144 /* requestCode= */ 0, 145 intent, 146 PendingIntent.FLAG_IMMUTABLE, 147 ) 148 } 149 150 private data class NotificationInfo(val title: String, val text: String, val type: String) 151 getNotificationInfonull152 private fun getNotificationInfo(tutorialType: TutorialType): NotificationInfo? = 153 when (tutorialType) { 154 TutorialType.KEYBOARD -> 155 NotificationInfo( 156 context.getString(R.string.launch_keyboard_tutorial_notification_title), 157 context.getString(R.string.launch_keyboard_tutorial_notification_content), 158 INTENT_TUTORIAL_SCOPE_KEYBOARD, 159 ) 160 TutorialType.TOUCHPAD -> 161 NotificationInfo( 162 context.getString(R.string.launch_touchpad_tutorial_notification_title), 163 context.getString(R.string.launch_touchpad_tutorial_notification_content), 164 INTENT_TUTORIAL_SCOPE_TOUCHPAD, 165 ) 166 TutorialType.BOTH -> 167 NotificationInfo( 168 context.getString( 169 R.string.launch_keyboard_touchpad_tutorial_notification_title 170 ), 171 context.getString( 172 R.string.launch_keyboard_touchpad_tutorial_notification_content 173 ), 174 INTENT_TUTORIAL_SCOPE_ALL, 175 ) 176 TutorialType.NONE -> null 177 } 178 179 companion object { 180 private const val CHANNEL_ID = "TutorialSchedulerNotificationChannel" 181 private const val NOTIFICATION_ID = 5566 182 } 183 } 184