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
This commit is contained in:
Bryce Lee
2022-02-01 22:02:22 -08:00
parent 2cb1f3534f
commit f55ef591ee
13 changed files with 1231 additions and 24 deletions

View File

@@ -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,

View File

@@ -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<? extends Parcelable> 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;
}

View File

@@ -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<SmartspaceTargetFilter>,
@Named(DREAM_SMARTSPACE_DATA_PLUGIN) optionalPlugin: Optional<BcSmartspaceDataPlugin>
) {
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<BcSmartspaceDataPlugin.SmartspaceTargetListener>()
// Smartspace can be used on multiple displays, such as when the user casts their screen
private var smartspaceViews = mutableSetOf<SmartspaceView>()
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()
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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?
}

View File

@@ -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
}
}
}

View File

@@ -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<SmartspaceTargetFilter.Listener> = 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() }
}
}

View File

@@ -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<SmartspacePrecondition.Listener>()
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
}
}

View File

@@ -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<DreamOverlayStateController.Callback> dreamCallbackCaptor =
ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
verify(mDreamOverlayStateController).addCallback(dreamCallbackCaptor.capture());
when(mDreamOverlayStateController.isOverlayActive()).thenReturn(true);
dreamCallbackCaptor.getValue().onStateChanged();
final ArgumentCaptor<BcSmartspaceDataPlugin.SmartspaceTargetListener> 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));
}
}

View File

@@ -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<SmartspaceSession.OnTargetsAvailableListener> {
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<List<SmartspaceTarget>> {
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<View.OnAttachStateChangeListener> {
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()
}
}

View File

@@ -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()
}
}

View File

@@ -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<UserTracker.Callback> {
verify(userTracker).addCallback(capture(), any())
}
clearInvocations(listener)
userTrackerCallback.onUserChanged(0, context)
verify(listener).onCriteriaChanged()
}
}