blob: 5b36e93ec5e1a66555819922cac4fb63b0bd7181 [file] [log] [blame]
/*
* Copyright (C) 2022 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 android.app.KeyguardManager
import android.app.admin.DevicePolicyManager
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.os.Build
import android.os.UserManager
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled
import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity
import com.android.systemui.settings.UserTracker
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
/**
* Entry point for creating and managing note.
*
* The controller decides how a note is launched based in the device state: locked or unlocked.
*
* Currently, we only support a single task per time.
*/
@SysUISingleton
class NoteTaskController
@Inject
constructor(
private val context: Context,
private val resolver: NoteTaskInfoResolver,
private val eventLogger: NoteTaskEventLogger,
private val optionalBubbles: Optional<Bubbles>,
private val userManager: UserManager,
private val keyguardManager: KeyguardManager,
@NoteTaskEnabledKey private val isEnabled: Boolean,
private val devicePolicyManager: DevicePolicyManager,
private val userTracker: UserTracker,
) {
@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.
*
* If in multi-window mode, notes will open as a full screen experience. That is particularly
* important for Large screen devices. These devices may support a taskbar that let users to
* drag and drop a shortcut into multi-window mode, and notes should comply with this behaviour.
*
* If the keyguard is locked, notes will open as a full screen experience. A locked device has
* no contextual information which let us use the whole screen space available.
*
* If not in multi-window or the keyguard is unlocked, notes will open as a bubble OR it will be
* collapsed if the notes bubble is already opened.
*
* That will let users open other apps in full screen, and take contextual notes.
*/
@JvmOverloads
fun showNoteTask(
entryPoint: NoteTaskEntryPoint,
isInMultiWindowMode: Boolean = false,
) {
if (!isEnabled) return
val bubbles = optionalBubbles.getOrNull() ?: return
// TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
if (!userManager.isUserUnlocked) return
val isKeyguardLocked = keyguardManager.isKeyguardLocked
// KeyguardQuickAffordanceInteractor blocks the quick affordance from showing in the
// keyguard if it is not allowed by the admin policy. Here we block any other way to show
// note task when the screen is locked.
if (
isKeyguardLocked &&
devicePolicyManager.areKeyguardShortcutsDisabled(userId = userTracker.userId)
) {
logDebug { "Enterprise policy disallows launching note app when the screen is locked." }
return
}
val info =
resolver.resolveInfo(
entryPoint = entryPoint,
isInMultiWindowMode = isInMultiWindowMode,
isKeyguardLocked = isKeyguardLocked,
)
?: return
infoReference.set(info)
// TODO(b/266686199): We should handle when app not available. For now, we log.
val intent = createNoteIntent(info)
try {
logDebug { "onShowNoteTask - start: $info" }
when (info.launchMode) {
is NoteTaskLaunchMode.AppBubble -> {
// TODO(b/267634412, b/268351693): Should use `showOrHideAppBubbleAsUser`
bubbles.showOrHideAppBubble(intent)
// App bubble logging happens on `onBubbleExpandChanged`.
logDebug { "onShowNoteTask - opened as app bubble: $info" }
}
is NoteTaskLaunchMode.Activity -> {
context.startActivityAsUser(intent, userTracker.userHandle)
eventLogger.logNoteTaskOpened(info)
logDebug { "onShowNoteTask - opened as activity: $info" }
}
}
logDebug { "onShowNoteTask - success: $info" }
} catch (e: ActivityNotFoundException) {
logDebug { "onShowNoteTask - failed: $info" }
}
logDebug { "onShowNoteTask - compoleted: $info" }
}
/**
* Set `android:enabled` property in the `AndroidManifest` associated with the Shortcut
* component to [value].
*
* If the shortcut entry `android:enabled` is set to `true`, the shortcut will be visible in the
* Widget Picker to all users.
*/
fun setNoteTaskShortcutEnabled(value: Boolean) {
val componentName = ComponentName(context, CreateNoteTaskShortcutActivity::class.java)
val enabledState =
if (value) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
context.packageManager.setComponentEnabledSetting(
componentName,
enabledState,
PackageManager.DONT_KILL_APP,
)
logDebug { "setNoteTaskShortcutEnabled - completed: $isEnabled" }
}
companion object {
val TAG = NoteTaskController::class.simpleName.orEmpty()
// TODO(b/254604589): Use final KeyEvent.KEYCODE_* instead.
const val NOTE_TASK_KEY_EVENT = 311
// TODO(b/265912743): Use Intent.ACTION_CREATE_NOTE instead.
const val ACTION_CREATE_NOTE = "android.intent.action.CREATE_NOTE"
// TODO(b/265912743): Use Intent.INTENT_EXTRA_USE_STYLUS_MODE instead.
const val INTENT_EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE"
}
}
private fun createNoteIntent(info: NoteTaskInfo): Intent =
Intent(NoteTaskController.ACTION_CREATE_NOTE).apply {
setPackage(info.packageName)
// 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)
addFlags(FLAG_ACTIVITY_NEW_TASK)
// We should ensure the note experience can be open both as a full screen (lock screen)
// and inside the app bubble (contextual). These additional flags will do that.
if (info.launchMode == NoteTaskLaunchMode.Activity) {
addFlags(FLAG_ACTIVITY_MULTIPLE_TASK)
addFlags(FLAG_ACTIVITY_NEW_DOCUMENT)
}
}
private inline fun logDebug(message: () -> String) {
if (Build.IS_DEBUGGABLE) {
Log.d(NoteTaskController.TAG, message())
}
}