From f55ef591eedc3748d011d625c563fc9ed9b0a49d Mon Sep 17 00:00:00 2001 From: Bryce Lee Date: Tue, 1 Feb 2022 22:02:22 -0800 Subject: [PATCH] Dedicated Smartspace handling for Dreams. This changelist introduces a separate DreamSmartSpaceController to handle smartspace sessions for dreams. The separation from the lockscreen logic allows for dream specific filtering and loading logic. Test: atest DreamSmartspaceControllerTest LockscreenPreconditionTest LockscreenTargetFilterTest Bug: 217562997 Change-Id: Ib857cfaf9614c27810110e569525ae6d88613429 --- .../systemui/dagger/SystemUIModule.java | 2 + .../dreams/SmartSpaceComplication.java | 45 ++-- .../smartspace/DreamsSmartspaceController.kt | 219 ++++++++++++++++++ .../smartspace/SmartspacePrecondition.kt | 45 ++++ .../smartspace/SmartspaceTargetFilter.kt | 49 ++++ .../smartspace/dagger/SmartspaceModule.kt | 72 ++++++ .../dagger/SmartspaceViewComponent.kt | 84 +++++++ .../filters/LockscreenTargetFilter.kt | 152 ++++++++++++ .../preconditions/LockscreenPrecondition.kt | 95 ++++++++ .../dreams/SmartSpaceComplicationTest.java | 30 ++- .../DreamSmartspaceControllerTest.kt | 181 +++++++++++++++ .../smartspace/LockscreenPreconditionTest.kt | 132 +++++++++++ .../smartspace/LockscreenTargetFilterTest.kt | 149 ++++++++++++ 13 files changed, 1231 insertions(+), 24 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamsSmartspaceController.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartspace/SmartspacePrecondition.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartspace/SmartspaceTargetFilter.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt create mode 100644 packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 8e323051012d6..166c2654cbaa8 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -48,6 +48,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.recents.Recents; import com.android.systemui.screenshot.dagger.ScreenshotModule; import com.android.systemui.settings.dagger.SettingsModule; +import com.android.systemui.smartspace.dagger.SmartspaceModule; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -119,6 +120,7 @@ import dagger.Provides; SettingsModule.class, SettingsUtilModule.class, SmartRepliesInflationModule.class, + SmartspaceModule.class, StatusBarPolicyModule.class, StatusBarWindowModule.class, SysUIConcurrencyModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java index a5dcd39264df0..a83e006dfa2f9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java @@ -17,6 +17,7 @@ package com.android.systemui.dreams; import android.content.Context; +import android.os.Parcelable; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -25,7 +26,10 @@ import com.android.systemui.CoreStartable; import com.android.systemui.dreams.complication.Complication; import com.android.systemui.dreams.complication.ComplicationLayoutParams; import com.android.systemui.dreams.complication.ComplicationViewModel; -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; +import com.android.systemui.dreams.smartspace.DreamsSmartspaceController; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; + +import java.util.List; import javax.inject.Inject; @@ -39,10 +43,22 @@ public class SmartSpaceComplication implements Complication { * SystemUI. */ public static class Registrant extends CoreStartable { - private final LockscreenSmartspaceController mSmartSpaceController; + private final DreamsSmartspaceController mSmartSpaceController; private final DreamOverlayStateController mDreamOverlayStateController; private final SmartSpaceComplication mComplication; + private final BcSmartspaceDataPlugin.SmartspaceTargetListener mSmartspaceListener = + new BcSmartspaceDataPlugin.SmartspaceTargetListener() { + @Override + public void onSmartspaceTargetsUpdated(List targets) { + if (!targets.isEmpty()) { + mDreamOverlayStateController.addComplication(mComplication); + } else { + mDreamOverlayStateController.removeComplication(mComplication); + } + } + }; + /** * Default constructor for {@link SmartSpaceComplication}. */ @@ -50,7 +66,7 @@ public class SmartSpaceComplication implements Complication { public Registrant(Context context, DreamOverlayStateController dreamOverlayStateController, SmartSpaceComplication smartSpaceComplication, - LockscreenSmartspaceController smartSpaceController) { + DreamsSmartspaceController smartSpaceController) { super(context); mDreamOverlayStateController = dreamOverlayStateController; mComplication = smartSpaceComplication; @@ -59,32 +75,27 @@ public class SmartSpaceComplication implements Complication { @Override public void start() { - addOrRemoveOverlay(); mDreamOverlayStateController.addCallback(new DreamOverlayStateController.Callback() { @Override public void onStateChanged() { - addOrRemoveOverlay(); + if (mDreamOverlayStateController.isOverlayActive()) { + mSmartSpaceController.addListener(mSmartspaceListener); + } else { + mSmartSpaceController.removeListener(mSmartspaceListener); + } } }); } - - private void addOrRemoveOverlay() { - if (mDreamOverlayStateController.isPreviewMode()) { - mDreamOverlayStateController.removeComplication(mComplication); - } else if (mSmartSpaceController.isEnabled()) { - mDreamOverlayStateController.addComplication(mComplication); - } - } } private static class SmartSpaceComplicationViewHolder implements ViewHolder { private static final int SMARTSPACE_COMPLICATION_WEIGHT = 10; - private final LockscreenSmartspaceController mSmartSpaceController; + private final DreamsSmartspaceController mSmartSpaceController; private final Context mContext; protected SmartSpaceComplicationViewHolder( Context context, - LockscreenSmartspaceController smartSpaceController) { + DreamsSmartspaceController smartSpaceController) { mSmartSpaceController = smartSpaceController; mContext = context; } @@ -109,12 +120,12 @@ public class SmartSpaceComplication implements Complication { } } - private final LockscreenSmartspaceController mSmartSpaceController; + private final DreamsSmartspaceController mSmartSpaceController; private final Context mContext; @Inject public SmartSpaceComplication(Context context, - LockscreenSmartspaceController smartSpaceController) { + DreamsSmartspaceController smartSpaceController) { mContext = context; mSmartSpaceController = smartSpaceController; } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamsSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamsSmartspaceController.kt new file mode 100644 index 0000000000000..4e228a14fc88c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/smartspace/DreamsSmartspaceController.kt @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.smartspace + +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.content.Context +import android.graphics.Color +import android.util.Log +import android.view.View +import android.view.ViewGroup +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView +import com.android.systemui.smartspace.SmartspacePrecondition +import com.android.systemui.smartspace.SmartspaceTargetFilter +import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_DATA_PLUGIN +import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_PRECONDITION +import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DREAM_SMARTSPACE_TARGET_FILTER +import com.android.systemui.smartspace.dagger.SmartspaceViewComponent +import com.android.systemui.util.concurrency.Execution +import java.lang.RuntimeException +import java.util.Optional +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Named + +/** + * Controller for managing the smartspace view on the dream + */ +@SysUISingleton +class DreamsSmartspaceController @Inject constructor( + private val context: Context, + private val smartspaceManager: SmartspaceManager, + private val execution: Execution, + @Main private val uiExecutor: Executor, + private val smartspaceViewComponentFactory: SmartspaceViewComponent.Factory, + @Named(DREAM_SMARTSPACE_PRECONDITION) private val precondition: SmartspacePrecondition, + @Named(DREAM_SMARTSPACE_TARGET_FILTER) + private val optionalTargetFilter: Optional, + @Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional +) { + companion object { + private const val TAG = "DreamsSmartspaceCtrlr" + } + + private var session: SmartspaceSession? = null + private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null) + private var targetFilter: SmartspaceTargetFilter? = optionalTargetFilter.orElse(null) + + // A shadow copy of listeners is maintained to track whether the session should remain open. + private var listeners = mutableSetOf() + + // Smartspace can be used on multiple displays, such as when the user casts their screen + private var smartspaceViews = mutableSetOf() + + var preconditionListener = object : SmartspacePrecondition.Listener { + override fun onCriteriaChanged() { + reloadSmartspace() + } + } + + init { + precondition.addListener(preconditionListener) + } + + var filterListener = object : SmartspaceTargetFilter.Listener { + override fun onCriteriaChanged() { + reloadSmartspace() + } + } + + init { + targetFilter?.addListener(filterListener) + } + + var stateChangeListener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + val view = v as SmartspaceView + // Until there is dream color matching + view.setPrimaryTextColor(Color.WHITE) + smartspaceViews.add(view) + connectSession() + } + + override fun onViewDetachedFromWindow(v: View) { + smartspaceViews.remove(v as SmartspaceView) + + if (smartspaceViews.isEmpty()) { + disconnect() + } + } + } + + private val sessionListener = SmartspaceSession.OnTargetsAvailableListener { targets -> + execution.assertIsMainThread() + + val filteredTargets = targets.filter { targetFilter?.filterSmartspaceTarget(it) ?: true } + plugin?.onTargetsAvailable(filteredTargets) + } + + /** + * Constructs the smartspace view and connects it to the smartspace service. + */ + fun buildAndConnectView(parent: ViewGroup): View? { + execution.assertIsMainThread() + + if (!precondition.conditionsMet()) { + throw RuntimeException("Cannot build view when not enabled") + } + + val view = buildView(parent) + connectSession() + + return view + } + + private fun buildView(parent: ViewGroup): View? { + return if (plugin != null) { + var view = smartspaceViewComponentFactory.create(parent, plugin, stateChangeListener) + .getView() + + if (view is View) { + return view + } + + return null + } else { + null + } + } + + private fun hasActiveSessionListeners(): Boolean { + return smartspaceViews.isNotEmpty() || listeners.isNotEmpty() + } + + private fun connectSession() { + if (plugin == null || session != null || !hasActiveSessionListeners()) { + return + } + + if (!precondition.conditionsMet()) { + return + } + + // TODO(b/217559844): Replace with "dream" session when available. + val newSession = smartspaceManager.createSmartspaceSession( + SmartspaceConfig.Builder(context, "lockscreen").build()) + Log.d(TAG, "Starting smartspace session for dream") + newSession.addOnTargetsAvailableListener(uiExecutor, sessionListener) + this.session = newSession + + plugin.registerSmartspaceEventNotifier { + e -> session?.notifySmartspaceEvent(e) + } + + reloadSmartspace() + } + + /** + * Disconnects the smartspace view from the smartspace service and cleans up any resources. + */ + private fun disconnect() { + if (hasActiveSessionListeners()) return + + execution.assertIsMainThread() + + if (session == null) { + return + } + + session?.let { + it.removeOnTargetsAvailableListener(sessionListener) + it.close() + } + + session = null + + plugin?.registerSmartspaceEventNotifier(null) + plugin?.onTargetsAvailable(emptyList()) + Log.d(TAG, "Ending smartspace session for dream") + } + + fun addListener(listener: SmartspaceTargetListener) { + execution.assertIsMainThread() + plugin?.registerListener(listener) + listeners.add(listener) + + connectSession() + } + + fun removeListener(listener: SmartspaceTargetListener) { + execution.assertIsMainThread() + plugin?.unregisterListener(listener) + listeners.remove(listener) + disconnect() + } + + private fun reloadSmartspace() { + session?.requestSmartspaceUpdate() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/SmartspacePrecondition.kt b/packages/SystemUI/src/com/android/systemui/smartspace/SmartspacePrecondition.kt new file mode 100644 index 0000000000000..aa2bcecbd224c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartspace/SmartspacePrecondition.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.smartspace + +/** + * A {@link SmartspacePrecondition} captures the conditions that must be met for Smartspace to be + * used in a particular setting. + */ +interface SmartspacePrecondition { + /** + * A callback for receiving updates when conditions have changed. + */ + interface Listener { + fun onCriteriaChanged() + } + + /** + * Adds a listener to receive future updates. {@link Listener#onCriteriaChanged} will be called + * immediately upon adding. + */ + fun addListener(listener: Listener) + + /** + * Removes a listener from receiving future updates. + */ + fun removeListener(listener: Listener) + + /** + * Returns whether all conditions have been met. + */ + fun conditionsMet(): Boolean +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/SmartspaceTargetFilter.kt b/packages/SystemUI/src/com/android/systemui/smartspace/SmartspaceTargetFilter.kt new file mode 100644 index 0000000000000..7228550e4e41d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartspace/SmartspaceTargetFilter.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.smartspace + +import android.app.smartspace.SmartspaceTarget + +/** + * {@link SmartspaceTargetFilter} defines a way to locally filter targets from inclusion. This + * should be used for filtering that isn't available further upstream. + */ +interface SmartspaceTargetFilter { + /** + * An interface implemented by clients to receive updates when the filtering criteria changes. + * When this happens, the client should refresh their target set. + */ + interface Listener { + fun onCriteriaChanged() + } + + /** + * Adds a listener to receive future updates. {@link Listener#onCriteriaChanged} will be + * invoked immediately after. + */ + fun addListener(listener: Listener) + + /** + * Removes listener from receiving future updates. + */ + fun removeListener(listener: Listener) + + /** + * Returns {@code true} if the {@link SmartspaceTarget} should be included in the current + * target set, {@code false} otherwise. + */ + fun filterSmartspaceTarget(t: SmartspaceTarget): Boolean +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt new file mode 100644 index 0000000000000..1b74ac36ebf0d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceModule.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.smartspace.dagger + +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.smartspace.SmartspacePrecondition +import com.android.systemui.smartspace.SmartspaceTargetFilter +import com.android.systemui.smartspace.filters.LockscreenTargetFilter +import com.android.systemui.smartspace.preconditions.LockscreenPrecondition +import dagger.Binds +import dagger.BindsOptionalOf +import dagger.Module +import javax.inject.Named + +@Module(subcomponents = [SmartspaceViewComponent::class]) +abstract class SmartspaceModule { + @Module + companion object { + /** + * The BcSmartspaceDataProvider for dreams. + */ + const val DREAM_SMARTSPACE_DATA_PLUGIN = "dreams_smartspace_data_plugin" + + /** + * The lockscreen smartspace target filter. + */ + const val LOCKSCREEN_SMARTSPACE_TARGET_FILTER = "lockscreen_smartspace_target_filter" + + /** + * The dream smartspace target filter. + */ + const val DREAM_SMARTSPACE_TARGET_FILTER = "dream_smartspace_target_filter" + + /** + * The precondition for dream smartspace + */ + const val DREAM_SMARTSPACE_PRECONDITION = "dream_smartspace_precondition" + } + + @BindsOptionalOf + @Named(DREAM_SMARTSPACE_TARGET_FILTER) + abstract fun optionalDreamSmartspaceTargetFilter(): SmartspaceTargetFilter? + + @BindsOptionalOf + @Named(DREAM_SMARTSPACE_DATA_PLUGIN) + abstract fun optionalDreamsBcSmartspaceDataPlugin(): BcSmartspaceDataPlugin? + + @Binds + @Named(LOCKSCREEN_SMARTSPACE_TARGET_FILTER) + abstract fun provideLockscreenSmartspaceTargetFilter( + filter: LockscreenTargetFilter? + ): SmartspaceTargetFilter? + + @Binds + @Named(DREAM_SMARTSPACE_PRECONDITION) + abstract fun bindSmartspacePrecondition( + lockscreenPrecondition: LockscreenPrecondition? + ): SmartspacePrecondition? +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt new file mode 100644 index 0000000000000..d3ae198e8e35c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartspace/dagger/SmartspaceViewComponent.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.smartspace.dagger + +import android.app.PendingIntent +import android.content.Intent +import android.view.View +import android.view.ViewGroup +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.smartspace.dagger.SmartspaceViewComponent.SmartspaceViewModule.PLUGIN +import dagger.BindsInstance +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import javax.inject.Named + +@Subcomponent(modules = [SmartspaceViewComponent.SmartspaceViewModule::class]) +interface SmartspaceViewComponent { + @Subcomponent.Factory + interface Factory { + fun create( + @BindsInstance parent: ViewGroup, + @BindsInstance @Named(PLUGIN) plugin: BcSmartspaceDataPlugin, + @BindsInstance onAttachListener: View.OnAttachStateChangeListener + ): SmartspaceViewComponent + } + + fun getView(): BcSmartspaceDataPlugin.SmartspaceView + + @Module + object SmartspaceViewModule { + const val PLUGIN = "plugin" + + @Provides + fun providesSmartspaceView( + activityStarter: ActivityStarter, + falsingManager: FalsingManager, + parent: ViewGroup, + @Named(PLUGIN) plugin: BcSmartspaceDataPlugin, + onAttachListener: View.OnAttachStateChangeListener + ): + BcSmartspaceDataPlugin.SmartspaceView { + val ssView = plugin.getView(parent) + ssView.registerDataProvider(plugin) + + ssView.setIntentStarter(object : BcSmartspaceDataPlugin.IntentStarter { + override fun startIntent(view: View, intent: Intent, showOnLockscreen: Boolean) { + activityStarter.startActivity( + intent, + true, /* dismissShade */ + null, /* launch animator */ + showOnLockscreen + ) + } + + override fun startPendingIntent(pi: PendingIntent, showOnLockscreen: Boolean) { + if (showOnLockscreen) { + pi.send() + } else { + activityStarter.startPendingIntentDismissingKeyguard(pi) + } + } + }) + (ssView as View).addOnAttachStateChangeListener(onAttachListener) + ssView.setFalsingManager(falsingManager) + return ssView + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt b/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt new file mode 100644 index 0000000000000..6ad490169c17d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.smartspace.filters + +import android.app.smartspace.SmartspaceTarget +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.settings.UserTracker +import com.android.systemui.smartspace.SmartspaceTargetFilter +import com.android.systemui.util.concurrency.Execution +import com.android.systemui.util.settings.SecureSettings +import java.util.concurrent.Executor +import javax.inject.Inject + +/** + * {@link SmartspaceTargetFilter} for smartspace targets that show above the lockscreen. + */ +class LockscreenTargetFilter @Inject constructor( + private val secureSettings: SecureSettings, + private val userTracker: UserTracker, + private val execution: Execution, + @Main private val handler: Handler, + private val contentResolver: ContentResolver, + @Main private val uiExecutor: Executor +) : SmartspaceTargetFilter { + private var listeners: MutableSet = mutableSetOf() + private var showSensitiveContentForCurrentUser = false + set(value) { + val existing = field + field = value + if (existing != field) { + listeners.forEach { it.onCriteriaChanged() } + } + } + private var showSensitiveContentForManagedUser = false + set(value) { + val existing = field + field = value + if (existing != field) { + listeners.forEach { it.onCriteriaChanged() } + } + } + + private val settingsObserver = object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + execution.assertIsMainThread() + updateUserContentSettings() + } + } + + private var managedUserHandle: UserHandle? = null + + override fun addListener(listener: SmartspaceTargetFilter.Listener) { + listeners.add(listener) + + if (listeners.size != 1) { + return + } + + userTracker.addCallback(userTrackerCallback, uiExecutor) + + contentResolver.registerContentObserver( + secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS), + true, + settingsObserver, + UserHandle.USER_ALL + ) + + updateUserContentSettings() + } + + override fun removeListener(listener: SmartspaceTargetFilter.Listener) { + listeners.remove(listener) + + if (listeners.isNotEmpty()) { + return + } + + userTracker.removeCallback(userTrackerCallback) + contentResolver.unregisterContentObserver(settingsObserver) + } + + override 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 val userTrackerCallback = object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + execution.assertIsMainThread() + updateUserContentSettings() + } + } + + private fun getWorkProfileUser(): UserHandle? { + for (userInfo in userTracker.userProfiles) { + if (userInfo.isManagedProfile) { + return userInfo.userHandle + } + } + return null + } + + private fun updateUserContentSettings() { + 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 + } + + listeners.forEach { it.onCriteriaChanged() } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt b/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt new file mode 100644 index 0000000000000..1302ec9dbc559 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/smartspace/preconditions/LockscreenPrecondition.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.smartspace.preconditions + +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.smartspace.SmartspacePrecondition +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.util.concurrency.Execution +import javax.inject.Inject + +/** + * {@link LockscreenPrecondition} covers the conditions that must be met before Smartspace can be + * used over lockscreen. These conditions include the device being provisioned with a setup user + * and the Smartspace feature flag enabled. + */ +class LockscreenPrecondition @Inject constructor( + private val featureFlags: FeatureFlags, + private val deviceProvisionedController: DeviceProvisionedController, + private val execution: Execution +) : SmartspacePrecondition { + private var listeners = mutableSetOf() + + private val deviceProvisionedListener = + object : DeviceProvisionedController.DeviceProvisionedListener { + override fun onDeviceProvisionedChanged() { + updateDeviceReadiness() + } + + override fun onUserSetupChanged() { + updateDeviceReadiness() + } + } + + init { + deviceProvisionedController.addCallback(deviceProvisionedListener) + } + + var deviceReady: Boolean = false + private set + + init { + updateDeviceReadiness() + } + + private fun updateDeviceReadiness() { + if (deviceReady) { + return + } + + deviceReady = deviceProvisionedController.isDeviceProvisioned && + deviceProvisionedController.isCurrentUserSetup + + if (!deviceReady) { + return + } + + deviceProvisionedController.removeCallback(deviceProvisionedListener) + synchronized(listeners) { + listeners.forEach { it.onCriteriaChanged() } + } + } + + override fun addListener(listener: SmartspacePrecondition.Listener) { + synchronized(listeners) { + listeners += listener + } + // Always trigger a targeted callback upon addition of listener. + listener.onCriteriaChanged() + } + + override fun removeListener(listener: SmartspacePrecondition.Listener) { + synchronized(listeners) { + listeners -= listener + } + } + + override fun conditionsMet(): Boolean { + execution.assertIsMainThread() + return featureFlags.isEnabled(Flags.SMARTSPACE) && deviceReady + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java index 3b17a8071cb2a..ed1cf69ad8c02 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java @@ -15,26 +15,31 @@ */ package com.android.systemui.dreams; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.smartspace.SmartspaceTarget; import android.content.Context; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; +import com.android.systemui.dreams.smartspace.DreamsSmartspaceController; +import com.android.systemui.plugins.BcSmartspaceDataPlugin; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.Arrays; + @SmallTest @RunWith(AndroidTestingRunner.class) public class SmartSpaceComplicationTest extends SysuiTestCase { @@ -42,7 +47,7 @@ public class SmartSpaceComplicationTest extends SysuiTestCase { private Context mContext; @Mock - private LockscreenSmartspaceController mSmartspaceController; + private DreamsSmartspaceController mSmartspaceController; @Mock private DreamOverlayStateController mDreamOverlayStateController; @@ -60,7 +65,6 @@ public class SmartSpaceComplicationTest extends SysuiTestCase { */ @Test public void testAvailability() { - when(mSmartspaceController.isEnabled()).thenReturn(false); final SmartSpaceComplication.Registrant registrant = new SmartSpaceComplication.Registrant( mContext, @@ -68,10 +72,22 @@ public class SmartSpaceComplicationTest extends SysuiTestCase { mComplication, mSmartspaceController); registrant.start(); - verify(mDreamOverlayStateController, never()).addComplication(any()); + verify(mDreamOverlayStateController, never()).addComplication(eq(mComplication)); - when(mSmartspaceController.isEnabled()).thenReturn(true); - registrant.start(); + + final ArgumentCaptor dreamCallbackCaptor = + ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class); + verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture()); + + when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true); + dreamCallbackCaptor.getValue().onStateChanged(); + + final ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(BcSmartspaceDataPlugin.SmartspaceTargetListener.class); + verify(mSmartspaceController).addListener(listenerCaptor.capture()); + + final SmartspaceTarget target = Mockito.mock(SmartspaceTarget.class); + listenerCaptor.getValue().onSmartspaceTargetsUpdated(Arrays.asList(target)); verify(mDreamOverlayStateController).addComplication(eq(mComplication)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt new file mode 100644 index 0000000000000..57803e874f935 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.smartspace + +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceTarget +import android.content.Context +import android.graphics.drawable.Drawable +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import android.view.ViewGroup +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dreams.smartspace.DreamsSmartspaceController +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.smartspace.dagger.SmartspaceViewComponent +import com.android.systemui.util.concurrency.Execution +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import java.util.Optional +import java.util.concurrent.Executor + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class DreamSmartspaceControllerTest : SysuiTestCase() { + @Mock + private lateinit var smartspaceManager: SmartspaceManager + + @Mock + private lateinit var execution: Execution + + @Mock + private lateinit var uiExecutor: Executor + + @Mock + private lateinit var viewComponentFactory: SmartspaceViewComponent.Factory + + @Mock + private lateinit var viewComponent: SmartspaceViewComponent + + @Mock + private lateinit var targetFilter: SmartspaceTargetFilter + + @Mock + private lateinit var plugin: BcSmartspaceDataPlugin + + @Mock + private lateinit var precondition: SmartspacePrecondition + + @Mock + private lateinit var smartspaceView: BcSmartspaceDataPlugin.SmartspaceView + + @Mock + private lateinit var listener: BcSmartspaceDataPlugin.SmartspaceTargetListener + + @Mock + private lateinit var session: SmartspaceSession + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + `when`(viewComponentFactory.create(any(), eq(plugin), any())) + .thenReturn(viewComponent) + `when`(viewComponent.getView()).thenReturn(smartspaceView) + `when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(session) + } + + /** + * Ensures smartspace session begins on a listener only flow. + */ + @Test + fun testConnectOnListen() { + val controller = DreamsSmartspaceController(context, + smartspaceManager, execution, uiExecutor, viewComponentFactory, precondition, + Optional.of(targetFilter), Optional.of(plugin)) + + `when`(precondition.conditionsMet()).thenReturn(true) + controller.addListener(listener) + + verify(smartspaceManager).createSmartspaceSession(any()) + + var targetListener = withArgCaptor { + verify(session).addOnTargetsAvailableListener(any(), capture()) + } + + `when`(targetFilter.filterSmartspaceTarget(any())).thenReturn(true) + + var target = Mockito.mock(SmartspaceTarget::class.java) + targetListener.onTargetsAvailable(listOf(target)) + + var targets = withArgCaptor> { + verify(plugin).onTargetsAvailable(capture()) + } + + assertThat(targets.contains(target)).isTrue() + + controller.removeListener(listener) + + verify(session).close() + } + + /** + * A class which implements SmartspaceView and extends View. This is mocked to provide the right + * object inheritance and interface implementation used in DreamSmartspaceController + */ + private class TestView(context: Context?) : View(context), + BcSmartspaceDataPlugin.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?) {} + + override fun setMediaTarget(target: SmartspaceTarget?) {} + + override fun getSelectedPage(): Int { return 0; } + } + + /** + * Ensures session begins when a view is attached. + */ + @Test + fun testConnectOnViewCreate() { + val controller = DreamsSmartspaceController(context, + smartspaceManager, execution, uiExecutor, viewComponentFactory, precondition, + Optional.of(targetFilter), + Optional.of(plugin)) + + `when`(precondition.conditionsMet()).thenReturn(true) + controller.buildAndConnectView(Mockito.mock(ViewGroup::class.java)) + + var stateChangeListener = withArgCaptor { + verify(viewComponentFactory).create(any(), eq(plugin), capture()) + } + + var mockView = Mockito.mock(TestView::class.java) + `when`(precondition.conditionsMet()).thenReturn(true) + stateChangeListener.onViewAttachedToWindow(mockView) + + verify(smartspaceManager).createSmartspaceSession(any()) + + stateChangeListener.onViewDetachedFromWindow(mockView) + + verify(session).close() + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt new file mode 100644 index 0000000000000..d29e9a66a3313 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.smartspace + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.smartspace.preconditions.LockscreenPrecondition +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.util.concurrency.Execution +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class LockscreenPreconditionTest : SysuiTestCase() { + @Mock + private lateinit var featureFlags: FeatureFlags + + @Mock + private lateinit var deviceProvisionedController: DeviceProvisionedController + + @Mock + private lateinit var execution: Execution + + @Mock + private lateinit var listener: SmartspacePrecondition.Listener + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + /** + * Ensures fully enabled state is published. + */ + @Test + fun testFullyEnabled() { + `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true) + `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true) + `when`(featureFlags.isEnabled(Mockito.eq(Flags.SMARTSPACE) ?: Flags.SMARTSPACE)) + .thenReturn(true) + val precondition = LockscreenPrecondition(featureFlags, deviceProvisionedController, + execution) + precondition.addListener(listener) + + `verify`(listener).onCriteriaChanged() + assertThat(precondition.conditionsMet()).isTrue() + } + + /** + * Ensures fully enabled state is published. + */ + @Test + fun testProvisioning() { + `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true) + `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(false) + `when`(featureFlags.isEnabled(Mockito.eq(Flags.SMARTSPACE) ?: Flags.SMARTSPACE)) + .thenReturn(true) + val precondition = + LockscreenPrecondition(featureFlags, deviceProvisionedController, execution) + precondition.addListener(listener) + + verify(listener).onCriteriaChanged() + assertThat(precondition.conditionsMet()).isFalse() + + var argumentCaptor = ArgumentCaptor.forClass(DeviceProvisionedController + .DeviceProvisionedListener::class.java) + verify(deviceProvisionedController).addCallback(argumentCaptor.capture()) + + Mockito.clearInvocations(listener) + + `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true) + argumentCaptor.value.onDeviceProvisionedChanged() + verify(listener).onCriteriaChanged() + assertThat(precondition.conditionsMet()).isTrue() + } + + /** + * Makes sure user setup changes are propagated. + */ + @Test + fun testUserSetup() { + `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(false) + `when`(deviceProvisionedController.isDeviceProvisioned).thenReturn(true) + `when`(featureFlags.isEnabled(Mockito.eq(Flags.SMARTSPACE) ?: Flags.SMARTSPACE)) + .thenReturn(true) + val precondition = + LockscreenPrecondition(featureFlags, deviceProvisionedController, execution) + precondition.addListener(listener) + + verify(listener).onCriteriaChanged() + assertThat(precondition.conditionsMet()).isFalse() + + var argumentCaptor = ArgumentCaptor.forClass(DeviceProvisionedController + .DeviceProvisionedListener::class.java) + verify(deviceProvisionedController).addCallback(argumentCaptor.capture()) + + Mockito.clearInvocations(listener) + + `when`(deviceProvisionedController.isCurrentUserSetup).thenReturn(true) + argumentCaptor.value.onUserSetupChanged() + verify(listener).onCriteriaChanged() + assertThat(precondition.conditionsMet()).isTrue() + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt new file mode 100644 index 0000000000000..185a8384dd75d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.smartspace + +import android.app.smartspace.SmartspaceTarget +import android.content.ContentResolver +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.UserHandle +import android.provider.Settings +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.UserTracker +import com.android.systemui.smartspace.filters.LockscreenTargetFilter +import com.android.systemui.util.concurrency.Execution +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.settings.SecureSettings +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.atLeast +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import java.util.concurrent.Executor + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class LockscreenTargetFilterTest : SysuiTestCase() { + @Mock + private lateinit var secureSettings: SecureSettings + + @Mock + private lateinit var userTracker: UserTracker + + @Mock + private lateinit var execution: Execution + + @Mock + private lateinit var handler: Handler + + @Mock + private lateinit var contentResolver: ContentResolver + + @Mock + private lateinit var uiExecution: Executor + + @Mock + private lateinit var userHandle: UserHandle + + @Mock + private lateinit var listener: SmartspaceTargetFilter.Listener + + @Mock + private lateinit var lockScreenAllowPrivateNotificationsUri: Uri + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + `when`(userTracker.userHandle).thenReturn(userHandle) + `when`(secureSettings + .getUriFor(eq(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS))) + .thenReturn(lockScreenAllowPrivateNotificationsUri) + } + + /** + * Ensures {@link Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS} is + * tracked. + */ + @Test + fun testLockscreenAllowPrivateNotifications() { + var setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS + `when`(secureSettings + .getIntForUser(eq(setting) ?: setting, anyInt(), anyInt())) + .thenReturn(0) + var filter = LockscreenTargetFilter(secureSettings, userTracker, execution, handler, + contentResolver, uiExecution) + + filter.addListener(listener) + var smartspaceTarget = mock(SmartspaceTarget::class.java) + `when`(smartspaceTarget.userHandle).thenReturn(userHandle) + `when`(smartspaceTarget.isSensitive).thenReturn(true) + assertThat(filter.filterSmartspaceTarget(smartspaceTarget)).isFalse() + + var settingCaptor = ArgumentCaptor.forClass(ContentObserver::class.java) + + verify(contentResolver).registerContentObserver(eq(lockScreenAllowPrivateNotificationsUri), + anyBoolean(), settingCaptor.capture(), anyInt()) + + `when`(secureSettings + .getIntForUser(eq(setting) ?: setting, anyInt(), anyInt())) + .thenReturn(1) + + clearInvocations(listener) + settingCaptor.value.onChange(false, mock(Uri::class.java)) + verify(listener, atLeast(1)).onCriteriaChanged() + assertThat(filter.filterSmartspaceTarget(smartspaceTarget)).isTrue() + } + + /** + * Ensures user switches are tracked. + */ + @Test + fun testUserSwitchCallback() { + var setting = Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS + `when`(secureSettings + .getIntForUser(eq(setting) ?: setting, anyInt(), anyInt())) + .thenReturn(0) + var filter = LockscreenTargetFilter(secureSettings, userTracker, execution, handler, + contentResolver, uiExecution) + + filter.addListener(listener) + + var userTrackerCallback = withArgCaptor { + verify(userTracker).addCallback(capture(), any()) + } + + clearInvocations(listener) + userTrackerCallback.onUserChanged(0, context) + + verify(listener).onCriteriaChanged() + } +} \ No newline at end of file