diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index a4252bbe1d68f..30117d9880911 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -40,6 +40,7 @@ import com.android.systemui.reardisplay.RearDisplayDialogController import com.android.systemui.recents.Recents import com.android.systemui.settings.dagger.MultiUserUtilsModule import com.android.systemui.shortcut.ShortcutKeyDispatcher +import com.android.systemui.statusbar.notification.fsi.FsiChromeRepo import com.android.systemui.statusbar.notification.InstantAppNotifier import com.android.systemui.statusbar.phone.KeyguardLiftController import com.android.systemui.stylus.StylusUsiPowerStartable @@ -79,6 +80,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(ClipboardListener::class) abstract fun bindClipboardListener(sysui: ClipboardListener): CoreStartable + /** Inject into FsiChromeRepo. */ + @Binds + @IntoMap + @ClassKey(FsiChromeRepo::class) + abstract fun bindFSIChromeRepo(sysui: FsiChromeRepo): CoreStartable + /** Inject into GarbageMonitor.Service. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeRepo.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeRepo.kt new file mode 100644 index 0000000000000..b48322822c869 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeRepo.kt @@ -0,0 +1,102 @@ +package com.android.systemui.statusbar.notification.fsi + +import android.app.PendingIntent +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.RemoteException +import android.service.dreams.IDreamManager +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider +import com.android.systemui.statusbar.notification.fsi.FsiDebug.Companion.log +import com.android.systemui.statusbar.phone.CentralSurfaces +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * Class that bridges the gap between clean app architecture and existing code. Provides new + * implementation of StatusBarNotificationActivityStarter launchFullscreenIntent that pipes + * one-directional data => FsiChromeViewModel => FsiChromeView. + */ +@SysUISingleton +class FsiChromeRepo +@Inject +constructor( + private val context: Context, + private val pm: PackageManager, + private val keyguardRepo: KeyguardRepository, + private val launchFullScreenIntentProvider: LaunchFullScreenIntentProvider, + private val featureFlags: FeatureFlags, + private val uiBgExecutor: Executor, + private val dreamManager: IDreamManager, + private val centralSurfaces: CentralSurfaces +) : CoreStartable { + + companion object { + private const val classTag = "FsiChromeRepo" + } + + data class FSIInfo( + val appName: String, + val appIcon: Drawable, + val fullscreenIntent: PendingIntent + ) + + private val _infoFlow = MutableStateFlow(null) + val infoFlow: StateFlow = _infoFlow + + override fun start() { + log("$classTag start listening for FSI notifications") + + // Listen for FSI launch events for the lifetime of SystemUI. + launchFullScreenIntentProvider.registerListener { entry -> launchFullscreenIntent(entry) } + } + + fun dismiss() { + _infoFlow.value = null + } + + fun onFullscreen() { + // TODO(b/243421660) implement transition from container to fullscreen + } + + fun stopScreenSaver() { + uiBgExecutor.execute { + try { + dreamManager.awaken() + } catch (e: RemoteException) { + e.printStackTrace() + } + } + } + + fun launchFullscreenIntent(entry: NotificationEntry) { + if (!featureFlags.isEnabled(Flags.FSI_CHROME)) { + return + } + if (!keyguardRepo.isKeyguardShowing()) { + return + } + stopScreenSaver() + + var appName = pm.getApplicationLabel(context.applicationInfo) as String + val appIcon = pm.getApplicationIcon(context.packageName) + val fullscreenIntent = entry.sbn.notification.fullScreenIntent + + log("FsiChromeRepo launchFullscreenIntent appName=$appName appIcon $appIcon") + _infoFlow.value = FSIInfo(appName, appIcon, fullscreenIntent) + + // If screen is off or we're showing AOD, show lockscreen. + centralSurfaces.wakeUpForFullScreenIntent() + + // Don't show HUN since we're already showing FSI. + entry.notifyFullScreenIntentLaunched() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 05bf8604c2c04..be6e0cc8a9965 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -50,6 +50,8 @@ import com.android.systemui.ActivityIntentHelper; import com.android.systemui.EventLogTags; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.assist.AssistManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.shade.ShadeController; @@ -106,6 +108,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte private final LockPatternUtils mLockPatternUtils; private final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback; private final ActivityIntentHelper mActivityIntentHelper; + private final FeatureFlags mFeatureFlags; private final MetricsLogger mMetricsLogger; private final StatusBarNotificationActivityStarterLogger mLogger; @@ -149,7 +152,8 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte NotificationPanelViewController panel, ActivityLaunchAnimator activityLaunchAnimator, NotificationLaunchAnimatorControllerProvider notificationAnimationProvider, - LaunchFullScreenIntentProvider launchFullScreenIntentProvider) { + LaunchFullScreenIntentProvider launchFullScreenIntentProvider, + FeatureFlags featureFlags) { mContext = context; mMainThreadHandler = mainThreadHandler; mUiBgExecutor = uiBgExecutor; @@ -170,6 +174,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte mLockPatternUtils = lockPatternUtils; mStatusBarRemoteInputCallback = remoteInputCallback; mActivityIntentHelper = activityIntentHelper; + mFeatureFlags = featureFlags; mMetricsLogger = metricsLogger; mLogger = logger; mOnUserInteractionCallback = onUserInteractionCallback; @@ -548,7 +553,10 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte mLogger.logFullScreenIntentSuppressedByVR(entry); return; } - + if (mFeatureFlags.isEnabled(Flags.FSI_CHROME)) { + // FsiChromeRepo runs its own implementation of launchFullScreenIntent + return; + } // Stop screensaver if the notification has a fullscreen intent. // (like an incoming phone call) mUiBgExecutor.execute(() -> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/fsi/FsiChromeRepoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/fsi/FsiChromeRepoTest.kt new file mode 100644 index 0000000000000..a6a9e51aa5550 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/fsi/FsiChromeRepoTest.kt @@ -0,0 +1,210 @@ +/* + * 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.statusbar.notification.fsi + +import android.R +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.service.dreams.IDreamManager +import android.service.notification.StatusBarNotification +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.provider.LaunchFullScreenIntentProvider +import com.android.systemui.statusbar.phone.CentralSurfaces +import java.util.concurrent.Executor +import junit.framework.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +class FsiChromeRepoTest : SysuiTestCase() { + + @Mock lateinit var centralSurfaces: CentralSurfaces + @Mock lateinit var fsiChromeRepo: FsiChromeRepo + @Mock lateinit var packageManager: PackageManager + + var keyguardRepo = FakeKeyguardRepository() + @Mock private lateinit var applicationInfo: ApplicationInfo + + @Mock lateinit var launchFullScreenIntentProvider: LaunchFullScreenIntentProvider + var featureFlags = FakeFeatureFlags() + @Mock lateinit var dreamManager: IDreamManager + + // Execute all foreground & background requests immediately + private val uiBgExecutor = Executor { r -> r.run() } + + private val appName: String = "appName" + private val appIcon: Drawable = context.getDrawable(com.android.systemui.R.drawable.ic_android) + private val fsi: PendingIntent = Mockito.mock(PendingIntent::class.java) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + // Set up package manager mocks + whenever(packageManager.getApplicationIcon(anyString())).thenReturn(appIcon) + whenever(packageManager.getApplicationIcon(any(ApplicationInfo::class.java))) + .thenReturn(appIcon) + whenever(packageManager.getApplicationLabel(any())).thenReturn(appName) + mContext.setMockPackageManager(packageManager) + + fsiChromeRepo = + FsiChromeRepo( + mContext, + packageManager, + keyguardRepo, + launchFullScreenIntentProvider, + featureFlags, + uiBgExecutor, + dreamManager, + centralSurfaces + ) + } + + private fun createFsiEntry(fsi: PendingIntent): NotificationEntry { + val nb = + Notification.Builder(mContext, "a") + .setContentTitle("foo") + .setSmallIcon(R.drawable.sym_def_app_icon) + .setFullScreenIntent(fsi, /* highPriority= */ true) + + val sbn = + StatusBarNotification( + "pkg", + "opPkg", + /* id= */ 0, + "tag" + System.currentTimeMillis(), + /* uid= */ 0, + /* initialPid */ 0, + nb.build(), + UserHandle(0), + /* overrideGroupKey= */ null, + /* postTime= */ 0 + ) + + val entry = Mockito.mock(NotificationEntry::class.java) + whenever(entry.importance).thenReturn(NotificationManager.IMPORTANCE_HIGH) + whenever(entry.sbn).thenReturn(sbn) + return entry + } + + @Test + fun testLaunchFullscreenIntent_flagNotEnabled_noLaunch() { + // Setup + featureFlags.set(Flags.FSI_CHROME, false) + + // Test + val entry = createFsiEntry(fsi) + fsiChromeRepo.launchFullscreenIntent(entry) + + // Verify + Mockito.verify(centralSurfaces, never()).wakeUpForFullScreenIntent() + } + + @Test + fun testLaunchFullscreenIntent_notOnKeyguard_noLaunch() { + // Setup + featureFlags.set(Flags.FSI_CHROME, true) + keyguardRepo.setKeyguardShowing(false) + + // Test + val entry = createFsiEntry(fsi) + fsiChromeRepo.launchFullscreenIntent(entry) + + // Verify + Mockito.verify(centralSurfaces, never()).wakeUpForFullScreenIntent() + } + + @Test + fun testLaunchFullscreenIntent_stopsScreensaver() { + // Setup + featureFlags.set(Flags.FSI_CHROME, true) + keyguardRepo.setKeyguardShowing(true) + + // Test + val entry = createFsiEntry(fsi) + fsiChromeRepo.launchFullscreenIntent(entry) + + // Verify + Mockito.verify(dreamManager, times(1)).awaken() + } + + @Test + fun testLaunchFullscreenIntent_updatesFsiInfoFlow() { + // Setup + featureFlags.set(Flags.FSI_CHROME, true) + keyguardRepo.setKeyguardShowing(true) + + // Test + val entry = createFsiEntry(fsi) + fsiChromeRepo.launchFullscreenIntent(entry) + + // Verify + val expectedFsiInfo = FsiChromeRepo.FSIInfo(appName, appIcon, fsi) + assertEquals(expectedFsiInfo, fsiChromeRepo.infoFlow.value) + } + + @Test + fun testLaunchFullscreenIntent_notifyFsiLaunched() { + // Setup + featureFlags.set(Flags.FSI_CHROME, true) + keyguardRepo.setKeyguardShowing(true) + + // Test + val entry = createFsiEntry(fsi) + fsiChromeRepo.launchFullscreenIntent(entry) + + // Verify + Mockito.verify(entry, times(1)).notifyFullScreenIntentLaunched() + } + + @Test + fun testLaunchFullscreenIntent_wakesUpDevice() { + // Setup + featureFlags.set(Flags.FSI_CHROME, true) + keyguardRepo.setKeyguardShowing(true) + + // Test + val entry = createFsiEntry(fsi) + fsiChromeRepo.launchFullscreenIntent(entry) + + // Verify + Mockito.verify(centralSurfaces, times(1)).wakeUpForFullScreenIntent() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index cae414a3dc672..19658e6398c16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -55,6 +55,7 @@ import com.android.systemui.ActivityIntentHelper; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.assist.AssistManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -220,7 +221,8 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { mock(NotificationPanelViewController.class), mActivityLaunchAnimator, notificationAnimationProvider, - mock(LaunchFullScreenIntentProvider.class) + mock(LaunchFullScreenIntentProvider.class), + mock(FeatureFlags.class) ); // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg