Media Complication Introduction.

This changelist adds a complication to show media control on dream overlays.

Test: atest MediaDreamSentinelTest MediaComplicationViewControllerTest
Bug: 213906962
Change-Id: Id0d9ab8d668d815bd697ea6ca4ac34e62bdf6a42
This commit is contained in:
Bryce Lee
2022-01-26 23:45:07 -08:00
parent 5fadd87acb
commit 294de89b2a
11 changed files with 582 additions and 4 deletions

View File

@@ -31,6 +31,7 @@ import com.android.systemui.globalactions.GlobalActionsComponent;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.dagger.KeyguardModule;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.media.dream.MediaDreamSentinel;
import com.android.systemui.media.systemsounds.HomeSoundEffectController;
import com.android.systemui.power.PowerUI;
import com.android.systemui.privacy.television.TvOngoingPrivacyChip;
@@ -226,4 +227,11 @@ public abstract class SystemUIBinder {
@ClassKey(SmartSpaceComplication.Registrant.class)
public abstract CoreStartable bindSmartSpaceComplicationRegistrant(
SmartSpaceComplication.Registrant registrant);
/** Inject into MediaDreamSentinel. */
@Binds
@IntoMap
@ClassKey(MediaDreamSentinel.class)
public abstract CoreStartable bindMediaDreamSentinel(
MediaDreamSentinel sentinel);
}

View File

@@ -31,6 +31,7 @@ import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dreams.DreamOverlayStateController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.CrossFadeHelper
@@ -82,7 +83,8 @@ class MediaHierarchyManager @Inject constructor(
private val notifLockscreenUserManager: NotificationLockscreenUserManager,
configurationController: ConfigurationController,
wakefulnessLifecycle: WakefulnessLifecycle,
private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager
private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
private val dreamOverlayStateController: DreamOverlayStateController
) {
/**
@@ -167,7 +169,7 @@ class MediaHierarchyManager @Inject constructor(
})
}
private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_DREAM_OVERLAY + 1)
/**
* The last location where this view was at before going to the desired location. This is
* useful for guided transitions.
@@ -348,6 +350,17 @@ class MediaHierarchyManager @Inject constructor(
}
}
/**
* Is the doze animation currently Running
*/
private var dreamOverlayActive: Boolean = false
private set(value) {
if (field != value) {
field = value
updateDesiredLocation(forceNoAnimation = true)
}
}
/**
* The current cross fade progress. 0.5f means it's just switching
* between the start and the end location and the content is fully faded, while 0.75f means
@@ -444,6 +457,12 @@ class MediaHierarchyManager @Inject constructor(
}
})
dreamOverlayStateController.addCallback(object : DreamOverlayStateController.Callback {
override fun onStateChanged() {
dreamOverlayStateController.isOverlayActive.also { dreamOverlayActive = it }
}
})
wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
override fun onFinishedGoingToSleep() {
goingToSleep = false
@@ -940,6 +959,7 @@ class MediaHierarchyManager @Inject constructor(
statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
val allowedOnLockscreen = notifLockscreenUserManager.shouldShowLockscreenNotifications()
val location = when {
dreamOverlayActive -> LOCATION_DREAM_OVERLAY
(qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS
qsExpansion > 0.4f && onLockscreen -> LOCATION_QS
!hasActiveMedia -> LOCATION_QS
@@ -1034,6 +1054,11 @@ class MediaHierarchyManager @Inject constructor(
*/
const val LOCATION_LOCKSCREEN = 2
/**
* Attached on the dream overlay
*/
const val LOCATION_DREAM_OVERLAY = 3
/**
* Attached at the root of the hierarchy in an overlay
*/

View File

@@ -25,6 +25,7 @@ import com.android.systemui.media.MediaDataManager;
import com.android.systemui.media.MediaHierarchyManager;
import com.android.systemui.media.MediaHost;
import com.android.systemui.media.MediaHostStatesManager;
import com.android.systemui.media.dream.dagger.MediaComplicationComponent;
import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper;
import com.android.systemui.media.taptotransfer.MediaTttFlags;
import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver;
@@ -43,11 +44,14 @@ import dagger.multibindings.ClassKey;
import dagger.multibindings.IntoMap;
/** Dagger module for the media package. */
@Module
@Module(subcomponents = {
MediaComplicationComponent.class,
})
public interface MediaModule {
String QS_PANEL = "media_qs_panel";
String QUICK_QS_PANEL = "media_quick_qs_panel";
String KEYGUARD = "media_keyguard";
String DREAM = "dream";
/** */
@Provides
@@ -79,6 +83,16 @@ public interface MediaModule {
return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager);
}
/** */
@Provides
@SysUISingleton
@Named(DREAM)
static MediaHost providesDreamMediaHost(MediaHost.MediaHostStateHolder stateHolder,
MediaHierarchyManager hierarchyManager, MediaDataManager dataManager,
MediaHostStatesManager statesManager) {
return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager);
}
/** */
@Provides
@SysUISingleton

View File

@@ -0,0 +1,68 @@
/*
* 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.media.dream;
import static com.android.systemui.media.dagger.MediaModule.DREAM;
import static com.android.systemui.media.dream.dagger.MediaComplicationComponent.MediaComplicationModule.MEDIA_COMPLICATION_CONTAINER;
import android.widget.FrameLayout;
import com.android.systemui.media.MediaHierarchyManager;
import com.android.systemui.media.MediaHost;
import com.android.systemui.media.MediaHostState;
import com.android.systemui.util.ViewController;
import javax.inject.Inject;
import javax.inject.Named;
/**
* {@link MediaComplicationViewController} handles connecting the
* {@link com.android.systemui.dreams.complication.Complication} view to the {@link MediaHost}.
*/
public class MediaComplicationViewController extends ViewController<FrameLayout> {
private final MediaHost mMediaHost;
@Inject
public MediaComplicationViewController(
@Named(MEDIA_COMPLICATION_CONTAINER) FrameLayout view,
@Named(DREAM) MediaHost mediaHost) {
super(view);
mMediaHost = mediaHost;
}
@Override
protected void onInit() {
super.onInit();
mMediaHost.setExpansion(MediaHostState.COLLAPSED);
mMediaHost.setShowsOnlyActiveMedia(true);
mMediaHost.setFalsingProtectionNeeded(true);
mMediaHost.init(MediaHierarchyManager.LOCATION_DREAM_OVERLAY);
}
@Override
protected void onViewAttached() {
mMediaHost.hostView.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT));
mView.addView(mMediaHost.hostView);
}
@Override
protected void onViewDetached() {
mView.removeView(mMediaHost.hostView);
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.media.dream;
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.dreams.complication.ComplicationViewModel;
import com.android.systemui.media.dream.dagger.MediaComplicationComponent;
import javax.inject.Inject;
/**
* Media control complication for dream overlay.
*/
public class MediaDreamComplication implements Complication {
MediaComplicationComponent.Factory mComponentFactory;
/**
* Default constructor for {@link MediaDreamComplication}.
*/
@Inject
public MediaDreamComplication(MediaComplicationComponent.Factory componentFactory) {
mComponentFactory = componentFactory;
}
@Override
public ViewHolder createView(ComplicationViewModel model) {
return mComponentFactory.create().getViewHolder();
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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.media.dream;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.systemui.CoreStartable;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.media.MediaData;
import com.android.systemui.media.MediaDataManager;
import com.android.systemui.media.SmartspaceMediaData;
import javax.inject.Inject;
/**
* {@link MediaDreamSentinel} is responsible for tracking media state and registering/unregistering
* the media complication as appropriate
*/
public class MediaDreamSentinel extends CoreStartable {
private MediaDataManager.Listener mListener = new MediaDataManager.Listener() {
private boolean mAdded;
@Override
public void onSmartspaceMediaDataRemoved(@NonNull String key, boolean immediately) {
}
@Override
public void onMediaDataRemoved(@NonNull String key) {
if (!mAdded) {
return;
}
if (mMediaDataManager.hasActiveMedia()) {
return;
}
mAdded = false;
mDreamOverlayStateController.removeComplication(mComplication);
}
@Override
public void onSmartspaceMediaDataLoaded(@NonNull String key,
@NonNull SmartspaceMediaData data, boolean shouldPrioritize,
boolean isSsReactivated) {
}
@Override
public void onMediaDataLoaded(@NonNull String key, @Nullable String oldKey,
@NonNull MediaData data, boolean immediately, int receivedSmartspaceCardLatency) {
if (mAdded) {
return;
}
if (!mMediaDataManager.hasActiveMedia()) {
return;
}
mAdded = true;
mDreamOverlayStateController.addComplication(mComplication);
}
};
private final MediaDataManager mMediaDataManager;
private final DreamOverlayStateController mDreamOverlayStateController;
private final MediaDreamComplication mComplication;
@Inject
public MediaDreamSentinel(Context context, MediaDataManager mediaDataManager,
DreamOverlayStateController dreamOverlayStateController,
MediaDreamComplication complication) {
super(context);
mMediaDataManager = mediaDataManager;
mDreamOverlayStateController = dreamOverlayStateController;
mComplication = complication;
}
@Override
public void start() {
mMediaDataManager.addListener(mListener);
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.media.dream;
import static com.android.systemui.media.dream.dagger.MediaComplicationComponent.MediaComplicationModule.MEDIA_COMPLICATION_CONTAINER;
import static com.android.systemui.media.dream.dagger.MediaComplicationComponent.MediaComplicationModule.MEDIA_COMPLICATION_LAYOUT_PARAMS;
import android.view.View;
import android.widget.FrameLayout;
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.dreams.complication.ComplicationLayoutParams;
import javax.inject.Inject;
import javax.inject.Named;
/**
* {@link Complication.ViewHolder} implementation for media control.
*/
public class MediaViewHolder implements Complication.ViewHolder {
private final FrameLayout mContainer;
private final MediaComplicationViewController mViewController;
private final ComplicationLayoutParams mLayoutParams;
@Inject
MediaViewHolder(@Named(MEDIA_COMPLICATION_CONTAINER) FrameLayout container,
MediaComplicationViewController controller,
@Named(MEDIA_COMPLICATION_LAYOUT_PARAMS) ComplicationLayoutParams layoutParams) {
mContainer = container;
mViewController = controller;
mViewController.init();
mLayoutParams = layoutParams;
}
@Override
public View getView() {
return mContainer;
}
@Override
public ComplicationLayoutParams getLayoutParams() {
return mLayoutParams;
}
}

View File

@@ -0,0 +1,100 @@
/*
* 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.media.dream.dagger;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import android.content.Context;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.android.systemui.dreams.complication.ComplicationLayoutParams;
import com.android.systemui.media.dream.MediaViewHolder;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import javax.inject.Named;
import javax.inject.Scope;
import dagger.Module;
import dagger.Provides;
import dagger.Subcomponent;
/**
* {@link MediaComplicationComponent} is responsible for generating dependencies surrounding the
* media {@link com.android.systemui.dreams.complication.Complication}, such as view controllers
* and layout details.
*/
@Subcomponent(modules = {
MediaComplicationComponent.MediaComplicationModule.class,
})
@MediaComplicationComponent.MediaComplicationScope
public interface MediaComplicationComponent {
@Documented
@Retention(RUNTIME)
@Scope
@interface MediaComplicationScope {}
/**
* Generates {@link MediaComplicationComponent}.
*/
@Subcomponent.Factory
interface Factory {
MediaComplicationComponent create();
}
/**
* Creates {@link MediaViewHolder}.
*/
MediaViewHolder getViewHolder();
/**
* Scoped values for {@link MediaComplicationComponent}.
*/
@Module
interface MediaComplicationModule {
String MEDIA_COMPLICATION_CONTAINER = "media_complication_container";
String MEDIA_COMPLICATION_LAYOUT_PARAMS = "media_complication_layout_params";
/**
* Provides the complication view.
*/
@Provides
@MediaComplicationScope
@Named(MEDIA_COMPLICATION_CONTAINER)
static FrameLayout provideComplicationContainer(Context context) {
return new FrameLayout(context);
}
/**
* Provides the layout parameters for the complication view.
*/
@Provides
@MediaComplicationScope
@Named(MEDIA_COMPLICATION_LAYOUT_PARAMS)
static ComplicationLayoutParams provideLayoutParams() {
return new ComplicationLayoutParams(0,
ViewGroup.LayoutParams.WRAP_CONTENT,
ComplicationLayoutParams.POSITION_BOTTOM
| ComplicationLayoutParams.POSITION_END,
ComplicationLayoutParams.DIRECTION_UP,
0,
true);
}
}
}

View File

@@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
import com.android.systemui.dreams.DreamOverlayStateController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
@@ -85,6 +86,8 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
private lateinit var configurationController: ConfigurationController
@Mock
private lateinit var uniqueObjectHostView: UniqueObjectHostView
@Mock
private lateinit var dreamOverlayStateController: DreamOverlayStateController
@Captor
private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
@Captor
@@ -110,7 +113,8 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
notificationLockscreenUserManager,
configurationController,
wakefulnessLifecycle,
statusBarKeyguardViewManager)
statusBarKeyguardViewManager,
dreamOverlayStateController)
verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
verify(statusBarStateController).addCallback(statusBarCallback.capture())
setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN)

View File

@@ -0,0 +1,68 @@
/*
* 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.media.dream;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import android.testing.AndroidTestingRunner;
import android.widget.FrameLayout;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.media.MediaHost;
import com.android.systemui.util.animation.UniqueObjectHostView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class MediaComplicationViewControllerTest extends SysuiTestCase {
@Mock
private MediaHost mMediaHost;
@Mock
private UniqueObjectHostView mView;
@Mock
private FrameLayout mComplicationContainer;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mMediaHost.hostView = mView;
}
@Test
public void testMediaHostViewInteraction() {
final MediaComplicationViewController controller = new MediaComplicationViewController(
mComplicationContainer, mMediaHost);
controller.init();
controller.onViewAttached();
verify(mComplicationContainer).addView(eq(mView));
controller.onViewDetached();
verify(mComplicationContainer).removeView(eq(mView));
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.media.dream;
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.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.media.MediaData;
import com.android.systemui.media.MediaDataManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidTestingRunner.class)
public class MediaDreamSentinelTest extends SysuiTestCase {
@Mock
MediaDataManager mMediaDataManager;
@Mock
DreamOverlayStateController mDreamOverlayStateController;
@Mock
MediaDreamComplication mComplication;
final String mKey = "key";
final String mOldKey = "old_key";
@Mock
MediaData mData;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testComplicationAddition() {
final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
mDreamOverlayStateController, mComplication);
sentinel.start();
ArgumentCaptor<MediaDataManager.Listener> listenerCaptor =
ArgumentCaptor.forClass(MediaDataManager.Listener.class);
verify(mMediaDataManager).addListener(listenerCaptor.capture());
final MediaDataManager.Listener listener = listenerCaptor.getValue();
when(mMediaDataManager.hasActiveMedia()).thenReturn(false);
listener.onMediaDataLoaded(mKey, mOldKey, mData, true, 0);
verify(mDreamOverlayStateController, never()).addComplication(any());
when(mMediaDataManager.hasActiveMedia()).thenReturn(true);
listener.onMediaDataLoaded(mKey, mOldKey, mData, true, 0);
verify(mDreamOverlayStateController).addComplication(eq(mComplication));
listener.onMediaDataRemoved(mKey);
verify(mDreamOverlayStateController, never()).removeComplication(any());
when(mMediaDataManager.hasActiveMedia()).thenReturn(false);
listener.onMediaDataRemoved(mKey);
verify(mDreamOverlayStateController).removeComplication(eq(mComplication));
}
}