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