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

Commit ca626a8d authored by Marcello Galhardo's avatar Marcello Galhardo
Browse files

Add monitoring to note taking experience

Test: atest SystemUITests:NoteTaskEventLoggerTest
Test: atest SystemUITests:NoteTaskControllerTest
Test: atest SystemUITests:NoteTaskInfoTest

Fixes: b/267815384
Change-Id: Id2150ec0036cfef2313cc555327a42e76845c6f1
parent 6c4cd643
Loading
Loading
Loading
Loading
+75 −47
Original line number Diff line number Diff line
@@ -22,15 +22,18 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.UserManager
import android.util.Log
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.util.kotlin.getOrNull
import com.android.wm.shell.bubbles.Bubble
import com.android.wm.shell.bubbles.Bubbles
import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener
import java.util.Optional
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject

/**
@@ -41,18 +44,40 @@ import javax.inject.Inject
 * Currently, we only support a single task per time.
 */
@SysUISingleton
internal class NoteTaskController
class NoteTaskController
@Inject
constructor(
    private val context: Context,
    private val resolver: NoteTaskInfoResolver,
    private val eventLogger: NoteTaskEventLogger,
    private val optionalBubbles: Optional<Bubbles>,
    private val optionalKeyguardManager: Optional<KeyguardManager>,
    private val optionalUserManager: Optional<UserManager>,
    private val optionalKeyguardManager: Optional<KeyguardManager>,
    @NoteTaskEnabledKey private val isEnabled: Boolean,
    private val uiEventLogger: UiEventLogger,
) {

    @VisibleForTesting val infoReference = AtomicReference<NoteTaskInfo?>()

    /** @see BubbleExpandListener */
    fun onBubbleExpandChanged(isExpanding: Boolean, key: String?) {
        if (!isEnabled) return

        if (key != Bubble.KEY_APP_BUBBLE) return

        val info = infoReference.getAndSet(null)

        // Safe guard mechanism, this callback should only be called for app bubbles.
        if (info?.launchMode != NoteTaskLaunchMode.AppBubble) return

        if (isExpanding) {
            logDebug { "onBubbleExpandChanged - expanding: $info" }
            eventLogger.logNoteTaskOpened(info)
        } else {
            logDebug { "onBubbleExpandChanged - collapsing: $info" }
            eventLogger.logNoteTaskClosed(info)
        }
    }

    /**
     * Shows a note task. How the task is shown will depend on when the method is invoked.
     *
@@ -69,32 +94,50 @@ constructor(
     * That will let users open other apps in full screen, and take contextual notes.
     */
    @JvmOverloads
    fun showNoteTask(isInMultiWindowMode: Boolean = false, uiEvent: ShowNoteTaskUiEvent? = null) {

    fun showNoteTask(
        entryPoint: NoteTaskEntryPoint,
        isInMultiWindowMode: Boolean = false,
    ) {
        if (!isEnabled) return

        val bubbles = optionalBubbles.getOrNull() ?: return
        val keyguardManager = optionalKeyguardManager.getOrNull() ?: return
        val userManager = optionalUserManager.getOrNull() ?: return
        val keyguardManager = optionalKeyguardManager.getOrNull() ?: return

        // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
        if (!userManager.isUserUnlocked) return

        val noteTaskInfo = resolver.resolveInfo() ?: return
        val info =
            resolver.resolveInfo(
                entryPoint = entryPoint,
                isInMultiWindowMode = isInMultiWindowMode,
                isKeyguardLocked = keyguardManager.isKeyguardLocked,
            )
                ?: return

        uiEvent?.let { uiEventLogger.log(it, noteTaskInfo.uid, noteTaskInfo.packageName) }
        infoReference.set(info)

        // TODO(b/266686199): We should handle when app not available. For now, we log.
        val intent = noteTaskInfo.toCreateNoteIntent()
        val intent = createNoteIntent(info)
        try {
            if (isInMultiWindowMode || keyguardManager.isKeyguardLocked) {
                context.startActivity(intent)
            } else {
            logDebug { "onShowNoteTask - start: $info" }
            when (info.launchMode) {
                is NoteTaskLaunchMode.AppBubble -> {
                    bubbles.showOrHideAppBubble(intent)
                    // App bubble logging happens on `onBubbleExpandChanged`.
                    logDebug { "onShowNoteTask - opened as app bubble: $info" }
                }
                is NoteTaskLaunchMode.Activity -> {
                    context.startActivity(intent)
                    eventLogger.logNoteTaskOpened(info)
                    logDebug { "onShowNoteTask - opened as activity: $info" }
                }
            }
            logDebug { "onShowNoteTask - success: $info" }
        } catch (e: ActivityNotFoundException) {
            Log.e(TAG, "Activity not found for action: $ACTION_CREATE_NOTE.", e)
            logDebug { "onShowNoteTask - failed: $info" }
        }
        logDebug { "onShowNoteTask - compoleted: $info" }
    }

    /**
@@ -119,41 +162,12 @@ constructor(
            enabledState,
            PackageManager.DONT_KILL_APP,
        )
    }

    /** IDs of UI events accepted by [showNoteTask]. */
    enum class ShowNoteTaskUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
        @UiEvent(doc = "User opened a note by tapping on the lockscreen shortcut.")
        NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE(1294),

        /* ktlint-disable max-line-length */
        @UiEvent(
            doc =
                "User opened a note by pressing the stylus tail button while the screen was unlocked."
        )
        NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON(1295),
        @UiEvent(
            doc =
                "User opened a note by pressing the stylus tail button while the screen was locked."
        )
        NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED(1296),
        @UiEvent(doc = "User opened a note by tapping on an app shortcut.")
        NOTE_OPENED_VIA_SHORTCUT(1297);

        override fun getId() = _id
        logDebug { "setNoteTaskShortcutEnabled - completed: $isEnabled" }
    }

    companion object {
        private val TAG = NoteTaskController::class.simpleName.orEmpty()

        private fun NoteTaskInfoResolver.NoteTaskInfo.toCreateNoteIntent(): Intent {
            return Intent(ACTION_CREATE_NOTE)
                .setPackage(packageName)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint
                // was used to start it.
                .putExtra(INTENT_EXTRA_USE_STYLUS_MODE, true)
        }
        val TAG = NoteTaskController::class.simpleName.orEmpty()

        // TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
        const val NOTE_TASK_KEY_EVENT = 311
@@ -165,3 +179,17 @@ constructor(
        const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
    }
}

private fun createNoteIntent(info: NoteTaskInfo): Intent =
    Intent(NoteTaskController.ACTION_CREATE_NOTE)
        .setPackage(info.packageName)
        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        // EXTRA_USE_STYLUS_MODE does not mean a stylus is in-use, but a stylus entrypoint
        // was used to start it.
        .putExtra(NoteTaskController.INTENT_EXTRA_USE_STYLUS_MODE, true)

private inline fun logDebug(message: () -> String) {
    if (Build.IS_DEBUGGABLE) {
        Log.d(NoteTaskController.TAG, message())
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -19,4 +19,4 @@ package com.android.systemui.notetask
import javax.inject.Qualifier

/** Key associated with a [Boolean] flag that enables or disables the note task feature. */
@Qualifier internal annotation class NoteTaskEnabledKey
@Qualifier annotation class NoteTaskEnabledKey
+41 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.notetask

import com.android.systemui.notetask.quickaffordance.NoteTaskQuickAffordanceConfig
import com.android.systemui.notetask.shortcut.LaunchNoteTaskActivity
import com.android.systemui.screenshot.AppClipsTrampolineActivity

/**
 * Supported entry points for [NoteTaskController.showNoteTask].
 *
 * An entry point represents where the note task has ben called from. In rare cases, it may
 * represent a "re-entry" (i.e., [APP_CLIPS]).
 */
enum class NoteTaskEntryPoint {

    /** @see [LaunchNoteTaskActivity] */
    WIDGET_PICKER_SHORTCUT,

    /** @see [NoteTaskQuickAffordanceConfig] */
    QUICK_AFFORDANCE,

    /** @see [NoteTaskInitializer.callbacks] */
    TAIL_BUTTON,

    /** @see [AppClipsTrampolineActivity] */
    APP_CLIPS,
}
+101 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.notetask

import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import com.android.systemui.notetask.NoteTaskEntryPoint.APP_CLIPS
import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE
import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON
import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT
import com.android.systemui.notetask.NoteTaskEventLogger.NoteTaskUiEvent.NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE
import com.android.systemui.notetask.NoteTaskEventLogger.NoteTaskUiEvent.NOTE_OPENED_VIA_SHORTCUT
import com.android.systemui.notetask.NoteTaskEventLogger.NoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
import com.android.systemui.notetask.NoteTaskEventLogger.NoteTaskUiEvent.NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED
import javax.inject.Inject

/**
 * A wrapper around [UiEventLogger] specialized in the note taking UI events.
 *
 * if the accepted [NoteTaskInfo] contains a [NoteTaskInfo.entryPoint], it will be logged as the
 * correct [NoteTaskUiEvent]. If null, it will be ignored.
 *
 * @see NoteTaskController for usage examples.
 */
class NoteTaskEventLogger @Inject constructor(private val uiEventLogger: UiEventLogger) {

    /** Logs a [NoteTaskInfo] as an **open** [NoteTaskUiEvent], including package name and uid. */
    fun logNoteTaskOpened(info: NoteTaskInfo) {
        val event =
            when (info.entryPoint) {
                TAIL_BUTTON -> {
                    if (info.isKeyguardLocked) {
                        NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED
                    } else {
                        NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON
                    }
                }
                WIDGET_PICKER_SHORTCUT -> NOTE_OPENED_VIA_SHORTCUT
                QUICK_AFFORDANCE -> NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE
                APP_CLIPS -> return
                null -> return
            }
        uiEventLogger.log(event, info.uid, info.packageName)
    }

    /** Logs a [NoteTaskInfo] as a **closed** [NoteTaskUiEvent], including package name and uid. */
    fun logNoteTaskClosed(info: NoteTaskInfo) {
        val event =
            when (info.entryPoint) {
                TAIL_BUTTON -> {
                    if (info.isKeyguardLocked) {
                        NoteTaskUiEvent.NOTE_CLOSED_VIA_STYLUS_TAIL_BUTTON_LOCKED
                    } else {
                        NoteTaskUiEvent.NOTE_CLOSED_VIA_STYLUS_TAIL_BUTTON
                    }
                }
                WIDGET_PICKER_SHORTCUT -> return
                QUICK_AFFORDANCE -> return
                APP_CLIPS -> return
                null -> return
            }
        uiEventLogger.log(event, info.uid, info.packageName)
    }

    /** IDs of UI events accepted by [NoteTaskController]. */
    enum class NoteTaskUiEvent(private val _id: Int) : UiEventLogger.UiEventEnum {

        @UiEvent(doc = "User opened a note by tapping on the lockscreen shortcut.")
        NOTE_OPENED_VIA_KEYGUARD_QUICK_AFFORDANCE(1294),

        @UiEvent(doc = "User opened a note by pressing the stylus tail button while the screen was unlocked.") // ktlint-disable max-line-length
        NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON(1295),

        @UiEvent(doc = "User opened a note by pressing the stylus tail button while the screen was locked.") // ktlint-disable max-line-length
        NOTE_OPENED_VIA_STYLUS_TAIL_BUTTON_LOCKED(1296),

        @UiEvent(doc = "User opened a note by tapping on an app shortcut.")
        NOTE_OPENED_VIA_SHORTCUT(1297),

        @UiEvent(doc = "Note closed via a tail button while device is unlocked")
        NOTE_CLOSED_VIA_STYLUS_TAIL_BUTTON(1311),

        @UiEvent(doc = "Note closed via a tail button while device is locked")
        NOTE_CLOSED_VIA_STYLUS_TAIL_BUTTON_LOCKED(1312);

        override fun getId() = _id
    }
}
+33 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.notetask

/** Contextual information required to launch a Note Task by [NoteTaskController]. */
data class NoteTaskInfo(
    val packageName: String,
    val uid: Int,
    val entryPoint: NoteTaskEntryPoint? = null,
    val isInMultiWindowMode: Boolean = false,
    val isKeyguardLocked: Boolean = false,
) {

    val launchMode: NoteTaskLaunchMode =
        if (isInMultiWindowMode || isKeyguardLocked) {
            NoteTaskLaunchMode.Activity
        } else {
            NoteTaskLaunchMode.AppBubble
        }
}
Loading