From 7db37a067ead614dad8a8ea1434d3954e79e8a7a Mon Sep 17 00:00:00 2001 From: Ned Burns Date: Tue, 27 Apr 2021 00:08:48 -0400 Subject: [PATCH] Spin off smartspace behavior into SmartspaceController Spins off the logic for manging the smartspace view into LockscreenSmartspaceController. In addition to just cleaning things up, this allows us to expose the current contents of the smartspace to any listeners that might be interested (there's an interested listener in the following CL). Bug: 186481467 Test: atest Change-Id: I117afdc984215128ec47dfd3190d1685757c0f69 --- .../KeyguardClockSwitchController.java | 226 +------- .../dagger/FrameworkServicesModule.java | 7 + .../LockscreenSmartspaceController.kt | 277 ++++++++++ .../KeyguardClockSwitchControllerTest.java | 231 ++------ .../LockscreenSmartspaceControllerTest.kt | 518 ++++++++++++++++++ 5 files changed, 866 insertions(+), 393 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 4b71a3a8fbdc3..baf34589770c4 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -19,42 +19,22 @@ package com.android.keyguard; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import android.app.PendingIntent; import android.app.WallpaperManager; -import android.app.smartspace.SmartspaceConfig; -import android.app.smartspace.SmartspaceManager; -import android.app.smartspace.SmartspaceSession; -import android.app.smartspace.SmartspaceTarget; -import android.content.Intent; -import android.content.pm.UserInfo; import android.content.res.Resources; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Handler; -import android.os.UserHandle; -import android.provider.Settings; import android.text.TextUtils; import android.text.format.DateFormat; import android.view.View; import android.widget.FrameLayout; import android.widget.RelativeLayout; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor; import com.android.keyguard.clock.ClockManager; -import com.android.settingslib.Utils; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.BcSmartspaceDataPlugin; -import com.android.systemui.plugins.BcSmartspaceDataPlugin.IntentStarter; import com.android.systemui.plugins.ClockPlugin; -import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.FeatureFlags; +import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.PropertyAnimator; import com.android.systemui.statusbar.notification.stack.AnimationProperties; @@ -62,14 +42,10 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.util.ViewController; -import com.android.systemui.util.settings.SecureSettings; import java.util.Locale; -import java.util.Optional; import java.util.TimeZone; -import java.util.concurrent.Executor; import javax.inject.Inject; @@ -85,9 +61,8 @@ public class KeyguardClockSwitchController extends ViewController { + if ((which & WallpaperManager.FLAG_LOCK) != 0) { + mView.updateColors(getGradientColors()); } }; + private final ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin; + // If set, will replace keyguard_status_area - private BcSmartspaceDataPlugin.SmartspaceView mSmartspaceView; - private Optional mSmartspacePlugin; + private View mSmartspaceView; @Inject public KeyguardClockSwitchController( KeyguardClockSwitch keyguardClockSwitch, StatusBarStateController statusBarStateController, - SysuiColorExtractor colorExtractor, ClockManager clockManager, + SysuiColorExtractor colorExtractor, + ClockManager clockManager, KeyguardSliceViewController keyguardSliceViewController, NotificationIconAreaController notificationIconAreaController, BroadcastDispatcher broadcastDispatcher, - FeatureFlags featureFlags, - @Main Executor uiExecutor, BatteryController batteryController, - ConfigurationController configurationController, - ActivityStarter activityStarter, - FalsingManager falsingManager, KeyguardUpdateMonitor keyguardUpdateMonitor, KeyguardBypassController bypassController, - @Main Handler handler, - UserTracker userTracker, - SecureSettings secureSettings, - Optional smartspacePlugin) { + LockscreenSmartspaceController smartspaceController) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; mColorExtractor = colorExtractor; @@ -178,18 +112,10 @@ public class KeyguardClockSwitchController extends ViewController { - targets.removeIf(this::filterSmartspaceTarget); - smartspaceDataPlugin.onTargetsAvailable(targets); - }; - mSmartspaceSession.addOnTargetsAvailableListener(mUiExecutor, mSmartspaceCallback); - mSettingsObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange, Uri uri) { - reloadSmartspace(); - } - }; - - getContext().getContentResolver().registerContentObserver( - Settings.Secure.getUriFor( - Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS), - true, mSettingsObserver, UserHandle.USER_ALL); - reloadSmartspace(); - } - - float dozeAmount = mStatusBarStateController.getDozeAmount(); - mStatusBarStateListener.onDozeAmountChanged(dozeAmount, dozeAmount); - } - - @VisibleForTesting - boolean filterSmartspaceTarget(SmartspaceTarget t) { - if (!t.isSensitive()) return false; - - if (t.getUserHandle().equals(mUserTracker.getUserHandle())) { - return !mShowSensitiveContentForCurrentUser; - } - if (t.getUserHandle().equals(mManagedUserHandle)) { - return !mShowSensitiveContentForManagedUser; - } - - return false; - } - - private void reloadSmartspace() { - mManagedUserHandle = getWorkProfileUser(); - final String setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS; - - mShowSensitiveContentForCurrentUser = - mSecureSettings.getIntForUser(setting, 0, mUserTracker.getUserId()) == 1; - if (mManagedUserHandle != null) { - int id = mManagedUserHandle.getIdentifier(); - mShowSensitiveContentForManagedUser = - mSecureSettings.getIntForUser(setting, 0, id) == 1; - } - - mSmartspaceSession.requestSmartspaceUpdate(); - } - - private UserHandle getWorkProfileUser() { - for (UserInfo userInfo : mUserTracker.getUserProfiles()) { - if (userInfo.isManagedProfile()) { - return userInfo.getUserHandle(); - } - } - return null; - } - - private void updateWallpaperColor() { - if (mSmartspaceView != null) { - int color = Utils.getColorAttrDefaultColor(getContext(), R.attr.wallpaperTextColor); - mSmartspaceView.setPrimaryTextColor(color); } } @@ -356,16 +196,16 @@ public class KeyguardClockSwitchController extends ViewController +) { + private var session: SmartspaceSession? = null + private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null) + private lateinit var smartspaceView: SmartspaceView + + lateinit var view: View + private set + + private var showSensitiveContentForCurrentUser = false + private var showSensitiveContentForManagedUser = false + private var managedUserHandle: UserHandle? = null + + fun isEnabled(): Boolean { + execution.assertIsMainThread() + + return featureFlags.isSmartspaceEnabled && plugin != null + } + + /** + * Constructs the smartspace view and connects it to the smartspace service. Subsequent calls + * are idempotent until [disconnect] is called. + */ + fun buildAndConnectView(parent: ViewGroup): View { + execution.assertIsMainThread() + + if (!isEnabled()) { + throw RuntimeException("Cannot build view when not enabled") + } + + buildView(parent) + connectSession() + + return view + } + + private fun buildView(parent: ViewGroup) { + if (plugin == null || this::view.isInitialized) { + return + } + + val ssView = plugin.getView(parent) + ssView.registerDataProvider(plugin) + ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter { + override fun startIntent(v: View?, i: Intent?) { + activityStarter.startActivity(i, true /* dismissShade */) + } + + override fun startPendingIntent(pi: PendingIntent?) { + activityStarter.startPendingIntentDismissingKeyguard(pi) + } + }) + ssView.setFalsingManager(falsingManager) + + this.smartspaceView = ssView + this.view = ssView as View + + updateTextColorFromWallpaper() + statusBarStateListener.onDozeAmountChanged(0f, statusBarStateController.dozeAmount) + } + + private fun connectSession() { + if (plugin == null || session != null) { + return + } + val session = smartspaceManager.createSmartspaceSession( + SmartspaceConfig.Builder(context, "lockscreen").build()) + session.addOnTargetsAvailableListener(uiExecutor, sessionListener) + + userTracker.addCallback(userTrackerCallback, uiExecutor) + contentResolver.registerContentObserver( + secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS), + true, + settingsObserver, + UserHandle.USER_ALL + ) + configurationController.addCallback(configChangeListener) + statusBarStateController.addCallback(statusBarStateListener) + + this.session = session + + reloadSmartspace() + } + + /** + * Disconnects the smartspace view from the smartspace service and cleans up any resources. + * Calling [buildAndConnectView] again will cause the same view to be reconnected to the + * service. + */ + fun disconnect() { + execution.assertIsMainThread() + + if (session == null) { + return + } + + session?.let { + it.removeOnTargetsAvailableListener(sessionListener) + it.close() + } + userTracker.removeCallback(userTrackerCallback) + contentResolver.unregisterContentObserver(settingsObserver) + configurationController.removeCallback(configChangeListener) + statusBarStateController.removeCallback(statusBarStateListener) + session = null + + plugin?.onTargetsAvailable(emptyList()) + } + + fun addListener(listener: SmartspaceTargetListener) { + execution.assertIsMainThread() + plugin?.registerListener(listener) + } + + fun removeListener(listener: SmartspaceTargetListener) { + execution.assertIsMainThread() + plugin?.unregisterListener(listener) + } + + private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets -> + execution.assertIsMainThread() + val filteredTargets = targets.filter(::filterSmartspaceTarget) + plugin?.onTargetsAvailable(filteredTargets) + } + + private val userTrackerCallback = object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + execution.assertIsMainThread() + reloadSmartspace() + } + + override fun onProfilesChanged(profiles: List) { + } + } + + private val settingsObserver = object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + execution.assertIsMainThread() + reloadSmartspace() + } + } + + private val configChangeListener = object : ConfigurationController.ConfigurationListener { + override fun onThemeChanged() { + execution.assertIsMainThread() + updateTextColorFromWallpaper() + } + } + + private val statusBarStateListener = object : StatusBarStateController.StateListener { + override fun onDozeAmountChanged(linear: Float, eased: Float) { + execution.assertIsMainThread() + smartspaceView.setDozeAmount(eased) + } + } + + private fun filterSmartspaceTarget(t: SmartspaceTarget): Boolean { + return when (t.userHandle) { + userTracker.userHandle -> { + !t.isSensitive || showSensitiveContentForCurrentUser + } + managedUserHandle -> { + // Really, this should be "if this managed profile is associated with the current + // active user", but we don't have a good way to check that, so instead we cheat: + // Only the primary user can have an associated managed profile, so only show + // content for the managed profile if the primary user is active + userTracker.userHandle.identifier == UserHandle.USER_SYSTEM && + (!t.isSensitive || showSensitiveContentForManagedUser) + } + else -> { + false + } + } + } + + private fun updateTextColorFromWallpaper() { + val wallpaperTextColor = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor) + smartspaceView.setPrimaryTextColor(wallpaperTextColor) + } + + private fun reloadSmartspace() { + val setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS + + showSensitiveContentForCurrentUser = + secureSettings.getIntForUser(setting, 0, userTracker.userId) == 1 + + managedUserHandle = getWorkProfileUser() + val managedId = managedUserHandle?.identifier + if (managedId != null) { + showSensitiveContentForManagedUser = + secureSettings.getIntForUser(setting, 0, managedId) == 1 + } + + session?.requestSmartspaceUpdate() + } + + private fun getWorkProfileUser(): UserHandle? { + for (userInfo in userTracker.userProfiles) { + if (userInfo.isManagedProfile) { + return userInfo.userHandle + } + } + return null + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index 8c5f74dc7c93f..98467d4816b8b 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -19,30 +19,20 @@ package com.android.keyguard; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.smartspace.SmartspaceTarget; -import android.content.Context; -import android.content.pm.UserInfo; import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.UserHandle; -import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; -import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; import android.widget.RelativeLayout; -import androidx.annotation.Nullable; +import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor; import com.android.keyguard.clock.ClockManager; @@ -50,21 +40,14 @@ import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.BcSmartspaceDataPlugin; -import com.android.systemui.plugins.BcSmartspaceDataPlugin.IntentStarter; import com.android.systemui.plugins.ClockPlugin; -import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationIconAreaController; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.statusbar.policy.BatteryController; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.util.settings.SecureSettings; import org.junit.Before; import org.junit.Test; @@ -74,15 +57,12 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.verification.VerificationMode; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.Executor; - @SmallTest @RunWith(AndroidTestingRunner.class) public class KeyguardClockSwitchControllerTest extends SysuiTestCase { + @Mock + private KeyguardClockSwitch mView; @Mock private StatusBarStateController mStatusBarStateController; @Mock @@ -90,62 +70,41 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { @Mock private ClockManager mClockManager; @Mock - private KeyguardClockSwitch mView; - @Mock - private NotificationIconContainer mNotificationIcons; - @Mock - private ClockPlugin mClockPlugin; - @Mock - ColorExtractor.GradientColors mGradientColors; - @Mock KeyguardSliceViewController mKeyguardSliceViewController; @Mock - Resources mResources; - @Mock NotificationIconAreaController mNotificationIconAreaController; @Mock BroadcastDispatcher mBroadcastDispatcher; @Mock - private FeatureFlags mFeatureFlags; + BatteryController mBatteryController; @Mock - private Executor mExecutor; + KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock + KeyguardBypassController mBypassController; + @Mock + LockscreenSmartspaceController mSmartspaceController; + + @Mock + Resources mResources; + @Mock + private ClockPlugin mClockPlugin; + @Mock + ColorExtractor.GradientColors mGradientColors; + + @Mock + private NotificationIconContainer mNotificationIcons; @Mock private AnimatableClockView mClockView; @Mock private AnimatableClockView mLargeClockView; @Mock private FrameLayout mLargeClockFrame; - @Mock - BatteryController mBatteryController; - @Mock - ConfigurationController mConfigurationController; - @Mock - Optional mOptionalSmartspaceDataProvider; - @Mock - BcSmartspaceDataPlugin mSmartspaceDataProvider; - @Mock - SmartspaceView mSmartspaceView; - @Mock - ActivityStarter mActivityStarter; - @Mock - FalsingManager mFalsingManager; - @Mock - KeyguardUpdateMonitor mKeyguardUpdateMonitor; - @Mock - KeyguardBypassController mBypassController; - @Mock - Handler mHandler; - @Mock - UserTracker mUserTracker; - @Mock - SecureSettings mSecureSettings; + + private final View mFakeSmartspaceView = new View(mContext); private KeyguardClockSwitchController mController; private View mStatusArea; - private static final int USER_ID = 5; - private static final int MANAGED_USER_ID = 15; - @Before public void setup() { MockitoAnnotations.initMocks(this); @@ -162,9 +121,9 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { when(mClockView.getContext()).thenReturn(getContext()); when(mLargeClockView.getContext()).thenReturn(getContext()); - when(mFeatureFlags.isSmartspaceEnabled()).thenReturn(true); when(mView.isAttachedToWindow()).thenReturn(true); when(mResources.getString(anyInt())).thenReturn("h:mm"); + when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView); mController = new KeyguardClockSwitchController( mView, mStatusBarStateController, @@ -173,28 +132,16 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mKeyguardSliceViewController, mNotificationIconAreaController, mBroadcastDispatcher, - mFeatureFlags, - mExecutor, mBatteryController, - mConfigurationController, - mActivityStarter, - mFalsingManager, mKeyguardUpdateMonitor, mBypassController, - mHandler, - mUserTracker, - mSecureSettings, - mOptionalSmartspaceDataProvider - ); + mSmartspaceController); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); when(mColorExtractor.getColors(anyInt())).thenReturn(mGradientColors); mStatusArea = new View(getContext()); when(mView.findViewById(R.id.keyguard_status_area)).thenReturn(mStatusArea); - when(mOptionalSmartspaceDataProvider.isPresent()).thenReturn(true); - when(mOptionalSmartspaceDataProvider.get()).thenReturn(mSmartspaceDataProvider); - when(mSmartspaceDataProvider.getView(any())).thenReturn(mSmartspaceView); } @Test @@ -255,119 +202,34 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { @Test public void testSmartspaceEnabledRemovesKeyguardStatusArea() { - when(mFeatureFlags.isSmartspaceEnabled()).thenReturn(true); + when(mSmartspaceController.isEnabled()).thenReturn(true); + when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView); mController.init(); assertEquals(View.GONE, mStatusArea.getVisibility()); } - @Test - public void testSmartspaceEnabledNoDataProviderShowsKeyguardStatusArea() { - when(mFeatureFlags.isSmartspaceEnabled()).thenReturn(true); - when(mOptionalSmartspaceDataProvider.isPresent()).thenReturn(false); - mController.init(); - - assertEquals(View.VISIBLE, mStatusArea.getVisibility()); - } - @Test public void testSmartspaceDisabledShowsKeyguardStatusArea() { - when(mFeatureFlags.isSmartspaceEnabled()).thenReturn(false); + when(mSmartspaceController.isEnabled()).thenReturn(false); mController.init(); assertEquals(View.VISIBLE, mStatusArea.getVisibility()); } @Test - public void testThemeChangeNotifiesSmartspace() { + public void testDetachRemovesSmartspaceView() { + when(mSmartspaceController.isEnabled()).thenReturn(true); + when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mFakeSmartspaceView); mController.init(); - verify(mSmartspaceView).setPrimaryTextColor(anyInt()); + verify(mView).addView(eq(mFakeSmartspaceView), anyInt(), any()); - mController.getConfigurationListener().onThemeChanged(); - verify(mSmartspaceView, times(2)).setPrimaryTextColor(anyInt()); - } + ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); + verify(mView).addOnAttachStateChangeListener(listenerArgumentCaptor.capture()); - @Test - public void doNotFilterRegularTarget() { - setupPrimaryAndManagedUser(); - mController.init(); - - when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(USER_ID))).thenReturn(0); - when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(MANAGED_USER_ID))) - .thenReturn(0); - - mController.getSettingsObserver().onChange(true, null); - - SmartspaceTarget t = mock(SmartspaceTarget.class); - when(t.isSensitive()).thenReturn(false); - when(t.getUserHandle()).thenReturn(new UserHandle(USER_ID)); - assertEquals(false, mController.filterSmartspaceTarget(t)); - - reset(t); - when(t.isSensitive()).thenReturn(false); - when(t.getUserHandle()).thenReturn(new UserHandle(MANAGED_USER_ID)); - assertEquals(false, mController.filterSmartspaceTarget(t)); - } - - @Test - public void filterAllSensitiveTargetsAllUsers() { - setupPrimaryAndManagedUser(); - mController.init(); - - when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(USER_ID))).thenReturn(0); - when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(MANAGED_USER_ID))) - .thenReturn(0); - - mController.getSettingsObserver().onChange(true, null); - - SmartspaceTarget t = mock(SmartspaceTarget.class); - when(t.isSensitive()).thenReturn(true); - when(t.getUserHandle()).thenReturn(new UserHandle(USER_ID)); - assertEquals(true, mController.filterSmartspaceTarget(t)); - - reset(t); - when(t.isSensitive()).thenReturn(true); - when(t.getUserHandle()).thenReturn(new UserHandle(MANAGED_USER_ID)); - assertEquals(true, mController.filterSmartspaceTarget(t)); - } - - @Test - public void filterSensitiveManagedUserTargets() { - setupPrimaryAndManagedUser(); - mController.init(); - - when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(USER_ID))).thenReturn(1); - when(mSecureSettings.getIntForUser(anyString(), anyInt(), eq(MANAGED_USER_ID))) - .thenReturn(0); - - mController.getSettingsObserver().onChange(true, null); - - SmartspaceTarget t = mock(SmartspaceTarget.class); - when(t.isSensitive()).thenReturn(true); - when(t.getUserHandle()).thenReturn(new UserHandle(USER_ID)); - assertEquals(false, mController.filterSmartspaceTarget(t)); - - reset(t); - when(t.isSensitive()).thenReturn(true); - when(t.getUserHandle()).thenReturn(new UserHandle(MANAGED_USER_ID)); - assertEquals(true, mController.filterSmartspaceTarget(t)); - } - - private void setupPrimaryAndManagedUser() { - UserInfo userInfo = mock(UserInfo.class); - when(userInfo.isManagedProfile()).thenReturn(true); - when(userInfo.getUserHandle()).thenReturn(new UserHandle(MANAGED_USER_ID)); - when(mUserTracker.getUserProfiles()).thenReturn(List.of(userInfo)); - - when(mUserTracker.getUserId()).thenReturn(USER_ID); - when(mUserTracker.getUserHandle()).thenReturn(new UserHandle(USER_ID)); - } - - private void setupPrimaryAndNoManagedUser() { - when(mUserTracker.getUserProfiles()).thenReturn(Collections.emptyList()); - - when(mUserTracker.getUserId()).thenReturn(USER_ID); - when(mUserTracker.getUserHandle()).thenReturn(new UserHandle(USER_ID)); + listenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView); + verify(mView).removeView(mFakeSmartspaceView); } private void verifyAttachment(VerificationMode times) { @@ -377,25 +239,4 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { any(ColorExtractor.OnColorsChangedListener.class)); verify(mView, times).updateColors(mGradientColors); } - - private static class SmartspaceView extends View - implements BcSmartspaceDataPlugin.SmartspaceView { - SmartspaceView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void registerDataProvider(BcSmartspaceDataPlugin plugin) { } - - public void setPrimaryTextColor(int color) { } - - public void setDozeAmount(float amount) { } - - public void setIntentStarter(IntentStarter intentStarter) { } - - public void setFalsingManager(FalsingManager falsingManager) { } - - public void setDnd(@Nullable Drawable dndIcon, @Nullable String description) { } - - public void setNextAlarm(@Nullable Drawable dndIcon, @Nullable String description) { } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt new file mode 100644 index 0000000000000..5366858231f07 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt @@ -0,0 +1,518 @@ +/* + * Copyright (C) 2021 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.lockscreen + + +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceSession.OnTargetsAvailableListener +import android.app.smartspace.SmartspaceTarget +import android.content.ComponentName +import android.content.ContentResolver +import android.content.pm.UserInfo +import android.database.ContentObserver +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings +import android.view.View +import android.widget.FrameLayout +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.FeatureFlags +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener +import com.android.systemui.util.concurrency.FakeExecution +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.time.FakeSystemClock +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import java.util.Optional + +@SmallTest +class LockscreenSmartspaceControllerTest : SysuiTestCase() { + @Mock + private lateinit var featureFlags: FeatureFlags + @Mock + private lateinit var smartspaceManager: SmartspaceManager + @Mock + private lateinit var smartspaceSession: SmartspaceSession + @Mock + private lateinit var activityStarter: ActivityStarter + @Mock + private lateinit var falsingManager: FalsingManager + @Mock + private lateinit var secureSettings: SecureSettings + @Mock + private lateinit var userTracker: UserTracker + @Mock + private lateinit var contentResolver: ContentResolver + @Mock + private lateinit var configurationController: ConfigurationController + @Mock + private lateinit var statusBarStateController: StatusBarStateController + @Mock + private lateinit var handler: Handler + + @Mock + private lateinit var plugin: BcSmartspaceDataPlugin + @Mock + private lateinit var controllerListener: SmartspaceTargetListener + + @Captor + private lateinit var sessionListenerCaptor: ArgumentCaptor + @Captor + private lateinit var userTrackerCaptor: ArgumentCaptor + @Captor + private lateinit var settingsObserverCaptor: ArgumentCaptor + @Captor + private lateinit var configChangeListenerCaptor: ArgumentCaptor + @Captor + private lateinit var statusBarStateListenerCaptor: ArgumentCaptor + + private lateinit var sessionListener: OnTargetsAvailableListener + private lateinit var userListener: UserTracker.Callback + private lateinit var settingsObserver: ContentObserver + private lateinit var configChangeListener: ConfigurationListener + private lateinit var statusBarStateListener: StateListener + + private val clock = FakeSystemClock() + private val executor = FakeExecutor(clock) + private val execution = FakeExecution() + private val fakeParent = FrameLayout(context) + private val fakePrivateLockscreenSettingUri = Uri.Builder().appendPath("test").build() + + private val userHandlePrimary: UserHandle = UserHandle(0) + private val userHandleManaged: UserHandle = UserHandle(2) + private val userHandleSecondary: UserHandle = UserHandle(3) + + private val userList = listOf( + mockUserInfo(userHandlePrimary, isManagedProfile = false), + mockUserInfo(userHandleManaged, isManagedProfile = true), + mockUserInfo(userHandleSecondary, isManagedProfile = false) + ) + + private lateinit var controller: LockscreenSmartspaceController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + `when`(featureFlags.isSmartspaceEnabled).thenReturn(true) + + `when`(secureSettings.getUriFor(PRIVATE_LOCKSCREEN_SETTING)) + .thenReturn(fakePrivateLockscreenSettingUri) + `when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(smartspaceSession) + `when`(plugin.getView(any())).thenReturn(fakeSmartspaceView) + `when`(userTracker.userProfiles).thenReturn(userList) + `when`(statusBarStateController.dozeAmount).thenReturn(0.5f) + + setActiveUser(userHandlePrimary) + setAllowPrivateNotifications(userHandlePrimary, true) + setAllowPrivateNotifications(userHandleManaged, true) + setAllowPrivateNotifications(userHandleSecondary, true) + + controller = LockscreenSmartspaceController( + context, + featureFlags, + smartspaceManager, + activityStarter, + falsingManager, + secureSettings, + userTracker, + contentResolver, + configurationController, + statusBarStateController, + execution, + executor, + handler, + Optional.of(plugin) + ) + } + + @Test(expected = RuntimeException::class) + fun testThrowsIfFlagIsDisabled() { + // GIVEN the feature flag is disabled + `when`(featureFlags.isSmartspaceEnabled).thenReturn(false) + + // WHEN we try to build the view + controller.buildAndConnectView(fakeParent) + + // THEN an exception is thrown + } + + @Test + fun testListenersAreRegistered() { + // GIVEN a listener is added after a session is created + connectSession() + + // WHEN a listener is registered + controller.addListener(controllerListener) + + // THEN the listener is registered to the underlying plugin + verify(plugin).registerListener(controllerListener) + } + + @Test + fun testEarlyRegisteredListenersAreAttachedAfterConnected() { + // GIVEN a listener that is registered before the session is created + controller.addListener(controllerListener) + + // WHEN the session is created + connectSession() + + // THEN the listener is subsequently registered + verify(plugin).registerListener(controllerListener) + } + + @Test + fun testEmptyListIsEmittedAfterDisconnect() { + // GIVEN a registered listener on an active session + connectSession() + clearInvocations(plugin) + + // WHEN the session is closed + controller.disconnect() + + // THEN the listener receives an empty list of targets + verify(plugin).onTargetsAvailable(emptyList()) + } + + @Test + fun testUserChangeReloadsSmartspace() { + // GIVEN a connected smartspace session + connectSession() + + // WHEN the active user changes + userListener.onUserChanged(-1, context) + + // THEN we request a new smartspace update + verify(smartspaceSession).requestSmartspaceUpdate() + } + + @Test + fun testSettingsChangeReloadsSmartspace() { + // GIVEN a connected smartspace session + connectSession() + + // WHEN the lockscreen privacy setting changes + settingsObserver.onChange(true, null) + + // THEN we request a new smartspace update + verify(smartspaceSession).requestSmartspaceUpdate() + } + + @Test + fun testThemeChangeUpdatesTextColor() { + // GIVEN a connected smartspace session + connectSession() + + // WHEN the theme changes + configChangeListener.onThemeChanged() + + // We update the new text color to match the wallpaper color + verify(fakeSmartspaceView).setPrimaryTextColor(anyInt()) + } + + @Test + fun testDozeAmountChangeUpdatesView() { + // GIVEN a connected smartspace session + connectSession() + + // WHEN the doze amount changes + statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f) + + // We pass that along to the view + verify(fakeSmartspaceView).setDozeAmount(0.7f) + } + + @Test + fun testSensitiveTargetsAreNotFilteredIfAllowed() { + // GIVEN the active and managed users allow sensitive content + connectSession() + + // WHEN we receive a list of targets + val targets = listOf( + makeTarget(1, userHandlePrimary, isSensitive = true), + makeTarget(2, userHandleManaged, isSensitive = true), + makeTarget(3, userHandlePrimary, isSensitive = true) + ) + sessionListener.onTargetsAvailable(targets) + + // THEN all sensitive content is still shown + verify(plugin).onTargetsAvailable(eq(targets)) + } + + @Test + fun testNonSensitiveTargetsAreNeverFiltered() { + // GIVEN the active user doesn't allow sensitive lockscreen content + setAllowPrivateNotifications(userHandlePrimary, false) + connectSession() + + // WHEN we receive a list of targets + val targets = listOf( + makeTarget(1, userHandlePrimary), + makeTarget(2, userHandlePrimary), + makeTarget(3, userHandlePrimary) + ) + sessionListener.onTargetsAvailable(targets) + + // THEN all non-sensitive content is still shown + verify(plugin).onTargetsAvailable(eq(targets)) + } + + @Test + fun testSensitiveTargetsAreFilteredOutForAppropriateUsers() { + // GIVEN the active and managed users don't allow sensitive lockscreen content + setAllowPrivateNotifications(userHandlePrimary, false) + setAllowPrivateNotifications(userHandleManaged, false) + connectSession() + + // WHEN we receive a list of targets + val targets = listOf( + makeTarget(0, userHandlePrimary), + makeTarget(1, userHandlePrimary, isSensitive = true), + makeTarget(2, userHandleManaged, isSensitive = true), + makeTarget(3, userHandleManaged), + makeTarget(4, userHandlePrimary, isSensitive = true), + makeTarget(5, userHandlePrimary), + makeTarget(6, userHandleSecondary, isSensitive = true) + ) + sessionListener.onTargetsAvailable(targets) + + // THEN only non-sensitive content from those accounts is shown + verify(plugin).onTargetsAvailable(eq(listOf( + targets[0], + targets[3], + targets[5] + ))) + } + + @Test + fun testSettingsAreReloaded() { + // GIVEN a connected session where the privacy settings later flip to false + connectSession() + setAllowPrivateNotifications(userHandlePrimary, false) + setAllowPrivateNotifications(userHandleManaged, false) + settingsObserver.onChange(true, fakePrivateLockscreenSettingUri) + + // WHEN we receive a new list of targets + val targets = listOf( + makeTarget(1, userHandlePrimary, isSensitive = true), + makeTarget(2, userHandleManaged, isSensitive = true), + makeTarget(4, userHandlePrimary, isSensitive = true) + ) + sessionListener.onTargetsAvailable(targets) + + // THEN we filter based on the new settings values + verify(plugin).onTargetsAvailable(emptyList()) + } + + @Test + fun testRecognizeSwitchToSecondaryUser() { + // GIVEN an inactive secondary user that doesn't allow sensitive content + setAllowPrivateNotifications(userHandleSecondary, false) + connectSession() + + // WHEN the secondary user becomes the active user + setActiveUser(userHandleSecondary) + userListener.onUserChanged(userHandleSecondary.identifier, context) + + // WHEN we receive a new list of targets + val targets = listOf( + makeTarget(0, userHandlePrimary), + makeTarget(1, userHandleSecondary), + makeTarget(2, userHandleSecondary, isSensitive = true), + makeTarget(3, userHandleManaged), + makeTarget(4, userHandleSecondary), + makeTarget(5, userHandleManaged), + makeTarget(6, userHandlePrimary) + ) + sessionListener.onTargetsAvailable(targets) + + // THEN only non-sensitive content from the secondary user is shown + verify(plugin).onTargetsAvailable(listOf( + targets[1], + targets[4] + )) + } + + @Test + fun testUnregisterListenersOnCleanup() { + // GIVEN a connected session + connectSession() + + // WHEN we are told to cleanup + controller.disconnect() + + // THEN we disconnect from the session and unregister any listeners + verify(smartspaceSession).removeOnTargetsAvailableListener(sessionListener) + verify(smartspaceSession).close() + verify(userTracker).removeCallback(userListener) + verify(contentResolver).unregisterContentObserver(settingsObserver) + verify(configurationController).removeCallback(configChangeListener) + verify(statusBarStateController).removeCallback(statusBarStateListener) + } + + @Test + fun testBuildViewIsIdempotent() { + // GIVEN a connected session + connectSession() + clearInvocations(plugin) + + // WHEN we disconnect and then reconnect + controller.disconnect() + controller.buildAndConnectView(fakeParent) + + // THEN the view is not rebuilt + verify(plugin, never()).getView(any()) + assertEquals(fakeSmartspaceView, controller.view) + } + + @Test + fun testDoubleConnectIsIgnored() { + // GIVEN a connected session + connectSession() + clearInvocations(smartspaceManager) + clearInvocations(plugin) + + // WHEN we're asked to connect a second time + controller.buildAndConnectView(fakeParent) + + // THEN the existing view and session are reused + verify(smartspaceManager, never()).createSmartspaceSession(any()) + verify(plugin, never()).getView(any()) + assertEquals(fakeSmartspaceView, controller.view) + } + + private fun connectSession(): View { + val view = controller.buildAndConnectView(fakeParent) + + verify(smartspaceSession) + .addOnTargetsAvailableListener(any(), capture(sessionListenerCaptor)) + sessionListener = sessionListenerCaptor.value + + verify(userTracker).addCallback(capture(userTrackerCaptor), any()) + userListener = userTrackerCaptor.value + + verify(contentResolver).registerContentObserver( + eq(fakePrivateLockscreenSettingUri), + eq(true), + capture(settingsObserverCaptor), + eq(UserHandle.USER_ALL)) + settingsObserver = settingsObserverCaptor.value + + verify(configurationController).addCallback(configChangeListenerCaptor.capture()) + configChangeListener = configChangeListenerCaptor.value + + verify(statusBarStateController).addCallback(statusBarStateListenerCaptor.capture()) + statusBarStateListener = statusBarStateListenerCaptor.value + + verify(smartspaceSession).requestSmartspaceUpdate() + clearInvocations(smartspaceSession) + + verify(fakeSmartspaceView).setPrimaryTextColor(anyInt()) + verify(fakeSmartspaceView).setDozeAmount(0.5f) + clearInvocations(fakeSmartspaceView) + + return view + } + + private fun setActiveUser(userHandle: UserHandle) { + `when`(userTracker.userId).thenReturn(userHandle.identifier) + `when`(userTracker.userHandle).thenReturn(userHandle) + } + + private fun mockUserInfo(userHandle: UserHandle, isManagedProfile: Boolean): UserInfo { + val userInfo = mock(UserInfo::class.java) + `when`(userInfo.userHandle).thenReturn(userHandle) + `when`(userInfo.isManagedProfile).thenReturn(isManagedProfile) + return userInfo + } + + fun makeTarget( + id: Int, + userHandle: UserHandle, + isSensitive: Boolean = false + ): SmartspaceTarget { + return SmartspaceTarget.Builder( + "target$id", + ComponentName("testpackage", "testclass$id"), + userHandle) + .setSensitive(isSensitive) + .build() + } + + private fun setAllowPrivateNotifications(user: UserHandle, value: Boolean) { + `when`(secureSettings.getIntForUser( + eq(PRIVATE_LOCKSCREEN_SETTING), + anyInt(), + eq(user.identifier)) + ).thenReturn(if (value) 1 else 0) + } + + private val fakeSmartspaceView = spy(object : View(context), SmartspaceView { + override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) { + } + + override fun setPrimaryTextColor(color: Int) { + } + + override fun setDozeAmount(amount: Float) { + } + + override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) { + } + + override fun setFalsingManager(falsingManager: FalsingManager?) { + } + + override fun setDnd(image: Drawable?, description: String?) { + } + + override fun setNextAlarm(image: Drawable?, description: String?) { + } + }) +} + +private const val PRIVATE_LOCKSCREEN_SETTING = + Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS \ No newline at end of file