Add Unit Tests to guarantee non Pip feature device quality

Since Android Go disabled PIP feature, the current PIP arch
register a bunch of callback at ctor, however, with dagger's
mechanism it's not easy to predict which pip components was inited
and register callback, that could introduce NPE unexpectly.
Adding more test to verify disabled pip is a good option

Adding new unit tests for PIP :
+ PipControllerTest
+ PipTaskOrganizerTest
+ OverviewProxyServiceTest

Test: atest PipControllerTest
Test: atest PipTaskOrganizerTest
Test: atest WMShellTest
Test: make SystemUI
Test: make ArcSystemUI
Test: make WMShellUnitTests
Test: lunch aosp_tv_arm-userdebug & make
Test: atest SystemUITests
Test: atest WindowManagerShellTests
Test: atest WMShellUnitTests
Test: adb shell input keyevent 171(KEYCODE_WINDOW)
Test: manual test Pip demo AP
Test: adb shell dumpsys activity service com.android.systemui

Bug: 168715160
Change-Id: Idee90d00265d2ced629d4ba4eb0bb85c2a7905ac
This commit is contained in:
Bill Lin
2020-09-17 17:22:08 +08:00
parent 692483238d
commit d5c1ec0ad9
9 changed files with 378 additions and 15 deletions

View File

@@ -62,6 +62,7 @@ import com.android.internal.os.SomeArgs;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.pip.phone.PipMenuActivityController;
import com.android.systemui.pip.phone.PipUpdateThread;
import com.android.systemui.pip.phone.PipUtils;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
@@ -260,6 +261,11 @@ public class PipTaskOrganizer extends TaskOrganizer implements ShellTaskOrganize
mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
mSplitScreenOptional = splitScreenOptional;
mTaskOrganizer = shellTaskOrganizer;
if (!PipUtils.hasSystemFeature(context)) {
Log.w(TAG, "Device not support PIP feature");
return;
}
mTaskOrganizer.addListener(this, WINDOWING_MODE_PINNED);
displayController.addDisplayWindowListener(this);
}

View File

@@ -18,7 +18,6 @@ package com.android.systemui.pip.phone;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static com.android.systemui.pip.PipAnimationController.isOutPipDirection;
@@ -30,7 +29,6 @@ import android.app.IActivityManager;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.res.Configuration;
import android.graphics.Rect;
@@ -278,9 +276,7 @@ public class PipController implements Pip, PipTaskOrganizer.PipTransitionCallbac
mContext = context;
mActivityManager = ActivityManager.getService();
PackageManager pm = context.getPackageManager();
boolean supportsPip = pm.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE);
if (supportsPip) {
if (PipUtils.hasSystemFeature(mContext)) {
initController(context, broadcastDispatcher, configController, deviceConfig,
displayController, floatingContentCoordinator, sysUiState, pipBoundsHandler,
pipSurfaceTransactionHelper, pipTaskOrganizer, pipUiEventLogger);

View File

@@ -18,6 +18,7 @@ package com.android.systemui.pip.phone;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import android.app.ActivityTaskManager;
import android.app.ActivityTaskManager.RootTaskInfo;
@@ -57,4 +58,14 @@ public class PipUtils {
}
return new Pair<>(null, 0);
}
/**
* The util to check if device has PIP feature
*
* @param context application context
* @return true if device has PIP feature, false otherwise.
*/
public static boolean hasSystemFeature(Context context) {
return context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE);
}
}

View File

@@ -16,7 +16,6 @@
package com.android.systemui.recents;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
@@ -65,6 +64,7 @@ import android.view.accessibility.AccessibilityManager;
import androidx.annotation.NonNull;
import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.util.ScreenshotHelper;
import com.android.systemui.Dumpable;
@@ -77,6 +77,7 @@ import com.android.systemui.navigationbar.NavigationBarView;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.pip.Pip;
import com.android.systemui.pip.PipAnimationController;
import com.android.systemui.pip.phone.PipUtils;
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener;
import com.android.systemui.settings.CurrentUserTracker;
import com.android.systemui.shared.recents.IOverviewProxy;
@@ -155,7 +156,8 @@ public class OverviewProxyService extends CurrentUserTracker implements
private boolean mSupportsRoundedCornersOnWindows;
private int mNavBarMode = NAV_BAR_MODE_3BUTTON;
private ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() {
@VisibleForTesting
public ISystemUiProxy mSysUiProxy = new ISystemUiProxy.Stub() {
@Override
public void startScreenPinning(int taskId) {
@@ -624,7 +626,7 @@ public class OverviewProxyService extends CurrentUserTracker implements
super(broadcastDispatcher);
mContext = context;
mPipOptional = pipOptional;
mHasPipFeature = mContext.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE);
mHasPipFeature = PipUtils.hasSystemFeature(mContext);
mStatusBarOptionalLazy = statusBarOptionalLazy;
mHandler = new Handler();
mNavBarControllerLazy = navBarControllerLazy;

View File

@@ -40,6 +40,7 @@ import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.pip.Pip;
import com.android.systemui.pip.phone.PipUtils;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.tracing.ProtoTraceable;
@@ -57,7 +58,6 @@ import com.android.wm.shell.protolog.ShellProtoLogImpl;
import com.android.wm.shell.splitscreen.SplitScreen;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Optional;
@@ -103,7 +103,6 @@ public final class WMShell extends SystemUI
ProtoTracer protoTracer) {
super(context);
mCommandQueue = commandQueue;
mCommandQueue.addCallback(this);
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mActivityManagerWrapper = activityManagerWrapper;
mDisplayImeController = displayImeController;
@@ -120,6 +119,7 @@ public final class WMShell extends SystemUI
@Override
public void start() {
mCommandQueue.addCallback(this);
// This is to prevent circular init problem by separating registration step out of its
// constructor. And make sure the initialization of DisplayImeController won't depend on
// specific feature anymore.
@@ -131,6 +131,9 @@ public final class WMShell extends SystemUI
@VisibleForTesting
void initPip(Pip pip) {
if (!PipUtils.hasSystemFeature(mContext)) {
return;
}
mCommandQueue.addCallback(new CommandQueue.Callbacks() {
@Override
public void showPictureInPictureMenu() {

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2020 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.pip.phone;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.pm.PackageManager;
import android.os.RemoteException;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.model.SysUiState;
import com.android.systemui.pip.PipBoundsHandler;
import com.android.systemui.pip.PipSurfaceTransactionHelper;
import com.android.systemui.pip.PipTaskOrganizer;
import com.android.systemui.pip.PipUiEventLogger;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.util.DeviceConfigProxy;
import com.android.systemui.util.FloatingContentCoordinator;
import com.android.wm.shell.common.DisplayController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Unit tests for {@link PipController}
*/
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class PipControllerTest extends SysuiTestCase {
private PipController mPipController;
private TestableContext mSpiedContext;
@Mock private ActivityManagerWrapper mMockActivityManagerWrapper;
@Mock private BroadcastDispatcher mMockBroadcastDispatcher;
@Mock private ConfigurationController mMockConfigurationController;
@Mock private DeviceConfigProxy mMockDeviceConfigProxy;
@Mock private DisplayController mMockdDisplayController;
@Mock private FloatingContentCoordinator mMockFloatingContentCoordinator;
@Mock private PackageManager mPackageManager;
@Mock private PipBoundsHandler mMockPipBoundsHandler;
@Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper;
@Mock private PipTaskOrganizer mMockPipTaskOrganizer;
@Mock private PipUiEventLogger mPipUiEventLogger;
@Mock private SysUiState mMockSysUiState;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
mSpiedContext = spy(mContext);
when(mPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)).thenReturn(false);
when(mSpiedContext.getPackageManager()).thenReturn(mPackageManager);
mPipController = new PipController(mSpiedContext, mMockBroadcastDispatcher,
mMockConfigurationController, mMockDeviceConfigProxy, mMockdDisplayController,
mMockFloatingContentCoordinator, mMockSysUiState, mMockPipBoundsHandler,
mMockPipSurfaceTransactionHelper, mMockPipTaskOrganizer, mPipUiEventLogger);
}
@Test
public void testNonPipDevice_shouldNotRegisterTaskStackListener() {
verify(mMockActivityManagerWrapper, never()).registerTaskStackListener(any());
}
@Test
public void testNonPipDevice_shouldNotRegisterPipTransitionCallback() {
verify(mMockPipTaskOrganizer, never()).registerPipTransitionCallback(any());
}
@Test
public void testNonPipDevice_shouldNotAddDisplayChangingController() {
verify(mMockdDisplayController, never()).addDisplayChangingController(any());
}
@Test
public void testNonPipDevice_shouldNotAddDisplayWindowListener() {
verify(mMockdDisplayController, never()).addDisplayWindowListener(any());
}
@Test
public void testNonPipDevice_shouldNotAddCallback() {
verify(mMockConfigurationController, never()).addCallback(any());
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2020 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.pip.phone;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.pm.PackageManager;
import android.os.RemoteException;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.pip.PipBoundsHandler;
import com.android.systemui.pip.PipSurfaceTransactionHelper;
import com.android.systemui.pip.PipTaskOrganizer;
import com.android.systemui.pip.PipUiEventLogger;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.splitscreen.SplitScreen;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Optional;
/**
* Unit tests for {@link PipTaskOrganizer}
*/
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class PipTaskOrganizerTest extends SysuiTestCase {
private PipTaskOrganizer mSpiedPipTaskOrganizer;
private TestableContext mSpiedContext;
@Mock private DisplayController mMockdDisplayController;
@Mock private PackageManager mPackageManager;
@Mock private PipBoundsHandler mMockPipBoundsHandler;
@Mock private PipSurfaceTransactionHelper mMockPipSurfaceTransactionHelper;
@Mock private PipUiEventLogger mMockPipUiEventLogger;
@Mock private Optional<SplitScreen> mMockOptionalSplitScreen;
@Mock private ShellTaskOrganizer mMockShellTaskOrganizer;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
mSpiedContext = spy(mContext);
when(mPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)).thenReturn(false);
when(mSpiedContext.getPackageManager()).thenReturn(mPackageManager);
mSpiedPipTaskOrganizer = spy(new PipTaskOrganizer(mSpiedContext, mMockPipBoundsHandler,
mMockPipSurfaceTransactionHelper, mMockOptionalSplitScreen, mMockdDisplayController,
mMockPipUiEventLogger, mMockShellTaskOrganizer));
}
@Test
public void testNonPipDevice_shellTaskOrganizer_shouldNotAddListener() {
verify(mMockShellTaskOrganizer, never()).addListener(any(), anyInt());
}
@Test
public void testNonPipDevice_displayController_shouldNotAddDisplayWindowListener() {
verify(mMockdDisplayController, never()).addDisplayWindowListener(any());
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (C) 2020 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.recents;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.pm.PackageManager;
import android.os.RemoteException;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarController;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.pip.Pip;
import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.wm.shell.splitscreen.SplitScreen;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Optional;
import dagger.Lazy;
/**
* Unit tests for {@link com.android.systemui.recents.OverviewProxyService}
*/
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class OverviewProxyServiceTest extends SysuiTestCase {
private OverviewProxyService mSpiedOverviewProxyService;
private TestableContext mSpiedContext;
@Mock private BroadcastDispatcher mMockBroadcastDispatcher;
@Mock private CommandQueue mMockCommandQueue;
@Mock private Lazy<NavigationBarController> mMockNavBarControllerLazy;
@Mock private IPinnedStackAnimationListener mMockPinnedStackAnimationListener;
@Mock private NavigationModeController mMockNavModeController;
@Mock private NotificationShadeWindowController mMockStatusBarWinController;
@Mock private Optional<Pip> mMockPipOptional;
@Mock private Optional<SplitScreen> mMockSplitScreenOptional;
@Mock private Optional<Lazy<StatusBar>> mMockStatusBarOptionalLazy;
@Mock private Optional<com.android.wm.shell.onehanded.OneHanded> mMockOneHandedOptional;
@Mock private PackageManager mPackageManager;
@Mock private SysUiState mMockSysUiState;
@Before
public void setUp() throws RemoteException {
MockitoAnnotations.initMocks(this);
mSpiedContext = spy(mContext);
when(mPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)).thenReturn(false);
when(mSpiedContext.getPackageManager()).thenReturn(mPackageManager);
mSpiedOverviewProxyService = spy(new OverviewProxyService(mSpiedContext, mMockCommandQueue,
mMockNavBarControllerLazy, mMockNavModeController, mMockStatusBarWinController,
mMockSysUiState, mMockPipOptional, mMockSplitScreenOptional,
mMockStatusBarOptionalLazy, mMockOneHandedOptional,
mMockBroadcastDispatcher));
}
@Test
public void testNonPipDevice_shouldNotNotifySwipeToHomeFinished() throws RemoteException {
mSpiedOverviewProxyService.mSysUiProxy.notifySwipeToHomeFinished();
verify(mMockPipOptional, never()).ifPresent(any());
}
@Test
public void testNonPipDevice_shouldNotSetPinnedStackAnimationListener() throws RemoteException {
mSpiedOverviewProxyService.mSysUiProxy.setPinnedStackAnimationListener(
mMockPinnedStackAnimationListener);
verify(mMockPipOptional, never()).ifPresent(any());
}
@Test
public void testNonPipDevice_shouldNotSetShelfHeight() throws RemoteException {
mSpiedOverviewProxyService.mSysUiProxy.setShelfHeight(true /* visible */,
100 /* shelfHeight */);
verify(mMockPipOptional, never()).ifPresent(any());
}
}

View File

@@ -16,12 +16,17 @@
package com.android.systemui.wmshell;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.pm.PackageManager;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.TestableContext;
import androidx.test.runner.AndroidJUnit4;
@@ -68,6 +73,7 @@ public class WMShellTest extends SysuiTestCase {
@Mock OneHanded mOneHanded;
@Mock ShellTaskOrganizer mTaskOrganizer;
@Mock ProtoTracer mProtoTracer;
@Mock PackageManager mMockPackageManager;
@Before
public void setUp() {
@@ -89,8 +95,23 @@ public class WMShellTest extends SysuiTestCase {
public void initPip_registersCommandQueueCallback() {
mWMShell.initPip(mPip);
// Once for the shell, once for pip
verify(mCommandQueue, times(2)).addCallback(any(CommandQueue.Callbacks.class));
verify(mCommandQueue).addCallback(any(CommandQueue.Callbacks.class));
}
@Test
public void nonPipDevice_shouldNotInitPip() {
TestableContext spiedContext = spy(mContext);
when(mMockPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)).thenReturn(false);
when(spiedContext.getPackageManager()).thenReturn(mMockPackageManager);
final WMShell nonPipWMShell = new WMShell(spiedContext, mCommandQueue,
mKeyguardUpdateMonitor,
mActivityManagerWrapper, mDisplayImeController, mNavigationModeController,
mScreenLifecycle, mSysUiState, Optional.of(mPip), Optional.of(mSplitScreen),
Optional.of(mOneHanded), mTaskOrganizer, mProtoTracer);
nonPipWMShell.initPip(mPip);
verify(mCommandQueue, never()).addCallback(any());
}
@Test
@@ -108,8 +129,7 @@ public class WMShellTest extends SysuiTestCase {
mWMShell.initOneHanded(mOneHanded);
verify(mKeyguardUpdateMonitor).registerCallback(any(KeyguardUpdateMonitorCallback.class));
// Once for the shell, once for the one handed mode
verify(mCommandQueue, times(2)).addCallback(any(CommandQueue.Callbacks.class));
verify(mCommandQueue).addCallback(any(CommandQueue.Callbacks.class));
verify(mScreenLifecycle).addObserver(any(ScreenLifecycle.Observer.class));
verify(mNavigationModeController).addListener(
any(NavigationModeController.ModeChangedListener.class));