diff options
| author | 2021-05-12 18:30:09 +0000 | |
|---|---|---|
| committer | 2021-05-12 18:30:09 +0000 | |
| commit | 7504ce278914b9533d3b83dab442046ec822a95f (patch) | |
| tree | 4a5d2404c1435b0509d0bdb37d4e4d510ffa9077 | |
| parent | aac9987aed66c5cc28d36d2e63eaecff1718da75 (diff) | |
| parent | 7db37a067ead614dad8a8ea1434d3954e79e8a7a (diff) | |
Merge "Spin off smartspace behavior into SmartspaceController" into sc-dev
5 files changed, 862 insertions, 389 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 4b71a3a8fbdc..baf34589770c 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<KeyguardClockS private final KeyguardSliceViewController mKeyguardSliceViewController; private final NotificationIconAreaController mNotificationIconAreaController; private final BroadcastDispatcher mBroadcastDispatcher; - private final Executor mUiExecutor; private final BatteryController mBatteryController; - private final FeatureFlags mFeatureFlags; + private final LockscreenSmartspaceController mSmartspaceController; /** * Clock for both small and large sizes @@ -97,20 +72,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private AnimatableClockController mLargeClockViewController; private FrameLayout mLargeClockFrame; - private SmartspaceSession mSmartspaceSession; - private SmartspaceSession.OnTargetsAvailableListener mSmartspaceCallback; - private ConfigurationController mConfigurationController; - private ActivityStarter mActivityStarter; - private FalsingManager mFalsingManager; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final KeyguardBypassController mBypassController; - private Handler mHandler; - private UserTracker mUserTracker; - private SecureSettings mSecureSettings; - private ContentObserver mSettingsObserver; - private boolean mShowSensitiveContentForCurrentUser; - private boolean mShowSensitiveContentForManagedUser; - private UserHandle mManagedUserHandle; /** * Listener for changes to the color palette. @@ -118,59 +81,30 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS * The color palette changes when the wallpaper is changed. */ private final ColorExtractor.OnColorsChangedListener mColorsListener = - new ColorExtractor.OnColorsChangedListener() { - @Override - public void onColorsChanged(ColorExtractor extractor, int which) { - if ((which & WallpaperManager.FLAG_LOCK) != 0) { - mView.updateColors(getGradientColors()); - } - } - }; - - private final ConfigurationController.ConfigurationListener mConfigurationListener = - new ConfigurationController.ConfigurationListener() { - @Override - public void onThemeChanged() { - updateWallpaperColor(); - } - }; - - private ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin; - - private final StatusBarStateController.StateListener mStatusBarStateListener = - new StatusBarStateController.StateListener() { - @Override - public void onDozeAmountChanged(float linear, float eased) { - if (mSmartspaceView != null) { - mSmartspaceView.setDozeAmount(eased); - } + (extractor, which) -> { + 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<BcSmartspaceDataPlugin> 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<BcSmartspaceDataPlugin> smartspacePlugin) { + LockscreenSmartspaceController smartspaceController) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; mColorExtractor = colorExtractor; @@ -178,18 +112,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mKeyguardSliceViewController = keyguardSliceViewController; mNotificationIconAreaController = notificationIconAreaController; mBroadcastDispatcher = broadcastDispatcher; - mFeatureFlags = featureFlags; - mUiExecutor = uiExecutor; mBatteryController = batteryController; - mConfigurationController = configurationController; - mActivityStarter = activityStarter; - mFalsingManager = falsingManager; mKeyguardUpdateMonitor = keyguardUpdateMonitor; mBypassController = bypassController; - mHandler = handler; - mUserTracker = userTracker; - mSecureSettings = secureSettings; - mSmartspacePlugin = smartspacePlugin; + mSmartspaceController = smartspaceController; } /** @@ -232,119 +158,33 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mBypassController); mLargeClockViewController.init(); - mStatusBarStateController.addCallback(mStatusBarStateListener); - mConfigurationController.addCallback(mConfigurationListener); + if (mSmartspaceController.isEnabled()) { + mSmartspaceView = mSmartspaceController.buildAndConnectView(mView); - if (mFeatureFlags.isSmartspaceEnabled() && mSmartspacePlugin.isPresent()) { - BcSmartspaceDataPlugin smartspaceDataPlugin = mSmartspacePlugin.get(); View ksa = mView.findViewById(R.id.keyguard_status_area); int ksaIndex = mView.indexOfChild(ksa); ksa.setVisibility(View.GONE); - mSmartspaceView = smartspaceDataPlugin.getView(mView); - mSmartspaceView.registerDataProvider(smartspaceDataPlugin); - mSmartspaceView.setIntentStarter(new IntentStarter() { - public void startIntent(View v, Intent i) { - mActivityStarter.startActivity(i, true /* dismissShade */); - } - - public void startPendingIntent(PendingIntent pi) { - mActivityStarter.startPendingIntentDismissingKeyguard(pi); - } - }); - mSmartspaceView.setFalsingManager(mFalsingManager); - updateWallpaperColor(); - View asView = (View) mSmartspaceView; - // Place smartspace view below normal clock... RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams( MATCH_PARENT, WRAP_CONTENT); lp.addRule(RelativeLayout.BELOW, R.id.lockscreen_clock_view); - mView.addView(asView, ksaIndex, lp); + mView.addView(mSmartspaceView, ksaIndex, lp); int padding = getContext().getResources() .getDimensionPixelSize(R.dimen.below_clock_padding_start); - asView.setPadding(padding, 0, padding, 0); + mSmartspaceView.setPadding(padding, 0, padding, 0); // ... but above the large clock lp = new RelativeLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT); - lp.addRule(RelativeLayout.BELOW, asView.getId()); + lp.addRule(RelativeLayout.BELOW, mSmartspaceView.getId()); mLargeClockFrame.setLayoutParams(lp); View nic = mView.findViewById( R.id.left_aligned_notification_icon_container); lp = (RelativeLayout.LayoutParams) nic.getLayoutParams(); - lp.addRule(RelativeLayout.BELOW, asView.getId()); + lp.addRule(RelativeLayout.BELOW, mSmartspaceView.getId()); nic.setLayoutParams(lp); - - mSmartspaceSession = getContext().getSystemService(SmartspaceManager.class) - .createSmartspaceSession( - new SmartspaceConfig.Builder(getContext(), "lockscreen").build()); - mSmartspaceCallback = targets -> { - 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<KeyguardClockS mColorExtractor.removeOnColorsChangedListener(mColorsListener); mView.setClockPlugin(null, mStatusBarStateController.getState()); - if (mSmartspaceSession != null) { - mSmartspaceSession.removeOnTargetsAvailableListener(mSmartspaceCallback); - mSmartspaceSession.close(); - mSmartspaceSession = null; - } - mStatusBarStateController.removeCallback(mStatusBarStateListener); - mConfigurationController.removeCallback(mConfigurationListener); + mSmartspaceController.disconnect(); - if (mSettingsObserver != null) { - getContext().getContentResolver().unregisterContentObserver(mSettingsObserver); + // TODO: This is an unfortunate necessity since smartspace plugin retains a single instance + // of the smartspace view -- if we don't remove the view, it can't be reused by a later + // instance of this class. In order to fix this, we need to modify the plugin so that + // (a) we get a new view each time and (b) we can properly clean up an old view by making + // it unregister itself as a plugin listener. + if (mSmartspaceView != null) { + mView.removeView(mSmartspaceView); + mSmartspaceView = null; } } @@ -436,7 +276,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS scale, props, animate); if (mSmartspaceView != null) { - PropertyAnimator.setProperty((View) mSmartspaceView, AnimatableProperty.TRANSLATION_X, + PropertyAnimator.setProperty(mSmartspaceView, AnimatableProperty.TRANSLATION_X, x, props, animate); } @@ -510,14 +350,4 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private int getCurrentLayoutDirection() { return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()); } - - @VisibleForTesting - ConfigurationController.ConfigurationListener getConfigurationListener() { - return mConfigurationListener; - } - - @VisibleForTesting - ContentObserver getSettingsObserver() { - return mSettingsObserver; - } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index fd80d50c2894..26db33d6dea8 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -29,6 +29,7 @@ import android.app.StatsManager; import android.app.WallpaperManager; import android.app.admin.DevicePolicyManager; import android.app.role.RoleManager; +import android.app.smartspace.SmartspaceManager; import android.app.trust.TrustManager; import android.content.ContentResolver; import android.content.Context; @@ -400,4 +401,10 @@ public class FrameworkServicesModule { static PermissionManager providePermissionManager(Context context) { return context.getSystemService(PermissionManager.class); } + + @Provides + @Singleton + static SmartspaceManager provideSmartspaceManager(Context context) { + return context.getSystemService(SmartspaceManager.class); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt new file mode 100644 index 000000000000..ce60c859e9bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -0,0 +1,277 @@ +/* + * 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.PendingIntent +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceTarget +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.pm.UserInfo +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings +import android.view.View +import android.view.ViewGroup +import com.android.settingslib.Utils +import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton +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.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.settings.UserTracker +import com.android.systemui.statusbar.FeatureFlags +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.concurrency.Execution +import com.android.systemui.util.settings.SecureSettings +import java.lang.RuntimeException +import java.util.Optional +import java.util.concurrent.Executor +import javax.inject.Inject + +/** + * Controller for managing the smartspace view on the lockscreen + */ +@SysUISingleton +class LockscreenSmartspaceController @Inject constructor( + private val context: Context, + private val featureFlags: FeatureFlags, + private val smartspaceManager: SmartspaceManager, + private val activityStarter: ActivityStarter, + private val falsingManager: FalsingManager, + private val secureSettings: SecureSettings, + private val userTracker: UserTracker, + private val contentResolver: ContentResolver, + private val configurationController: ConfigurationController, + private val statusBarStateController: StatusBarStateController, + private val execution: Execution, + @Main private val uiExecutor: Executor, + @Main private val handler: Handler, + optionalPlugin: Optional<BcSmartspaceDataPlugin> +) { + 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<UserInfo>) { + } + } + + 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 8c5f74dc7c93..98467d4816b8 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,78 +57,54 @@ 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 private SysuiColorExtractor mColorExtractor; @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; - @Mock - private Executor mExecutor; - @Mock - private AnimatableClockView mClockView; - @Mock - private AnimatableClockView mLargeClockView; - @Mock - private FrameLayout mLargeClockFrame; - @Mock BatteryController mBatteryController; @Mock - ConfigurationController mConfigurationController; - @Mock - Optional<BcSmartspaceDataPlugin> mOptionalSmartspaceDataProvider; + KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock - BcSmartspaceDataPlugin mSmartspaceDataProvider; + KeyguardBypassController mBypassController; @Mock - SmartspaceView mSmartspaceView; + LockscreenSmartspaceController mSmartspaceController; + @Mock - ActivityStarter mActivityStarter; + Resources mResources; @Mock - FalsingManager mFalsingManager; + private ClockPlugin mClockPlugin; @Mock - KeyguardUpdateMonitor mKeyguardUpdateMonitor; + ColorExtractor.GradientColors mGradientColors; + @Mock - KeyguardBypassController mBypassController; + private NotificationIconContainer mNotificationIcons; @Mock - Handler mHandler; + private AnimatableClockView mClockView; @Mock - UserTracker mUserTracker; + private AnimatableClockView mLargeClockView; @Mock - SecureSettings mSecureSettings; + private FrameLayout mLargeClockFrame; + + 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()); - } - - @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()); + ArgumentCaptor<View.OnAttachStateChangeListener> listenerArgumentCaptor = + ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); + verify(mView).addOnAttachStateChangeListener(listenerArgumentCaptor.capture()); - 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 000000000000..5366858231f0 --- /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<OnTargetsAvailableListener> + @Captor + private lateinit var userTrackerCaptor: ArgumentCaptor<UserTracker.Callback> + @Captor + private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver> + @Captor + private lateinit var configChangeListenerCaptor: ArgumentCaptor<ConfigurationListener> + @Captor + private lateinit var statusBarStateListenerCaptor: ArgumentCaptor<StateListener> + + 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 |