Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit df8f00c3 authored by yyalan's avatar yyalan
Browse files

Show notification to launch tutorial when scheduler is ready

Bug: 344862874
Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial
Test: TutorialSchedulerInteractorTest.kt
Test: TutorialNotificationInteractorTest.kt
Change-Id: I6e0e6fe9a2369086ac8925bc7e57fc6f95832885
parent e72031df
Loading
Loading
Loading
Loading
+16 −0
Original line number Diff line number Diff line
@@ -3690,6 +3690,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>
+4 −4
Original line number Diff line number Diff line
@@ -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()
        }
    }
}
+17 −26
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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)
@@ -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 {
+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
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -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