Loading packages/SystemUI/res/values/strings.xml +16 −0 Original line number Diff line number Diff line Loading @@ -3699,6 +3699,22 @@ --> <string name="shortcut_helper_key_combinations_or_separator">or</string> <!-- Keyboard touchpad tutorial scheduler--> <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_keyboard_tutorial_notification_title">Navigate using your keyboard</string> <!-- Notification text for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_keyboard_tutorial_notification_content">Learn keyboards shortcuts</string> <!-- Notification title for launching touchpad tutorial [CHAR_LIMIT=100] --> <string name="launch_touchpad_tutorial_notification_title">Navigate using your touchpad</string> <!-- Notification text for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_touchpad_tutorial_notification_content">Learn touchpad gestures</string> <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_keyboard_touchpad_tutorial_notification_title">Navigate using your keyboard and touchpad</string> <!-- Notification text for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_keyboard_touchpad_tutorial_notification_content">Learn touchpad gestures, keyboards shortcuts, and more</string> <!-- TOUCHPAD TUTORIAL--> <!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] --> <string name="touchpad_tutorial_back_gesture_button">Back gesture</string> Loading packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt +4 −4 Original line number Diff line number Diff line Loading @@ -18,20 +18,20 @@ package com.android.systemui.inputdevice.tutorial import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor import com.android.systemui.inputdevice.tutorial.ui.TutorialNotificationCoordinator import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial import dagger.Lazy import javax.inject.Inject /** A [CoreStartable] to launch a scheduler for keyboard and touchpad education */ /** A [CoreStartable] to launch a scheduler for keyboard and touchpad tutorial notification */ @SysUISingleton class KeyboardTouchpadTutorialCoreStartable @Inject constructor(private val tutorialSchedulerInteractor: Lazy<TutorialSchedulerInteractor>) : constructor(private val tutorialNotificationCoordinator: Lazy<TutorialNotificationCoordinator>) : CoreStartable { override fun start() { if (newTouchpadGesturesTutorial()) { tutorialSchedulerInteractor.get().start() tutorialNotificationCoordinator.get().start() } } } packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt +17 −26 Original line number Diff line number Diff line Loading @@ -17,9 +17,7 @@ package com.android.systemui.inputdevice.tutorial.domain.interactor import android.os.SystemProperties import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD Loading @@ -31,23 +29,22 @@ import java.time.Instant import javax.inject.Inject import kotlin.time.Duration.Companion.hours import kotlin.time.toKotlinDuration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch /** * When the first time a keyboard or touchpad is connected, wait for [LAUNCH_DELAY], then launch the * tutorial as soon as there's a connected device * When the first time a keyboard or touchpad is connected, wait for [LAUNCH_DELAY], and as soon as * there's a connected device, show a notification to launch the tutorial. */ @SysUISingleton class TutorialSchedulerInteractor @Inject constructor( @Background private val backgroundScope: CoroutineScope, keyboardRepository: KeyboardRepository, touchpadRepository: TouchpadRepository, private val repo: TutorialSchedulerRepository Loading @@ -58,17 +55,6 @@ constructor( TOUCHPAD to touchpadRepository.isAnyTouchpadConnected ) fun start() { backgroundScope.launch { // Merging two flows to ensure that launch tutorial is launched consecutively in order // to avoid race condition merge(touchpadScheduleFlow, keyboardScheduleFlow).collect { val tutorialType = resolveTutorialType(it) launchTutorial(tutorialType) } } } private val touchpadScheduleFlow = flow { if (!repo.isLaunched(TOUCHPAD)) { schedule(TOUCHPAD) Loading @@ -95,13 +81,18 @@ constructor( private suspend fun waitForDeviceConnection(deviceType: DeviceType) = isAnyDeviceConnected[deviceType]!!.filter { it }.first() private suspend fun launchTutorial(tutorialType: TutorialType) { // Merging two flows ensures that tutorial is launched consecutively to avoid race condition val tutorials: Flow<TutorialType> = merge(touchpadScheduleFlow, keyboardScheduleFlow).map { val tutorialType = resolveTutorialType(it) // TODO: notifying time is not oobe launching time - move these updates into oobe if (tutorialType == TutorialType.KEYBOARD || tutorialType == TutorialType.BOTH) repo.updateLaunchTime(KEYBOARD, Instant.now()) if (tutorialType == TutorialType.TOUCHPAD || tutorialType == TutorialType.BOTH) repo.updateLaunchTime(TOUCHPAD, Instant.now()) // TODO: launch tutorial Log.d(TAG, "Launch tutorial for $tutorialType") tutorialType } private suspend fun resolveTutorialType(deviceType: DeviceType): TutorialType { Loading packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt 0 → 100644 +146 −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.inputdevice.tutorial.ui import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Bundle import androidx.core.app.NotificationCompat import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.Companion.TAG import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.TutorialType import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_BOTH import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_TOUCHPAD import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** When the scheduler is due, show a notification to launch tutorial */ @SysUISingleton class TutorialNotificationCoordinator @Inject constructor( @Background private val backgroundScope: CoroutineScope, @Application private val context: Context, private val tutorialSchedulerInteractor: TutorialSchedulerInteractor, private val notificationManager: NotificationManager ) { fun start() { backgroundScope.launch { tutorialSchedulerInteractor.tutorials.collect { showNotification(it) } } } // By sharing the same tag and id, we update the content of existing notification instead of // creating multiple notifications private fun showNotification(tutorialType: TutorialType) { if (tutorialType == TutorialType.NONE) return if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) createNotificationChannel() // Replace "System UI" app name with "Android System" val extras = Bundle() extras.putString( Notification.EXTRA_SUBSTITUTE_APP_NAME, context.getString(com.android.internal.R.string.android_system_label) ) val info = getNotificationInfo(tutorialType)!! val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_settings) .setContentTitle(info.title) .setContentText(info.text) .setContentIntent(createPendingIntent(info.type)) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true) .addExtras(extras) .build() notificationManager.notify(TAG, NOTIFICATION_ID, notification) } private fun createNotificationChannel() { val channel = NotificationChannel( CHANNEL_ID, context.getString(com.android.internal.R.string.android_system_label), NotificationManager.IMPORTANCE_DEFAULT ) notificationManager.createNotificationChannel(channel) } private fun createPendingIntent(tutorialType: String): PendingIntent { val intent = Intent(context, KeyboardTouchpadTutorialActivity::class.java).apply { putExtra(INTENT_TUTORIAL_TYPE_KEY, tutorialType) flags = Intent.FLAG_ACTIVITY_NEW_TASK } return PendingIntent.getActivity( context, /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE ) } private data class NotificationInfo(val title: String, val text: String, val type: String) private fun getNotificationInfo(tutorialType: TutorialType): NotificationInfo? = when (tutorialType) { TutorialType.KEYBOARD -> NotificationInfo( context.getString(R.string.launch_keyboard_tutorial_notification_title), context.getString(R.string.launch_keyboard_tutorial_notification_content), INTENT_TUTORIAL_TYPE_KEYBOARD ) TutorialType.TOUCHPAD -> NotificationInfo( context.getString(R.string.launch_touchpad_tutorial_notification_title), context.getString(R.string.launch_touchpad_tutorial_notification_content), INTENT_TUTORIAL_TYPE_TOUCHPAD ) TutorialType.BOTH -> NotificationInfo( context.getString( R.string.launch_keyboard_touchpad_tutorial_notification_title ), context.getString( R.string.launch_keyboard_touchpad_tutorial_notification_content ), INTENT_TUTORIAL_TYPE_BOTH ) TutorialType.NONE -> null } companion object { private const val CHANNEL_ID = "TutorialSchedulerNotificationChannel" private const val NOTIFICATION_ID = 5566 } } packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt +1 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,7 @@ constructor( const val INTENT_TUTORIAL_TYPE_KEY = "tutorial_type" const val INTENT_TUTORIAL_TYPE_TOUCHPAD = "touchpad" const val INTENT_TUTORIAL_TYPE_KEYBOARD = "keyboard" const val INTENT_TUTORIAL_TYPE_BOTH = "both" } private val vm by Loading Loading
packages/SystemUI/res/values/strings.xml +16 −0 Original line number Diff line number Diff line Loading @@ -3699,6 +3699,22 @@ --> <string name="shortcut_helper_key_combinations_or_separator">or</string> <!-- Keyboard touchpad tutorial scheduler--> <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_keyboard_tutorial_notification_title">Navigate using your keyboard</string> <!-- Notification text for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_keyboard_tutorial_notification_content">Learn keyboards shortcuts</string> <!-- Notification title for launching touchpad tutorial [CHAR_LIMIT=100] --> <string name="launch_touchpad_tutorial_notification_title">Navigate using your touchpad</string> <!-- Notification text for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_touchpad_tutorial_notification_content">Learn touchpad gestures</string> <!-- Notification title for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_keyboard_touchpad_tutorial_notification_title">Navigate using your keyboard and touchpad</string> <!-- Notification text for launching keyboard tutorial [CHAR_LIMIT=100] --> <string name="launch_keyboard_touchpad_tutorial_notification_content">Learn touchpad gestures, keyboards shortcuts, and more</string> <!-- TOUCHPAD TUTORIAL--> <!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] --> <string name="touchpad_tutorial_back_gesture_button">Back gesture</string> Loading
packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/KeyboardTouchpadTutorialCoreStartable.kt +4 −4 Original line number Diff line number Diff line Loading @@ -18,20 +18,20 @@ package com.android.systemui.inputdevice.tutorial import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor import com.android.systemui.inputdevice.tutorial.ui.TutorialNotificationCoordinator import com.android.systemui.shared.Flags.newTouchpadGesturesTutorial import dagger.Lazy import javax.inject.Inject /** A [CoreStartable] to launch a scheduler for keyboard and touchpad education */ /** A [CoreStartable] to launch a scheduler for keyboard and touchpad tutorial notification */ @SysUISingleton class KeyboardTouchpadTutorialCoreStartable @Inject constructor(private val tutorialSchedulerInteractor: Lazy<TutorialSchedulerInteractor>) : constructor(private val tutorialNotificationCoordinator: Lazy<TutorialNotificationCoordinator>) : CoreStartable { override fun start() { if (newTouchpadGesturesTutorial()) { tutorialSchedulerInteractor.get().start() tutorialNotificationCoordinator.get().start() } } }
packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt +17 −26 Original line number Diff line number Diff line Loading @@ -17,9 +17,7 @@ package com.android.systemui.inputdevice.tutorial.domain.interactor import android.os.SystemProperties import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD Loading @@ -31,23 +29,22 @@ import java.time.Instant import javax.inject.Inject import kotlin.time.Duration.Companion.hours import kotlin.time.toKotlinDuration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch /** * When the first time a keyboard or touchpad is connected, wait for [LAUNCH_DELAY], then launch the * tutorial as soon as there's a connected device * When the first time a keyboard or touchpad is connected, wait for [LAUNCH_DELAY], and as soon as * there's a connected device, show a notification to launch the tutorial. */ @SysUISingleton class TutorialSchedulerInteractor @Inject constructor( @Background private val backgroundScope: CoroutineScope, keyboardRepository: KeyboardRepository, touchpadRepository: TouchpadRepository, private val repo: TutorialSchedulerRepository Loading @@ -58,17 +55,6 @@ constructor( TOUCHPAD to touchpadRepository.isAnyTouchpadConnected ) fun start() { backgroundScope.launch { // Merging two flows to ensure that launch tutorial is launched consecutively in order // to avoid race condition merge(touchpadScheduleFlow, keyboardScheduleFlow).collect { val tutorialType = resolveTutorialType(it) launchTutorial(tutorialType) } } } private val touchpadScheduleFlow = flow { if (!repo.isLaunched(TOUCHPAD)) { schedule(TOUCHPAD) Loading @@ -95,13 +81,18 @@ constructor( private suspend fun waitForDeviceConnection(deviceType: DeviceType) = isAnyDeviceConnected[deviceType]!!.filter { it }.first() private suspend fun launchTutorial(tutorialType: TutorialType) { // Merging two flows ensures that tutorial is launched consecutively to avoid race condition val tutorials: Flow<TutorialType> = merge(touchpadScheduleFlow, keyboardScheduleFlow).map { val tutorialType = resolveTutorialType(it) // TODO: notifying time is not oobe launching time - move these updates into oobe if (tutorialType == TutorialType.KEYBOARD || tutorialType == TutorialType.BOTH) repo.updateLaunchTime(KEYBOARD, Instant.now()) if (tutorialType == TutorialType.TOUCHPAD || tutorialType == TutorialType.BOTH) repo.updateLaunchTime(TOUCHPAD, Instant.now()) // TODO: launch tutorial Log.d(TAG, "Launch tutorial for $tutorialType") tutorialType } private suspend fun resolveTutorialType(deviceType: DeviceType): TutorialType { Loading
packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/TutorialNotificationCoordinator.kt 0 → 100644 +146 −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.inputdevice.tutorial.ui import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Bundle import androidx.core.app.NotificationCompat import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.Companion.TAG import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.TutorialType import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_BOTH import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEY import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_KEYBOARD import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutorialActivity.Companion.INTENT_TUTORIAL_TYPE_TOUCHPAD import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** When the scheduler is due, show a notification to launch tutorial */ @SysUISingleton class TutorialNotificationCoordinator @Inject constructor( @Background private val backgroundScope: CoroutineScope, @Application private val context: Context, private val tutorialSchedulerInteractor: TutorialSchedulerInteractor, private val notificationManager: NotificationManager ) { fun start() { backgroundScope.launch { tutorialSchedulerInteractor.tutorials.collect { showNotification(it) } } } // By sharing the same tag and id, we update the content of existing notification instead of // creating multiple notifications private fun showNotification(tutorialType: TutorialType) { if (tutorialType == TutorialType.NONE) return if (notificationManager.getNotificationChannel(CHANNEL_ID) == null) createNotificationChannel() // Replace "System UI" app name with "Android System" val extras = Bundle() extras.putString( Notification.EXTRA_SUBSTITUTE_APP_NAME, context.getString(com.android.internal.R.string.android_system_label) ) val info = getNotificationInfo(tutorialType)!! val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(R.drawable.ic_settings) .setContentTitle(info.title) .setContentText(info.text) .setContentIntent(createPendingIntent(info.type)) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true) .addExtras(extras) .build() notificationManager.notify(TAG, NOTIFICATION_ID, notification) } private fun createNotificationChannel() { val channel = NotificationChannel( CHANNEL_ID, context.getString(com.android.internal.R.string.android_system_label), NotificationManager.IMPORTANCE_DEFAULT ) notificationManager.createNotificationChannel(channel) } private fun createPendingIntent(tutorialType: String): PendingIntent { val intent = Intent(context, KeyboardTouchpadTutorialActivity::class.java).apply { putExtra(INTENT_TUTORIAL_TYPE_KEY, tutorialType) flags = Intent.FLAG_ACTIVITY_NEW_TASK } return PendingIntent.getActivity( context, /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE ) } private data class NotificationInfo(val title: String, val text: String, val type: String) private fun getNotificationInfo(tutorialType: TutorialType): NotificationInfo? = when (tutorialType) { TutorialType.KEYBOARD -> NotificationInfo( context.getString(R.string.launch_keyboard_tutorial_notification_title), context.getString(R.string.launch_keyboard_tutorial_notification_content), INTENT_TUTORIAL_TYPE_KEYBOARD ) TutorialType.TOUCHPAD -> NotificationInfo( context.getString(R.string.launch_touchpad_tutorial_notification_title), context.getString(R.string.launch_touchpad_tutorial_notification_content), INTENT_TUTORIAL_TYPE_TOUCHPAD ) TutorialType.BOTH -> NotificationInfo( context.getString( R.string.launch_keyboard_touchpad_tutorial_notification_title ), context.getString( R.string.launch_keyboard_touchpad_tutorial_notification_content ), INTENT_TUTORIAL_TYPE_BOTH ) TutorialType.NONE -> null } companion object { private const val CHANNEL_ID = "TutorialSchedulerNotificationChannel" private const val NOTIFICATION_ID = 5566 } }
packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/view/KeyboardTouchpadTutorialActivity.kt +1 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,7 @@ constructor( const val INTENT_TUTORIAL_TYPE_KEY = "tutorial_type" const val INTENT_TUTORIAL_TYPE_TOUCHPAD = "touchpad" const val INTENT_TUTORIAL_TYPE_KEYBOARD = "keyboard" const val INTENT_TUTORIAL_TYPE_BOTH = "both" } private val vm by Loading