Merge "Migrate Bubbles to wm-shell (5/n)"
This commit is contained in:
@@ -22,6 +22,13 @@
|
||||
<application android:debuggable="true" android:largeHeap="true">
|
||||
<uses-library android:name="android.test.mock" />
|
||||
<uses-library android:name="android.test.runner" />
|
||||
|
||||
<activity android:name=".bubbles.BubblesTestActivity"
|
||||
android:allowEmbedded="true"
|
||||
android:documentLaunchMode="always"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:resizeableActivity="true" />
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
|
||||
32
libs/WindowManager/Shell/tests/unittest/res/layout/main.xml
Normal file
32
libs/WindowManager/Shell/tests/unittest/res/layout/main.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
>
|
||||
<TextView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="this is a test activity"
|
||||
/>
|
||||
<EditText
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/editText1"
|
||||
android:layout_width="match_parent">
|
||||
<requestFocus></requestFocus>
|
||||
</EditText>
|
||||
</LinearLayout>
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.wm.shell.pip;
|
||||
package com.android.wm.shell;
|
||||
|
||||
import static android.view.Display.DEFAULT_DISPLAY;
|
||||
|
||||
@@ -24,17 +24,18 @@ import android.testing.TestableContext;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
|
||||
/**
|
||||
* Base class that does One Handed specific setup.
|
||||
* Base class that does shell test case setup.
|
||||
*/
|
||||
public abstract class PipTestCase {
|
||||
public abstract class ShellTestCase {
|
||||
|
||||
protected TestableContext mContext;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
public void shellSetup() {
|
||||
final Context context =
|
||||
InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
final DisplayManager dm = context.getSystemService(DisplayManager.class);
|
||||
@@ -47,6 +48,14 @@ public abstract class PipTestCase {
|
||||
.adoptShellPermissionIdentity();
|
||||
}
|
||||
|
||||
@After
|
||||
public void shellTearDown() {
|
||||
InstrumentationRegistry
|
||||
.getInstrumentation()
|
||||
.getUiAutomation()
|
||||
.dropShellPermissionIdentity();
|
||||
}
|
||||
|
||||
protected Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* 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.wm.shell;
|
||||
|
||||
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceSession;
|
||||
import android.window.WindowContainerToken;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.common.HandlerExecutor;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||
public class TaskViewTest extends ShellTestCase {
|
||||
|
||||
@Mock
|
||||
TaskView.Listener mViewListener;
|
||||
@Mock
|
||||
ActivityManager.RunningTaskInfo mTaskInfo;
|
||||
@Mock
|
||||
WindowContainerToken mToken;
|
||||
@Mock
|
||||
ShellTaskOrganizer mOrganizer;
|
||||
@Mock
|
||||
HandlerExecutor mExecutor;
|
||||
|
||||
SurfaceSession mSession;
|
||||
SurfaceControl mLeash;
|
||||
|
||||
Context mContext;
|
||||
TaskView mTaskView;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mLeash = new SurfaceControl.Builder(mSession)
|
||||
.setName("test")
|
||||
.build();
|
||||
|
||||
mContext = getContext();
|
||||
|
||||
mTaskInfo = new ActivityManager.RunningTaskInfo();
|
||||
mTaskInfo.token = mToken;
|
||||
mTaskInfo.taskId = 314;
|
||||
mTaskInfo.taskDescription = mock(ActivityManager.TaskDescription.class);
|
||||
|
||||
doAnswer((InvocationOnMock invocationOnMock) -> {
|
||||
final Runnable r = invocationOnMock.getArgument(0);
|
||||
r.run();
|
||||
return null;
|
||||
}).when(mExecutor).execute(any());
|
||||
|
||||
mTaskView = new TaskView(mContext, mOrganizer);
|
||||
mTaskView.setExecutor(mExecutor);
|
||||
mTaskView.setListener(mViewListener);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (mTaskView != null) {
|
||||
mTaskView.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetPendingListener_throwsException() {
|
||||
TaskView taskView = new TaskView(mContext, mOrganizer);
|
||||
mTaskView.setExecutor(mExecutor);
|
||||
taskView.setListener(mViewListener);
|
||||
try {
|
||||
taskView.setListener(mViewListener);
|
||||
} catch (IllegalStateException e) {
|
||||
// pass
|
||||
return;
|
||||
}
|
||||
fail("Expected IllegalStateException");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartActivity() {
|
||||
ActivityOptions options = ActivityOptions.makeBasic();
|
||||
mTaskView.startActivity(mock(PendingIntent.class), null, options);
|
||||
|
||||
verify(mOrganizer).setPendingLaunchCookieListener(any(), eq(mTaskView));
|
||||
assertThat(options.getLaunchWindowingMode()).isEqualTo(WINDOWING_MODE_MULTI_WINDOW);
|
||||
assertThat(options.getTaskAlwaysOnTop()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnTaskAppeared_noSurface() {
|
||||
mTaskView.onTaskAppeared(mTaskInfo, mLeash);
|
||||
|
||||
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
|
||||
verify(mViewListener, never()).onInitialized();
|
||||
// If there's no surface the task should be made invisible
|
||||
verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnTaskAppeared_withSurface() {
|
||||
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
|
||||
mTaskView.onTaskAppeared(mTaskInfo, mLeash);
|
||||
|
||||
verify(mViewListener).onTaskCreated(eq(mTaskInfo.taskId), any());
|
||||
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSurfaceCreated_noTask() {
|
||||
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
|
||||
|
||||
verify(mViewListener).onInitialized();
|
||||
// No task, no visibility change
|
||||
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSurfaceCreated_withTask() {
|
||||
mTaskView.onTaskAppeared(mTaskInfo, mLeash);
|
||||
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
|
||||
|
||||
verify(mViewListener).onInitialized();
|
||||
verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSurfaceDestroyed_noTask() {
|
||||
SurfaceHolder sh = mock(SurfaceHolder.class);
|
||||
mTaskView.surfaceCreated(sh);
|
||||
mTaskView.surfaceDestroyed(sh);
|
||||
|
||||
verify(mViewListener, never()).onTaskVisibilityChanged(anyInt(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSurfaceDestroyed_withTask() {
|
||||
SurfaceHolder sh = mock(SurfaceHolder.class);
|
||||
mTaskView.onTaskAppeared(mTaskInfo, mLeash);
|
||||
mTaskView.surfaceCreated(sh);
|
||||
reset(mViewListener);
|
||||
mTaskView.surfaceDestroyed(sh);
|
||||
|
||||
verify(mViewListener).onTaskVisibilityChanged(eq(mTaskInfo.taskId), eq(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnReleased() {
|
||||
mTaskView.onTaskAppeared(mTaskInfo, mLeash);
|
||||
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
|
||||
mTaskView.release();
|
||||
|
||||
verify(mOrganizer).removeListener(eq(mTaskView));
|
||||
verify(mViewListener).onReleased();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnTaskVanished() {
|
||||
mTaskView.onTaskAppeared(mTaskInfo, mLeash);
|
||||
mTaskView.surfaceCreated(mock(SurfaceHolder.class));
|
||||
mTaskView.onTaskVanished(mTaskInfo);
|
||||
|
||||
verify(mViewListener).onTaskRemovalStarted(eq(mTaskInfo.taskId));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnBackPressedOnTaskRoot() {
|
||||
mTaskView.onTaskAppeared(mTaskInfo, mLeash);
|
||||
mTaskView.onBackPressedOnTaskRoot(mTaskInfo);
|
||||
|
||||
verify(mViewListener).onBackPressedOnTaskRoot(eq(mTaskInfo.taskId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,907 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Bundle;
|
||||
import android.os.UserHandle;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.bubbles.BubbleData.TimeSource;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
/**
|
||||
* Tests operations and the resulting state managed by BubbleData.
|
||||
* <p>
|
||||
* After each operation to verify, {@link #verifyUpdateReceived()} ensures the listener was called
|
||||
* and captures the Update object received there.
|
||||
* <p>
|
||||
* Other methods beginning with 'assert' access the captured update object and assert on specific
|
||||
* aspects of it.
|
||||
*/
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||
public class BubbleDataTest extends ShellTestCase {
|
||||
|
||||
private BubbleEntry mEntryA1;
|
||||
private BubbleEntry mEntryA2;
|
||||
private BubbleEntry mEntryA3;
|
||||
private BubbleEntry mEntryB1;
|
||||
private BubbleEntry mEntryB2;
|
||||
private BubbleEntry mEntryB3;
|
||||
private BubbleEntry mEntryC1;
|
||||
private BubbleEntry mEntryInterruptive;
|
||||
private BubbleEntry mEntryDismissed;
|
||||
|
||||
private Bubble mBubbleA1;
|
||||
private Bubble mBubbleA2;
|
||||
private Bubble mBubbleA3;
|
||||
private Bubble mBubbleB1;
|
||||
private Bubble mBubbleB2;
|
||||
private Bubble mBubbleB3;
|
||||
private Bubble mBubbleC1;
|
||||
private Bubble mBubbleInterruptive;
|
||||
private Bubble mBubbleDismissed;
|
||||
|
||||
private BubbleData mBubbleData;
|
||||
|
||||
@Mock
|
||||
private TimeSource mTimeSource;
|
||||
@Mock
|
||||
private BubbleData.Listener mListener;
|
||||
@Mock
|
||||
private PendingIntent mExpandIntent;
|
||||
@Mock
|
||||
private PendingIntent mDeleteIntent;
|
||||
@Mock
|
||||
private BubbleLogger mBubbleLogger;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<BubbleData.Update> mUpdateCaptor;
|
||||
|
||||
@Mock
|
||||
private Bubbles.NotificationSuppressionChangedListener mSuppressionListener;
|
||||
|
||||
@Mock
|
||||
private Bubbles.PendingIntentCanceledListener mPendingIntentCanceledListener;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mEntryA1 = createBubbleEntry(1, "a1", "package.a", null);
|
||||
mEntryA2 = createBubbleEntry(1, "a2", "package.a", null);
|
||||
mEntryA3 = createBubbleEntry(1, "a3", "package.a", null);
|
||||
mEntryB1 = createBubbleEntry(1, "b1", "package.b", null);
|
||||
mEntryB2 = createBubbleEntry(1, "b2", "package.b", null);
|
||||
mEntryB3 = createBubbleEntry(1, "b3", "package.b", null);
|
||||
mEntryC1 = createBubbleEntry(1, "c1", "package.c", null);
|
||||
|
||||
NotificationListenerService.Ranking ranking =
|
||||
mock(NotificationListenerService.Ranking.class);
|
||||
when(ranking.visuallyInterruptive()).thenReturn(true);
|
||||
mEntryInterruptive = createBubbleEntry(1, "interruptive", "package.d", ranking);
|
||||
mBubbleInterruptive = new Bubble(mEntryInterruptive, mSuppressionListener, null);
|
||||
|
||||
mEntryDismissed = createBubbleEntry(1, "dismissed", "package.d", null);
|
||||
mBubbleDismissed = new Bubble(mEntryDismissed, mSuppressionListener, null);
|
||||
|
||||
mBubbleA1 = new Bubble(mEntryA1, mSuppressionListener, mPendingIntentCanceledListener);
|
||||
mBubbleA2 = new Bubble(mEntryA2, mSuppressionListener, mPendingIntentCanceledListener);
|
||||
mBubbleA3 = new Bubble(mEntryA3, mSuppressionListener, mPendingIntentCanceledListener);
|
||||
mBubbleB1 = new Bubble(mEntryB1, mSuppressionListener, mPendingIntentCanceledListener);
|
||||
mBubbleB2 = new Bubble(mEntryB2, mSuppressionListener, mPendingIntentCanceledListener);
|
||||
mBubbleB3 = new Bubble(mEntryB3, mSuppressionListener, mPendingIntentCanceledListener);
|
||||
mBubbleC1 = new Bubble(mEntryC1, mSuppressionListener, mPendingIntentCanceledListener);
|
||||
|
||||
mBubbleData = new BubbleData(getContext(), mBubbleLogger);
|
||||
|
||||
// Used by BubbleData to set lastAccessedTime
|
||||
when(mTimeSource.currentTimeMillis()).thenReturn(1000L);
|
||||
mBubbleData.setTimeSource(mTimeSource);
|
||||
|
||||
// Assert baseline starting state
|
||||
assertThat(mBubbleData.hasBubbles()).isFalse();
|
||||
assertThat(mBubbleData.isExpanded()).isFalse();
|
||||
assertThat(mBubbleData.getSelectedBubble()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddBubble() {
|
||||
// Setup
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
|
||||
// Verify
|
||||
verifyUpdateReceived();
|
||||
assertBubbleAdded(mBubbleA1);
|
||||
assertSelectionChangedTo(mBubbleA1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveBubble() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA3, 3000);
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
|
||||
// Verify
|
||||
verifyUpdateReceived();
|
||||
assertBubbleRemoved(mBubbleA1, Bubbles.DISMISS_USER_GESTURE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ifSuppress_hideFlyout() {
|
||||
// Setup
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.notificationEntryUpdated(mBubbleC1, /* suppressFlyout */ true, /* showInShade */
|
||||
true);
|
||||
|
||||
// Verify
|
||||
verifyUpdateReceived();
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.addedBubble.showFlyout()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ifInterruptiveAndNotSuppressed_thenShowFlyout() {
|
||||
// Setup
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.notificationEntryUpdated(mBubbleInterruptive,
|
||||
false /* suppressFlyout */, true /* showInShade */);
|
||||
|
||||
// Verify
|
||||
verifyUpdateReceived();
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.addedBubble.showFlyout()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameUpdate_InShade_thenHideFlyout() {
|
||||
// Setup
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.notificationEntryUpdated(mBubbleC1, false /* suppressFlyout */,
|
||||
true /* showInShade */);
|
||||
verifyUpdateReceived();
|
||||
|
||||
mBubbleData.notificationEntryUpdated(mBubbleC1, false /* suppressFlyout */,
|
||||
true /* showInShade */);
|
||||
verifyUpdateReceived();
|
||||
|
||||
// Verify
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.updatedBubble.showFlyout()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sameUpdate_NotInShade_NotVisuallyInterruptive_dontShowFlyout() {
|
||||
// Setup
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.notificationEntryUpdated(mBubbleDismissed, false /* suppressFlyout */,
|
||||
true /* showInShade */);
|
||||
verifyUpdateReceived();
|
||||
|
||||
// Suppress the notif / make it look dismissed
|
||||
mBubbleDismissed.setSuppressNotification(true);
|
||||
|
||||
mBubbleData.notificationEntryUpdated(mBubbleDismissed, false /* suppressFlyout */,
|
||||
true /* showInShade */);
|
||||
verifyUpdateReceived();
|
||||
|
||||
// Verify
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.updatedBubble.showFlyout()).isFalse();
|
||||
}
|
||||
|
||||
//
|
||||
// Overflow
|
||||
//
|
||||
|
||||
/**
|
||||
* Verifies that when the bubble stack reaches its maximum, the oldest bubble is overflowed.
|
||||
*/
|
||||
@Test
|
||||
public void testOverflow_add_stackAtMaxBubbles_overflowsOldest() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA3, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 4000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 5000);
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryC1, 6000);
|
||||
verifyUpdateReceived();
|
||||
assertBubbleRemoved(mBubbleA1, Bubbles.DISMISS_AGED);
|
||||
assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
|
||||
|
||||
Bubble bubbleA1 = mBubbleData.getOrCreateBubble(mEntryA1, null /* persistedBubble */);
|
||||
bubbleA1.markUpdatedAt(7000L);
|
||||
mBubbleData.notificationEntryUpdated(bubbleA1, false /* suppressFlyout*/,
|
||||
true /* showInShade */);
|
||||
verifyUpdateReceived();
|
||||
assertBubbleRemoved(mBubbleA2, Bubbles.DISMISS_AGED);
|
||||
assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that once the number of overflowed bubbles reaches its maximum, the oldest
|
||||
* overflow bubble is removed.
|
||||
*/
|
||||
@Test
|
||||
public void testOverflow_maxReached_bubbleRemoved() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA3, 3000);
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
mBubbleData.setMaxOverflowBubbles(1);
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
verifyUpdateReceived();
|
||||
assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
|
||||
|
||||
// Overflow max of 1 is reached; A1 is oldest, so it gets removed
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
verifyUpdateReceived();
|
||||
assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that overflow bubbles are canceled on notif entry removal.
|
||||
*/
|
||||
@Test
|
||||
public void testOverflow_notifCanceled_removesOverflowBubble() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA3, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 4000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 5000);
|
||||
sendUpdatedEntryAtTime(mEntryB3, 6000); // [A2, A3, B1, B2, B3], overflow: [A1]
|
||||
sendUpdatedEntryAtTime(mEntryC1, 7000); // [A3, B1, B2, B3, C1], overflow: [A2, A1]
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
|
||||
verifyUpdateReceived();
|
||||
assertOverflowChangedTo(ImmutableList.of(mBubbleA2));
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_GROUP_CANCELLED);
|
||||
verifyUpdateReceived();
|
||||
assertOverflowChangedTo(ImmutableList.of());
|
||||
}
|
||||
|
||||
// COLLAPSED / ADD
|
||||
|
||||
/**
|
||||
* Verifies that new bubbles insert to the left when collapsed.
|
||||
* <p>
|
||||
* Placement within the list is based on {@link Bubble#getLastActivity()}, descending
|
||||
* order (with most recent first).
|
||||
*/
|
||||
@Test
|
||||
public void test_collapsed_addBubble() {
|
||||
// Setup
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderNotChanged();
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleB1, mBubbleA1);
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryB2, 3000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleB2, mBubbleB1, mBubbleA1);
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryA2, 4000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleA2, mBubbleB2, mBubbleB1, mBubbleA1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that new bubbles become the selected bubble when they appear when the stack is in
|
||||
* the collapsed state.
|
||||
*
|
||||
* @see #test_collapsed_updateBubble_selectionChanges()
|
||||
*/
|
||||
@Test
|
||||
public void test_collapsed_addBubble_selectionChanges() {
|
||||
// Setup
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
verifyUpdateReceived();
|
||||
assertSelectionChangedTo(mBubbleA1);
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
verifyUpdateReceived();
|
||||
assertSelectionChangedTo(mBubbleB1);
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryB2, 3000);
|
||||
verifyUpdateReceived();
|
||||
assertSelectionChangedTo(mBubbleB2);
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryA2, 4000);
|
||||
verifyUpdateReceived();
|
||||
assertSelectionChangedTo(mBubbleA2);
|
||||
}
|
||||
|
||||
// COLLAPSED / REMOVE
|
||||
|
||||
/**
|
||||
* Verifies order of bubbles after a removal.
|
||||
*/
|
||||
@Test
|
||||
public void test_collapsed_removeBubble_sort() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
verifyUpdateReceived();
|
||||
// TODO: this should fail if things work as I expect them to?
|
||||
assertOrderChangedTo(mBubbleB2, mBubbleB1, mBubbleA1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that onOrderChanged is not called when a bubble is removed if the removal does not
|
||||
* cause other bubbles to change position.
|
||||
*/
|
||||
@Test
|
||||
public void test_collapsed_removeOldestBubble_doesNotCallOnOrderChanged() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
verifyUpdateReceived();
|
||||
assertOrderNotChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when the selected bubble is removed with the stack in the collapsed state,
|
||||
* the selection moves to the next most-recently updated bubble.
|
||||
*/
|
||||
@Test
|
||||
public void test_collapsed_removeBubble_selectionChanges() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
|
||||
verifyUpdateReceived();
|
||||
assertSelectionChangedTo(mBubbleB2);
|
||||
}
|
||||
|
||||
// COLLAPSED / UPDATE
|
||||
|
||||
/**
|
||||
* Verifies that bubble ordering changes with updates while the stack is in the
|
||||
* collapsed state.
|
||||
*/
|
||||
@Test
|
||||
public void test_collapsed_updateBubble() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
sendUpdatedEntryAtTime(mEntryB1, 5000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleB1, mBubbleA2, mBubbleB2, mBubbleA1);
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryA1, 6000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleA1, mBubbleB1, mBubbleA2, mBubbleB2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that selection tracks the most recently updated bubble while in the collapsed state.
|
||||
*/
|
||||
@Test
|
||||
public void test_collapsed_updateBubble_selectionChanges() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
sendUpdatedEntryAtTime(mEntryB1, 5000);
|
||||
verifyUpdateReceived();
|
||||
assertSelectionChangedTo(mBubbleB1);
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryA1, 6000);
|
||||
verifyUpdateReceived();
|
||||
assertSelectionChangedTo(mBubbleA1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that when a non visually interruptive update occurs, that the selection does not
|
||||
* change.
|
||||
*/
|
||||
@Test
|
||||
public void test_notVisuallyInterruptive_updateBubble_selectionDoesntChange() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, B2, B1, A1]
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA2);
|
||||
|
||||
// Test
|
||||
sendUpdatedEntryAtTime(mEntryB1, 5000, false /* isVisuallyInterruptive */);
|
||||
assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a request to expand the stack has no effect if there are no bubbles.
|
||||
*/
|
||||
@Test
|
||||
public void test_collapsed_expansion_whenEmpty_doesNothing() {
|
||||
assertThat(mBubbleData.hasBubbles()).isFalse();
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
changeExpandedStateAtTime(true, 2000L);
|
||||
verifyZeroInteractions(mListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that removing the last bubble clears the selected bubble and collapses the stack.
|
||||
*/
|
||||
@Test
|
||||
public void test_collapsed_removeLastBubble_clearsSelectedBubble() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
|
||||
// Verify the selection was cleared.
|
||||
verifyUpdateReceived();
|
||||
assertThat(mBubbleData.isExpanded()).isFalse();
|
||||
assertThat(mBubbleData.getSelectedBubble()).isNull();
|
||||
}
|
||||
|
||||
// EXPANDED / ADD / UPDATE
|
||||
|
||||
/**
|
||||
* Verifies that bubbles are added at the front of the stack.
|
||||
* <p>
|
||||
* Placement within the list is based on {@link Bubble#getLastActivity()}, descending
|
||||
* order (with most recent first).
|
||||
*
|
||||
* @see #test_collapsed_addBubble()
|
||||
*/
|
||||
@Test
|
||||
public void test_expanded_addBubble() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 3000); // [B1, A2, A1]
|
||||
changeExpandedStateAtTime(true, 4000L); // B1 marked updated at 4000L
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
sendUpdatedEntryAtTime(mEntryC1, 4000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleC1, mBubbleB1, mBubbleA2, mBubbleA1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that updates to bubbles while expanded do not result in any change to sorting
|
||||
* of bubbles.
|
||||
*/
|
||||
@Test
|
||||
public void test_expanded_updateBubble_noChanges() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1]
|
||||
changeExpandedStateAtTime(true, 5000L);
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
sendUpdatedEntryAtTime(mEntryA1, 4000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderNotChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that updates to bubbles while expanded do not result in any change to selection.
|
||||
*
|
||||
* @see #test_collapsed_addBubble_selectionChanges()
|
||||
*/
|
||||
@Test
|
||||
public void test_expanded_updateBubble_noSelectionChanges() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1]
|
||||
changeExpandedStateAtTime(true, 5000L);
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
sendUpdatedEntryAtTime(mEntryA1, 6000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderNotChanged();
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryA2, 7000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderNotChanged();
|
||||
|
||||
sendUpdatedEntryAtTime(mEntryB1, 8000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderNotChanged();
|
||||
}
|
||||
|
||||
// EXPANDED / REMOVE
|
||||
|
||||
/**
|
||||
* Verifies that removing a bubble while expanded does not result in reordering of bubbles.
|
||||
*
|
||||
* @see #test_collapsed_addBubble()
|
||||
*/
|
||||
@Test
|
||||
public void test_expanded_removeBubble() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, A2, B1, A1]
|
||||
changeExpandedStateAtTime(true, 5000L);
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryB2.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleA1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that removing the selected bubble while expanded causes another bubble to become
|
||||
* selected. The replacement selection is the bubble which appears at the same index as the
|
||||
* previous one, or the previous index if this was the last position.
|
||||
*
|
||||
* @see #test_collapsed_addBubble()
|
||||
*/
|
||||
@Test
|
||||
public void test_expanded_removeBubble_selectionChanges_whenSelectedRemoved() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 4000);
|
||||
changeExpandedStateAtTime(true, 5000L);
|
||||
mBubbleData.setSelectedBubble(mBubbleA2); // [B2, A2^, B1, A1]
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
verifyUpdateReceived();
|
||||
assertSelectionChangedTo(mBubbleB1);
|
||||
|
||||
mBubbleData.dismissBubbleWithKey(mEntryB1.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
verifyUpdateReceived();
|
||||
assertSelectionChangedTo(mBubbleA1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_expandAndCollapse_callsOnExpandedChanged() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
changeExpandedStateAtTime(true, 3000L);
|
||||
verifyUpdateReceived();
|
||||
assertExpandedChangedTo(true);
|
||||
|
||||
changeExpandedStateAtTime(false, 4000L);
|
||||
verifyUpdateReceived();
|
||||
assertExpandedChangedTo(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that transitions between the collapsed and expanded state maintain sorting and
|
||||
* grouping rules.
|
||||
* <p>
|
||||
* While collapsing, sorting is applied since no sorting happens while expanded. The resulting
|
||||
* state is the new expanded ordering. This state is saved and restored if possible when next
|
||||
* expanded.
|
||||
* <p>
|
||||
* When the stack transitions to the collapsed state, the selected bubble is brought to the top.
|
||||
* <p>
|
||||
* When the stack transitions back to the expanded state, this new order is kept as is.
|
||||
*/
|
||||
@Test
|
||||
public void test_expansionChanges() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 4000);
|
||||
changeExpandedStateAtTime(true, 5000L); // [B2=4000, A2=3000, B1=2000, A1=1000]
|
||||
sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, A2=3000, B1=6000, A1=1000]
|
||||
setCurrentTime(7000);
|
||||
mBubbleData.setSelectedBubble(mBubbleA2);
|
||||
mBubbleData.setListener(mListener);
|
||||
assertThat(mBubbleData.getBubbles()).isEqualTo(
|
||||
ImmutableList.of(mBubbleB2, mBubbleA2, mBubbleB1, mBubbleA1));
|
||||
|
||||
// Test
|
||||
|
||||
// At this point, B1 has been updated but sorting has not been changed because the
|
||||
// stack is expanded. When next collapsed, sorting will be applied and saved, just prior
|
||||
// to moving the selected bubble to the top (first).
|
||||
//
|
||||
// In this case, the expected re-expand state will be: [A2^, B1, B2, A1]
|
||||
//
|
||||
// collapse -> selected bubble (A2) moves first.
|
||||
changeExpandedStateAtTime(false, 8000L);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleB2, mBubbleA1);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a change occurs while collapsed (any update, add, remove), the previous expanded
|
||||
* order becomes invalidated, the stack is resorted and will reflect that when next expanded.
|
||||
*/
|
||||
@Test
|
||||
public void test_expansionChanges_withUpdatesWhileCollapsed() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
sendUpdatedEntryAtTime(mEntryB1, 2000);
|
||||
sendUpdatedEntryAtTime(mEntryA2, 3000);
|
||||
sendUpdatedEntryAtTime(mEntryB2, 4000);
|
||||
changeExpandedStateAtTime(true, 5000L); // [B2=4000, A2=3000, B1=2000, A1=1000]
|
||||
sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, A2=3000, B1=6000, A1=1000]
|
||||
setCurrentTime(7000);
|
||||
mBubbleData.setSelectedBubble(mBubbleA2); // [B2, A2^, B1, A1]
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
|
||||
// At this point, B1 has been updated but sorting has not been changed because the
|
||||
// stack is expanded. When next collapsed, sorting will be applied and saved, just prior
|
||||
// to moving the selected bubble to the top (first).
|
||||
//
|
||||
// In this case, the expected re-expand state will be: [A2^, B1, B2, A1]
|
||||
//
|
||||
// That state is restored as long as no changes occur (add/remove/update) while in
|
||||
// the collapsed state.
|
||||
//
|
||||
// collapse -> selected bubble (A2) moves first.
|
||||
changeExpandedStateAtTime(false, 8000L);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleA2, mBubbleB1, mBubbleB2, mBubbleA1);
|
||||
|
||||
// An update occurs, which causes sorting, and this invalidates the previously saved order.
|
||||
sendUpdatedEntryAtTime(mEntryA1, 9000);
|
||||
verifyUpdateReceived();
|
||||
assertOrderChangedTo(mBubbleA1, mBubbleA2, mBubbleB1, mBubbleB2);
|
||||
|
||||
// No order changes when expanding because the new sorted order remains.
|
||||
changeExpandedStateAtTime(true, 10000L);
|
||||
verifyUpdateReceived();
|
||||
assertOrderNotChanged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_expanded_removeLastBubble_collapsesStack() {
|
||||
// Setup
|
||||
sendUpdatedEntryAtTime(mEntryA1, 1000);
|
||||
changeExpandedStateAtTime(true, 2000);
|
||||
mBubbleData.setListener(mListener);
|
||||
|
||||
// Test
|
||||
mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
|
||||
verifyUpdateReceived();
|
||||
assertExpandedChangedTo(false);
|
||||
}
|
||||
|
||||
private void verifyUpdateReceived() {
|
||||
verify(mListener).applyUpdate(mUpdateCaptor.capture());
|
||||
reset(mListener);
|
||||
}
|
||||
|
||||
private void assertBubbleAdded(Bubble expected) {
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertWithMessage("addedBubble").that(update.addedBubble).isEqualTo(expected);
|
||||
}
|
||||
|
||||
private void assertBubbleRemoved(Bubble expected, @BubbleController.DismissReason int reason) {
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertWithMessage("removedBubbles").that(update.removedBubbles)
|
||||
.isEqualTo(ImmutableList.of(Pair.create(expected, reason)));
|
||||
}
|
||||
|
||||
private void assertOrderNotChanged() {
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertWithMessage("orderChanged").that(update.orderChanged).isFalse();
|
||||
}
|
||||
|
||||
private void assertOrderChangedTo(Bubble... order) {
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertWithMessage("orderChanged").that(update.orderChanged).isTrue();
|
||||
assertWithMessage("bubble order").that(update.bubbles)
|
||||
.isEqualTo(ImmutableList.copyOf(order));
|
||||
}
|
||||
|
||||
private void assertSelectionNotChanged() {
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertWithMessage("selectionChanged").that(update.selectionChanged).isFalse();
|
||||
}
|
||||
|
||||
private void assertSelectionChangedTo(Bubble bubble) {
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertWithMessage("selectionChanged").that(update.selectionChanged).isTrue();
|
||||
assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble);
|
||||
}
|
||||
|
||||
private void assertSelectionCleared() {
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertWithMessage("selectionChanged").that(update.selectionChanged).isTrue();
|
||||
assertWithMessage("selectedBubble").that(update.selectedBubble).isNull();
|
||||
}
|
||||
|
||||
private void assertExpandedChangedTo(boolean expected) {
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertWithMessage("expandedChanged").that(update.expandedChanged).isTrue();
|
||||
assertWithMessage("expanded").that(update.expanded).isEqualTo(expected);
|
||||
}
|
||||
|
||||
private void assertOverflowChangedTo(ImmutableList<Bubble> bubbles) {
|
||||
BubbleData.Update update = mUpdateCaptor.getValue();
|
||||
assertThat(update.overflowBubbles).isEqualTo(bubbles);
|
||||
}
|
||||
|
||||
|
||||
private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName,
|
||||
NotificationListenerService.Ranking ranking) {
|
||||
return createBubbleEntry(userId, notifKey, packageName, ranking, 1000);
|
||||
}
|
||||
|
||||
private void setPostTime(BubbleEntry entry, long postTime) {
|
||||
when(entry.getStatusBarNotification().getPostTime()).thenReturn(postTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* No ExpandableNotificationRow is required to test BubbleData. This setup is all that is
|
||||
* required for BubbleData functionality and verification. NotificationTestHelper is used only
|
||||
* as a convenience to create a Notification w/BubbleMetadata.
|
||||
*/
|
||||
private BubbleEntry createBubbleEntry(int userId, String notifKey, String packageName,
|
||||
NotificationListenerService.Ranking ranking, long postTime) {
|
||||
// BubbleMetadata
|
||||
Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder(
|
||||
mExpandIntent, Icon.createWithResource("", 0))
|
||||
.setDeleteIntent(mDeleteIntent)
|
||||
.build();
|
||||
// Notification -> BubbleMetadata
|
||||
Notification notification = mock(Notification.class);
|
||||
notification.setBubbleMetadata(bubbleMetadata);
|
||||
|
||||
// Notification -> extras
|
||||
notification.extras = new Bundle();
|
||||
|
||||
// StatusBarNotification
|
||||
StatusBarNotification sbn = mock(StatusBarNotification.class);
|
||||
when(sbn.getKey()).thenReturn(notifKey);
|
||||
when(sbn.getUser()).thenReturn(new UserHandle(userId));
|
||||
when(sbn.getPackageName()).thenReturn(packageName);
|
||||
when(sbn.getPostTime()).thenReturn(postTime);
|
||||
when(sbn.getNotification()).thenReturn(notification);
|
||||
|
||||
// NotificationEntry -> StatusBarNotification -> Notification -> BubbleMetadata
|
||||
return new BubbleEntry(sbn, ranking, true, false, false, false);
|
||||
}
|
||||
|
||||
private void setCurrentTime(long time) {
|
||||
when(mTimeSource.currentTimeMillis()).thenReturn(time);
|
||||
}
|
||||
|
||||
private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime) {
|
||||
sendUpdatedEntryAtTime(entry, postTime, true /* visuallyInterruptive */);
|
||||
}
|
||||
|
||||
private void sendUpdatedEntryAtTime(BubbleEntry entry, long postTime,
|
||||
boolean visuallyInterruptive) {
|
||||
setPostTime(entry, postTime);
|
||||
// BubbleController calls this:
|
||||
Bubble b = mBubbleData.getOrCreateBubble(entry, null /* persistedBubble */);
|
||||
b.setVisuallyInterruptiveForTest(visuallyInterruptive);
|
||||
// And then this
|
||||
mBubbleData.notificationEntryUpdated(b, false /* suppressFlyout*/,
|
||||
true /* showInShade */);
|
||||
}
|
||||
|
||||
private void changeExpandedStateAtTime(boolean shouldBeExpanded, long time) {
|
||||
setCurrentTime(time);
|
||||
mBubbleData.setExpanded(shouldBeExpanded);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertNotSame;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PointF;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||
public class BubbleFlyoutViewTest extends ShellTestCase {
|
||||
private BubbleFlyoutView mFlyout;
|
||||
private TextView mFlyoutText;
|
||||
private TextView mSenderName;
|
||||
private float[] mDotCenter = new float[2];
|
||||
private Bubble.FlyoutMessage mFlyoutMessage;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mFlyoutMessage = new Bubble.FlyoutMessage();
|
||||
mFlyoutMessage.senderName = "Josh";
|
||||
mFlyoutMessage.message = "Hello";
|
||||
|
||||
mFlyout = new BubbleFlyoutView(getContext());
|
||||
|
||||
mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text);
|
||||
mSenderName = mFlyout.findViewById(R.id.bubble_flyout_name);
|
||||
mDotCenter[0] = 30;
|
||||
mDotCenter[1] = 30;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShowFlyout_isVisible() {
|
||||
mFlyout.setupFlyoutStartingAsDot(
|
||||
mFlyoutMessage,
|
||||
new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
|
||||
false);
|
||||
mFlyout.setVisibility(View.VISIBLE);
|
||||
|
||||
assertEquals("Hello", mFlyoutText.getText());
|
||||
assertEquals("Josh", mSenderName.getText());
|
||||
assertEquals(View.VISIBLE, mFlyout.getVisibility());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFlyoutHide_runsCallback() {
|
||||
Runnable after = Mockito.mock(Runnable.class);
|
||||
mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
|
||||
new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter,
|
||||
false);
|
||||
mFlyout.hideFlyout();
|
||||
|
||||
verify(after).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetCollapsePercent() {
|
||||
mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
|
||||
new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
|
||||
false);
|
||||
mFlyout.setVisibility(View.VISIBLE);
|
||||
|
||||
mFlyout.setCollapsePercent(1f);
|
||||
assertEquals(0f, mFlyoutText.getAlpha(), 0.01f);
|
||||
assertNotSame(0f, mFlyoutText.getTranslationX()); // Should have moved to collapse.
|
||||
|
||||
mFlyout.setCollapsePercent(0f);
|
||||
assertEquals(1f, mFlyoutText.getAlpha(), 0.01f);
|
||||
assertEquals(0f, mFlyoutText.getTranslationX());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
|
||||
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)
|
||||
@TestableLooper.RunWithLooper
|
||||
public class BubbleTest extends ShellTestCase {
|
||||
@Mock
|
||||
private Notification mNotif;
|
||||
@Mock
|
||||
private StatusBarNotification mSbn;
|
||||
|
||||
private BubbleEntry mBubbleEntry;
|
||||
private Bundle mExtras;
|
||||
private Bubble mBubble;
|
||||
|
||||
@Mock
|
||||
private Bubbles.NotificationSuppressionChangedListener mSuppressionListener;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mExtras = new Bundle();
|
||||
mNotif.extras = mExtras;
|
||||
|
||||
Intent target = new Intent(mContext, BubblesTestActivity.class);
|
||||
Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(
|
||||
PendingIntent.getActivity(mContext, 0, target, 0),
|
||||
Icon.createWithResource(mContext, R.drawable.bubble_ic_create_bubble))
|
||||
.build();
|
||||
when(mSbn.getNotification()).thenReturn(mNotif);
|
||||
when(mNotif.getBubbleMetadata()).thenReturn(metadata);
|
||||
when(mSbn.getKey()).thenReturn("mock");
|
||||
mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false);
|
||||
mBubble = new Bubble(mBubbleEntry, mSuppressionListener, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUpdateMessage_default() {
|
||||
final String msg = "Hello there!";
|
||||
doReturn(Notification.Style.class).when(mNotif).getNotificationStyle();
|
||||
mExtras.putCharSequence(Notification.EXTRA_TEXT, msg);
|
||||
assertEquals(msg, Bubble.extractFlyoutMessage(mBubbleEntry).message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUpdateMessage_bigText() {
|
||||
final String msg = "A big hello there!";
|
||||
doReturn(Notification.BigTextStyle.class).when(mNotif).getNotificationStyle();
|
||||
mExtras.putCharSequence(Notification.EXTRA_TEXT, "A small hello there.");
|
||||
mExtras.putCharSequence(Notification.EXTRA_BIG_TEXT, msg);
|
||||
|
||||
// Should be big text, not the small text.
|
||||
assertEquals(msg, Bubble.extractFlyoutMessage(mBubbleEntry).message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUpdateMessage_media() {
|
||||
doReturn(Notification.MediaStyle.class).when(mNotif).getNotificationStyle();
|
||||
|
||||
// Media notifs don't get update messages.
|
||||
assertNull(Bubble.extractFlyoutMessage(mBubbleEntry).message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUpdateMessage_inboxStyle() {
|
||||
doReturn(Notification.InboxStyle.class).when(mNotif).getNotificationStyle();
|
||||
mExtras.putCharSequenceArray(
|
||||
Notification.EXTRA_TEXT_LINES,
|
||||
new CharSequence[]{
|
||||
"How do you feel about tests?",
|
||||
"They're okay, I guess.",
|
||||
"I hate when they're flaky.",
|
||||
"Really? I prefer them that way."});
|
||||
|
||||
// Should be the last one only.
|
||||
assertEquals("Really? I prefer them that way.",
|
||||
Bubble.extractFlyoutMessage(mBubbleEntry).message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUpdateMessage_messagingStyle() {
|
||||
doReturn(Notification.MessagingStyle.class).when(mNotif).getNotificationStyle();
|
||||
mExtras.putParcelableArray(
|
||||
Notification.EXTRA_MESSAGES,
|
||||
new Bundle[]{
|
||||
new Notification.MessagingStyle.Message(
|
||||
"Hello", 0, "Josh").toBundle(),
|
||||
new Notification.MessagingStyle.Message(
|
||||
"Oh, hello!", 0, "Mady").toBundle()});
|
||||
|
||||
// Should be the last one only.
|
||||
assertEquals("Oh, hello!", Bubble.extractFlyoutMessage(mBubbleEntry).message);
|
||||
assertEquals("Mady", Bubble.extractFlyoutMessage(mBubbleEntry).senderName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuppressionListener_change_notified() {
|
||||
assertThat(mBubble.showInShade()).isTrue();
|
||||
|
||||
mBubble.setSuppressNotification(true);
|
||||
|
||||
assertThat(mBubble.showInShade()).isFalse();
|
||||
|
||||
verify(mSuppressionListener).onBubbleNotificationSuppressionChange(mBubble);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuppressionListener_noChange_doesntNotify() {
|
||||
assertThat(mBubble.showInShade()).isTrue();
|
||||
|
||||
mBubble.setSuppressNotification(false);
|
||||
|
||||
verify(mSuppressionListener, never()).onBubbleNotificationSuppressionChange(any());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
|
||||
/**
|
||||
* Referenced by NotificationTestHelper#makeBubbleMetadata
|
||||
*/
|
||||
public class BubblesTestActivity extends Activity {
|
||||
|
||||
public static final String BUBBLE_ACTIVITY_OPENED = "BUBBLE_ACTIVITY_OPENED";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.main);
|
||||
|
||||
Intent i = new Intent(BUBBLE_ACTIVITY_OPENED);
|
||||
sendBroadcast(i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles.animation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.bubbles.BubblePositioner;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Spy;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestCase {
|
||||
|
||||
private int mDisplayWidth = 500;
|
||||
private int mDisplayHeight = 1000;
|
||||
private int mExpandedViewPadding = 10;
|
||||
|
||||
private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class);
|
||||
@Spy
|
||||
ExpandedAnimationController mExpandedController;
|
||||
|
||||
private int mStackOffset;
|
||||
private PointF mExpansionPoint;
|
||||
|
||||
@SuppressLint("VisibleForTests")
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
BubblePositioner positioner = new BubblePositioner(getContext(), mock(WindowManager.class));
|
||||
positioner.update(Configuration.ORIENTATION_PORTRAIT,
|
||||
Insets.of(0, 0, 0, 0),
|
||||
new Rect(0, 0, mDisplayWidth, mDisplayHeight));
|
||||
mExpandedController = new ExpandedAnimationController(positioner, mExpandedViewPadding,
|
||||
mOnBubbleAnimatedOutAction);
|
||||
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
mLayout.setActiveController(mExpandedController);
|
||||
|
||||
Resources res = mLayout.getResources();
|
||||
mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
|
||||
mExpansionPoint = new PointF(100, 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testExpansionAndCollapse() throws InterruptedException {
|
||||
Runnable afterExpand = mock(Runnable.class);
|
||||
mExpandedController.expandFromStack(afterExpand);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
testBubblesInCorrectExpandedPositions();
|
||||
verify(afterExpand).run();
|
||||
|
||||
Runnable afterCollapse = mock(Runnable.class);
|
||||
mExpandedController.collapseBackToStack(mExpansionPoint, afterCollapse);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
|
||||
verify(afterExpand).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testOnChildAdded() throws InterruptedException {
|
||||
expand();
|
||||
|
||||
// Add another new view and wait for its animation.
|
||||
final View newView = new FrameLayout(getContext());
|
||||
mLayout.addView(newView, 0);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
testBubblesInCorrectExpandedPositions();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testOnChildRemoved() throws InterruptedException {
|
||||
expand();
|
||||
|
||||
// Remove some views and see if the remaining child views still pass the expansion test.
|
||||
mLayout.removeView(mViews.get(0));
|
||||
mLayout.removeView(mViews.get(3));
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
testBubblesInCorrectExpandedPositions();
|
||||
}
|
||||
|
||||
/** Expand the stack and wait for animations to finish. */
|
||||
private void expand() throws InterruptedException {
|
||||
mExpandedController.expandFromStack(mock(Runnable.class));
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
}
|
||||
|
||||
/** Check that children are in the correct positions for being stacked. */
|
||||
private void testStackedAtPosition(float x, float y, int offsetMultiplier) {
|
||||
// Make sure the rest of the stack moved again, including the first bubble not moving, and
|
||||
// is stacked to the right now that we're on the right side of the screen.
|
||||
for (int i = 0; i < mLayout.getChildCount(); i++) {
|
||||
assertEquals(x + i * offsetMultiplier * mStackOffset,
|
||||
mLayout.getChildAt(i).getTranslationX(), 2f);
|
||||
assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
|
||||
assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check that children are in the correct positions for being expanded. */
|
||||
private void testBubblesInCorrectExpandedPositions() {
|
||||
// Check all the visible bubbles to see if they're in the right place.
|
||||
for (int i = 0; i < mLayout.getChildCount(); i++) {
|
||||
float expectedPosition = mExpandedController.getBubbleXOrYForOrientation(i);
|
||||
assertEquals(expectedPosition,
|
||||
mLayout.getChildAt(i).getTranslationX(),
|
||||
2f);
|
||||
assertEquals(expectedPosition,
|
||||
mLayout.getChildAt(i).getTranslationY(), 2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles.animation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.google.android.collect.Sets;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.Spy;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
/** Tests the PhysicsAnimationLayout itself, with a basic test animation controller. */
|
||||
public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
|
||||
static final float TEST_TRANSLATION_X_OFFSET = 15f;
|
||||
|
||||
@Spy
|
||||
private TestableAnimationController mTestableController = new TestableAnimationController();
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
// By default, use translation animations, chain the X animations with the default
|
||||
// offset, and don't actually remove views immediately (since most implementations will wait
|
||||
// to animate child views out before actually removing them).
|
||||
mTestableController.setAnimatedProperties(Sets.newHashSet(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
DynamicAnimation.TRANSLATION_Y));
|
||||
mTestableController.setChainedProperties(Sets.newHashSet(DynamicAnimation.TRANSLATION_X));
|
||||
mTestableController.setOffsetForProperty(
|
||||
DynamicAnimation.TRANSLATION_X, TEST_TRANSLATION_X_OFFSET);
|
||||
mTestableController.setRemoveImmediately(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testHierarchyChanges() throws InterruptedException {
|
||||
mLayout.setActiveController(mTestableController);
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
|
||||
// Make sure the controller was notified of all the views we added.
|
||||
for (View mView : mViews) {
|
||||
Mockito.verify(mTestableController).onChildAdded(mView, 0);
|
||||
}
|
||||
|
||||
// Remove some views and ensure the controller was notified, with the proper indices.
|
||||
mTestableController.setRemoveImmediately(true);
|
||||
mLayout.removeView(mViews.get(1));
|
||||
mLayout.removeView(mViews.get(2));
|
||||
Mockito.verify(mTestableController).onChildRemoved(
|
||||
eq(mViews.get(1)), eq(1), any());
|
||||
Mockito.verify(mTestableController).onChildRemoved(
|
||||
eq(mViews.get(2)), eq(1), any());
|
||||
|
||||
// Make sure we still get view added notifications after doing some removals.
|
||||
final View newBubble = new FrameLayout(mContext);
|
||||
mLayout.addView(newBubble, 0);
|
||||
Mockito.verify(mTestableController).onChildAdded(newBubble, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testUpdateValueNotChained() throws InterruptedException {
|
||||
mLayout.setActiveController(mTestableController);
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
|
||||
// Don't chain any values.
|
||||
mTestableController.setChainedProperties(Sets.newHashSet());
|
||||
|
||||
// Child views should not be translated.
|
||||
assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
|
||||
assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
|
||||
|
||||
// Animate the first child's translation X.
|
||||
final CountDownLatch animLatch = new CountDownLatch(1);
|
||||
|
||||
mTestableController
|
||||
.animationForChildAtIndex(0)
|
||||
.translationX(100)
|
||||
.start(animLatch::countDown);
|
||||
animLatch.await(1, TimeUnit.SECONDS);
|
||||
|
||||
// Ensure that the first view has been translated, but not the second one.
|
||||
assertEquals(100, mLayout.getChildAt(0).getTranslationX(), .1f);
|
||||
assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testUpdateValueXChained() throws InterruptedException {
|
||||
testChainedTranslationAnimations();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testSetEndActions() throws InterruptedException {
|
||||
mLayout.setActiveController(mTestableController);
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
mTestableController.setChainedProperties(Sets.newHashSet());
|
||||
|
||||
final CountDownLatch xLatch = new CountDownLatch(1);
|
||||
Runnable xEndAction = Mockito.spy(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
xLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
final CountDownLatch yLatch = new CountDownLatch(1);
|
||||
Runnable yEndAction = Mockito.spy(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
yLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// Set end listeners for both x and y.
|
||||
mTestableController.setEndActionForProperty(xEndAction, DynamicAnimation.TRANSLATION_X);
|
||||
mTestableController.setEndActionForProperty(yEndAction, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Animate x, and wait for it to finish.
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.translationX(100)
|
||||
.start();
|
||||
|
||||
xLatch.await();
|
||||
yLatch.await(1, TimeUnit.SECONDS);
|
||||
|
||||
// Make sure the x end listener was called only one time, and the y listener was never
|
||||
// called since we didn't animate y. Wait 1 second after the original animation end trigger
|
||||
// to make sure it doesn't get called again.
|
||||
Mockito.verify(xEndAction, Mockito.after(1000).times(1)).run();
|
||||
Mockito.verify(yEndAction, Mockito.after(1000).never()).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testRemoveEndListeners() throws InterruptedException {
|
||||
mLayout.setActiveController(mTestableController);
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
mTestableController.setChainedProperties(Sets.newHashSet());
|
||||
|
||||
final CountDownLatch xLatch = new CountDownLatch(1);
|
||||
Runnable xEndListener = Mockito.spy(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
xLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
// Set the end listener.
|
||||
mTestableController.setEndActionForProperty(xEndListener, DynamicAnimation.TRANSLATION_X);
|
||||
|
||||
// Animate x, and wait for it to finish.
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.translationX(100)
|
||||
.start();
|
||||
xLatch.await();
|
||||
|
||||
InOrder endListenerCalls = inOrder(xEndListener);
|
||||
endListenerCalls.verify(xEndListener, Mockito.times(1)).run();
|
||||
|
||||
// Animate X again, remove the end listener.
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.translationX(1000)
|
||||
.start();
|
||||
mTestableController.removeEndActionForProperty(DynamicAnimation.TRANSLATION_X);
|
||||
xLatch.await(1, TimeUnit.SECONDS);
|
||||
|
||||
// Make sure the end listener was not called.
|
||||
endListenerCalls.verifyNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testSetController() throws InterruptedException {
|
||||
// Add the bubbles, then set the controller, to make sure that a controller added to an
|
||||
// already-initialized view works correctly.
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
mLayout.setActiveController(mTestableController);
|
||||
testChainedTranslationAnimations();
|
||||
|
||||
TestableAnimationController secondController =
|
||||
Mockito.spy(new TestableAnimationController());
|
||||
secondController.setAnimatedProperties(Sets.newHashSet(
|
||||
DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y));
|
||||
secondController.setChainedProperties(Sets.newHashSet(
|
||||
DynamicAnimation.SCALE_X));
|
||||
secondController.setOffsetForProperty(
|
||||
DynamicAnimation.SCALE_X, 10f);
|
||||
secondController.setRemoveImmediately(true);
|
||||
|
||||
mLayout.setActiveController(secondController);
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.scaleX(1.5f)
|
||||
.start();
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.SCALE_X);
|
||||
|
||||
// Make sure we never asked the original controller about any SCALE animations, that would
|
||||
// mean the controller wasn't switched over properly.
|
||||
Mockito.verify(mTestableController, Mockito.never())
|
||||
.getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt());
|
||||
Mockito.verify(mTestableController, Mockito.never())
|
||||
.getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X));
|
||||
|
||||
// Make sure we asked the new controller about its animated properties, and configuration
|
||||
// options.
|
||||
Mockito.verify(secondController, Mockito.atLeastOnce())
|
||||
.getAnimatedProperties();
|
||||
Mockito.verify(secondController, Mockito.atLeastOnce())
|
||||
.getNextAnimationInChain(eq(DynamicAnimation.SCALE_X), anyInt());
|
||||
Mockito.verify(secondController, Mockito.atLeastOnce())
|
||||
.getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.SCALE_X));
|
||||
|
||||
mLayout.setActiveController(mTestableController);
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.translationX(100f)
|
||||
.start();
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
|
||||
|
||||
// Make sure we never asked the second controller about the TRANSLATION_X animation.
|
||||
Mockito.verify(secondController, Mockito.never())
|
||||
.getNextAnimationInChain(eq(DynamicAnimation.TRANSLATION_X), anyInt());
|
||||
Mockito.verify(secondController, Mockito.never())
|
||||
.getOffsetForChainedPropertyAnimation(eq(DynamicAnimation.TRANSLATION_X));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testArePropertiesAnimating() throws InterruptedException {
|
||||
mLayout.setActiveController(mTestableController);
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
|
||||
assertFalse(mLayout.arePropertiesAnimating(
|
||||
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
|
||||
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.translationX(100f)
|
||||
.start();
|
||||
|
||||
// Wait for the animations to get underway.
|
||||
SystemClock.sleep(50);
|
||||
|
||||
assertTrue(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_X));
|
||||
assertFalse(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_Y));
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
|
||||
|
||||
assertFalse(mLayout.arePropertiesAnimating(
|
||||
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testCancelAllAnimations() throws InterruptedException {
|
||||
mLayout.setActiveController(mTestableController);
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.position(1000, 1000)
|
||||
.start();
|
||||
|
||||
mLayout.cancelAllAnimations();
|
||||
|
||||
// Animations should be somewhere before their end point.
|
||||
assertTrue(mViews.get(0).getTranslationX() < 1000);
|
||||
assertTrue(mViews.get(0).getTranslationY() < 1000);
|
||||
}
|
||||
|
||||
/** Standard test of chained translation animations. */
|
||||
private void testChainedTranslationAnimations() throws InterruptedException {
|
||||
mLayout.setActiveController(mTestableController);
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
|
||||
assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
|
||||
assertEquals(0, mLayout.getChildAt(1).getTranslationX(), .1f);
|
||||
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.translationX(100f)
|
||||
.start();
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
|
||||
|
||||
for (int i = 0; i < mLayout.getChildCount(); i++) {
|
||||
assertEquals(
|
||||
100 + i * TEST_TRANSLATION_X_OFFSET,
|
||||
mLayout.getChildAt(i).getTranslationX(), .1f);
|
||||
}
|
||||
|
||||
// Ensure that the Y translations were unaffected.
|
||||
assertEquals(0, mLayout.getChildAt(0).getTranslationY(), .1f);
|
||||
assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f);
|
||||
|
||||
// Animate the first child's Y translation.
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.translationY(100f)
|
||||
.start();
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Ensure that only the first view's Y translation chained, since we only chained X
|
||||
// translations.
|
||||
assertEquals(100, mLayout.getChildAt(0).getTranslationY(), .1f);
|
||||
assertEquals(0, mLayout.getChildAt(1).getTranslationY(), .1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testPhysicsAnimator() throws InterruptedException {
|
||||
mLayout.setActiveController(mTestableController);
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
|
||||
Runnable afterAll = Mockito.mock(Runnable.class);
|
||||
Runnable after = Mockito.spy(new Runnable() {
|
||||
int mCallCount = 0;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Make sure that if only one of the animations has finished, we didn't already call
|
||||
// afterAll.
|
||||
if (mCallCount == 1) {
|
||||
Mockito.verifyNoMoreInteractions(afterAll);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Animate from x = 7 to x = 100, and from y = 100 to 7 = 200, calling 'after' after each
|
||||
// property's animation completes, then call afterAll when they're all complete.
|
||||
mTestableController.animationForChildAtIndex(0)
|
||||
.translationX(7, 100, after)
|
||||
.translationY(100, 200, after)
|
||||
.start(afterAll);
|
||||
|
||||
// We should have immediately set the 'from' values.
|
||||
assertEquals(7, mViews.get(0).getTranslationX(), .01f);
|
||||
assertEquals(100, mViews.get(0).getTranslationY(), .01f);
|
||||
|
||||
waitForPropertyAnimations(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// We should have called the after callback twice, and afterAll once. We verify in the
|
||||
// mocked callback that afterAll isn't called before both finish.
|
||||
Mockito.verify(after, times(2)).run();
|
||||
Mockito.verify(afterAll).run();
|
||||
|
||||
// Make sure we actually animated the views.
|
||||
assertEquals(100, mViews.get(0).getTranslationX(), .01f);
|
||||
assertEquals(200, mViews.get(0).getTranslationY(), .01f);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testAnimationsForChildrenFromIndex() throws InterruptedException {
|
||||
// Don't chain since we're going to invoke each animation independently.
|
||||
mTestableController.setChainedProperties(new HashSet<>());
|
||||
|
||||
mLayout.setActiveController(mTestableController);
|
||||
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
|
||||
Runnable allEnd = Mockito.mock(Runnable.class);
|
||||
|
||||
mTestableController.animationsForChildrenFromIndex(
|
||||
1, (index, animation) -> animation.translationX((index - 1) * 50))
|
||||
.startAll(allEnd);
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
|
||||
|
||||
assertEquals(0, mViews.get(0).getTranslationX(), .1f);
|
||||
assertEquals(0, mViews.get(1).getTranslationX(), .1f);
|
||||
assertEquals(50, mViews.get(2).getTranslationX(), .1f);
|
||||
assertEquals(100, mViews.get(3).getTranslationX(), .1f);
|
||||
|
||||
Mockito.verify(allEnd, times(1)).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testAnimationsForChildrenFromIndex_noChildren() {
|
||||
mLayout.setActiveController(mTestableController);
|
||||
|
||||
final Runnable after = Mockito.mock(Runnable.class);
|
||||
mTestableController
|
||||
.animationsForChildrenFromIndex(0, (index, animation) -> { })
|
||||
.startAll(after);
|
||||
|
||||
verify(after, Mockito.times(1)).run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation controller with configuration methods whose return values can be set by individual
|
||||
* tests.
|
||||
*/
|
||||
private class TestableAnimationController
|
||||
extends PhysicsAnimationLayout.PhysicsAnimationController {
|
||||
private Set<DynamicAnimation.ViewProperty> mAnimatedProperties = new HashSet<>();
|
||||
private Set<DynamicAnimation.ViewProperty> mChainedProperties = new HashSet<>();
|
||||
private HashMap<DynamicAnimation.ViewProperty, Float> mOffsetForProperty = new HashMap<>();
|
||||
private boolean mRemoveImmediately = false;
|
||||
|
||||
void setAnimatedProperties(
|
||||
Set<DynamicAnimation.ViewProperty> animatedProperties) {
|
||||
mAnimatedProperties = animatedProperties;
|
||||
}
|
||||
|
||||
void setChainedProperties(
|
||||
Set<DynamicAnimation.ViewProperty> chainedProperties) {
|
||||
mChainedProperties = chainedProperties;
|
||||
}
|
||||
|
||||
void setOffsetForProperty(
|
||||
DynamicAnimation.ViewProperty property, float offset) {
|
||||
mOffsetForProperty.put(property, offset);
|
||||
}
|
||||
|
||||
public void setRemoveImmediately(boolean removeImmediately) {
|
||||
mRemoveImmediately = removeImmediately;
|
||||
}
|
||||
|
||||
@Override
|
||||
Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
|
||||
return mAnimatedProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
|
||||
return mChainedProperties.contains(property) ? index + 1 : NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
|
||||
return mOffsetForProperty.getOrDefault(property, 0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
|
||||
return new SpringForce();
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildAdded(View child, int index) {}
|
||||
|
||||
@Override
|
||||
void onChildRemoved(View child, int index, Runnable finishRemoval) {
|
||||
if (mRemoveImmediately) {
|
||||
finishRemoval.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildReordered(View child, int oldIndex, int newIndex) {}
|
||||
|
||||
@Override
|
||||
void onActiveControllerForLayout(PhysicsAnimationLayout layout) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles.animation;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Test case for tests that involve the {@link PhysicsAnimationLayout}. This test case constructs a
|
||||
* testable version of the layout, and provides some helpful methods to add views to the layout and
|
||||
* wait for physics animations to finish running.
|
||||
*
|
||||
* See physics-animation-testing.md.
|
||||
*/
|
||||
public class PhysicsAnimationLayoutTestCase extends ShellTestCase {
|
||||
TestablePhysicsAnimationLayout mLayout;
|
||||
List<View> mViews = new ArrayList<>();
|
||||
|
||||
Handler mMainThreadHandler;
|
||||
|
||||
int mSystemWindowInsetSize = 50;
|
||||
int mCutoutInsetSize = 100;
|
||||
|
||||
int mWidth = 1000;
|
||||
int mHeight = 1000;
|
||||
|
||||
@Mock
|
||||
private WindowInsets mWindowInsets;
|
||||
|
||||
@Mock
|
||||
private DisplayCutout mCutout;
|
||||
|
||||
protected int mMaxBubbles;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mLayout = new TestablePhysicsAnimationLayout(mContext);
|
||||
mLayout.setLeft(0);
|
||||
mLayout.setRight(mWidth);
|
||||
mLayout.setTop(0);
|
||||
mLayout.setBottom(mHeight);
|
||||
|
||||
mMaxBubbles =
|
||||
getContext().getResources().getInteger(R.integer.bubbles_max_rendered);
|
||||
mMainThreadHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
when(mWindowInsets.getSystemWindowInsetTop()).thenReturn(mSystemWindowInsetSize);
|
||||
when(mWindowInsets.getSystemWindowInsetBottom()).thenReturn(mSystemWindowInsetSize);
|
||||
when(mWindowInsets.getSystemWindowInsetLeft()).thenReturn(mSystemWindowInsetSize);
|
||||
when(mWindowInsets.getSystemWindowInsetRight()).thenReturn(mSystemWindowInsetSize);
|
||||
|
||||
when(mWindowInsets.getDisplayCutout()).thenReturn(mCutout);
|
||||
when(mCutout.getSafeInsetTop()).thenReturn(mCutoutInsetSize);
|
||||
when(mCutout.getSafeInsetBottom()).thenReturn(mCutoutInsetSize);
|
||||
when(mCutout.getSafeInsetLeft()).thenReturn(mCutoutInsetSize);
|
||||
when(mCutout.getSafeInsetRight()).thenReturn(mCutoutInsetSize);
|
||||
}
|
||||
|
||||
/** Add one extra bubble over the limit, so we can make sure it's gone/chains appropriately. */
|
||||
void addOneMoreThanBubbleLimitBubbles() throws InterruptedException {
|
||||
for (int i = 0; i < mMaxBubbles + 1; i++) {
|
||||
final View newView = new FrameLayout(mContext);
|
||||
mLayout.addView(newView, 0);
|
||||
mViews.add(0, newView);
|
||||
|
||||
newView.setTranslationX(0);
|
||||
newView.setTranslationY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a {@link java.util.concurrent.CountDownLatch} to wait for the given properties'
|
||||
* animations to finish before allowing the test to proceed.
|
||||
*/
|
||||
void waitForPropertyAnimations(DynamicAnimation.ViewProperty... properties)
|
||||
throws InterruptedException {
|
||||
final CountDownLatch animLatch = new CountDownLatch(properties.length);
|
||||
for (DynamicAnimation.ViewProperty property : properties) {
|
||||
mLayout.setTestEndActionForProperty(animLatch::countDown, property);
|
||||
}
|
||||
|
||||
animLatch.await(2, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/** Uses a latch to wait for the main thread message queue to finish. */
|
||||
void waitForLayoutMessageQueue() throws InterruptedException {
|
||||
CountDownLatch layoutLatch = new CountDownLatch(1);
|
||||
mMainThreadHandler.post(layoutLatch::countDown);
|
||||
layoutLatch.await(2, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testable subclass of the PhysicsAnimationLayout that ensures methods that trigger animations
|
||||
* are run on the main thread, which is a requirement of DynamicAnimation.
|
||||
*/
|
||||
protected class TestablePhysicsAnimationLayout extends PhysicsAnimationLayout {
|
||||
public TestablePhysicsAnimationLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isActiveController(PhysicsAnimationController controller) {
|
||||
// Return true since otherwise all test controllers will be seen as inactive since they
|
||||
// are wrapped by MainThreadAnimationControllerWrapper.
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean post(Runnable action) {
|
||||
return mMainThreadHandler.post(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean postDelayed(Runnable action, long delayMillis) {
|
||||
return mMainThreadHandler.postDelayed(action, delayMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActiveController(PhysicsAnimationController controller) {
|
||||
runOnMainThreadAndBlock(
|
||||
() -> super.setActiveController(
|
||||
new MainThreadAnimationControllerWrapper(controller)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAllAnimations() {
|
||||
mMainThreadHandler.post(super::cancelAllAnimations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelAnimationsOnView(View view) {
|
||||
mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsets getRootWindowInsets() {
|
||||
return mWindowInsets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(View child, int index) {
|
||||
child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child));
|
||||
super.addView(child, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addView(View child, int index, ViewGroup.LayoutParams params) {
|
||||
child.setTag(R.id.physics_animator_tag, new TestablePhysicsPropertyAnimator(child));
|
||||
super.addView(child, index, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an end action that will be called after the 'real' end action that was already set.
|
||||
*/
|
||||
private void setTestEndActionForProperty(
|
||||
Runnable action, DynamicAnimation.ViewProperty property) {
|
||||
final Runnable realEndAction = mEndActionForProperty.get(property);
|
||||
mLayout.mEndActionForProperty.put(property, () -> {
|
||||
if (realEndAction != null) {
|
||||
realEndAction.run();
|
||||
}
|
||||
|
||||
action.run();
|
||||
});
|
||||
}
|
||||
|
||||
/** PhysicsPropertyAnimator that posts its animations to the main thread. */
|
||||
protected class TestablePhysicsPropertyAnimator extends PhysicsPropertyAnimator {
|
||||
public TestablePhysicsPropertyAnimator(View view) {
|
||||
super(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void animateValueForChild(DynamicAnimation.ViewProperty property, View view,
|
||||
float value, float startVel, long startDelay, float stiffness,
|
||||
float dampingRatio, Runnable[] afterCallbacks) {
|
||||
mMainThreadHandler.post(() -> super.animateValueForChild(
|
||||
property, view, value, startVel, startDelay, stiffness, dampingRatio,
|
||||
afterCallbacks));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startPathAnimation() {
|
||||
mMainThreadHandler.post(super::startPathAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around an animation controller that dispatches methods that could start
|
||||
* animations to the main thread.
|
||||
*/
|
||||
protected class MainThreadAnimationControllerWrapper extends PhysicsAnimationController {
|
||||
|
||||
private final PhysicsAnimationController mWrappedController;
|
||||
|
||||
protected MainThreadAnimationControllerWrapper(PhysicsAnimationController controller) {
|
||||
mWrappedController = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setLayout(PhysicsAnimationLayout layout) {
|
||||
mWrappedController.setLayout(layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PhysicsAnimationLayout getLayout() {
|
||||
return mWrappedController.getLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
Set<DynamicAnimation.ViewProperty> getAnimatedProperties() {
|
||||
return mWrappedController.getAnimatedProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) {
|
||||
return mWrappedController.getNextAnimationInChain(property, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) {
|
||||
return mWrappedController.getOffsetForChainedPropertyAnimation(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) {
|
||||
return mWrappedController.getSpringForce(property, view);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildAdded(View child, int index) {
|
||||
runOnMainThreadAndBlock(() -> mWrappedController.onChildAdded(child, index));
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildRemoved(View child, int index, Runnable finishRemoval) {
|
||||
runOnMainThreadAndBlock(
|
||||
() -> mWrappedController.onChildRemoved(child, index, finishRemoval));
|
||||
}
|
||||
|
||||
@Override
|
||||
void onChildReordered(View child, int oldIndex, int newIndex) {
|
||||
runOnMainThreadAndBlock(
|
||||
() -> mWrappedController.onChildReordered(child, oldIndex, newIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
void onActiveControllerForLayout(PhysicsAnimationLayout layout) {
|
||||
runOnMainThreadAndBlock(
|
||||
() -> mWrappedController.onActiveControllerForLayout(layout));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PhysicsPropertyAnimator animationForChild(View child) {
|
||||
PhysicsPropertyAnimator animator =
|
||||
(PhysicsPropertyAnimator) child.getTag(R.id.physics_animator_tag);
|
||||
|
||||
if (!(animator instanceof TestablePhysicsPropertyAnimator)) {
|
||||
animator = new TestablePhysicsPropertyAnimator(child);
|
||||
child.setTag(R.id.physics_animator_tag, animator);
|
||||
}
|
||||
|
||||
return animator;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts the given Runnable on the main thread, and blocks the calling thread until it's run.
|
||||
*/
|
||||
private void runOnMainThreadAndBlock(Runnable action) {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
mMainThreadHandler.post(() -> {
|
||||
action.run();
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles.animation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.graphics.PointF;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.dynamicanimation.animation.DynamicAnimation;
|
||||
import androidx.dynamicanimation.animation.SpringForce;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.bubbles.BubblePositioner;
|
||||
import com.android.wm.shell.common.FloatingContentCoordinator;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase {
|
||||
|
||||
@Mock
|
||||
private FloatingContentCoordinator mFloatingContentCoordinator;
|
||||
|
||||
private TestableStackController mStackController;
|
||||
|
||||
private int mStackOffset;
|
||||
private Runnable mCheckStartPosSet;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mStackController = spy(new TestableStackController(
|
||||
mFloatingContentCoordinator, new IntSupplier() {
|
||||
@Override
|
||||
public int getAsInt() {
|
||||
return mLayout.getChildCount();
|
||||
}
|
||||
}, mock(Runnable.class)));
|
||||
mLayout.setActiveController(mStackController);
|
||||
addOneMoreThanBubbleLimitBubbles();
|
||||
mStackOffset = mLayout.getResources().getDimensionPixelSize(R.dimen.bubble_stack_offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test moving around the stack, and make sure the position is updated correctly, and the stack
|
||||
* direction is correct.
|
||||
*/
|
||||
@Test
|
||||
@Ignore("Flaking")
|
||||
public void testMoveFirstBubbleWithStackFollowing() throws InterruptedException {
|
||||
mStackController.moveFirstBubbleWithStackFollowing(200, 100);
|
||||
|
||||
// The first bubble should have moved instantly, the rest should be waiting for animation.
|
||||
assertEquals(200, mViews.get(0).getTranslationX(), .1f);
|
||||
assertEquals(100, mViews.get(0).getTranslationY(), .1f);
|
||||
assertEquals(0, mViews.get(1).getTranslationX(), .1f);
|
||||
assertEquals(0, mViews.get(1).getTranslationY(), .1f);
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Make sure the rest of the stack got moved to the right place and is stacked to the left.
|
||||
testStackedAtPosition(200, 100, -1);
|
||||
assertEquals(new PointF(200, 100), mStackController.getStackPosition());
|
||||
|
||||
mStackController.moveFirstBubbleWithStackFollowing(1000, 500);
|
||||
|
||||
// The first bubble again should have moved instantly while the rest remained where they
|
||||
// were until the animation takes over.
|
||||
assertEquals(1000, mViews.get(0).getTranslationX(), .1f);
|
||||
assertEquals(500, mViews.get(0).getTranslationY(), .1f);
|
||||
assertEquals(200 + -mStackOffset, mViews.get(1).getTranslationX(), .1f);
|
||||
assertEquals(100, mViews.get(1).getTranslationY(), .1f);
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Make sure the rest of the stack moved again, including the first bubble not moving, and
|
||||
// is stacked to the right now that we're on the right side of the screen.
|
||||
testStackedAtPosition(1000, 500, 1);
|
||||
assertEquals(new PointF(1000, 500), mStackController.getStackPosition());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Sporadically failing due to DynamicAnimation not settling.")
|
||||
public void testFlingSideways() throws InterruptedException {
|
||||
// Hard fling directly upwards, no X velocity. The X fling should terminate pretty much
|
||||
// immediately, and spring to 0f, the y fling is hard enough that it will overshoot the top
|
||||
// but should bounce back down.
|
||||
mStackController.flingThenSpringFirstBubbleWithStackFollowing(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
5000f, 1.15f, new SpringForce(), mWidth * 1f);
|
||||
mStackController.flingThenSpringFirstBubbleWithStackFollowing(
|
||||
DynamicAnimation.TRANSLATION_Y,
|
||||
0f, 1.15f, new SpringForce(), 0f);
|
||||
|
||||
// Nothing should move initially since the animations haven't begun, including the first
|
||||
// view.
|
||||
assertEquals(0f, mViews.get(0).getTranslationX(), 1f);
|
||||
assertEquals(0f, mViews.get(0).getTranslationY(), 1f);
|
||||
|
||||
// Wait for the flinging.
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X,
|
||||
DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Wait for the springing.
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X,
|
||||
DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Once the dust has settled, we should have flung all the way to the right side, with the
|
||||
// stack stacked off to the right now.
|
||||
testStackedAtPosition(mWidth * 1f, 0f, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Sporadically failing due to DynamicAnimation not settling.")
|
||||
public void testFlingUpFromBelowBottomCenter() throws InterruptedException {
|
||||
// Move to the center of the screen, just past the bottom.
|
||||
mStackController.moveFirstBubbleWithStackFollowing(mWidth / 2f, mHeight + 100);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Hard fling directly upwards, no X velocity. The X fling should terminate pretty much
|
||||
// immediately, and spring to 0f, the y fling is hard enough that it will overshoot the top
|
||||
// but should bounce back down.
|
||||
mStackController.flingThenSpringFirstBubbleWithStackFollowing(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
0, 1.15f, new SpringForce(), 27f);
|
||||
mStackController.flingThenSpringFirstBubbleWithStackFollowing(
|
||||
DynamicAnimation.TRANSLATION_Y,
|
||||
5000f, 1.15f, new SpringForce(), 27f);
|
||||
|
||||
// Nothing should move initially since the animations haven't begun.
|
||||
assertEquals(mWidth / 2f, mViews.get(0).getTranslationX(), .1f);
|
||||
assertEquals(mHeight + 100, mViews.get(0).getTranslationY(), .1f);
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X,
|
||||
DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
// Once the dust has settled, we should have flung a bit but then sprung to the final
|
||||
// destination which is (27, 27).
|
||||
testStackedAtPosition(27, 27, -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Flaking")
|
||||
public void testChildAdded() throws InterruptedException {
|
||||
// Move the stack to y = 500.
|
||||
mStackController.moveFirstBubbleWithStackFollowing(0f, 500f);
|
||||
waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X,
|
||||
DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
final View newView = new FrameLayout(mContext);
|
||||
mLayout.addView(
|
||||
newView,
|
||||
0,
|
||||
new FrameLayout.LayoutParams(50, 50));
|
||||
|
||||
waitForStartPosToBeSet();
|
||||
waitForLayoutMessageQueue();
|
||||
waitForPropertyAnimations(
|
||||
DynamicAnimation.TRANSLATION_X,
|
||||
DynamicAnimation.TRANSLATION_Y,
|
||||
DynamicAnimation.SCALE_X,
|
||||
DynamicAnimation.SCALE_Y);
|
||||
|
||||
// The new view should be at the top of the stack, in the correct position.
|
||||
assertEquals(0f, newView.getTranslationX(), .1f);
|
||||
assertEquals(500f, newView.getTranslationY(), .1f);
|
||||
assertEquals(1f, newView.getScaleX(), .1f);
|
||||
assertEquals(1f, newView.getScaleY(), .1f);
|
||||
assertEquals(1f, newView.getAlpha(), .1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Occasionally flakes, ignoring pending investigation.")
|
||||
public void testChildRemoved() throws InterruptedException {
|
||||
assertEquals(0, mLayout.getTransientViewCount());
|
||||
|
||||
final View firstView = mLayout.getChildAt(0);
|
||||
mLayout.removeView(firstView);
|
||||
|
||||
// The view should now be transient, and missing from the view's normal hierarchy.
|
||||
assertEquals(1, mLayout.getTransientViewCount());
|
||||
assertEquals(-1, mLayout.indexOfChild(firstView));
|
||||
|
||||
waitForPropertyAnimations(DynamicAnimation.ALPHA);
|
||||
waitForLayoutMessageQueue();
|
||||
|
||||
// The view should now be gone entirely, no transient views left.
|
||||
assertEquals(0, mLayout.getTransientViewCount());
|
||||
|
||||
// The subsequent view should have been translated over to 0, not stacked off to the left.
|
||||
assertEquals(0, mLayout.getChildAt(0).getTranslationX(), .1f);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("Flaky")
|
||||
public void testRestoredAtRestingPosition() throws InterruptedException {
|
||||
mStackController.flingStackThenSpringToEdge(0, 5000, 5000);
|
||||
|
||||
waitForPropertyAnimations(
|
||||
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
waitForLayoutMessageQueue();
|
||||
|
||||
final PointF prevStackPos = mStackController.getStackPosition();
|
||||
|
||||
mLayout.removeAllViews();
|
||||
|
||||
waitForLayoutMessageQueue();
|
||||
|
||||
mLayout.addView(new FrameLayout(getContext()));
|
||||
|
||||
waitForLayoutMessageQueue();
|
||||
waitForPropertyAnimations(
|
||||
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
|
||||
|
||||
assertEquals(prevStackPos, mStackController.getStackPosition());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFloatingCoordinator() {
|
||||
// We should have called onContentAdded only once while adding all of the bubbles in
|
||||
// setup().
|
||||
verify(mFloatingContentCoordinator, times(1)).onContentAdded(any());
|
||||
verify(mFloatingContentCoordinator, never()).onContentRemoved(any());
|
||||
|
||||
// Remove all views and verify that we called onContentRemoved only once.
|
||||
while (mLayout.getChildCount() > 0) {
|
||||
mLayout.removeView(mLayout.getChildAt(0));
|
||||
}
|
||||
|
||||
verify(mFloatingContentCoordinator, times(1)).onContentRemoved(any());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks every child view to make sure it's stacked at the given coordinates, off to the left
|
||||
* or right side depending on offset multiplier.
|
||||
*/
|
||||
private void testStackedAtPosition(float x, float y, int offsetMultiplier) {
|
||||
// Make sure the rest of the stack moved again, including the first bubble not moving, and
|
||||
// is stacked to the right now that we're on the right side of the screen.
|
||||
for (int i = 0; i < mLayout.getChildCount(); i++) {
|
||||
assertEquals(x + i * offsetMultiplier * mStackOffset,
|
||||
mViews.get(i).getTranslationX(), 2f);
|
||||
assertEquals(y, mViews.get(i).getTranslationY(), 2f);
|
||||
}
|
||||
}
|
||||
|
||||
/** Waits up to 2 seconds for the initial stack position to be initialized. */
|
||||
private void waitForStartPosToBeSet() throws InterruptedException {
|
||||
final CountDownLatch animLatch = new CountDownLatch(1);
|
||||
|
||||
mCheckStartPosSet = () -> {
|
||||
if (mStackController.getStackPosition().x >= 0) {
|
||||
animLatch.countDown();
|
||||
} else {
|
||||
mMainThreadHandler.post(mCheckStartPosSet);
|
||||
}
|
||||
};
|
||||
|
||||
mMainThreadHandler.post(mCheckStartPosSet);
|
||||
|
||||
try {
|
||||
animLatch.await(2, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
mMainThreadHandler.removeCallbacks(mCheckStartPosSet);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Testable version of the stack controller that dispatches its animations on the main thread.
|
||||
*/
|
||||
private class TestableStackController extends StackAnimationController {
|
||||
TestableStackController(
|
||||
FloatingContentCoordinator floatingContentCoordinator,
|
||||
IntSupplier bubbleCountSupplier,
|
||||
Runnable onBubbleAnimatedOutAction) {
|
||||
super(floatingContentCoordinator,
|
||||
bubbleCountSupplier,
|
||||
onBubbleAnimatedOutAction,
|
||||
mock(BubblePositioner.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void flingThenSpringFirstBubbleWithStackFollowing(
|
||||
DynamicAnimation.ViewProperty property, float vel, float friction,
|
||||
SpringForce spring, Float finalPosition) {
|
||||
mMainThreadHandler.post(() ->
|
||||
super.flingThenSpringFirstBubbleWithStackFollowing(
|
||||
property, vel, friction, spring, finalPosition));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property,
|
||||
SpringForce spring, float vel, float finalPosition, Runnable... after) {
|
||||
mMainThreadHandler.post(() ->
|
||||
super.springFirstBubbleWithStackFollowing(
|
||||
property, spring, vel, finalPosition, after));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles.storage
|
||||
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.wm.shell.ShellTestCase
|
||||
import junit.framework.Assert.assertEquals
|
||||
import junit.framework.Assert.assertNotNull
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class BubblePersistentRepositoryTest : ShellTestCase() {
|
||||
|
||||
private val bubbles = listOf(
|
||||
BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1", 120, 0),
|
||||
BubbleEntity(10, "com.example.chat", "alice and bob", "key-2", 0, 16537428, "title"),
|
||||
BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3", 120, 0)
|
||||
)
|
||||
private lateinit var repository: BubblePersistentRepository
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
repository = BubblePersistentRepository(mContext)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReadWriteOperation() {
|
||||
// Verify read before write doesn't cause FileNotFoundException
|
||||
val actual = repository.readFromDisk()
|
||||
assertNotNull(actual)
|
||||
assertTrue(actual.isEmpty())
|
||||
|
||||
repository.persistsToDisk(bubbles)
|
||||
assertEquals(bubbles, repository.readFromDisk())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles.storage
|
||||
|
||||
import android.content.pm.LauncherApps
|
||||
import android.os.UserHandle
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.util.mockito.eq
|
||||
import com.android.wm.shell.ShellTestCase
|
||||
import junit.framework.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class BubbleVolatileRepositoryTest : ShellTestCase() {
|
||||
|
||||
private val user0 = UserHandle.of(0)
|
||||
private val user10 = UserHandle.of(10)
|
||||
|
||||
private val bubble1 = BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1", 120, 0)
|
||||
private val bubble2 = BubbleEntity(10, "com.example.chat", "alice and bob",
|
||||
"key-2", 0, 16537428, "title")
|
||||
private val bubble3 = BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3", 120, 0)
|
||||
|
||||
private val bubbles = listOf(bubble1, bubble2, bubble3)
|
||||
|
||||
private lateinit var repository: BubbleVolatileRepository
|
||||
private lateinit var launcherApps: LauncherApps
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
launcherApps = mock(LauncherApps::class.java)
|
||||
repository = BubbleVolatileRepository(launcherApps)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddBubbles() {
|
||||
repository.addBubbles(bubbles)
|
||||
assertEquals(bubbles, repository.bubbles)
|
||||
verify(launcherApps).cacheShortcuts(eq(PKG_MESSENGER),
|
||||
eq(listOf("shortcut-1", "shortcut-2")), eq(user0),
|
||||
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
|
||||
verify(launcherApps).cacheShortcuts(eq(PKG_CHAT),
|
||||
eq(listOf("alice and bob")), eq(user10),
|
||||
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
|
||||
|
||||
repository.addBubbles(listOf(bubble1))
|
||||
assertEquals(listOf(bubble2, bubble3, bubble1), repository.bubbles)
|
||||
verifyNoMoreInteractions(launcherApps)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRemoveBubbles() {
|
||||
repository.addBubbles(bubbles)
|
||||
assertEquals(bubbles, repository.bubbles)
|
||||
|
||||
repository.removeBubbles(listOf(bubble3))
|
||||
assertEquals(listOf(bubble1, bubble2), repository.bubbles)
|
||||
verify(launcherApps).uncacheShortcuts(eq(PKG_MESSENGER),
|
||||
eq(listOf("shortcut-2")), eq(user0),
|
||||
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddAndRemoveBubblesWhenExceedingCapacity() {
|
||||
repository.capacity = 2
|
||||
// push bubbles beyond capacity
|
||||
repository.addBubbles(bubbles)
|
||||
// verify it is trim down to capacity
|
||||
assertEquals(listOf(bubble2, bubble3), repository.bubbles)
|
||||
verify(launcherApps).cacheShortcuts(eq(PKG_MESSENGER),
|
||||
eq(listOf("shortcut-2")), eq(user0),
|
||||
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
|
||||
verify(launcherApps).cacheShortcuts(eq(PKG_CHAT),
|
||||
eq(listOf("alice and bob")), eq(user10),
|
||||
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
|
||||
|
||||
repository.addBubbles(listOf(bubble1))
|
||||
// verify the oldest bubble is popped
|
||||
assertEquals(listOf(bubble3, bubble1), repository.bubbles)
|
||||
verify(launcherApps).uncacheShortcuts(eq(PKG_CHAT),
|
||||
eq(listOf("alice and bob")), eq(user10),
|
||||
eq(LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddBubbleMatchesByKey() {
|
||||
val bubble = BubbleEntity(0, "com.example.pkg", "shortcut-id", "key", 120, 0, "title")
|
||||
repository.addBubbles(listOf(bubble))
|
||||
assertEquals(bubble, repository.bubbles.get(0))
|
||||
|
||||
// Same key as first bubble but different entry
|
||||
val bubbleModified = BubbleEntity(0, "com.example.pkg", "shortcut-id", "key", 120, 0,
|
||||
"different title")
|
||||
repository.addBubbles(listOf(bubbleModified))
|
||||
assertEquals(bubbleModified, repository.bubbles.get(0))
|
||||
}
|
||||
}
|
||||
|
||||
private const val PKG_MESSENGER = "com.example.messenger"
|
||||
private const val PKG_CHAT = "com.example.chat"
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.wm.shell.bubbles.storage
|
||||
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.wm.shell.ShellTestCase
|
||||
import junit.framework.Assert.assertEquals
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class BubbleXmlHelperTest : ShellTestCase() {
|
||||
|
||||
private val bubbles = listOf(
|
||||
BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1", 120, 0),
|
||||
BubbleEntity(10, "com.example.chat", "alice and bob", "k2", 0, 16537428, "title"),
|
||||
BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3", 120, 0)
|
||||
)
|
||||
|
||||
@Test
|
||||
fun testWriteXml() {
|
||||
val expectedEntries = """
|
||||
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" />
|
||||
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" />
|
||||
<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" />
|
||||
""".trimIndent()
|
||||
ByteArrayOutputStream().use {
|
||||
writeXml(it, bubbles)
|
||||
val actual = it.toString()
|
||||
assertTrue("cannot find expected entry in \n$actual",
|
||||
actual.contains(expectedEntries))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testReadXml() {
|
||||
val src = """
|
||||
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
|
||||
<bs v="1">
|
||||
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" />
|
||||
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" />
|
||||
<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" />
|
||||
</bs>
|
||||
""".trimIndent()
|
||||
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
|
||||
assertEquals("failed parsing bubbles from xml\n$src", bubbles, actual)
|
||||
}
|
||||
|
||||
// TODO: We should handle upgrades gracefully but this is v1
|
||||
@Test
|
||||
fun testUpgradeDropsPreviousData() {
|
||||
val src = """
|
||||
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
|
||||
<bs>
|
||||
<bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" />
|
||||
<bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" t="title" />
|
||||
<bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" />
|
||||
</bs>
|
||||
""".trimIndent()
|
||||
val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
|
||||
assertEquals("failed parsing bubbles from xml\n$src", emptyList<BubbleEntity>(), actual)
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,7 @@ import android.view.SurfaceControl;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.pip.PipAnimationController;
|
||||
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
|
||||
import com.android.wm.shell.pip.PipTestCase;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -49,7 +47,7 @@ import org.mockito.MockitoAnnotations;
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@SmallTest
|
||||
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||
public class PipAnimationControllerTest extends PipTestCase {
|
||||
public class PipAnimationControllerTest extends ShellTestCase {
|
||||
|
||||
private PipAnimationController mPipAnimationController;
|
||||
|
||||
|
||||
@@ -30,8 +30,7 @@ import android.view.Gravity;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.pip.PipBoundsHandler;
|
||||
import com.android.wm.shell.pip.PipTestCase;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -46,7 +45,7 @@ import org.junit.runner.RunWith;
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@SmallTest
|
||||
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||
public class PipBoundsHandlerTest extends PipTestCase {
|
||||
public class PipBoundsHandlerTest extends ShellTestCase {
|
||||
private static final int ROUNDING_ERROR_MARGIN = 16;
|
||||
private static final float ASPECT_RATIO_ERROR_MARGIN = 0.01f;
|
||||
private static final float DEFAULT_ASPECT_RATIO = 1f;
|
||||
|
||||
@@ -28,6 +28,8 @@ import android.util.Size;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -38,7 +40,7 @@ import org.junit.runner.RunWith;
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@TestableLooper.RunWithLooper
|
||||
@SmallTest
|
||||
public class PipBoundsStateTest extends PipTestCase {
|
||||
public class PipBoundsStateTest extends ShellTestCase {
|
||||
|
||||
private static final Rect DEFAULT_BOUNDS = new Rect(0, 0, 10, 10);
|
||||
private static final float DEFAULT_SNAP_FRACTION = 1.0f;
|
||||
|
||||
@@ -40,6 +40,7 @@ import android.view.DisplayInfo;
|
||||
import android.window.WindowContainerToken;
|
||||
|
||||
import com.android.wm.shell.ShellTaskOrganizer;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.pip.phone.PipMenuActivityController;
|
||||
import com.android.wm.shell.splitscreen.SplitScreen;
|
||||
@@ -58,7 +59,7 @@ import java.util.Optional;
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@TestableLooper.RunWithLooper
|
||||
public class PipTaskOrganizerTest extends PipTestCase {
|
||||
public class PipTaskOrganizerTest extends ShellTestCase {
|
||||
private PipTaskOrganizer mSpiedPipTaskOrganizer;
|
||||
|
||||
@Mock private DisplayController mMockdDisplayController;
|
||||
|
||||
@@ -35,6 +35,7 @@ import android.test.suitebuilder.annotation.SmallTest;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.WindowManagerShellWrapper;
|
||||
import com.android.wm.shell.common.DisplayController;
|
||||
import com.android.wm.shell.common.ShellExecutor;
|
||||
@@ -42,7 +43,6 @@ import com.android.wm.shell.pip.PipBoundsHandler;
|
||||
import com.android.wm.shell.pip.PipBoundsState;
|
||||
import com.android.wm.shell.pip.PipMediaController;
|
||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||
import com.android.wm.shell.pip.PipTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -56,7 +56,7 @@ import org.mockito.MockitoAnnotations;
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@TestableLooper.RunWithLooper
|
||||
public class PipControllerTest extends PipTestCase {
|
||||
public class PipControllerTest extends ShellTestCase {
|
||||
private PipController mPipController;
|
||||
|
||||
@Mock private DisplayController mMockDisplayController;
|
||||
|
||||
@@ -31,17 +31,13 @@ import android.util.Size;
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.R;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
import com.android.wm.shell.common.FloatingContentCoordinator;
|
||||
import com.android.wm.shell.pip.PipBoundsHandler;
|
||||
import com.android.wm.shell.pip.PipBoundsState;
|
||||
import com.android.wm.shell.pip.PipSnapAlgorithm;
|
||||
import com.android.wm.shell.pip.PipTaskOrganizer;
|
||||
import com.android.wm.shell.pip.PipTestCase;
|
||||
import com.android.wm.shell.pip.PipUiEventLogger;
|
||||
import com.android.wm.shell.pip.phone.PipMenuActivityController;
|
||||
import com.android.wm.shell.pip.phone.PipMotionHelper;
|
||||
import com.android.wm.shell.pip.phone.PipResizeGestureHandler;
|
||||
import com.android.wm.shell.pip.phone.PipTouchHandler;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -59,7 +55,7 @@ import org.mockito.MockitoAnnotations;
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@SmallTest
|
||||
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||
public class PipTouchHandlerTest extends PipTestCase {
|
||||
public class PipTouchHandlerTest extends ShellTestCase {
|
||||
|
||||
private PipTouchHandler mPipTouchHandler;
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@ import android.view.ViewConfiguration;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
|
||||
import com.android.wm.shell.pip.PipTestCase;
|
||||
import com.android.wm.shell.pip.phone.PipTouchState;
|
||||
import com.android.wm.shell.ShellTestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -47,7 +46,7 @@ import java.util.concurrent.CountDownLatch;
|
||||
@RunWith(AndroidTestingRunner.class)
|
||||
@SmallTest
|
||||
@RunWithLooper
|
||||
public class PipTouchStateTest extends PipTestCase {
|
||||
public class PipTouchStateTest extends ShellTestCase {
|
||||
|
||||
private PipTouchState mTouchState;
|
||||
private CountDownLatch mDoubleTapCallbackTriggeredLatch;
|
||||
|
||||
Reference in New Issue
Block a user