• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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