update:2021.03.19
fix:重新提交 add:
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.uiuios.allapps.search;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.ComponentName;
|
||||
|
||||
import com.android.uiuios.AppInfo;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DefaultAppSearchAlgorithm}
|
||||
*/
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DefaultAppSearchAlgorithmTest {
|
||||
private static final DefaultAppSearchAlgorithm.StringMatcher MATCHER =
|
||||
DefaultAppSearchAlgorithm.StringMatcher.getInstance();
|
||||
|
||||
@Test
|
||||
public void testMatches() {
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white cow"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCow"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whiteCOW"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCOW"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("white2cow"), "cow", MATCHER));
|
||||
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecow"), "cow", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitEcow"), "cow", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecowCow"), "cow", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("whitecow cow"), "cow", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whitecowcow"), "cow", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("whit ecowcow"), "cow", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&dogs"), "dog", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "dog", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("cats&Dogs"), "&", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "43", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("2+43"), "3", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Q"), "q", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo(" Q"), "q", MATCHER));
|
||||
|
||||
// match lower case words
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("elephant"), "e", MATCHER));
|
||||
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "电子", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "子", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("电子邮件"), "邮件", MATCHER));
|
||||
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("Bot"), "ba", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("bot"), "ba", MATCHER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchesVN() {
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드"), "다", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("드라이브"), "드", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷ", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("운로 드라이브"), "ㄷ", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åbç", MATCHER));
|
||||
assertTrue(DefaultAppSearchAlgorithm.matches(getInfo("Alpha"), "ål", MATCHER));
|
||||
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("다운로드 드라이브"), "ㄷㄷ", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("로드라이브"), "ㄷ", MATCHER));
|
||||
assertFalse(DefaultAppSearchAlgorithm.matches(getInfo("abc"), "åç", MATCHER));
|
||||
}
|
||||
|
||||
private AppInfo getInfo(String title) {
|
||||
AppInfo info = new AppInfo();
|
||||
info.title = title;
|
||||
info.componentName = new ComponentName("Test", title);
|
||||
return info;
|
||||
}
|
||||
}
|
||||
211
tests/src/com/android/uiuios/model/LoaderCursorTest.java
Normal file
211
tests/src/com/android/uiuios/model/LoaderCursorTest.java
Normal file
@@ -0,0 +1,211 @@
|
||||
package com.android.uiuios.model;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.MatrixCursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Process;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.uiuios.WorkspaceItemInfo;
|
||||
import com.android.uiuios.icons.IconCache;
|
||||
import com.android.uiuios.InvariantDeviceProfile;
|
||||
import com.android.uiuios.ItemInfo;
|
||||
import com.android.uiuios.LauncherAppState;
|
||||
import com.android.uiuios.Utilities;
|
||||
import com.android.uiuios.compat.LauncherAppsCompat;
|
||||
import com.android.uiuios.icons.BitmapInfo;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.INTENT;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.CELLX;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.CELLY;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.CONTAINER;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.CONTAINER_DESKTOP;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.ICON;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.ICON_PACKAGE;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.ICON_RESOURCE;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.ITEM_TYPE;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.PROFILE_ID;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.RESTORED;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.SCREEN;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites.TITLE;
|
||||
import static com.android.uiuios.LauncherSettings.Favorites._ID;
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.Assert.assertNotNull;
|
||||
import static junit.framework.Assert.assertNull;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link LoaderCursor}
|
||||
*/
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class LoaderCursorTest {
|
||||
|
||||
private LauncherAppState mMockApp;
|
||||
private IconCache mMockIconCache;
|
||||
|
||||
private MatrixCursor mCursor;
|
||||
private InvariantDeviceProfile mIDP;
|
||||
private Context mContext;
|
||||
private LauncherAppsCompat mLauncherApps;
|
||||
|
||||
private LoaderCursor mLoaderCursor;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
mIDP = new InvariantDeviceProfile();
|
||||
mCursor = new MatrixCursor(new String[] {
|
||||
ICON, ICON_PACKAGE, ICON_RESOURCE, TITLE,
|
||||
_ID, CONTAINER, ITEM_TYPE, PROFILE_ID,
|
||||
SCREEN, CELLX, CELLY, RESTORED, INTENT
|
||||
});
|
||||
mContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
mMockApp = mock(LauncherAppState.class);
|
||||
mMockIconCache = mock(IconCache.class);
|
||||
when(mMockApp.getIconCache()).thenReturn(mMockIconCache);
|
||||
when(mMockApp.getInvariantDeviceProfile()).thenReturn(mIDP);
|
||||
when(mMockApp.getContext()).thenReturn(mContext);
|
||||
mLauncherApps = LauncherAppsCompat.getInstance(mContext);
|
||||
|
||||
mLoaderCursor = new LoaderCursor(mCursor, mMockApp);
|
||||
mLoaderCursor.allUsers.put(0, Process.myUserHandle());
|
||||
}
|
||||
|
||||
private void initCursor(int itemType, String title) {
|
||||
mCursor.newRow()
|
||||
.add(_ID, 1)
|
||||
.add(PROFILE_ID, 0)
|
||||
.add(ITEM_TYPE, itemType)
|
||||
.add(TITLE, title)
|
||||
.add(CONTAINER, CONTAINER_DESKTOP);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAppShortcutInfo_dontAllowMissing_invalidComponent() {
|
||||
initCursor(ITEM_TYPE_APPLICATION, "");
|
||||
assertTrue(mLoaderCursor.moveToNext());
|
||||
ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
|
||||
assertNull(mLoaderCursor.getAppShortcutInfo(
|
||||
new Intent().setComponent(cn), false /* allowMissingTarget */, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAppShortcutInfo_dontAllowMissing_validComponent() {
|
||||
initCursor(ITEM_TYPE_APPLICATION, "");
|
||||
assertTrue(mLoaderCursor.moveToNext());
|
||||
|
||||
ComponentName cn = mLauncherApps.getActivityList(null, mLoaderCursor.user)
|
||||
.get(0).getComponentName();
|
||||
WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
|
||||
new Intent().setComponent(cn), false /* allowMissingTarget */, true);
|
||||
assertNotNull(info);
|
||||
assertTrue(Utilities.isLauncherAppTarget(info.intent));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAppShortcutInfo_allowMissing_invalidComponent() {
|
||||
initCursor(ITEM_TYPE_APPLICATION, "");
|
||||
assertTrue(mLoaderCursor.moveToNext());
|
||||
|
||||
ComponentName cn = new ComponentName(mContext.getPackageName(), "dummy-do");
|
||||
WorkspaceItemInfo info = mLoaderCursor.getAppShortcutInfo(
|
||||
new Intent().setComponent(cn), true /* allowMissingTarget */, true);
|
||||
assertNotNull(info);
|
||||
assertTrue(Utilities.isLauncherAppTarget(info.intent));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadSimpleShortcut() {
|
||||
initCursor(ITEM_TYPE_SHORTCUT, "my-shortcut");
|
||||
assertTrue(mLoaderCursor.moveToNext());
|
||||
|
||||
Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
|
||||
when(mMockIconCache.getDefaultIcon(eq(mLoaderCursor.user)))
|
||||
.thenReturn(BitmapInfo.fromBitmap(icon));
|
||||
WorkspaceItemInfo info = mLoaderCursor.loadSimpleWorkspaceItem();
|
||||
assertEquals(icon, info.iconBitmap);
|
||||
assertEquals("my-shortcut", info.title);
|
||||
assertEquals(ITEM_TYPE_SHORTCUT, info.itemType);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkItemPlacement_outsideBounds() {
|
||||
mIDP.numRows = 4;
|
||||
mIDP.numColumns = 4;
|
||||
mIDP.numHotseatIcons = 3;
|
||||
|
||||
// Item outside screen bounds are not placed
|
||||
assertFalse(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(4, 4, 1, 1, CONTAINER_DESKTOP, 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkItemPlacement_overlappingItems() {
|
||||
mIDP.numRows = 4;
|
||||
mIDP.numColumns = 4;
|
||||
mIDP.numHotseatIcons = 3;
|
||||
|
||||
// Overlapping items are not placed
|
||||
assertTrue(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1)));
|
||||
assertFalse(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 1)));
|
||||
|
||||
assertTrue(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2)));
|
||||
assertFalse(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(0, 0, 1, 1, CONTAINER_DESKTOP, 2)));
|
||||
|
||||
assertTrue(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(1, 1, 1, 1, CONTAINER_DESKTOP, 1)));
|
||||
assertTrue(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(2, 2, 2, 2, CONTAINER_DESKTOP, 1)));
|
||||
|
||||
assertFalse(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(3, 2, 1, 2, CONTAINER_DESKTOP, 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkItemPlacement_hotseat() {
|
||||
mIDP.numRows = 4;
|
||||
mIDP.numColumns = 4;
|
||||
mIDP.numHotseatIcons = 3;
|
||||
|
||||
// Hotseat items are only placed based on screenId
|
||||
assertTrue(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 1)));
|
||||
assertTrue(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 2)));
|
||||
|
||||
assertFalse(mLoaderCursor.checkItemPlacement(
|
||||
newItemInfo(3, 3, 1, 1, CONTAINER_HOTSEAT, 3)));
|
||||
}
|
||||
|
||||
private ItemInfo newItemInfo(int cellX, int cellY, int spanX, int spanY,
|
||||
int container, int screenId) {
|
||||
ItemInfo info = new ItemInfo();
|
||||
info.cellX = cellX;
|
||||
info.cellY = cellY;
|
||||
info.spanX = spanX;
|
||||
info.spanY = spanY;
|
||||
info.container = container;
|
||||
info.screenId = screenId;
|
||||
return info;
|
||||
}
|
||||
}
|
||||
99
tests/src/com/android/uiuios/provider/RestoreDbTaskTest.java
Normal file
99
tests/src/com/android/uiuios/provider/RestoreDbTaskTest.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package com.android.uiuios.provider;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.uiuios.LauncherProvider.DatabaseHelper;
|
||||
import com.android.uiuios.LauncherSettings.Favorites;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Tests for {@link RestoreDbTask}
|
||||
*/
|
||||
@MediumTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RestoreDbTaskTest {
|
||||
|
||||
@Test
|
||||
public void testGetProfileId() throws Exception {
|
||||
SQLiteDatabase db = new MyDatabaseHelper(23).getWritableDatabase();
|
||||
assertEquals(23, new RestoreDbTask().getDefaultProfileId(db));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMigrateProfileId() throws Exception {
|
||||
SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
|
||||
// Add some dummy data
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Favorites._ID, i);
|
||||
values.put(Favorites.TITLE, "item " + i);
|
||||
db.insert(Favorites.TABLE_NAME, null, values);
|
||||
}
|
||||
// Verify item add
|
||||
assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
|
||||
|
||||
new RestoreDbTask().migrateProfileId(db, 42, 33);
|
||||
|
||||
// verify data migrated
|
||||
assertEquals(0, getCount(db, "select * from favorites where profileId = 42"));
|
||||
assertEquals(5, getCount(db, "select * from favorites where profileId = 33"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChangeDefaultColumn() throws Exception {
|
||||
SQLiteDatabase db = new MyDatabaseHelper(42).getWritableDatabase();
|
||||
// Add some dummy data
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Favorites._ID, i);
|
||||
values.put(Favorites.TITLE, "item " + i);
|
||||
db.insert(Favorites.TABLE_NAME, null, values);
|
||||
}
|
||||
// Verify default column is 42
|
||||
assertEquals(5, getCount(db, "select * from favorites where profileId = 42"));
|
||||
|
||||
new RestoreDbTask().changeDefaultColumn(db, 33);
|
||||
|
||||
// Verify default value changed
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(Favorites._ID, 100);
|
||||
values.put(Favorites.TITLE, "item 100");
|
||||
db.insert(Favorites.TABLE_NAME, null, values);
|
||||
assertEquals(1, getCount(db, "select * from favorites where profileId = 33"));
|
||||
}
|
||||
|
||||
private int getCount(SQLiteDatabase db, String sql) {
|
||||
try (Cursor c = db.rawQuery(sql, null)) {
|
||||
return c.getCount();
|
||||
}
|
||||
}
|
||||
|
||||
private class MyDatabaseHelper extends DatabaseHelper {
|
||||
|
||||
private final long mProfileId;
|
||||
|
||||
MyDatabaseHelper(long profileId) {
|
||||
super(InstrumentationRegistry.getContext(), null, null);
|
||||
mProfileId = profileId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDefaultUserSerial() {
|
||||
return mProfileId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { }
|
||||
|
||||
protected void onEmptyDbCreated() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.uiuios.testcomponent;
|
||||
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
|
||||
/**
|
||||
* A simple app widget without any configuration screen and is hidden in picker.
|
||||
*/
|
||||
public class AppWidgetHidden extends AppWidgetProvider { }
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.testcomponent;
|
||||
|
||||
import android.appwidget.AppWidgetProvider;
|
||||
|
||||
/**
|
||||
* A simple app widget without any configuration screen.
|
||||
*/
|
||||
public class AppWidgetNoConfig extends AppWidgetProvider {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.testcomponent;
|
||||
|
||||
/**
|
||||
* A simple app widget with configuration sceen.
|
||||
*/
|
||||
public class AppWidgetWithConfig extends AppWidgetNoConfig {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.testcomponent;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.LinearLayout.LayoutParams;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
/**
|
||||
* Base activity with utility methods to help automate testing.
|
||||
*/
|
||||
public class BaseTestingActivity extends Activity implements View.OnClickListener {
|
||||
|
||||
public static final String SUFFIX_COMMAND = "-command";
|
||||
public static final String EXTRA_METHOD = "method";
|
||||
public static final String EXTRA_PARAM = "param_";
|
||||
|
||||
private static final int MARGIN_DP = 20;
|
||||
|
||||
private final String mAction = this.getClass().getName();
|
||||
|
||||
private LinearLayout mView;
|
||||
private int mMargin;
|
||||
|
||||
private final BroadcastReceiver mCommandReceiver = new BroadcastReceiver() {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
handleCommand(intent);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mMargin = Math.round(TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP, MARGIN_DP, getResources().getDisplayMetrics()));
|
||||
mView = new LinearLayout(this);
|
||||
mView.setPadding(mMargin, mMargin, mMargin, mMargin);
|
||||
mView.setOrientation(LinearLayout.VERTICAL);
|
||||
mView.setBackgroundColor(Color.BLUE);
|
||||
setContentView(mView);
|
||||
|
||||
registerReceiver(mCommandReceiver, new IntentFilter(mAction + SUFFIX_COMMAND));
|
||||
}
|
||||
|
||||
protected void addButton(String title, String method) {
|
||||
Button button = new Button(this);
|
||||
button.setText(title);
|
||||
button.setTag(method);
|
||||
button.setOnClickListener(this);
|
||||
|
||||
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||
lp.bottomMargin = mMargin;
|
||||
mView.addView(button, lp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
sendBroadcast(new Intent(mAction).putExtra(Intent.EXTRA_INTENT, getIntent()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterReceiver(mCommandReceiver);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
handleCommand(new Intent().putExtra(EXTRA_METHOD, (String) view.getTag()));
|
||||
}
|
||||
|
||||
private void handleCommand(Intent cmd) {
|
||||
String methodName = cmd.getStringExtra(EXTRA_METHOD);
|
||||
try {
|
||||
Method method = null;
|
||||
for (Method m : this.getClass().getDeclaredMethods()) {
|
||||
if (methodName.equals(m.getName()) &&
|
||||
!Modifier.isStatic(m.getModifiers()) &&
|
||||
Modifier.isPublic(m.getModifiers())) {
|
||||
method = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Object[] args = new Object[method.getParameterTypes().length];
|
||||
Bundle extras = cmd.getExtras();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
args[i] = extras.get(EXTRA_PARAM + i);
|
||||
}
|
||||
method.invoke(this, args);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Intent getCommandIntent(Class<?> clazz, String method) {
|
||||
return new Intent(clazz.getName() + SUFFIX_COMMAND)
|
||||
.putExtra(EXTRA_METHOD, method);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.testcomponent;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.IntentSender;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Bundle;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
/**
|
||||
* Sample activity to request pinning an item.
|
||||
*/
|
||||
@TargetApi(26)
|
||||
public class RequestPinItemActivity extends BaseTestingActivity {
|
||||
|
||||
private PendingIntent mCallback = null;
|
||||
private String mShortcutId = "test-id";
|
||||
private int mRemoteViewColor = Color.TRANSPARENT;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
addButton("Pin Shortcut", "pinShortcut");
|
||||
addButton("Pin Widget without config ", "pinWidgetNoConfig");
|
||||
addButton("Pin Widget with config", "pinWidgetWithConfig");
|
||||
}
|
||||
|
||||
public void setCallback(PendingIntent callback) {
|
||||
mCallback = callback;
|
||||
}
|
||||
|
||||
public void setRemoteViewColor(int color) {
|
||||
mRemoteViewColor = color;
|
||||
}
|
||||
|
||||
public void setShortcutId(String id) {
|
||||
mShortcutId = id;
|
||||
}
|
||||
|
||||
public void pinShortcut() {
|
||||
ShortcutManager sm = getSystemService(ShortcutManager.class);
|
||||
|
||||
// Generate icon
|
||||
int r = sm.getIconMaxWidth() / 2;
|
||||
Bitmap icon = Bitmap.createBitmap(r * 2, r * 2, Bitmap.Config.ARGB_8888);
|
||||
Paint p = new Paint();
|
||||
p.setColor(Color.RED);
|
||||
new Canvas(icon).drawCircle(r, r, r, p);
|
||||
|
||||
ShortcutInfo info = new ShortcutInfo.Builder(this, mShortcutId)
|
||||
.setIntent(getPackageManager().getLaunchIntentForPackage(getPackageName()))
|
||||
.setIcon(Icon.createWithBitmap(icon))
|
||||
.setShortLabel("Test shortcut")
|
||||
.build();
|
||||
|
||||
IntentSender callback = mCallback == null ? null : mCallback.getIntentSender();
|
||||
sm.requestPinShortcut(info, callback);
|
||||
}
|
||||
|
||||
public void pinWidgetNoConfig() {
|
||||
requestWidget(new ComponentName(this, AppWidgetNoConfig.class));
|
||||
}
|
||||
|
||||
public void pinWidgetWithConfig() {
|
||||
requestWidget(new ComponentName(this, AppWidgetWithConfig.class));
|
||||
}
|
||||
|
||||
private void requestWidget(ComponentName cn) {
|
||||
Bundle extras = null;
|
||||
if (mRemoteViewColor != Color.TRANSPARENT) {
|
||||
int layoutId = getResources().getIdentifier(
|
||||
"test_layout_appwidget_view", "layout", getPackageName());
|
||||
RemoteViews views = new RemoteViews(getPackageName(), layoutId);
|
||||
views.setInt(android.R.id.icon, "setBackgroundColor", mRemoteViewColor);
|
||||
extras = new Bundle();
|
||||
extras.putParcelable(AppWidgetManager.EXTRA_APPWIDGET_PREVIEW, views);
|
||||
}
|
||||
|
||||
AppWidgetManager.getInstance(this).requestPinAppWidget(cn, extras, mCallback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.uiuios.testcomponent;
|
||||
|
||||
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
|
||||
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
|
||||
import static android.content.pm.PackageManager.DONT_KILL_APP;
|
||||
import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Base64;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
|
||||
/**
|
||||
* Content provider to receive commands from tests
|
||||
*/
|
||||
public class TestCommandReceiver extends ContentProvider {
|
||||
|
||||
public static final String ENABLE_TEST_LAUNCHER = "enable-test-launcher";
|
||||
public static final String DISABLE_TEST_LAUNCHER = "disable-test-launcher";
|
||||
public static final String KILL_PROCESS = "kill-process";
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("unimplemented mock method");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
throw new UnsupportedOperationException("unimplemented mock method");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
throw new UnsupportedOperationException("unimplemented mock method");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
throw new UnsupportedOperationException("unimplemented mock method");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("unimplemented mock method");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle call(String method, String arg, Bundle extras) {
|
||||
switch (method) {
|
||||
case ENABLE_TEST_LAUNCHER: {
|
||||
getContext().getPackageManager().setComponentEnabledSetting(
|
||||
new ComponentName(getContext(), TestLauncherActivity.class),
|
||||
COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
|
||||
return null;
|
||||
}
|
||||
case DISABLE_TEST_LAUNCHER: {
|
||||
getContext().getPackageManager().setComponentEnabledSetting(
|
||||
new ComponentName(getContext(), TestLauncherActivity.class),
|
||||
COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
|
||||
return null;
|
||||
}
|
||||
case KILL_PROCESS: {
|
||||
((ActivityManager) getContext().getSystemService(Activity.ACTIVITY_SERVICE)).
|
||||
killBackgroundProcesses(arg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return super.call(method, arg, extras);
|
||||
}
|
||||
|
||||
public static Bundle callCommand(String command) {
|
||||
return callCommand(command, null);
|
||||
}
|
||||
|
||||
public static Bundle callCommand(String command, String arg) {
|
||||
Instrumentation inst = InstrumentationRegistry.getInstrumentation();
|
||||
Uri uri = Uri.parse("content://" + inst.getContext().getPackageName() + ".commands");
|
||||
return inst.getTargetContext().getContentResolver().call(uri, command, arg, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
||||
String path = Base64.encodeToString(uri.getPath().getBytes(),
|
||||
Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP);
|
||||
File file = new File(getContext().getCacheDir(), path);
|
||||
if (!file.exists()) {
|
||||
// Create an empty file so that we can pass its descriptor
|
||||
try {
|
||||
file.createNewFile();
|
||||
} catch (IOException e) { }
|
||||
}
|
||||
|
||||
return ParcelFileDescriptor.open(file, MODE_READ_WRITE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.uiuios.testcomponent;
|
||||
|
||||
import static android.content.Intent.ACTION_MAIN;
|
||||
import static android.content.Intent.CATEGORY_LAUNCHER;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED;
|
||||
|
||||
import android.app.LauncherActivity;
|
||||
import android.content.Intent;
|
||||
|
||||
public class TestLauncherActivity extends LauncherActivity {
|
||||
|
||||
@Override
|
||||
protected Intent getTargetIntent() {
|
||||
return new Intent(ACTION_MAIN, null)
|
||||
.addCategory(CATEGORY_LAUNCHER)
|
||||
.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.testcomponent;
|
||||
|
||||
import android.graphics.Point;
|
||||
import android.util.Pair;
|
||||
import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.MotionEvent.PointerCoords;
|
||||
import android.view.MotionEvent.PointerProperties;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility class to generate MotionEvent event sequences for testing touch gesture detectors.
|
||||
*/
|
||||
public class TouchEventGenerator {
|
||||
|
||||
/**
|
||||
* Amount of time between two generated events.
|
||||
*/
|
||||
private static final long TIME_INCREMENT_MS = 20L;
|
||||
|
||||
/**
|
||||
* Id of the fake device generating the events.
|
||||
*/
|
||||
private static final int DEVICE_ID = 2104;
|
||||
|
||||
/**
|
||||
* The fingers currently present on the emulated touch screen.
|
||||
*/
|
||||
private Map<Integer, Point> mFingers;
|
||||
|
||||
/**
|
||||
* Initial event time for the current sequence.
|
||||
*/
|
||||
private long mInitialTime;
|
||||
|
||||
/**
|
||||
* Time of the last generated event.
|
||||
*/
|
||||
private long mLastEventTime;
|
||||
|
||||
/**
|
||||
* Time of the next event.
|
||||
*/
|
||||
private long mTime;
|
||||
|
||||
/**
|
||||
* Receives the generated events.
|
||||
*/
|
||||
public interface Listener {
|
||||
|
||||
/**
|
||||
* Called when an event was generated.
|
||||
*/
|
||||
void onTouchEvent(MotionEvent event);
|
||||
}
|
||||
private final Listener mListener;
|
||||
|
||||
public TouchEventGenerator(Listener listener) {
|
||||
mListener = listener;
|
||||
mFingers = new HashMap<Integer, Point>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a finger on the touchscreen.
|
||||
*/
|
||||
public TouchEventGenerator put(int id, int x, int y, long ms) {
|
||||
checkFingerExistence(id, false);
|
||||
boolean isInitialDown = mFingers.isEmpty();
|
||||
mFingers.put(id, new Point(x, y));
|
||||
int action;
|
||||
if (isInitialDown) {
|
||||
action = MotionEvent.ACTION_DOWN;
|
||||
} else {
|
||||
action = MotionEvent.ACTION_POINTER_DOWN;
|
||||
// Set the id of the changed pointer.
|
||||
action |= id << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
|
||||
}
|
||||
generateEvent(action, ms);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a finger on the touchscreen after advancing default time interval.
|
||||
*/
|
||||
public TouchEventGenerator put(int id, int x, int y) {
|
||||
return put(id, x, y, TIME_INCREMENT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the position of a finger for an upcoming move event.
|
||||
*
|
||||
* @see #move(long ms)
|
||||
*/
|
||||
public TouchEventGenerator position(int id, int x, int y) {
|
||||
checkFingerExistence(id, true);
|
||||
mFingers.get(id).set(x, y);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the finger position changes of {@link #position(int, int, int)} by generating a move
|
||||
* event.
|
||||
*
|
||||
* @see #position(int, int, int)
|
||||
*/
|
||||
public TouchEventGenerator move(long ms) {
|
||||
generateEvent(MotionEvent.ACTION_MOVE, ms);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the finger position changes of {@link #position(int, int, int)} by generating a move
|
||||
* event after advancing the default time interval.
|
||||
*
|
||||
* @see #position(int, int, int)
|
||||
*/
|
||||
public TouchEventGenerator move() {
|
||||
return move(TIME_INCREMENT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a single finger on the touchscreen.
|
||||
*/
|
||||
public TouchEventGenerator move(int id, int x, int y, long ms) {
|
||||
return position(id, x, y).move(ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a single finger on the touchscreen after advancing default time interval.
|
||||
*/
|
||||
public TouchEventGenerator move(int id, int x, int y) {
|
||||
return move(id, x, y, TIME_INCREMENT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing finger from the touchscreen.
|
||||
*/
|
||||
public TouchEventGenerator lift(int id, long ms) {
|
||||
checkFingerExistence(id, true);
|
||||
boolean isFinalUp = mFingers.size() == 1;
|
||||
int action;
|
||||
if (isFinalUp) {
|
||||
action = MotionEvent.ACTION_UP;
|
||||
} else {
|
||||
action = MotionEvent.ACTION_POINTER_UP;
|
||||
// Set the id of the changed pointer.
|
||||
action |= id << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
|
||||
}
|
||||
generateEvent(action, ms);
|
||||
mFingers.remove(id);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a finger from the touchscreen.
|
||||
*/
|
||||
public TouchEventGenerator lift(int id, int x, int y, long ms) {
|
||||
checkFingerExistence(id, true);
|
||||
mFingers.get(id).set(x, y);
|
||||
return lift(id, ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing finger from the touchscreen after advancing default time interval.
|
||||
*/
|
||||
public TouchEventGenerator lift(int id) {
|
||||
return lift(id, TIME_INCREMENT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an ongoing sequence.
|
||||
*/
|
||||
public TouchEventGenerator cancel(long ms) {
|
||||
generateEvent(MotionEvent.ACTION_CANCEL, ms);
|
||||
mFingers.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an ongoing sequence.
|
||||
*/
|
||||
public TouchEventGenerator cancel() {
|
||||
return cancel(TIME_INCREMENT_MS);
|
||||
}
|
||||
|
||||
private void checkFingerExistence(int id, boolean shouldExist) {
|
||||
if (shouldExist != mFingers.containsKey(id)) {
|
||||
throw new IllegalArgumentException(
|
||||
shouldExist ? "Finger does not exist" : "Finger already exists");
|
||||
}
|
||||
}
|
||||
|
||||
private void generateEvent(int action, long ms) {
|
||||
mTime = mLastEventTime + ms;
|
||||
Pair<PointerProperties[], PointerCoords[]> state = getFingerState();
|
||||
MotionEvent event = MotionEvent.obtain(
|
||||
mInitialTime,
|
||||
mTime,
|
||||
action,
|
||||
state.first.length,
|
||||
state.first,
|
||||
state.second,
|
||||
0 /* metaState */,
|
||||
0 /* buttonState */,
|
||||
1.0f /* xPrecision */,
|
||||
1.0f /* yPrecision */,
|
||||
DEVICE_ID,
|
||||
0 /* edgeFlags */,
|
||||
InputDevice.SOURCE_TOUCHSCREEN,
|
||||
0 /* flags */);
|
||||
mListener.onTouchEvent(event);
|
||||
if (action == MotionEvent.ACTION_UP) {
|
||||
resetTime();
|
||||
}
|
||||
event.recycle();
|
||||
mLastEventTime = mTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the fingers' state expected by MotionEvent.
|
||||
*/
|
||||
private Pair<PointerProperties[], PointerCoords[]> getFingerState() {
|
||||
int nFingers = mFingers.size();
|
||||
PointerProperties[] properties = new PointerProperties[nFingers];
|
||||
PointerCoords[] coordinates = new PointerCoords[nFingers];
|
||||
|
||||
int index = 0;
|
||||
for (Map.Entry<Integer, Point> entry : mFingers.entrySet()) {
|
||||
int id = entry.getKey();
|
||||
Point location = entry.getValue();
|
||||
|
||||
PointerProperties property = new PointerProperties();
|
||||
property.id = id;
|
||||
property.toolType = MotionEvent.TOOL_TYPE_FINGER;
|
||||
properties[index] = property;
|
||||
|
||||
PointerCoords coordinate = new PointerCoords();
|
||||
coordinate.x = location.x;
|
||||
coordinate.y = location.y;
|
||||
coordinate.pressure = 1.0f;
|
||||
coordinates[index] = coordinate;
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return new Pair<MotionEvent.PointerProperties[], MotionEvent.PointerCoords[]>(
|
||||
properties, coordinates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the time references for a new sequence.
|
||||
*/
|
||||
private void resetTime() {
|
||||
mInitialTime = 0L;
|
||||
mLastEventTime = -1L;
|
||||
mTime = 0L;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.testcomponent;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* Simple activity for widget configuration
|
||||
*/
|
||||
public class WidgetConfigActivity extends BaseTestingActivity {
|
||||
|
||||
public static final String SUFFIX_FINISH = "-finish";
|
||||
public static final String EXTRA_CODE = "code";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
addButton("Cancel", "clickCancel");
|
||||
addButton("OK", "clickOK");
|
||||
}
|
||||
|
||||
public void clickCancel() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
|
||||
public void clickOK() {
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
165
tests/src/com/android/uiuios/touch/SwipeDetectorTest.java
Normal file
165
tests/src/com/android/uiuios/touch/SwipeDetectorTest.java
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.touch;
|
||||
|
||||
import static org.mockito.Matchers.anyBoolean;
|
||||
import static org.mockito.Matchers.anyFloat;
|
||||
import static org.mockito.Matchers.anyObject;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.android.uiuios.testcomponent.TouchEventGenerator;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SwipeDetectorTest {
|
||||
|
||||
private static final String TAG = SwipeDetectorTest.class.getSimpleName();
|
||||
public static void L(String s, Object... parts) {
|
||||
Log.d(TAG, (parts.length == 0) ? s : String.format(s, parts));
|
||||
}
|
||||
|
||||
private TouchEventGenerator mGenerator;
|
||||
private SwipeDetector mDetector;
|
||||
private int mTouchSlop;
|
||||
|
||||
@Mock
|
||||
private SwipeDetector.Listener mMockListener;
|
||||
|
||||
@Mock
|
||||
private ViewConfiguration mMockConfig;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mGenerator = new TouchEventGenerator((ev) -> mDetector.onTouchEvent(ev));
|
||||
ViewConfiguration orgConfig = ViewConfiguration
|
||||
.get(InstrumentationRegistry.getTargetContext());
|
||||
doReturn(orgConfig.getScaledMaximumFlingVelocity()).when(mMockConfig)
|
||||
.getScaledMaximumFlingVelocity();
|
||||
|
||||
mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
|
||||
mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
|
||||
mTouchSlop = orgConfig.getScaledTouchSlop();
|
||||
doReturn(mTouchSlop).when(mMockConfig).getScaledTouchSlop();
|
||||
|
||||
L("mTouchSlop=", mTouchSlop);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart_verticalPositive() {
|
||||
mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
|
||||
mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100, 100 - mTouchSlop);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart_verticalNegative() {
|
||||
mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.VERTICAL, false);
|
||||
mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100, 100 + mTouchSlop);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart_failed() {
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100 + mTouchSlop, 100);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener, never()).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart_horizontalPositive() {
|
||||
mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false);
|
||||
mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
|
||||
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100 + mTouchSlop, 100);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart_horizontalNegative() {
|
||||
mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, false);
|
||||
mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
|
||||
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100 - mTouchSlop, 100);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart_horizontalRtlPositive() {
|
||||
mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true);
|
||||
mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_POSITIVE, false);
|
||||
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100 - mTouchSlop, 100);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart_horizontalRtlNegative() {
|
||||
mDetector = new SwipeDetector(mMockConfig, mMockListener, SwipeDetector.HORIZONTAL, true);
|
||||
mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_NEGATIVE, false);
|
||||
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100 + mTouchSlop, 100);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDrag() {
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100, 100 + mTouchSlop);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDrag(anyFloat(), anyObject());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragEnd() {
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100, 100 + mTouchSlop);
|
||||
mGenerator.move(0, 100, 100 + mTouchSlop * 2);
|
||||
mGenerator.lift(0);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDragEnd(anyFloat(), anyBoolean());
|
||||
}
|
||||
}
|
||||
390
tests/src/com/android/uiuios/ui/AbstractLauncherUiTest.java
Normal file
390
tests/src/com/android/uiuios/ui/AbstractLauncherUiTest.java
Normal file
@@ -0,0 +1,390 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.ui;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import static com.android.uiuios.ui.TaplTestsLauncher3.getAppPackageName;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import static java.lang.System.exit;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.LauncherActivityInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.uiautomator.By;
|
||||
import androidx.test.uiautomator.BySelector;
|
||||
import androidx.test.uiautomator.Direction;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
import androidx.test.uiautomator.UiObject2;
|
||||
import androidx.test.uiautomator.Until;
|
||||
|
||||
import com.android.uiuios.Launcher;
|
||||
import com.android.uiuios.LauncherAppState;
|
||||
import com.android.uiuios.LauncherModel;
|
||||
import com.android.uiuios.LauncherSettings;
|
||||
import com.android.uiuios.LauncherState;
|
||||
import com.android.uiuios.MainThreadExecutor;
|
||||
import com.android.uiuios.ResourceUtils;
|
||||
import com.android.uiuios.Utilities;
|
||||
import com.android.uiuios.compat.LauncherAppsCompat;
|
||||
import com.android.uiuios.model.AppLaunchTracker;
|
||||
import com.android.uiuios.tapl.LauncherInstrumentation;
|
||||
import com.android.uiuios.tapl.TestHelpers;
|
||||
import com.android.uiuios.util.Wait;
|
||||
import com.android.uiuios.util.rule.FailureWatcher;
|
||||
import com.android.uiuios.util.rule.LauncherActivityRule;
|
||||
import com.android.uiuios.util.rule.ShellCommandRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.RuleChain;
|
||||
import org.junit.rules.TestRule;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Base class for all instrumentation tests providing various utility methods.
|
||||
*/
|
||||
public abstract class AbstractLauncherUiTest {
|
||||
|
||||
public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
|
||||
public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
|
||||
|
||||
public static final long SHORT_UI_TIMEOUT = 300;
|
||||
public static final long DEFAULT_UI_TIMEOUT = 10000;
|
||||
private static final String TAG = "AbstractLauncherUiTest";
|
||||
|
||||
protected MainThreadExecutor mMainThreadExecutor = new MainThreadExecutor();
|
||||
protected final UiDevice mDevice = UiDevice.getInstance(getInstrumentation());
|
||||
protected final LauncherInstrumentation mLauncher =
|
||||
new LauncherInstrumentation(getInstrumentation());
|
||||
protected Context mTargetContext;
|
||||
protected String mTargetPackage;
|
||||
|
||||
protected AbstractLauncherUiTest() {
|
||||
try {
|
||||
mDevice.setOrientationNatural();
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (TestHelpers.isInLauncherProcess()) Utilities.enableRunningInTestHarnessForTests();
|
||||
}
|
||||
|
||||
protected final LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
|
||||
|
||||
@Rule
|
||||
public ShellCommandRule mDefaultLauncherRule =
|
||||
TestHelpers.isInLauncherProcess() ? ShellCommandRule.setDefaultLauncher() : null;
|
||||
|
||||
@Rule
|
||||
public ShellCommandRule mDisableHeadsUpNotification =
|
||||
ShellCommandRule.disableHeadsUpNotification();
|
||||
|
||||
// Annotation for tests that need to be run in portrait and landscape modes.
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
protected @interface PortraitLandscape {
|
||||
}
|
||||
|
||||
protected TestRule getRulesInsideActivityMonitor() {
|
||||
return RuleChain.
|
||||
outerRule(new PortraitLandscapeRunner(this)).
|
||||
around(new FailureWatcher(mDevice));
|
||||
}
|
||||
|
||||
@Rule
|
||||
public TestRule mOrderSensitiveRules = RuleChain.
|
||||
outerRule(mActivityMonitor).
|
||||
around(getRulesInsideActivityMonitor());
|
||||
|
||||
public UiDevice getDevice() {
|
||||
return mDevice;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
// Disable app tracker
|
||||
AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
|
||||
|
||||
mTargetContext = InstrumentationRegistry.getTargetContext();
|
||||
mTargetPackage = mTargetContext.getPackageName();
|
||||
// Unlock the phone
|
||||
mDevice.executeShellCommand("input keyevent 82");
|
||||
}
|
||||
|
||||
@After
|
||||
public void verifyLauncherState() {
|
||||
try {
|
||||
// Limits UI tests affecting tests running after them.
|
||||
waitForModelLoaded();
|
||||
} catch (Throwable t) {
|
||||
Log.e(TAG,
|
||||
"Couldn't deinit after a test, exiting tests, see logs for failures that "
|
||||
+ "could have caused this",
|
||||
t);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
protected void lockRotation(boolean naturalOrientation) throws RemoteException {
|
||||
if (naturalOrientation) {
|
||||
mDevice.setOrientationNatural();
|
||||
} else {
|
||||
mDevice.setOrientationRight();
|
||||
}
|
||||
}
|
||||
|
||||
protected void clearLauncherData() throws IOException {
|
||||
if (TestHelpers.isInLauncherProcess()) {
|
||||
LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
|
||||
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
|
||||
resetLoaderState();
|
||||
} else {
|
||||
mDevice.executeShellCommand("pm clear " + mDevice.getLauncherPackageName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the {@param container} until it finds an object matching {@param condition}.
|
||||
*
|
||||
* @return the matching object.
|
||||
*/
|
||||
protected UiObject2 scrollAndFind(UiObject2 container, BySelector condition) {
|
||||
final int margin = ResourceUtils.getNavbarSize(
|
||||
ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, mLauncher.getResources()) + 1;
|
||||
container.setGestureMargins(0, 0, 0, margin);
|
||||
|
||||
int i = 0;
|
||||
for (; ; ) {
|
||||
// findObject can only execute after spring settles.
|
||||
mDevice.wait(Until.findObject(condition), SHORT_UI_TIMEOUT);
|
||||
UiObject2 widget = container.findObject(condition);
|
||||
if (widget != null && widget.getVisibleBounds().intersects(
|
||||
0, 0, mDevice.getDisplayWidth(),
|
||||
mDevice.getDisplayHeight() - margin)) {
|
||||
return widget;
|
||||
}
|
||||
if (++i > 40) fail("Too many attempts");
|
||||
container.scroll(Direction.DOWN, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all icons from homescreen and hotseat.
|
||||
*/
|
||||
public void clearHomescreen() throws Throwable {
|
||||
LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
|
||||
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
|
||||
LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
|
||||
LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
|
||||
resetLoaderState();
|
||||
}
|
||||
|
||||
protected void resetLoaderState() {
|
||||
try {
|
||||
mMainThreadExecutor.execute(
|
||||
() -> LauncherAppState.getInstance(mTargetContext).getModel().forceReload());
|
||||
} catch (Throwable t) {
|
||||
throw new IllegalArgumentException(t);
|
||||
}
|
||||
waitForModelLoaded();
|
||||
}
|
||||
|
||||
protected void waitForModelLoaded() {
|
||||
waitForLauncherCondition("Launcher model didn't load", launcher -> {
|
||||
final LauncherModel model = LauncherAppState.getInstance(mTargetContext).getModel();
|
||||
return model.getCallback() == null || model.isModelLoaded();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the callback on the UI thread and returns the result.
|
||||
*/
|
||||
protected <T> T getOnUiThread(final Callable<T> callback) {
|
||||
try {
|
||||
return mMainThreadExecutor.submit(callback).get();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> T getFromLauncher(Function<Launcher, T> f) {
|
||||
if (!TestHelpers.isInLauncherProcess()) return null;
|
||||
return getOnUiThread(() -> f.apply(mActivityMonitor.getActivity()));
|
||||
}
|
||||
|
||||
protected void executeOnLauncher(Consumer<Launcher> f) {
|
||||
getFromLauncher(launcher -> {
|
||||
f.accept(launcher);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call expecting
|
||||
// the results of that gesture because the wait can hide flakeness.
|
||||
protected void waitForState(String message, LauncherState state) {
|
||||
waitForLauncherCondition(message,
|
||||
launcher -> launcher.getStateManager().getCurrentStableState() == state);
|
||||
}
|
||||
|
||||
protected void waitForResumed(String message) {
|
||||
waitForLauncherCondition(message, launcher -> launcher.hasBeenResumed());
|
||||
}
|
||||
|
||||
// Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
|
||||
// flakiness.
|
||||
protected void waitForLauncherCondition(String message, Function<Launcher, Boolean> condition) {
|
||||
waitForLauncherCondition(message, condition, DEFAULT_ACTIVITY_TIMEOUT);
|
||||
}
|
||||
|
||||
// Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
|
||||
// flakiness.
|
||||
protected void waitForLauncherCondition(
|
||||
String message, Function<Launcher, Boolean> condition, long timeout) {
|
||||
if (!TestHelpers.isInLauncherProcess()) return;
|
||||
Wait.atMost(message, () -> getFromLauncher(condition), timeout);
|
||||
}
|
||||
|
||||
// Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
|
||||
// flakiness.
|
||||
protected void waitForLauncherCondition(
|
||||
String message,
|
||||
Runnable testThreadAction, Function<Launcher, Boolean> condition,
|
||||
long timeout) {
|
||||
if (!TestHelpers.isInLauncherProcess()) return;
|
||||
Wait.atMost(message, () -> {
|
||||
testThreadAction.run();
|
||||
return getFromLauncher(condition);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
protected LauncherActivityInfo getSettingsApp() {
|
||||
return LauncherAppsCompat.getInstance(mTargetContext)
|
||||
.getActivityList("com.android.settings",
|
||||
Process.myUserHandle()).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast receiver which blocks until the result is received.
|
||||
*/
|
||||
public class BlockingBroadcastReceiver extends BroadcastReceiver {
|
||||
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private Intent mIntent;
|
||||
|
||||
public BlockingBroadcastReceiver(String action) {
|
||||
mTargetContext.registerReceiver(this, new IntentFilter(action));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
mIntent = intent;
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
public Intent blockingGetIntent() throws InterruptedException {
|
||||
latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS);
|
||||
mTargetContext.unregisterReceiver(this);
|
||||
return mIntent;
|
||||
}
|
||||
|
||||
public Intent blockingGetExtraIntent() throws InterruptedException {
|
||||
Intent intent = blockingGetIntent();
|
||||
return intent == null ? null : (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
}
|
||||
}
|
||||
|
||||
protected void startAppFast(String packageName) {
|
||||
final Instrumentation instrumentation = getInstrumentation();
|
||||
final Intent intent = instrumentation.getContext().getPackageManager().
|
||||
getLaunchIntentForPackage(packageName);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
instrumentation.getTargetContext().startActivity(intent);
|
||||
assertTrue(packageName + " didn't start",
|
||||
mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), DEFAULT_UI_TIMEOUT));
|
||||
}
|
||||
|
||||
protected void startTestActivity(int activityNumber) {
|
||||
final String packageName = getAppPackageName();
|
||||
final Instrumentation instrumentation = getInstrumentation();
|
||||
final Intent intent = instrumentation.getContext().getPackageManager().
|
||||
getLaunchIntentForPackage(packageName);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.setComponent(new ComponentName(packageName,
|
||||
"com.android.uiuios.tests.Activity" + activityNumber));
|
||||
instrumentation.getTargetContext().startActivity(intent);
|
||||
assertTrue(packageName + " didn't start",
|
||||
mDevice.wait(
|
||||
Until.hasObject(By.pkg(packageName).text("TestActivity" + activityNumber)),
|
||||
DEFAULT_UI_TIMEOUT));
|
||||
}
|
||||
|
||||
public static String resolveSystemApp(String category) {
|
||||
return getInstrumentation().getContext().getPackageManager().resolveActivity(
|
||||
new Intent(Intent.ACTION_MAIN).addCategory(category),
|
||||
PackageManager.MATCH_SYSTEM_ONLY).
|
||||
activityInfo.packageName;
|
||||
}
|
||||
|
||||
protected void closeLauncherActivity() {
|
||||
// Destroy Launcher activity.
|
||||
executeOnLauncher(launcher -> {
|
||||
if (launcher != null) {
|
||||
launcher.finish();
|
||||
}
|
||||
});
|
||||
waitForLauncherCondition(
|
||||
"Launcher still active", launcher -> launcher == null, DEFAULT_UI_TIMEOUT);
|
||||
}
|
||||
|
||||
protected boolean isInBackground(Launcher launcher) {
|
||||
return !launcher.hasBeenResumed();
|
||||
}
|
||||
|
||||
protected boolean isInState(LauncherState state) {
|
||||
if (!TestHelpers.isInLauncherProcess()) return true;
|
||||
return getFromLauncher(launcher -> launcher.getStateManager().getState() == state);
|
||||
}
|
||||
|
||||
protected int getAllAppsScroll(Launcher launcher) {
|
||||
return launcher.getAppsView().getActiveRecyclerView().getCurrentScrollY();
|
||||
}
|
||||
}
|
||||
129
tests/src/com/android/uiuios/ui/DefaultLayoutProviderTest.java
Normal file
129
tests/src/com/android/uiuios/ui/DefaultLayoutProviderTest.java
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (C) 2019 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.uiuios.ui;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.uiuios.LauncherAppWidgetProviderInfo;
|
||||
import com.android.uiuios.testcomponent.TestCommandReceiver;
|
||||
import com.android.uiuios.util.LauncherLayoutBuilder;
|
||||
import com.android.uiuios.util.rule.ShellCommandRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
@MediumTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DefaultLayoutProviderTest extends AbstractLauncherUiTest {
|
||||
|
||||
@Rule
|
||||
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
|
||||
|
||||
private static final String SETTINGS_APP = "com.android.settings";
|
||||
|
||||
private Context mContext;
|
||||
private String mAuthority;
|
||||
|
||||
@Before
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mContext = InstrumentationRegistry.getContext();
|
||||
|
||||
PackageManager pm = mTargetContext.getPackageManager();
|
||||
ProviderInfo pi = pm.getProviderInfo(new ComponentName(mContext,
|
||||
TestCommandReceiver.class), 0);
|
||||
mAuthority = pi.authority;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomProfileLoaded_with_icon_on_hotseat() throws Exception {
|
||||
writeLayout(new LauncherLayoutBuilder().atHotseat(0).putApp(SETTINGS_APP, SETTINGS_APP));
|
||||
|
||||
// Launch the home activity
|
||||
mActivityMonitor.startLauncher();
|
||||
waitForModelLoaded();
|
||||
|
||||
mLauncher.getWorkspace().getHotseatAppIcon(getSettingsApp().getLabel().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomProfileLoaded_with_widget() throws Exception {
|
||||
// A non-restored widget with no config screen gets restored automatically.
|
||||
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
|
||||
|
||||
writeLayout(new LauncherLayoutBuilder().atWorkspace(0, 1, 0)
|
||||
.putWidget(info.getComponent().getPackageName(),
|
||||
info.getComponent().getClassName(), 2, 2));
|
||||
|
||||
// Launch the home activity
|
||||
mActivityMonitor.startLauncher();
|
||||
waitForModelLoaded();
|
||||
|
||||
// Verify widget present
|
||||
assertTrue("Widget is not present",
|
||||
mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomProfileLoaded_with_folder() throws Exception {
|
||||
writeLayout(new LauncherLayoutBuilder().atHotseat(0).putFolder(android.R.string.copy)
|
||||
.addApp(SETTINGS_APP, SETTINGS_APP)
|
||||
.addApp(SETTINGS_APP, SETTINGS_APP)
|
||||
.addApp(SETTINGS_APP, SETTINGS_APP)
|
||||
.build());
|
||||
|
||||
// Launch the home activity
|
||||
mActivityMonitor.startLauncher();
|
||||
waitForModelLoaded();
|
||||
|
||||
mLauncher.getWorkspace().getHotseatFolder("Folder: Copy");
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() throws Exception {
|
||||
mDevice.executeShellCommand("settings delete secure launcher3.layout.provider");
|
||||
}
|
||||
|
||||
private void writeLayout(LauncherLayoutBuilder builder) throws Exception {
|
||||
mDevice.executeShellCommand("settings put secure launcher3.layout.provider " + mAuthority);
|
||||
ParcelFileDescriptor pfd = mTargetContext.getContentResolver().openFileDescriptor(
|
||||
Uri.parse("content://" + mAuthority + "/launcher_layout"), "w");
|
||||
|
||||
try (OutputStreamWriter writer = new OutputStreamWriter(new AutoCloseOutputStream(pfd))) {
|
||||
builder.build(writer);
|
||||
}
|
||||
clearLauncherData();
|
||||
}
|
||||
}
|
||||
63
tests/src/com/android/uiuios/ui/PortraitLandscapeRunner.java
Normal file
63
tests/src/com/android/uiuios/ui/PortraitLandscapeRunner.java
Normal file
@@ -0,0 +1,63 @@
|
||||
package com.android.uiuios.ui;
|
||||
|
||||
import android.view.Surface;
|
||||
|
||||
import com.android.uiuios.tapl.TestHelpers;
|
||||
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
class PortraitLandscapeRunner implements TestRule {
|
||||
private AbstractLauncherUiTest mTest;
|
||||
|
||||
public PortraitLandscapeRunner(AbstractLauncherUiTest test) {
|
||||
mTest = test;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(Statement base, Description description) {
|
||||
if (!TestHelpers.isInLauncherProcess() ||
|
||||
description.getAnnotation(AbstractLauncherUiTest.PortraitLandscape.class) == null) {
|
||||
return base;
|
||||
}
|
||||
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
try {
|
||||
mTest.mDevice.pressHome();
|
||||
mTest.waitForLauncherCondition("Launcher activity wasn't created",
|
||||
launcher -> launcher != null);
|
||||
|
||||
mTest.executeOnLauncher(launcher ->
|
||||
launcher.getRotationHelper().forceAllowRotationForTesting(
|
||||
true));
|
||||
|
||||
evaluateInPortrait();
|
||||
evaluateInLandscape();
|
||||
} finally {
|
||||
mTest.mDevice.setOrientationNatural();
|
||||
mTest.executeOnLauncher(launcher ->
|
||||
launcher.getRotationHelper().forceAllowRotationForTesting(
|
||||
false));
|
||||
mTest.mLauncher.setExpectedRotation(Surface.ROTATION_0);
|
||||
}
|
||||
}
|
||||
|
||||
private void evaluateInPortrait() throws Throwable {
|
||||
mTest.mDevice.setOrientationNatural();
|
||||
mTest.mLauncher.setExpectedRotation(Surface.ROTATION_0);
|
||||
base.evaluate();
|
||||
mTest.getDevice().pressHome();
|
||||
}
|
||||
|
||||
private void evaluateInLandscape() throws Throwable {
|
||||
mTest.mDevice.setOrientationLeft();
|
||||
mTest.mLauncher.setExpectedRotation(Surface.ROTATION_90);
|
||||
base.evaluate();
|
||||
mTest.getDevice().pressHome();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
353
tests/src/com/android/uiuios/ui/TaplTestsLauncher3.java
Normal file
353
tests/src/com/android/uiuios/ui/TaplTestsLauncher3.java
Normal file
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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.uiuios.ui;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.uiuios.Launcher;
|
||||
import com.android.uiuios.LauncherState;
|
||||
import com.android.uiuios.popup.ArrowPopup;
|
||||
import com.android.uiuios.tapl.AllApps;
|
||||
import com.android.uiuios.tapl.AppIcon;
|
||||
import com.android.uiuios.tapl.AppIconMenu;
|
||||
import com.android.uiuios.tapl.AppIconMenuItem;
|
||||
import com.android.uiuios.tapl.TestHelpers;
|
||||
import com.android.uiuios.tapl.Widgets;
|
||||
import com.android.uiuios.tapl.Workspace;
|
||||
import com.android.uiuios.views.OptionsPopupView;
|
||||
import com.android.uiuios.widget.WidgetsFullSheet;
|
||||
import com.android.uiuios.widget.WidgetsRecyclerView;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TaplTestsLauncher3 extends AbstractLauncherUiTest {
|
||||
private static final String APP_NAME = "LauncherTestApp";
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
initialize(this);
|
||||
}
|
||||
|
||||
public static void initialize(AbstractLauncherUiTest test) throws Exception {
|
||||
test.clearLauncherData();
|
||||
test.mDevice.pressHome();
|
||||
test.waitForLauncherCondition("Launcher didn't start", launcher -> launcher != null);
|
||||
test.waitForState("Launcher internal state didn't switch to Home", LauncherState.NORMAL);
|
||||
test.waitForResumed("Launcher internal state is still Background");
|
||||
// Check that we switched to home.
|
||||
test.mLauncher.getWorkspace();
|
||||
}
|
||||
|
||||
// Please don't add negative test cases for methods that fail only after a long wait.
|
||||
public static void expectFail(String message, Runnable action) {
|
||||
boolean failed = false;
|
||||
try {
|
||||
action.run();
|
||||
} catch (AssertionError e) {
|
||||
failed = true;
|
||||
}
|
||||
assertTrue(message, failed);
|
||||
}
|
||||
|
||||
private boolean isWorkspaceScrollable(Launcher launcher) {
|
||||
return launcher.getWorkspace().getPageCount() > 1;
|
||||
}
|
||||
|
||||
private int getCurrentWorkspacePage(Launcher launcher) {
|
||||
return launcher.getWorkspace().getCurrentPage();
|
||||
}
|
||||
|
||||
private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
|
||||
return WidgetsFullSheet.getWidgetsView(launcher);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDevicePressMenu() throws Exception {
|
||||
mDevice.pressMenu();
|
||||
mDevice.waitForIdle();
|
||||
executeOnLauncher(
|
||||
launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
|
||||
OptionsPopupView.getOptionsPopup(launcher) != null));
|
||||
// Check that pressHome works when the menu is shown.
|
||||
mLauncher.pressHome();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void testPressHomeOnAllAppsContextMenu() throws Exception {
|
||||
final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
|
||||
allApps.freeze();
|
||||
try {
|
||||
allApps.getAppIcon("TestActivity7").openMenu();
|
||||
} finally {
|
||||
allApps.unfreeze();
|
||||
}
|
||||
mLauncher.pressHome();
|
||||
}
|
||||
|
||||
public static void runAllAppsTest(AbstractLauncherUiTest test, AllApps allApps) {
|
||||
allApps.freeze();
|
||||
try {
|
||||
assertNotNull("allApps parameter is null", allApps);
|
||||
|
||||
assertTrue(
|
||||
"Launcher internal state is not All Apps",
|
||||
test.isInState(LauncherState.ALL_APPS));
|
||||
|
||||
// Test flinging forward and backward.
|
||||
test.executeOnLauncher(launcher -> assertEquals(
|
||||
"All Apps started in already scrolled state", 0,
|
||||
test.getAllAppsScroll(launcher)));
|
||||
|
||||
allApps.flingForward();
|
||||
assertTrue("Launcher internal state is not All Apps",
|
||||
test.isInState(LauncherState.ALL_APPS));
|
||||
final Integer flingForwardY = test.getFromLauncher(
|
||||
launcher -> test.getAllAppsScroll(launcher));
|
||||
test.executeOnLauncher(
|
||||
launcher -> assertTrue("flingForward() didn't scroll App Apps",
|
||||
flingForwardY > 0));
|
||||
|
||||
allApps.flingBackward();
|
||||
assertTrue(
|
||||
"Launcher internal state is not All Apps",
|
||||
test.isInState(LauncherState.ALL_APPS));
|
||||
final Integer flingBackwardY = test.getFromLauncher(
|
||||
launcher -> test.getAllAppsScroll(launcher));
|
||||
test.executeOnLauncher(launcher -> assertTrue("flingBackward() didn't scroll App Apps",
|
||||
flingBackwardY < flingForwardY));
|
||||
|
||||
// Test scrolling down to YouTube.
|
||||
assertNotNull("All apps: can't fine YouTube", allApps.getAppIcon("YouTube"));
|
||||
// Test scrolling up to Camera.
|
||||
assertNotNull("All apps: can't fine Camera", allApps.getAppIcon("Camera"));
|
||||
// Test failing to find a non-existing app.
|
||||
final AllApps allAppsFinal = allApps;
|
||||
expectFail("All apps: could find a non-existing app",
|
||||
() -> allAppsFinal.getAppIcon("NO APP"));
|
||||
|
||||
assertTrue(
|
||||
"Launcher internal state is not All Apps",
|
||||
test.isInState(LauncherState.ALL_APPS));
|
||||
} finally {
|
||||
allApps.unfreeze();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PortraitLandscape
|
||||
public void testWorkspaceSwitchToAllApps() {
|
||||
assertNotNull("switchToAllApps() returned null",
|
||||
mLauncher.getWorkspace().switchToAllApps());
|
||||
assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkspace() throws Exception {
|
||||
final Workspace workspace = mLauncher.getWorkspace();
|
||||
|
||||
// Test that ensureWorkspaceIsScrollable adds a page by dragging an icon there.
|
||||
executeOnLauncher(launcher -> assertFalse("Initial workspace state is scrollable",
|
||||
isWorkspaceScrollable(launcher)));
|
||||
assertNull("Chrome app was found on empty workspace",
|
||||
workspace.tryGetWorkspaceAppIcon("Chrome"));
|
||||
|
||||
workspace.ensureWorkspaceIsScrollable();
|
||||
|
||||
executeOnLauncher(
|
||||
launcher -> assertEquals("Ensuring workspace scrollable didn't switch to page #1",
|
||||
1, getCurrentWorkspacePage(launcher)));
|
||||
executeOnLauncher(
|
||||
launcher -> assertTrue("ensureScrollable didn't make workspace scrollable",
|
||||
isWorkspaceScrollable(launcher)));
|
||||
assertNotNull("ensureScrollable didn't add Chrome app",
|
||||
workspace.tryGetWorkspaceAppIcon("Chrome"));
|
||||
|
||||
// Test flinging workspace.
|
||||
workspace.flingBackward();
|
||||
assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
|
||||
executeOnLauncher(
|
||||
launcher -> assertEquals("Flinging back didn't switch workspace to page #0",
|
||||
0, getCurrentWorkspacePage(launcher)));
|
||||
|
||||
workspace.flingForward();
|
||||
executeOnLauncher(
|
||||
launcher -> assertEquals("Flinging forward didn't switch workspace to page #1",
|
||||
1, getCurrentWorkspacePage(launcher)));
|
||||
assertTrue("Launcher internal state is not Home", isInState(LauncherState.NORMAL));
|
||||
|
||||
// Test starting a workspace app.
|
||||
final AppIcon app = workspace.tryGetWorkspaceAppIcon("Chrome");
|
||||
assertNotNull("No Chrome app in workspace", app);
|
||||
}
|
||||
|
||||
public static void runIconLaunchFromAllAppsTest(AbstractLauncherUiTest test, AllApps allApps) {
|
||||
allApps.freeze();
|
||||
try {
|
||||
final AppIcon app = allApps.getAppIcon("TestActivity7");
|
||||
assertNotNull("AppIcon.launch returned null", app.launch(getAppPackageName()));
|
||||
test.executeOnLauncher(launcher -> assertTrue(
|
||||
"Launcher activity is the top activity; expecting another activity to be the top "
|
||||
+ "one",
|
||||
test.isInBackground(launcher)));
|
||||
} finally {
|
||||
allApps.unfreeze();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PortraitLandscape
|
||||
public void testAppIconLaunchFromAllAppsFromHome() throws Exception {
|
||||
final AllApps allApps = mLauncher.getWorkspace().switchToAllApps();
|
||||
assertTrue("Launcher internal state is not All Apps", isInState(LauncherState.ALL_APPS));
|
||||
|
||||
runIconLaunchFromAllAppsTest(this, allApps);
|
||||
}
|
||||
|
||||
@Test
|
||||
@PortraitLandscape
|
||||
public void testWidgets() throws Exception {
|
||||
// Test opening widgets.
|
||||
executeOnLauncher(launcher ->
|
||||
assertTrue("Widgets is initially opened", getWidgetsView(launcher) == null));
|
||||
Widgets widgets = mLauncher.getWorkspace().openAllWidgets();
|
||||
assertNotNull("openAllWidgets() returned null", widgets);
|
||||
widgets = mLauncher.getAllWidgets();
|
||||
assertNotNull("getAllWidgets() returned null", widgets);
|
||||
executeOnLauncher(launcher ->
|
||||
assertTrue("Widgets is not shown", getWidgetsView(launcher).isShown()));
|
||||
executeOnLauncher(launcher -> assertEquals("Widgets is scrolled upon opening",
|
||||
0, getWidgetsScroll(launcher)));
|
||||
|
||||
// Test flinging widgets.
|
||||
widgets.flingForward();
|
||||
Integer flingForwardY = getFromLauncher(launcher -> getWidgetsScroll(launcher));
|
||||
executeOnLauncher(launcher -> assertTrue("Flinging forward didn't scroll widgets",
|
||||
flingForwardY > 0));
|
||||
|
||||
widgets.flingBackward();
|
||||
executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets",
|
||||
getWidgetsScroll(launcher) < flingForwardY));
|
||||
|
||||
mLauncher.pressHome();
|
||||
waitForLauncherCondition("Widgets were not closed",
|
||||
launcher -> getWidgetsView(launcher) == null);
|
||||
}
|
||||
|
||||
private int getWidgetsScroll(Launcher launcher) {
|
||||
return getWidgetsView(launcher).getCurrentScrollY();
|
||||
}
|
||||
|
||||
private boolean isOptionsPopupVisible(Launcher launcher) {
|
||||
final ArrowPopup popup = OptionsPopupView.getOptionsPopup(launcher);
|
||||
return popup != null && popup.isShown();
|
||||
}
|
||||
|
||||
@Test
|
||||
@PortraitLandscape
|
||||
public void testLaunchMenuItem() throws Exception {
|
||||
final AllApps allApps = mLauncher.
|
||||
getWorkspace().
|
||||
switchToAllApps();
|
||||
allApps.freeze();
|
||||
try {
|
||||
final AppIconMenu menu = allApps.
|
||||
getAppIcon(APP_NAME).
|
||||
openMenu();
|
||||
|
||||
executeOnLauncher(
|
||||
launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu",
|
||||
isOptionsPopupVisible(launcher)));
|
||||
|
||||
menu.getMenuItem(1).launch(getAppPackageName());
|
||||
} finally {
|
||||
allApps.unfreeze();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@PortraitLandscape
|
||||
public void testDragAppIcon() throws Throwable {
|
||||
// 1. Open all apps and wait for load complete.
|
||||
// 2. Drag icon to homescreen.
|
||||
// 3. Verify that the icon works on homescreen.
|
||||
final AllApps allApps = mLauncher.getWorkspace().
|
||||
switchToAllApps();
|
||||
allApps.freeze();
|
||||
try {
|
||||
allApps.
|
||||
getAppIcon(APP_NAME).
|
||||
dragToWorkspace().
|
||||
getWorkspaceAppIcon(APP_NAME).
|
||||
launch(getAppPackageName());
|
||||
} finally {
|
||||
allApps.unfreeze();
|
||||
}
|
||||
executeOnLauncher(launcher -> assertTrue(
|
||||
"Launcher activity is the top activity; expecting another activity to be the top "
|
||||
+ "one",
|
||||
isInBackground(launcher)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@PortraitLandscape
|
||||
public void testDragShortcut() throws Throwable {
|
||||
// 1. Open all apps and wait for load complete.
|
||||
// 2. Find the app and long press it to show shortcuts.
|
||||
// 3. Press icon center until shortcuts appear
|
||||
final AllApps allApps = mLauncher.
|
||||
getWorkspace().
|
||||
switchToAllApps();
|
||||
allApps.freeze();
|
||||
try {
|
||||
final AppIconMenuItem menuItem = allApps.
|
||||
getAppIcon(APP_NAME).
|
||||
openMenu().
|
||||
getMenuItem(0);
|
||||
final String shortcutName = menuItem.getText();
|
||||
|
||||
// 4. Drag the first shortcut to the home screen.
|
||||
// 5. Verify that the shortcut works on home screen
|
||||
// (the app opens and has the same text as the shortcut).
|
||||
menuItem.
|
||||
dragToWorkspace().
|
||||
getWorkspaceAppIcon(shortcutName).
|
||||
launch(getAppPackageName());
|
||||
} finally {
|
||||
allApps.unfreeze();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getAppPackageName() {
|
||||
return getInstrumentation().getContext().getPackageName();
|
||||
}
|
||||
}
|
||||
187
tests/src/com/android/uiuios/ui/TestViewHelpers.java
Normal file
187
tests/src/com/android/uiuios/ui/TestViewHelpers.java
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.ui;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
import static androidx.test.InstrumentationRegistry.getTargetContext;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.os.Process;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.test.uiautomator.By;
|
||||
import androidx.test.uiautomator.BySelector;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
import androidx.test.uiautomator.UiObject2;
|
||||
import androidx.test.uiautomator.Until;
|
||||
|
||||
import com.android.uiuios.LauncherAppWidgetProviderInfo;
|
||||
import com.android.uiuios.R;
|
||||
import com.android.uiuios.compat.AppWidgetManagerCompat;
|
||||
import com.android.uiuios.testcomponent.AppWidgetNoConfig;
|
||||
import com.android.uiuios.testcomponent.AppWidgetWithConfig;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class TestViewHelpers {
|
||||
private static final String TAG = "TestViewHelpers";
|
||||
|
||||
private static UiDevice getDevice() {
|
||||
return UiDevice.getInstance(getInstrumentation());
|
||||
}
|
||||
|
||||
public static UiObject2 findViewById(int id) {
|
||||
return getDevice().wait(Until.findObject(getSelectorForId(id)),
|
||||
AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT);
|
||||
}
|
||||
|
||||
public static BySelector getSelectorForId(int id) {
|
||||
final Context targetContext = getTargetContext();
|
||||
String name = targetContext.getResources().getResourceEntryName(id);
|
||||
return By.res(targetContext.getPackageName(), name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a widget provider which can fit on the home screen.
|
||||
*
|
||||
* @param test test suite.
|
||||
* @param hasConfigureScreen if true, a provider with a config screen is returned.
|
||||
*/
|
||||
public static LauncherAppWidgetProviderInfo findWidgetProvider(AbstractLauncherUiTest test,
|
||||
final boolean hasConfigureScreen) {
|
||||
LauncherAppWidgetProviderInfo info =
|
||||
test.getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
|
||||
@Override
|
||||
public LauncherAppWidgetProviderInfo call() throws Exception {
|
||||
ComponentName cn = new ComponentName(getInstrumentation().getContext(),
|
||||
hasConfigureScreen ? AppWidgetWithConfig.class
|
||||
: AppWidgetNoConfig.class);
|
||||
Log.d(TAG, "findWidgetProvider componentName=" + cn.flattenToString());
|
||||
return AppWidgetManagerCompat.getInstance(getTargetContext())
|
||||
.findProvider(cn, Process.myUserHandle());
|
||||
}
|
||||
});
|
||||
if (info == null) {
|
||||
throw new IllegalArgumentException("No valid widget provider");
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drags an icon to the center of homescreen.
|
||||
*
|
||||
* @param icon object that is either app icon or shortcut icon
|
||||
*/
|
||||
public static void dragToWorkspace(UiObject2 icon, boolean expectedToShowShortcuts) {
|
||||
Point center = icon.getVisibleCenter();
|
||||
|
||||
// Action Down
|
||||
final long downTime = SystemClock.uptimeMillis();
|
||||
sendPointer(downTime, MotionEvent.ACTION_DOWN, center);
|
||||
|
||||
UiObject2 dragLayer = findViewById(R.id.drag_layer);
|
||||
|
||||
if (expectedToShowShortcuts) {
|
||||
// Make sure shortcuts show up, and then move a bit to hide them.
|
||||
assertNotNull(findViewById(R.id.deep_shortcuts_container));
|
||||
|
||||
Point moveLocation = new Point(center);
|
||||
int distanceToMove =
|
||||
getTargetContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.deep_shortcuts_start_drag_threshold) + 50;
|
||||
if (moveLocation.y - distanceToMove >= dragLayer.getVisibleBounds().top) {
|
||||
moveLocation.y -= distanceToMove;
|
||||
} else {
|
||||
moveLocation.y += distanceToMove;
|
||||
}
|
||||
movePointer(downTime, center, moveLocation);
|
||||
|
||||
assertNull(findViewById(R.id.deep_shortcuts_container));
|
||||
}
|
||||
|
||||
// Wait until Remove/Delete target is visible
|
||||
assertNotNull(findViewById(R.id.delete_target_text));
|
||||
|
||||
Point moveLocation = dragLayer.getVisibleCenter();
|
||||
|
||||
// Move to center
|
||||
movePointer(downTime, center, moveLocation);
|
||||
sendPointer(downTime, MotionEvent.ACTION_UP, moveLocation);
|
||||
|
||||
// Wait until remove target is gone.
|
||||
getDevice().wait(Until.gone(getSelectorForId(R.id.delete_target_text)),
|
||||
AbstractLauncherUiTest.DEFAULT_UI_TIMEOUT);
|
||||
}
|
||||
|
||||
private static void movePointer(long downTime, Point from, Point to) {
|
||||
while (!from.equals(to)) {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
from.x = getNextMoveValue(to.x, from.x);
|
||||
from.y = getNextMoveValue(to.y, from.y);
|
||||
sendPointer(downTime, MotionEvent.ACTION_MOVE, from);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getNextMoveValue(int targetValue, int oldValue) {
|
||||
if (targetValue - oldValue > 10) {
|
||||
return oldValue + 10;
|
||||
} else if (targetValue - oldValue < -10) {
|
||||
return oldValue - 10;
|
||||
} else {
|
||||
return targetValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendPointer(long downTime, int action, Point point) {
|
||||
MotionEvent event = MotionEvent.obtain(downTime,
|
||||
SystemClock.uptimeMillis(), action, point.x, point.y, 0);
|
||||
getInstrumentation().sendPointerSync(event);
|
||||
event.recycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens widget tray and returns the recycler view.
|
||||
*/
|
||||
public static UiObject2 openWidgetsTray() {
|
||||
final UiDevice device = getDevice();
|
||||
device.pressKeyCode(KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON);
|
||||
device.waitForIdle();
|
||||
return findViewById(R.id.widgets_list_view);
|
||||
}
|
||||
|
||||
public static View findChildView(ViewGroup parent, Function<View, Boolean> condition) {
|
||||
for (int i = 0; i < parent.getChildCount(); ++i) {
|
||||
final View child = parent.getChildAt(i);
|
||||
if (condition.apply(child)) return child;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
67
tests/src/com/android/uiuios/ui/WorkTabTest.java
Normal file
67
tests/src/com/android/uiuios/ui/WorkTabTest.java
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2018, 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.uiuios.ui;
|
||||
|
||||
import static com.android.uiuios.LauncherState.ALL_APPS;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class WorkTabTest extends AbstractLauncherUiTest {
|
||||
|
||||
private int mProfileUserId;
|
||||
|
||||
@Before
|
||||
public void createWorkProfile() throws Exception {
|
||||
String output =
|
||||
mDevice.executeShellCommand(
|
||||
"pm create-user --profileOf 0 --managed TestProfile");
|
||||
assertTrue("Failed to create work profile", output.startsWith("Success"));
|
||||
|
||||
String[] tokens = output.split("\\s+");
|
||||
mProfileUserId = Integer.parseInt(tokens[tokens.length - 1]);
|
||||
|
||||
mDevice.executeShellCommand("am start-user " + mProfileUserId);
|
||||
}
|
||||
|
||||
@After
|
||||
public void removeWorkProfile() throws Exception {
|
||||
mDevice.executeShellCommand("pm remove-user " + mProfileUserId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void workTabExists() {
|
||||
mActivityMonitor.startLauncher();
|
||||
|
||||
executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
|
||||
|
||||
/*
|
||||
assertTrue("Personal tab is missing", waitForLauncherCondition(
|
||||
launcher -> launcher.getAppsView().isPersonalTabVisible()));
|
||||
assertTrue("Work tab is missing", waitForLauncherCondition(
|
||||
launcher -> launcher.getAppsView().isWorkTabVisible()));
|
||||
*/
|
||||
}
|
||||
}
|
||||
184
tests/src/com/android/uiuios/ui/widget/AddConfigWidgetTest.java
Normal file
184
tests/src/com/android/uiuios/ui/widget/AddConfigWidgetTest.java
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.ui.widget;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
import androidx.test.uiautomator.By;
|
||||
import androidx.test.uiautomator.UiObject2;
|
||||
|
||||
import com.android.uiuios.ItemInfo;
|
||||
import com.android.uiuios.LauncherAppWidgetInfo;
|
||||
import com.android.uiuios.LauncherAppWidgetProviderInfo;
|
||||
import com.android.uiuios.Workspace;
|
||||
import com.android.uiuios.testcomponent.WidgetConfigActivity;
|
||||
import com.android.uiuios.ui.AbstractLauncherUiTest;
|
||||
import com.android.uiuios.ui.TestViewHelpers;
|
||||
import com.android.uiuios.util.Condition;
|
||||
import com.android.uiuios.util.Wait;
|
||||
import com.android.uiuios.util.rule.ShellCommandRule;
|
||||
import com.android.uiuios.widget.WidgetCell;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Test to verify widget configuration is properly shown.
|
||||
*/
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AddConfigWidgetTest extends AbstractLauncherUiTest {
|
||||
|
||||
@Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
|
||||
|
||||
private LauncherAppWidgetProviderInfo mWidgetInfo;
|
||||
private AppWidgetManager mAppWidgetManager;
|
||||
|
||||
private int mWidgetId;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mWidgetInfo = TestViewHelpers.findWidgetProvider(this, true /* hasConfigureScreen */);
|
||||
mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
// Convert test to TAPL b/131116002
|
||||
public void testWidgetConfig() throws Throwable {
|
||||
runTest(false, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // b/121280703
|
||||
public void testWidgetConfig_rotate() throws Throwable {
|
||||
runTest(true, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
// Convert test to TAPL b/131116002
|
||||
public void testConfigCancelled() throws Throwable {
|
||||
runTest(false, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // b/121280703
|
||||
public void testConfigCancelled_rotate() throws Throwable {
|
||||
runTest(true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rotateConfig should the config screen be rotated
|
||||
* @param acceptConfig accept the config activity
|
||||
*/
|
||||
private void runTest(boolean rotateConfig, boolean acceptConfig) throws Throwable {
|
||||
lockRotation(true);
|
||||
|
||||
clearHomescreen();
|
||||
mActivityMonitor.startLauncher();
|
||||
|
||||
// Open widget tray and wait for load complete.
|
||||
final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray();
|
||||
Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT);
|
||||
|
||||
// Drag widget to homescreen
|
||||
WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
|
||||
UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class)
|
||||
.hasDescendant(By.text(mWidgetInfo.getLabel(mTargetContext.getPackageManager()))));
|
||||
TestViewHelpers.dragToWorkspace(widget, false);
|
||||
// Widget id for which the config activity was opened
|
||||
mWidgetId = monitor.getWidgetId();
|
||||
|
||||
if (rotateConfig) {
|
||||
// Rotate the screen and verify that the config activity is recreated
|
||||
monitor = new WidgetConfigStartupMonitor();
|
||||
lockRotation(false);
|
||||
assertEquals(mWidgetId, monitor.getWidgetId());
|
||||
}
|
||||
|
||||
// Verify that the widget id is valid and bound
|
||||
assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
|
||||
|
||||
setResult(acceptConfig);
|
||||
if (acceptConfig) {
|
||||
Wait.atMost(null, new WidgetSearchCondition(), DEFAULT_ACTIVITY_TIMEOUT);
|
||||
assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
|
||||
} else {
|
||||
// Verify that the widget id is deleted.
|
||||
Wait.atMost(null, () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
|
||||
DEFAULT_ACTIVITY_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
private void setResult(boolean success) {
|
||||
getInstrumentation().getTargetContext().sendBroadcast(
|
||||
WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class,
|
||||
success ? "clickOK" : "clickCancel"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition for searching widget id
|
||||
*/
|
||||
private class WidgetSearchCondition implements Condition, Workspace.ItemOperator {
|
||||
|
||||
@Override
|
||||
public boolean isTrue() throws Throwable {
|
||||
return mMainThreadExecutor.submit(mActivityMonitor.itemExists(this)).get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(ItemInfo info, View view) {
|
||||
return info instanceof LauncherAppWidgetInfo &&
|
||||
((LauncherAppWidgetInfo) info).providerName.getClassName().equals(
|
||||
mWidgetInfo.provider.getClassName()) &&
|
||||
((LauncherAppWidgetInfo) info).appWidgetId == mWidgetId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast receiver for receiving widget config activity status.
|
||||
*/
|
||||
private class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
|
||||
|
||||
public WidgetConfigStartupMonitor() {
|
||||
super(WidgetConfigActivity.class.getName());
|
||||
}
|
||||
|
||||
public int getWidgetId() throws InterruptedException {
|
||||
Intent intent = blockingGetExtraIntent();
|
||||
assertNotNull(intent);
|
||||
assertEquals(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE, intent.getAction());
|
||||
int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
LauncherAppWidgetInfo.NO_ID);
|
||||
assertNotSame(widgetId, LauncherAppWidgetInfo.NO_ID);
|
||||
return widgetId;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
tests/src/com/android/uiuios/ui/widget/AddWidgetTest.java
Normal file
90
tests/src/com/android/uiuios/ui/widget/AddWidgetTest.java
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.ui.widget;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
import androidx.test.uiautomator.By;
|
||||
import androidx.test.uiautomator.UiObject2;
|
||||
import android.view.View;
|
||||
|
||||
import com.android.uiuios.ItemInfo;
|
||||
import com.android.uiuios.LauncherAppWidgetInfo;
|
||||
import com.android.uiuios.LauncherAppWidgetProviderInfo;
|
||||
import com.android.uiuios.Workspace.ItemOperator;
|
||||
import com.android.uiuios.ui.AbstractLauncherUiTest;
|
||||
import com.android.uiuios.ui.TestViewHelpers;
|
||||
import com.android.uiuios.util.Condition;
|
||||
import com.android.uiuios.util.Wait;
|
||||
import com.android.uiuios.util.rule.ShellCommandRule;
|
||||
import com.android.uiuios.widget.WidgetCell;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* Test to add widget from widget tray
|
||||
*/
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class AddWidgetTest extends AbstractLauncherUiTest {
|
||||
|
||||
@Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
|
||||
|
||||
@Test
|
||||
public void testDragIcon_portrait() throws Throwable {
|
||||
lockRotation(true);
|
||||
performTest();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // b/121280703
|
||||
public void testDragIcon_landscape() throws Throwable {
|
||||
lockRotation(false);
|
||||
performTest();
|
||||
}
|
||||
|
||||
// Convert to TAPL b/131116002
|
||||
private void performTest() throws Throwable {
|
||||
clearHomescreen();
|
||||
mActivityMonitor.startLauncher();
|
||||
|
||||
final LauncherAppWidgetProviderInfo widgetInfo =
|
||||
TestViewHelpers.findWidgetProvider(this, false /* hasConfigureScreen */);
|
||||
|
||||
// Open widget tray and wait for load complete.
|
||||
final UiObject2 widgetContainer = TestViewHelpers.openWidgetsTray();
|
||||
Wait.atMost(null, Condition.minChildCount(widgetContainer, 2), DEFAULT_UI_TIMEOUT);
|
||||
|
||||
// Drag widget to homescreen
|
||||
UiObject2 widget = scrollAndFind(widgetContainer, By.clazz(WidgetCell.class)
|
||||
.hasDescendant(By.text(widgetInfo.getLabel(mTargetContext.getPackageManager()))));
|
||||
TestViewHelpers.dragToWorkspace(widget, false);
|
||||
|
||||
assertTrue(mActivityMonitor.itemExists(new ItemOperator() {
|
||||
@Override
|
||||
public boolean evaluate(ItemInfo info, View view) {
|
||||
return info instanceof LauncherAppWidgetInfo &&
|
||||
((LauncherAppWidgetInfo) info).providerName.getClassName().equals(
|
||||
widgetInfo.provider.getClassName());
|
||||
}
|
||||
}).call());
|
||||
}
|
||||
}
|
||||
355
tests/src/com/android/uiuios/ui/widget/BindWidgetTest.java
Normal file
355
tests/src/com/android/uiuios/ui/widget/BindWidgetTest.java
Normal file
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.ui.widget;
|
||||
|
||||
import static com.android.uiuios.WorkspaceLayoutManager.FIRST_SCREEN_ID;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import android.appwidget.AppWidgetHost;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.pm.PackageInstaller;
|
||||
import android.content.pm.PackageInstaller.SessionParams;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.uiuios.LauncherAppWidgetHost;
|
||||
import com.android.uiuios.LauncherAppWidgetInfo;
|
||||
import com.android.uiuios.LauncherAppWidgetProviderInfo;
|
||||
import com.android.uiuios.LauncherSettings;
|
||||
import com.android.uiuios.compat.AppWidgetManagerCompat;
|
||||
import com.android.uiuios.compat.PackageInstallerCompat;
|
||||
import com.android.uiuios.tapl.Workspace;
|
||||
import com.android.uiuios.ui.AbstractLauncherUiTest;
|
||||
import com.android.uiuios.ui.TestViewHelpers;
|
||||
import com.android.uiuios.util.ContentWriter;
|
||||
import com.android.uiuios.util.rule.ShellCommandRule;
|
||||
import com.android.uiuios.widget.PendingAddWidgetInfo;
|
||||
import com.android.uiuios.widget.WidgetHostViewLoader;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Tests for bind widget flow.
|
||||
*
|
||||
* Note running these tests will clear the workspace on the device.
|
||||
*/
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class BindWidgetTest extends AbstractLauncherUiTest {
|
||||
|
||||
@Rule
|
||||
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
|
||||
|
||||
private ContentResolver mResolver;
|
||||
private AppWidgetManagerCompat mWidgetManager;
|
||||
|
||||
// Objects created during test, which should be cleaned up in the end.
|
||||
private Cursor mCursor;
|
||||
// App install session id.
|
||||
private int mSessionId = -1;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mResolver = mTargetContext.getContentResolver();
|
||||
mWidgetManager = AppWidgetManagerCompat.getInstance(mTargetContext);
|
||||
|
||||
// Clear all existing data
|
||||
LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
|
||||
LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
if (mCursor != null) {
|
||||
mCursor.close();
|
||||
}
|
||||
|
||||
if (mSessionId > -1) {
|
||||
mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindNormalWidget_withConfig() {
|
||||
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
|
||||
LauncherAppWidgetInfo item = createWidgetInfo(info, true);
|
||||
|
||||
setupContents(item);
|
||||
verifyWidgetPresent(info);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindNormalWidget_withoutConfig() {
|
||||
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
|
||||
LauncherAppWidgetInfo item = createWidgetInfo(info, true);
|
||||
|
||||
setupContents(item);
|
||||
verifyWidgetPresent(info);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnboundWidget_removed() {
|
||||
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
|
||||
LauncherAppWidgetInfo item = createWidgetInfo(info, false);
|
||||
item.appWidgetId = -33;
|
||||
|
||||
setupContents(item);
|
||||
|
||||
final Workspace workspace = mLauncher.getWorkspace();
|
||||
// Item deleted from db
|
||||
mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
|
||||
null, null, null, null, null);
|
||||
assertEquals(0, mCursor.getCount());
|
||||
|
||||
// The view does not exist
|
||||
assertTrue("Widget exists", workspace.tryGetWidget(info.label, 0) == null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPendingWidget_autoRestored() {
|
||||
// A non-restored widget with no config screen gets restored automatically.
|
||||
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, false);
|
||||
|
||||
// Do not bind the widget
|
||||
LauncherAppWidgetInfo item = createWidgetInfo(info, false);
|
||||
item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
|
||||
|
||||
setupContents(item);
|
||||
verifyWidgetPresent(info);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPendingWidget_withConfigScreen() {
|
||||
// A non-restored widget with config screen get bound and shows a 'Click to setup' UI.
|
||||
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(this, true);
|
||||
|
||||
// Do not bind the widget
|
||||
LauncherAppWidgetInfo item = createWidgetInfo(info, false);
|
||||
item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
|
||||
|
||||
setupContents(item);
|
||||
verifyPendingWidgetPresent();
|
||||
|
||||
// Item deleted from db
|
||||
mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
|
||||
null, null, null, null, null);
|
||||
mCursor.moveToNext();
|
||||
|
||||
// Widget has a valid Id now.
|
||||
assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
|
||||
& LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
|
||||
assertNotNull(AppWidgetManager.getInstance(mTargetContext)
|
||||
.getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex(
|
||||
LauncherSettings.Favorites.APPWIDGET_ID))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPendingWidget_notRestored_removed() {
|
||||
LauncherAppWidgetInfo item = getInvalidWidgetInfo();
|
||||
item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
|
||||
| LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
|
||||
|
||||
setupContents(item);
|
||||
|
||||
assertTrue("Pending widget exists",
|
||||
mLauncher.getWorkspace().tryGetPendingWidget(0) == null);
|
||||
// Item deleted from db
|
||||
mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
|
||||
null, null, null, null, null);
|
||||
assertEquals(0, mCursor.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPendingWidget_notRestored_brokenInstall() {
|
||||
// A widget which is was being installed once, even if its not being
|
||||
// installed at the moment is not removed.
|
||||
LauncherAppWidgetInfo item = getInvalidWidgetInfo();
|
||||
item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
|
||||
| LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
|
||||
| LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
|
||||
|
||||
setupContents(item);
|
||||
verifyPendingWidgetPresent();
|
||||
|
||||
// Verify item still exists in db
|
||||
mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
|
||||
null, null, null, null, null);
|
||||
assertEquals(1, mCursor.getCount());
|
||||
|
||||
// Widget still has an invalid id.
|
||||
mCursor.moveToNext();
|
||||
assertEquals(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID,
|
||||
mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
|
||||
& LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPendingWidget_notRestored_activeInstall() throws Exception {
|
||||
// A widget which is being installed is not removed
|
||||
LauncherAppWidgetInfo item = getInvalidWidgetInfo();
|
||||
item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
|
||||
| LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
|
||||
|
||||
// Create an active installer session
|
||||
SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
|
||||
params.setAppPackageName(item.providerName.getPackageName());
|
||||
PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
|
||||
mSessionId = installer.createSession(params);
|
||||
|
||||
setupContents(item);
|
||||
verifyPendingWidgetPresent();
|
||||
|
||||
// Verify item still exists in db
|
||||
mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
|
||||
null, null, null, null, null);
|
||||
assertEquals(1, mCursor.getCount());
|
||||
|
||||
// Widget still has an invalid id.
|
||||
mCursor.moveToNext();
|
||||
assertEquals(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID,
|
||||
mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
|
||||
& LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@param item} on the homescreen on the 0th screen at 0,0, and verifies that the
|
||||
* widget class is displayed on the homescreen.
|
||||
*/
|
||||
private void setupContents(LauncherAppWidgetInfo item) {
|
||||
int screenId = FIRST_SCREEN_ID;
|
||||
// Update the screen id counter for the provider.
|
||||
LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID);
|
||||
|
||||
if (screenId > FIRST_SCREEN_ID) {
|
||||
screenId = FIRST_SCREEN_ID;
|
||||
}
|
||||
|
||||
// Insert the item
|
||||
ContentWriter writer = new ContentWriter(mTargetContext);
|
||||
item.id = LauncherSettings.Settings.call(
|
||||
mResolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
|
||||
.getInt(LauncherSettings.Settings.EXTRA_VALUE);
|
||||
item.screenId = screenId;
|
||||
item.onAddToDatabase(writer);
|
||||
writer.put(LauncherSettings.Favorites._ID, item.id);
|
||||
mResolver.insert(LauncherSettings.Favorites.CONTENT_URI, writer.getValues(mTargetContext));
|
||||
resetLoaderState();
|
||||
|
||||
// Launch the home activity
|
||||
mActivityMonitor.startLauncher();
|
||||
waitForModelLoaded();
|
||||
}
|
||||
|
||||
private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
|
||||
assertTrue("Widget is not present",
|
||||
mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT) != null);
|
||||
}
|
||||
|
||||
private void verifyPendingWidgetPresent() {
|
||||
assertTrue("Pending widget is not present",
|
||||
mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT) != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LauncherAppWidgetInfo corresponding to {@param info}
|
||||
* @param bindWidget if true the info is bound and a valid widgetId is assigned to
|
||||
* the LauncherAppWidgetInfo
|
||||
*/
|
||||
private LauncherAppWidgetInfo createWidgetInfo(
|
||||
LauncherAppWidgetProviderInfo info, boolean bindWidget) {
|
||||
LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(
|
||||
LauncherAppWidgetInfo.NO_ID, info.provider);
|
||||
item.spanX = info.minSpanX;
|
||||
item.spanY = info.minSpanY;
|
||||
item.minSpanX = info.minSpanX;
|
||||
item.minSpanY = info.minSpanY;
|
||||
item.user = info.getProfile();
|
||||
item.cellX = 0;
|
||||
item.cellY = 1;
|
||||
item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
|
||||
|
||||
if (bindWidget) {
|
||||
PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(info);
|
||||
pendingInfo.spanX = item.spanX;
|
||||
pendingInfo.spanY = item.spanY;
|
||||
pendingInfo.minSpanX = item.minSpanX;
|
||||
pendingInfo.minSpanY = item.minSpanY;
|
||||
Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(mTargetContext, pendingInfo);
|
||||
|
||||
AppWidgetHost host = new LauncherAppWidgetHost(mTargetContext);
|
||||
int widgetId = host.allocateAppWidgetId();
|
||||
if (!mWidgetManager.bindAppWidgetIdIfAllowed(widgetId, info, options)) {
|
||||
host.deleteAppWidgetId(widgetId);
|
||||
throw new IllegalArgumentException("Unable to bind widget id");
|
||||
}
|
||||
item.appWidgetId = widgetId;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a LauncherAppWidgetInfo with package name which is not present on the device
|
||||
*/
|
||||
private LauncherAppWidgetInfo getInvalidWidgetInfo() {
|
||||
String invalidPackage = "com.invalidpackage";
|
||||
int count = 0;
|
||||
String pkg = invalidPackage;
|
||||
|
||||
Set<String> activePackage = getOnUiThread(() ->
|
||||
PackageInstallerCompat.getInstance(mTargetContext)
|
||||
.updateAndGetActiveSessionCache().keySet());
|
||||
while(true) {
|
||||
try {
|
||||
mTargetContext.getPackageManager().getPackageInfo(
|
||||
pkg, PackageManager.GET_UNINSTALLED_PACKAGES);
|
||||
} catch (Exception e) {
|
||||
if (!activePackage.contains(pkg)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
pkg = invalidPackage + count;
|
||||
count ++;
|
||||
}
|
||||
LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(10,
|
||||
new ComponentName(pkg, "com.test.widgetprovider"));
|
||||
item.spanX = 2;
|
||||
item.spanY = 2;
|
||||
item.minSpanX = 2;
|
||||
item.minSpanY = 2;
|
||||
item.cellX = 0;
|
||||
item.cellY = 1;
|
||||
item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
193
tests/src/com/android/uiuios/ui/widget/RequestPinItemTest.java
Normal file
193
tests/src/com/android/uiuios/ui/widget/RequestPinItemTest.java
Normal file
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.ui.widget;
|
||||
|
||||
import static com.android.uiuios.ui.TaplTestsLauncher3.getAppPackageName;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNotSame;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.uiuios.ItemInfo;
|
||||
import com.android.uiuios.LauncherAppWidgetInfo;
|
||||
import com.android.uiuios.LauncherSettings.Favorites;
|
||||
import com.android.uiuios.Utilities;
|
||||
import com.android.uiuios.Workspace.ItemOperator;
|
||||
import com.android.uiuios.WorkspaceItemInfo;
|
||||
import com.android.uiuios.shortcuts.ShortcutKey;
|
||||
import com.android.uiuios.tapl.AddToHomeScreenPrompt;
|
||||
import com.android.uiuios.testcomponent.AppWidgetNoConfig;
|
||||
import com.android.uiuios.testcomponent.AppWidgetWithConfig;
|
||||
import com.android.uiuios.testcomponent.RequestPinItemActivity;
|
||||
import com.android.uiuios.ui.AbstractLauncherUiTest;
|
||||
import com.android.uiuios.util.Condition;
|
||||
import com.android.uiuios.util.Wait;
|
||||
import com.android.uiuios.util.rule.ShellCommandRule;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Test to verify pin item request flow.
|
||||
*/
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RequestPinItemTest extends AbstractLauncherUiTest {
|
||||
|
||||
@Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
|
||||
|
||||
private String mCallbackAction;
|
||||
private String mShortcutId;
|
||||
private int mAppWidgetId;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mCallbackAction = UUID.randomUUID().toString();
|
||||
mShortcutId = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmpty() throws Throwable { /* needed while the broken tests are being fixed */ }
|
||||
|
||||
@Test
|
||||
public void testPinWidgetNoConfig() throws Throwable {
|
||||
runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
|
||||
((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
|
||||
((LauncherAppWidgetInfo) info).providerName.getClassName()
|
||||
.equals(AppWidgetNoConfig.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPinWidgetNoConfig_customPreview() throws Throwable {
|
||||
// Command to set custom preview
|
||||
Intent command = RequestPinItemActivity.getCommandIntent(
|
||||
RequestPinItemActivity.class, "setRemoteViewColor").putExtra(
|
||||
RequestPinItemActivity.EXTRA_PARAM + "0", Color.RED);
|
||||
|
||||
runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
|
||||
((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
|
||||
((LauncherAppWidgetInfo) info).providerName.getClassName()
|
||||
.equals(AppWidgetNoConfig.class.getName()), command);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPinWidgetWithConfig() throws Throwable {
|
||||
runTest("pinWidgetWithConfig", true,
|
||||
(info, view) -> info instanceof LauncherAppWidgetInfo &&
|
||||
((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
|
||||
((LauncherAppWidgetInfo) info).providerName.getClassName()
|
||||
.equals(AppWidgetWithConfig.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPinShortcut() throws Throwable {
|
||||
// Command to set the shortcut id
|
||||
Intent command = RequestPinItemActivity.getCommandIntent(
|
||||
RequestPinItemActivity.class, "setShortcutId").putExtra(
|
||||
RequestPinItemActivity.EXTRA_PARAM + "0", mShortcutId);
|
||||
|
||||
runTest("pinShortcut", false, new ItemOperator() {
|
||||
@Override
|
||||
public boolean evaluate(ItemInfo info, View view) {
|
||||
return info instanceof WorkspaceItemInfo &&
|
||||
info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
|
||||
ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId);
|
||||
}
|
||||
}, command);
|
||||
}
|
||||
|
||||
private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher,
|
||||
Intent... commandIntents) throws Throwable {
|
||||
if (!Utilities.ATLEAST_OREO) {
|
||||
return;
|
||||
}
|
||||
lockRotation(true);
|
||||
|
||||
clearHomescreen();
|
||||
mActivityMonitor.startLauncher();
|
||||
|
||||
// Open Pin item activity
|
||||
BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver(
|
||||
RequestPinItemActivity.class.getName());
|
||||
mLauncher.
|
||||
getWorkspace().
|
||||
switchToAllApps().
|
||||
getAppIcon("Test Pin Item").
|
||||
launch(getAppPackageName());
|
||||
assertNotNull(openMonitor.blockingGetExtraIntent());
|
||||
|
||||
// Set callback
|
||||
PendingIntent callback = PendingIntent.getBroadcast(mTargetContext, 0,
|
||||
new Intent(mCallbackAction), PendingIntent.FLAG_ONE_SHOT);
|
||||
mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
|
||||
RequestPinItemActivity.class, "setCallback").putExtra(
|
||||
RequestPinItemActivity.EXTRA_PARAM + "0", callback));
|
||||
|
||||
for (Intent command : commandIntents) {
|
||||
mTargetContext.sendBroadcast(command);
|
||||
}
|
||||
|
||||
// call the requested method to start the flow
|
||||
mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
|
||||
RequestPinItemActivity.class, activityMethod));
|
||||
final AddToHomeScreenPrompt addToHomeScreenPrompt = mLauncher.getAddToHomeScreenPrompt();
|
||||
|
||||
// Accept confirmation:
|
||||
BlockingBroadcastReceiver resultReceiver = new BlockingBroadcastReceiver(mCallbackAction);
|
||||
addToHomeScreenPrompt.addAutomatically();
|
||||
Intent result = resultReceiver.blockingGetIntent();
|
||||
assertNotNull(result);
|
||||
mAppWidgetId = result.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
|
||||
if (isWidget) {
|
||||
assertNotSame(-1, mAppWidgetId);
|
||||
}
|
||||
|
||||
// Go back to home
|
||||
mLauncher.pressHome();
|
||||
Wait.atMost(null, new ItemSearchCondition(itemMatcher), DEFAULT_ACTIVITY_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition for for an item
|
||||
*/
|
||||
private class ItemSearchCondition implements Condition {
|
||||
|
||||
private final ItemOperator mOp;
|
||||
|
||||
ItemSearchCondition(ItemOperator op) {
|
||||
mOp = op;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTrue() throws Throwable {
|
||||
return mMainThreadExecutor.submit(mActivityMonitor.itemExists(mOp)).get();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
tests/src/com/android/uiuios/util/Condition.java
Normal file
43
tests/src/com/android/uiuios/util/Condition.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.android.uiuios.util;
|
||||
|
||||
import androidx.test.uiautomator.UiObject2;
|
||||
|
||||
import com.android.uiuios.MainThreadExecutor;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public interface Condition {
|
||||
|
||||
boolean isTrue() throws Throwable;
|
||||
|
||||
/**
|
||||
* Converts the condition to be run on UI thread.
|
||||
*/
|
||||
static Condition runOnUiThread(final Condition condition) {
|
||||
final MainThreadExecutor executor = new MainThreadExecutor();
|
||||
return () -> {
|
||||
final AtomicBoolean value = new AtomicBoolean(false);
|
||||
final Throwable[] exceptions = new Throwable[1];
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
value.set(condition.isTrue());
|
||||
} catch (Throwable e) {
|
||||
exceptions[0] = e;
|
||||
}
|
||||
|
||||
});
|
||||
latch.await(1, TimeUnit.SECONDS);
|
||||
if (exceptions[0] != null) {
|
||||
throw exceptions[0];
|
||||
}
|
||||
return value.get();
|
||||
};
|
||||
}
|
||||
|
||||
static Condition minChildCount(final UiObject2 obj, final int childCount) {
|
||||
return () -> obj.getChildCount() >= childCount;
|
||||
}
|
||||
}
|
||||
172
tests/src/com/android/uiuios/util/LauncherLayoutBuilder.java
Normal file
172
tests/src/com/android/uiuios/util/LauncherLayoutBuilder.java
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (C) 2019 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.uiuios.util;
|
||||
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.util.Xml;
|
||||
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Helper class to build xml for Launcher Layout
|
||||
*/
|
||||
public class LauncherLayoutBuilder {
|
||||
|
||||
// Object Tags
|
||||
private static final String TAG_WORKSPACE = "workspace";
|
||||
private static final String TAG_AUTO_INSTALL = "autoinstall";
|
||||
private static final String TAG_FOLDER = "folder";
|
||||
private static final String TAG_APPWIDGET = "appwidget";
|
||||
private static final String TAG_EXTRA = "extra";
|
||||
|
||||
private static final String ATTR_CONTAINER = "container";
|
||||
private static final String ATTR_RANK = "rank";
|
||||
|
||||
private static final String ATTR_PACKAGE_NAME = "packageName";
|
||||
private static final String ATTR_CLASS_NAME = "className";
|
||||
private static final String ATTR_TITLE = "title";
|
||||
private static final String ATTR_SCREEN = "screen";
|
||||
|
||||
// x and y can be specified as negative integers, in which case -1 represents the
|
||||
// last row / column, -2 represents the second last, and so on.
|
||||
private static final String ATTR_X = "x";
|
||||
private static final String ATTR_Y = "y";
|
||||
private static final String ATTR_SPAN_X = "spanX";
|
||||
private static final String ATTR_SPAN_Y = "spanY";
|
||||
|
||||
private static final String ATTR_CHILDREN = "children";
|
||||
|
||||
|
||||
// Style attrs -- "Extra"
|
||||
private static final String ATTR_KEY = "key";
|
||||
private static final String ATTR_VALUE = "value";
|
||||
|
||||
private static final String CONTAINER_DESKTOP = "desktop";
|
||||
private static final String CONTAINER_HOTSEAT = "hotseat";
|
||||
|
||||
private final ArrayList<Pair<String, HashMap<String, Object>>> mNodes = new ArrayList<>();
|
||||
|
||||
public Location atHotseat(int rank) {
|
||||
Location l = new Location();
|
||||
l.items.put(ATTR_CONTAINER, CONTAINER_HOTSEAT);
|
||||
l.items.put(ATTR_RANK, Integer.toString(rank));
|
||||
return l;
|
||||
}
|
||||
|
||||
public Location atWorkspace(int x, int y, int screen) {
|
||||
Location l = new Location();
|
||||
l.items.put(ATTR_CONTAINER, CONTAINER_DESKTOP);
|
||||
l.items.put(ATTR_X, Integer.toString(x));
|
||||
l.items.put(ATTR_Y, Integer.toString(y));
|
||||
l.items.put(ATTR_SCREEN, Integer.toString(screen));
|
||||
return l;
|
||||
}
|
||||
|
||||
public String build() throws IOException {
|
||||
StringWriter writer = new StringWriter();
|
||||
build(writer);
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
public void build(Writer writer) throws IOException {
|
||||
XmlSerializer serializer = Xml.newSerializer();
|
||||
serializer.setOutput(writer);
|
||||
|
||||
serializer.startDocument("UTF-8", true);
|
||||
serializer.startTag(null, TAG_WORKSPACE);
|
||||
writeNodes(serializer, mNodes);
|
||||
serializer.endTag(null, TAG_WORKSPACE);
|
||||
serializer.endDocument();
|
||||
serializer.flush();
|
||||
}
|
||||
|
||||
private static void writeNodes(XmlSerializer serializer,
|
||||
ArrayList<Pair<String, HashMap<String, Object>>> nodes) throws IOException {
|
||||
for (Pair<String, HashMap<String, Object>> node : nodes) {
|
||||
ArrayList<Pair<String, HashMap<String, Object>>> children = null;
|
||||
|
||||
serializer.startTag(null, node.first);
|
||||
for (Map.Entry<String, Object> attr : node.second.entrySet()) {
|
||||
if (ATTR_CHILDREN.equals(attr.getKey())) {
|
||||
children = (ArrayList<Pair<String, HashMap<String, Object>>>) attr.getValue();
|
||||
} else {
|
||||
serializer.attribute(null, attr.getKey(), (String) attr.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if (children != null) {
|
||||
writeNodes(serializer, children);
|
||||
}
|
||||
serializer.endTag(null, node.first);
|
||||
}
|
||||
}
|
||||
|
||||
public class Location {
|
||||
|
||||
final HashMap<String, Object> items = new HashMap<>();
|
||||
|
||||
public LauncherLayoutBuilder putApp(String packageName, String className) {
|
||||
items.put(ATTR_PACKAGE_NAME, packageName);
|
||||
items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
|
||||
mNodes.add(Pair.create(TAG_AUTO_INSTALL, items));
|
||||
return LauncherLayoutBuilder.this;
|
||||
}
|
||||
|
||||
public LauncherLayoutBuilder putWidget(String packageName, String className,
|
||||
int spanX, int spanY) {
|
||||
items.put(ATTR_PACKAGE_NAME, packageName);
|
||||
items.put(ATTR_CLASS_NAME, className);
|
||||
items.put(ATTR_SPAN_X, Integer.toString(spanX));
|
||||
items.put(ATTR_SPAN_Y, Integer.toString(spanY));
|
||||
mNodes.add(Pair.create(TAG_APPWIDGET, items));
|
||||
return LauncherLayoutBuilder.this;
|
||||
}
|
||||
|
||||
public FolderBuilder putFolder(int titleResId) {
|
||||
FolderBuilder folderBuilder = new FolderBuilder();
|
||||
items.put(ATTR_TITLE, Integer.toString(titleResId));
|
||||
items.put(ATTR_CHILDREN, folderBuilder.mChildren);
|
||||
mNodes.add(Pair.create(TAG_FOLDER, items));
|
||||
return folderBuilder;
|
||||
}
|
||||
}
|
||||
|
||||
public class FolderBuilder {
|
||||
|
||||
final ArrayList<Pair<String, HashMap<String, Object>>> mChildren = new ArrayList<>();
|
||||
|
||||
public FolderBuilder addApp(String packageName, String className) {
|
||||
HashMap<String, Object> items = new HashMap<>();
|
||||
items.put(ATTR_PACKAGE_NAME, packageName);
|
||||
items.put(ATTR_CLASS_NAME, TextUtils.isEmpty(className) ? packageName : className);
|
||||
mChildren.add(Pair.create(TAG_AUTO_INSTALL, items));
|
||||
return this;
|
||||
}
|
||||
|
||||
public LauncherLayoutBuilder build() {
|
||||
return LauncherLayoutBuilder.this;
|
||||
}
|
||||
}
|
||||
}
|
||||
488
tests/src/com/android/uiuios/util/RaceConditionReproducer.java
Normal file
488
tests/src/com/android/uiuios/util/RaceConditionReproducer.java
Normal file
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.uiuios.util;
|
||||
|
||||
import static com.android.uiuios.util.RaceConditionTracker.ENTER_POSTFIX;
|
||||
import static com.android.uiuios.util.RaceConditionTracker.EXIT_POSTFIX;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Event processor for reliably reproducing multithreaded apps race conditions in tests.
|
||||
*
|
||||
* The app notifies us about “events” that happen in its threads. The race condition test runs the
|
||||
* test action multiple times (aka iterations), trying to generate all possible permutations of
|
||||
* these events. It keeps a set of all seen event sequences and steers the execution towards
|
||||
* executing events in previously unseen order. It does it by postponing execution of threads that
|
||||
* would lead to an already seen sequence.
|
||||
*
|
||||
* If an event A occurs before event B in the sequence, this is how execution order looks like:
|
||||
* Events: ... A ... B ...
|
||||
* Events and instructions, guaranteed order:
|
||||
* (instructions executed prior to A) A ... B (instructions executed after B)
|
||||
*
|
||||
* Each iteration has 3 parts (phases).
|
||||
* Phase 1. Picking a previously seen event subsequence that we believe can have previously unseen
|
||||
* continuations. Reproducing this sequence by pausing threads that would lead to other sequences.
|
||||
* Phase 2. Trying to generate previously unseen continuation of the sequence from Phase 1. We need
|
||||
* one new event after that sequence. All threads leading to seen continuations will be postponed
|
||||
* for some short period of time. The phase ends once the new event is registered, or after the
|
||||
* period of time ends (in which case we declare that the sequence can’t have new continuations).
|
||||
* Phase 3. Releasing all threads and letting the test iteration run till its end.
|
||||
*
|
||||
* The iterations end when all seen paths have been declared “uncontinuable”.
|
||||
*
|
||||
* When we register event XXX:enter, we hold all other events until we register XXX:exit.
|
||||
*/
|
||||
public class RaceConditionReproducer implements RaceConditionTracker.EventProcessor {
|
||||
private static final String TAG = "RaceConditionReproducer";
|
||||
private static final long SHORT_TIMEOUT_MS = 2000;
|
||||
private static final long LONG_TIMEOUT_MS = 60000;
|
||||
// Handler used to resume postponed events.
|
||||
private static final Handler POSTPONED_EVENT_RESUME_HANDLER = createEventResumeHandler();
|
||||
|
||||
private static Handler createEventResumeHandler() {
|
||||
final HandlerThread thread = new HandlerThread("RaceConditionEventResumer");
|
||||
thread.start();
|
||||
return new Handler(thread.getLooper());
|
||||
}
|
||||
|
||||
/**
|
||||
* Event in a particular sequence of events. A node in the prefix tree of all seen event
|
||||
* sequences.
|
||||
*/
|
||||
private class EventNode {
|
||||
// Events that were seen just after this event.
|
||||
private final Map<String, EventNode> mNextEvents = new HashMap<>();
|
||||
// Whether we believe that further iterations will not be able to add more events to
|
||||
// mNextEvents.
|
||||
private boolean mStoppedAddingChildren = true;
|
||||
|
||||
private void debugDump(StringBuilder sb, int indent, String name) {
|
||||
for (int i = 0; i < indent; ++i) sb.append('.');
|
||||
sb.append(!mStoppedAddingChildren ? "+" : "-");
|
||||
sb.append(" : ");
|
||||
sb.append(name);
|
||||
if (mLastRegisteredEvent == this) sb.append(" <");
|
||||
sb.append('\n');
|
||||
|
||||
for (String key : mNextEvents.keySet()) {
|
||||
mNextEvents.get(key).debugDump(sb, indent + 2, key);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of leaves in the subtree with this node as a root. */
|
||||
private int numberOfLeafNodes() {
|
||||
if (mNextEvents.isEmpty()) return 1;
|
||||
|
||||
int leaves = 0;
|
||||
for (String event : mNextEvents.keySet()) {
|
||||
leaves += mNextEvents.get(event).numberOfLeafNodes();
|
||||
}
|
||||
return leaves;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we believe that further iterations will not be able add nodes to the subtree with
|
||||
* this node as a root.
|
||||
*/
|
||||
private boolean stoppedAddingChildrenToTree() {
|
||||
if (!mStoppedAddingChildren) return false;
|
||||
|
||||
for (String event : mNextEvents.keySet()) {
|
||||
if (!mNextEvents.get(event).stoppedAddingChildrenToTree()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* In the subtree with this node as a root, tries finding a node where we may have a
|
||||
* chance to add new children.
|
||||
* If succeeds, returns true and fills 'path' with the sequence of events to that node;
|
||||
* otherwise returns false.
|
||||
*/
|
||||
private boolean populatePathToGrowthPoint(List<String> path) {
|
||||
for (String event : mNextEvents.keySet()) {
|
||||
if (mNextEvents.get(event).populatePathToGrowthPoint(path)) {
|
||||
path.add(0, event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!mStoppedAddingChildren) {
|
||||
// Mark that we have finished adding children. It will remain true if no new
|
||||
// children are added, or will be set to false upon adding a new child.
|
||||
mStoppedAddingChildren = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Starting point of all event sequences; the root of the prefix tree representation all
|
||||
// sequences generated by test iterations. A test iteration can add nodes int it.
|
||||
private EventNode mRoot = new EventNode();
|
||||
// During a test iteration, the last event that was registered.
|
||||
private EventNode mLastRegisteredEvent;
|
||||
// Length of the current sequence of registered events for the current test iteration.
|
||||
private int mRegisteredEventCount = 0;
|
||||
// During the first part of a test iteration, we go to a specific node under mRoot by
|
||||
// 'playing back' mSequenceToFollow. During this part, all events that don't belong to this
|
||||
// sequence get postponed.
|
||||
private List<String> mSequenceToFollow = new ArrayList<>();
|
||||
// Collection of events that got postponed, with corresponding wait objects used to let them go.
|
||||
private Map<String, Semaphore> mPostponedEvents = new HashMap<>();
|
||||
// Callback to run by POSTPONED_EVENT_RESUME_HANDLER, used to let go of all currently
|
||||
// postponed events.
|
||||
private Runnable mResumeAllEventsCallback;
|
||||
// String representation of the sequence of events registered so far for the current test
|
||||
// iteration. After registering any event, we output it to the log. The last output before
|
||||
// the test failure can be later played back to reliable reproduce the exact sequence of
|
||||
// events that broke the test.
|
||||
// Format: EV1|EV2|...\EVN
|
||||
private StringBuilder mCurrentSequence;
|
||||
// When not null, we are in a repro mode. We run only one test iteration, and are trying to
|
||||
// reproduce the event sequence represented by this string. The format is same as for
|
||||
// mCurrentSequence.
|
||||
private final String mReproString;
|
||||
|
||||
/* Constructor for a normal test. */
|
||||
public RaceConditionReproducer() {
|
||||
mReproString = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for reliably reproducing a race condition failure. The developer should find in
|
||||
* the log the latest "Repro sequence:" record and locally modify the test by passing that
|
||||
* string to the constructor. Running the test will have only one iteration that will reliably
|
||||
* "play back" that sequence.
|
||||
*/
|
||||
public RaceConditionReproducer(String reproString) {
|
||||
mReproString = reproString;
|
||||
}
|
||||
|
||||
public RaceConditionReproducer(String... reproSequence) {
|
||||
this(String.join("|", reproSequence));
|
||||
}
|
||||
|
||||
public synchronized String getCurrentSequenceString() {
|
||||
return mCurrentSequence.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new test iteration. Events reported via RaceConditionTracker.onEvent before this
|
||||
* call will be ignored.
|
||||
*/
|
||||
public synchronized void startIteration() {
|
||||
mLastRegisteredEvent = mRoot;
|
||||
mRegisteredEventCount = 0;
|
||||
mCurrentSequence = new StringBuilder();
|
||||
Log.d(TAG, "Repro sequence: " + mCurrentSequence);
|
||||
mSequenceToFollow = mReproString != null ?
|
||||
parseReproString(mReproString) : generateSequenceToFollowLocked();
|
||||
Log.e(TAG, "---- Start of iteration; state:\n" + dumpStateLocked());
|
||||
checkIfCompletedSequenceToFollowLocked();
|
||||
RaceConditionTracker.setEventProcessor(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends a new test iteration. Events reported via RaceConditionTracker.onEvent after this call
|
||||
* will be ignored.
|
||||
* Returns whether we need more iterations.
|
||||
*/
|
||||
public synchronized boolean finishIteration() {
|
||||
RaceConditionTracker.setEventProcessor(null);
|
||||
runResumeAllEventsCallbackLocked();
|
||||
assertTrue("Non-empty postponed events", mPostponedEvents.isEmpty());
|
||||
assertTrue("Last registered event is :enter", lastEventAsEnter() == null);
|
||||
|
||||
// No events came after mLastRegisteredEvent. It doesn't make sense to come to it again
|
||||
// because we won't see new continuations.
|
||||
mLastRegisteredEvent.mStoppedAddingChildren = true;
|
||||
Log.e(TAG, "---- End of iteration; state:\n" + dumpStateLocked());
|
||||
if (mReproString != null) {
|
||||
assertTrue("Repro mode: failed to reproduce the sequence",
|
||||
mCurrentSequence.toString().startsWith(mReproString));
|
||||
}
|
||||
// If we are in a repro mode, we need only one iteration. Otherwise, continue if the tree
|
||||
// has prospective growth points.
|
||||
return mReproString == null && !mRoot.stoppedAddingChildrenToTree();
|
||||
}
|
||||
|
||||
private static List<String> parseReproString(String reproString) {
|
||||
return Arrays.asList(reproString.split("\\|"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the app issues an event.
|
||||
*/
|
||||
@Override
|
||||
public void onEvent(String event) {
|
||||
final Semaphore waitObject = tryRegisterEvent(event);
|
||||
if (waitObject != null) {
|
||||
waitUntilCanRegister(event, waitObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the last event was not an XXX:enter, or this event is a matching XXX:exit.
|
||||
*/
|
||||
private boolean canRegisterEventNowLocked(String event) {
|
||||
final String lastEventAsEnter = lastEventAsEnter();
|
||||
final String thisEventAsExit = eventAsExit(event);
|
||||
|
||||
if (lastEventAsEnter != null) {
|
||||
if (!lastEventAsEnter.equals(thisEventAsExit)) {
|
||||
assertTrue("YYY:exit after XXX:enter", thisEventAsExit == null);
|
||||
// Last event was :enter, but this event is not :exit.
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Previous event was not :enter.
|
||||
assertTrue(":exit after a non-enter event", thisEventAsExit == null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event issued by the app and returns null or decides that the event must be
|
||||
* postponed, and returns an object to wait on.
|
||||
*/
|
||||
private synchronized Semaphore tryRegisterEvent(String event) {
|
||||
Log.d(TAG, "Event issued by the app: " + event);
|
||||
|
||||
if (!canRegisterEventNowLocked(event)) {
|
||||
return createWaitObjectForPostponedEventLocked(event);
|
||||
}
|
||||
|
||||
if (mRegisteredEventCount < mSequenceToFollow.size()) {
|
||||
// We are in the first part of the iteration. We only register events that follow the
|
||||
// mSequenceToFollow and postponing all other events.
|
||||
if (event.equals(mSequenceToFollow.get(mRegisteredEventCount))) {
|
||||
// The event is the next one expected in the sequence. Register it.
|
||||
registerEventLocked(event);
|
||||
|
||||
// If there are postponed events that could continue the sequence, register them.
|
||||
while (mRegisteredEventCount < mSequenceToFollow.size() &&
|
||||
mPostponedEvents.containsKey(
|
||||
mSequenceToFollow.get(mRegisteredEventCount))) {
|
||||
registerPostponedEventLocked(mSequenceToFollow.get(mRegisteredEventCount));
|
||||
}
|
||||
|
||||
// Perhaps we just completed the required sequence...
|
||||
checkIfCompletedSequenceToFollowLocked();
|
||||
} else {
|
||||
// The event is not the next one in the sequence. Postpone it.
|
||||
return createWaitObjectForPostponedEventLocked(event);
|
||||
}
|
||||
} else if (mRegisteredEventCount == mSequenceToFollow.size()) {
|
||||
// The second phase of the iteration. We have just registered the whole
|
||||
// mSequenceToFollow, and want to add previously not seen continuations for the last
|
||||
// node in the sequence aka 'growth point'.
|
||||
if (!mLastRegisteredEvent.mNextEvents.containsKey(event) || mReproString != null) {
|
||||
// The event was never seen as a continuation for the current node.
|
||||
// Or we are in repro mode, in which case we are not in business of generating
|
||||
// new sequences after we've played back the required sequence.
|
||||
// Register it immediately.
|
||||
registerEventLocked(event);
|
||||
} else {
|
||||
// The event was seen as a continuation for the current node. Postpone it, hoping
|
||||
// that a new event will come from other threads.
|
||||
return createWaitObjectForPostponedEventLocked(event);
|
||||
}
|
||||
} else {
|
||||
// The third phase of the iteration. We are past the growth point and register
|
||||
// everything that comes.
|
||||
registerEventLocked(event);
|
||||
// Register events that may have been postponed while waiting for an :exit event
|
||||
// during the third phase. We don't do this if just registered event is :enter.
|
||||
if (eventAsEnter(event) == null && mRegisteredEventCount > mSequenceToFollow.size()) {
|
||||
registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Called when there are chances that we just have registered the whole mSequenceToFollow. */
|
||||
private void checkIfCompletedSequenceToFollowLocked() {
|
||||
if (mRegisteredEventCount == mSequenceToFollow.size()) {
|
||||
// We just entered the second phase of the iteration. We have just registered the
|
||||
// whole mSequenceToFollow, and want to add previously not seen continuations for the
|
||||
// last node in the sequence aka 'growth point'. All seen continuations will be
|
||||
// postponed for SHORT_TIMEOUT_MS. At the end of this time period, we'll let them go.
|
||||
scheduleResumeAllEventsLocked();
|
||||
|
||||
// Among the events that were postponed during the first stage, there may be an event
|
||||
// that wasn't seen after the current. If so, register it immediately because this
|
||||
// creates a new sequence.
|
||||
final Set<String> keys = new HashSet<>(mPostponedEvents.keySet());
|
||||
keys.removeAll(mLastRegisteredEvent.mNextEvents.keySet());
|
||||
if (!keys.isEmpty()) {
|
||||
registerPostponedEventLocked(keys.iterator().next());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Semaphore createWaitObjectForPostponedEventLocked(String event) {
|
||||
final Semaphore waitObject = new Semaphore(0);
|
||||
assertTrue("Event already postponed: " + event, !mPostponedEvents.containsKey(event));
|
||||
mPostponedEvents.put(event, waitObject);
|
||||
return waitObject;
|
||||
}
|
||||
|
||||
private void waitUntilCanRegister(String event, Semaphore waitObject) {
|
||||
try {
|
||||
assertTrue("Never registered event: " + event,
|
||||
waitObject.tryAcquire(LONG_TIMEOUT_MS, TimeUnit.MILLISECONDS));
|
||||
} catch (InterruptedException e) {
|
||||
fail("Wait was interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
/** Schedules resuming all postponed events after SHORT_TIMEOUT_MS */
|
||||
private void scheduleResumeAllEventsLocked() {
|
||||
assertTrue(mResumeAllEventsCallback == null);
|
||||
mResumeAllEventsCallback = this::allEventsResumeCallback;
|
||||
POSTPONED_EVENT_RESUME_HANDLER.postDelayed(mResumeAllEventsCallback, SHORT_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
private synchronized void allEventsResumeCallback() {
|
||||
assertTrue("In callback, but callback is not set", mResumeAllEventsCallback != null);
|
||||
mResumeAllEventsCallback = null;
|
||||
registerPostponedEventsLocked(new HashSet<>(mPostponedEvents.keySet()));
|
||||
}
|
||||
|
||||
private void registerPostponedEventsLocked(Collection<String> events) {
|
||||
for (String event : events) {
|
||||
registerPostponedEventLocked(event);
|
||||
if (eventAsEnter(event) != null) {
|
||||
// Once :enter is registered, switch to waiting for :exit to come. Won't register
|
||||
// other postponed events.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void registerPostponedEventLocked(String event) {
|
||||
mPostponedEvents.remove(event).release();
|
||||
registerEventLocked(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the last registered event was XXX:enter, returns XXX, otherwise, null.
|
||||
*/
|
||||
private String lastEventAsEnter() {
|
||||
return eventAsEnter(mCurrentSequence.substring(mCurrentSequence.lastIndexOf("|") + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* If the event is XXX:postfix, returns XXX, otherwise, null.
|
||||
*/
|
||||
private static String prefixFromPostfixedEvent(String event, String postfix) {
|
||||
final int columnPos = event.indexOf(':');
|
||||
if (columnPos != -1 && postfix.equals(event.substring(columnPos + 1))) {
|
||||
return event.substring(0, columnPos);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the event is XXX:enter, returns XXX, otherwise, null.
|
||||
*/
|
||||
private static String eventAsEnter(String event) {
|
||||
return prefixFromPostfixedEvent(event, ENTER_POSTFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the event is XXX:exit, returns XXX, otherwise, null.
|
||||
*/
|
||||
private static String eventAsExit(String event) {
|
||||
return prefixFromPostfixedEvent(event, EXIT_POSTFIX);
|
||||
}
|
||||
|
||||
private void registerEventLocked(String event) {
|
||||
assertTrue(canRegisterEventNowLocked(event));
|
||||
|
||||
Log.d(TAG, "Actually registering event: " + event);
|
||||
EventNode next = mLastRegisteredEvent.mNextEvents.get(event);
|
||||
if (next == null) {
|
||||
// This event wasn't seen after mLastRegisteredEvent.
|
||||
next = new EventNode();
|
||||
mLastRegisteredEvent.mNextEvents.put(event, next);
|
||||
// The fact that we've added a new event after the previous one means that the
|
||||
// previous event is still a growth point, unless this event is :exit, which means
|
||||
// that the previous event is :enter.
|
||||
mLastRegisteredEvent.mStoppedAddingChildren = eventAsExit(event) != null;
|
||||
}
|
||||
|
||||
mLastRegisteredEvent = next;
|
||||
mRegisteredEventCount++;
|
||||
|
||||
if (mCurrentSequence.length() > 0) mCurrentSequence.append("|");
|
||||
mCurrentSequence.append(event);
|
||||
Log.d(TAG, "Repro sequence: " + mCurrentSequence);
|
||||
}
|
||||
|
||||
private void runResumeAllEventsCallbackLocked() {
|
||||
if (mResumeAllEventsCallback != null) {
|
||||
POSTPONED_EVENT_RESUME_HANDLER.removeCallbacks(mResumeAllEventsCallback);
|
||||
mResumeAllEventsCallback.run();
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence dumpStateLocked() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Sequence to follow: ");
|
||||
for (String event : mSequenceToFollow) sb.append(" " + event);
|
||||
sb.append(".\n");
|
||||
sb.append("Registered event count: " + mRegisteredEventCount);
|
||||
|
||||
sb.append("\nPostponed events: ");
|
||||
for (String event : mPostponedEvents.keySet()) sb.append(" " + event);
|
||||
sb.append(".");
|
||||
|
||||
sb.append("\nNodes: \n");
|
||||
mRoot.debugDump(sb, 0, "");
|
||||
return sb;
|
||||
}
|
||||
|
||||
public int numberOfLeafNodes() {
|
||||
return mRoot.numberOfLeafNodes();
|
||||
}
|
||||
|
||||
private List<String> generateSequenceToFollowLocked() {
|
||||
ArrayList<String> sequence = new ArrayList<>();
|
||||
mRoot.populatePathToGrowthPoint(sequence);
|
||||
return sequence;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.uiuios.util;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class RaceConditionReproducerTest {
|
||||
private final static String SOME_VALID_SEQUENCE_3_3 = "B1|A1|A2|B2|A3|B3";
|
||||
|
||||
private static int factorial(int n) {
|
||||
int res = 1;
|
||||
for (int i = 2; i <= n; ++i) res *= i;
|
||||
return res;
|
||||
}
|
||||
|
||||
private static void run3_3_TestAction() throws InterruptedException {
|
||||
Thread tb = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("B1");
|
||||
RaceConditionTracker.onEvent("B2");
|
||||
RaceConditionTracker.onEvent("B3");
|
||||
});
|
||||
tb.start();
|
||||
|
||||
RaceConditionTracker.onEvent("A1");
|
||||
RaceConditionTracker.onEvent("A2");
|
||||
RaceConditionTracker.onEvent("A3");
|
||||
|
||||
tb.join();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // The test is too long for continuous testing.
|
||||
// 2 threads, 3 events each.
|
||||
public void test3_3() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
|
||||
boolean sawTheValidSequence = false;
|
||||
|
||||
for (; ; ) {
|
||||
eventProcessor.startIteration();
|
||||
run3_3_TestAction();
|
||||
final boolean needMoreIterations = eventProcessor.finishIteration();
|
||||
|
||||
sawTheValidSequence = sawTheValidSequence ||
|
||||
SOME_VALID_SEQUENCE_3_3.equals(eventProcessor.getCurrentSequenceString());
|
||||
|
||||
if (!needMoreIterations) break;
|
||||
}
|
||||
|
||||
assertEquals("Wrong number of leaf nodes",
|
||||
factorial(3 + 3) / (factorial(3) * factorial(3)),
|
||||
eventProcessor.numberOfLeafNodes());
|
||||
assertTrue(sawTheValidSequence);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // The test is too long for continuous testing.
|
||||
// 2 threads, 3 events, including enter-exit pairs each.
|
||||
public void test3_3_enter_exit() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
|
||||
boolean sawTheValidSequence = false;
|
||||
|
||||
for (; ; ) {
|
||||
eventProcessor.startIteration();
|
||||
Thread tb = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("B1:enter");
|
||||
RaceConditionTracker.onEvent("B1:exit");
|
||||
RaceConditionTracker.onEvent("B2");
|
||||
RaceConditionTracker.onEvent("B3:enter");
|
||||
RaceConditionTracker.onEvent("B3:exit");
|
||||
});
|
||||
tb.start();
|
||||
|
||||
RaceConditionTracker.onEvent("A1");
|
||||
RaceConditionTracker.onEvent("A2:enter");
|
||||
RaceConditionTracker.onEvent("A2:exit");
|
||||
RaceConditionTracker.onEvent("A3:enter");
|
||||
RaceConditionTracker.onEvent("A3:exit");
|
||||
|
||||
tb.join();
|
||||
final boolean needMoreIterations = eventProcessor.finishIteration();
|
||||
|
||||
sawTheValidSequence = sawTheValidSequence ||
|
||||
"B1:enter|B1:exit|A1|A2:enter|A2:exit|B2|A3:enter|A3:exit|B3:enter|B3:exit".
|
||||
equals(eventProcessor.getCurrentSequenceString());
|
||||
|
||||
if (!needMoreIterations) break;
|
||||
}
|
||||
|
||||
assertEquals("Wrong number of leaf nodes",
|
||||
factorial(3 + 3) / (factorial(3) * factorial(3)),
|
||||
eventProcessor.numberOfLeafNodes());
|
||||
assertTrue(sawTheValidSequence);
|
||||
}
|
||||
|
||||
@Test
|
||||
// 2 threads, 3 events each; reproducing a particular event sequence.
|
||||
public void test3_3_ReproMode() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer(
|
||||
SOME_VALID_SEQUENCE_3_3);
|
||||
|
||||
eventProcessor.startIteration();
|
||||
run3_3_TestAction();
|
||||
assertTrue(!eventProcessor.finishIteration());
|
||||
assertEquals(SOME_VALID_SEQUENCE_3_3, eventProcessor.getCurrentSequenceString());
|
||||
|
||||
assertEquals("Wrong number of leaf nodes", 1, eventProcessor.numberOfLeafNodes());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // The test is too long for continuous testing.
|
||||
// 2 threads with 2 events; 1 thread with 1 event.
|
||||
public void test2_1_2() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
|
||||
|
||||
for (; ; ) {
|
||||
eventProcessor.startIteration();
|
||||
Thread tb = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("B1");
|
||||
RaceConditionTracker.onEvent("B2");
|
||||
});
|
||||
tb.start();
|
||||
|
||||
Thread tc = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("C1");
|
||||
});
|
||||
tc.start();
|
||||
|
||||
RaceConditionTracker.onEvent("A1");
|
||||
RaceConditionTracker.onEvent("A2");
|
||||
|
||||
tb.join();
|
||||
tc.join();
|
||||
|
||||
if (!eventProcessor.finishIteration()) break;
|
||||
}
|
||||
|
||||
assertEquals("Wrong number of leaf nodes",
|
||||
factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
|
||||
eventProcessor.numberOfLeafNodes());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore // The test is too long for continuous testing.
|
||||
// 2 threads with 2 events; 1 thread with 1 event. Includes enter-exit pairs.
|
||||
public void test2_1_2_enter_exit() throws Exception {
|
||||
final RaceConditionReproducer eventProcessor = new RaceConditionReproducer();
|
||||
|
||||
for (; ; ) {
|
||||
eventProcessor.startIteration();
|
||||
Thread tb = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("B1:enter");
|
||||
RaceConditionTracker.onEvent("B1:exit");
|
||||
RaceConditionTracker.onEvent("B2:enter");
|
||||
RaceConditionTracker.onEvent("B2:exit");
|
||||
});
|
||||
tb.start();
|
||||
|
||||
Thread tc = new Thread(() -> {
|
||||
RaceConditionTracker.onEvent("C1:enter");
|
||||
RaceConditionTracker.onEvent("C1:exit");
|
||||
});
|
||||
tc.start();
|
||||
|
||||
RaceConditionTracker.onEvent("A1:enter");
|
||||
RaceConditionTracker.onEvent("A1:exit");
|
||||
RaceConditionTracker.onEvent("A2:enter");
|
||||
RaceConditionTracker.onEvent("A2:exit");
|
||||
|
||||
tb.join();
|
||||
tc.join();
|
||||
|
||||
if (!eventProcessor.finishIteration()) break;
|
||||
}
|
||||
|
||||
assertEquals("Wrong number of leaf nodes",
|
||||
factorial(2 + 2 + 1) / (factorial(2) * factorial(2) * factorial(1)),
|
||||
eventProcessor.numberOfLeafNodes());
|
||||
}
|
||||
}
|
||||
58
tests/src/com/android/uiuios/util/TestUtil.java
Normal file
58
tests/src/com/android/uiuios/util/TestUtil.java
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.uiuios.util;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getContext;
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class TestUtil {
|
||||
public static final String DUMMY_PACKAGE = "com.example.android.aardwolf";
|
||||
|
||||
public static void installDummyApp() throws IOException {
|
||||
// Copy apk from resources to a local file and install from there.
|
||||
final Resources resources = getContext().getResources();
|
||||
final InputStream in = resources.openRawResource(
|
||||
resources.getIdentifier("aardwolf_dummy_app",
|
||||
"raw", getContext().getPackageName()));
|
||||
final String apkFilename = getInstrumentation().getTargetContext().
|
||||
getFilesDir().getPath() + "/dummy_app.apk";
|
||||
|
||||
final FileOutputStream out = new FileOutputStream(apkFilename);
|
||||
byte[] buff = new byte[1024];
|
||||
int read;
|
||||
|
||||
while ((read = in.read(buff)) > 0) {
|
||||
out.write(buff, 0, read);
|
||||
}
|
||||
in.close();
|
||||
out.close();
|
||||
|
||||
UiDevice.getInstance(getInstrumentation()).executeShellCommand("pm install " + apkFilename);
|
||||
}
|
||||
|
||||
public static void uninstallDummyApp() throws IOException {
|
||||
UiDevice.getInstance(getInstrumentation()).executeShellCommand(
|
||||
"pm uninstall " + DUMMY_PACKAGE);
|
||||
}
|
||||
}
|
||||
41
tests/src/com/android/uiuios/util/Wait.java
Normal file
41
tests/src/com/android/uiuios/util/Wait.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.android.uiuios.util;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import org.junit.Assert;
|
||||
|
||||
/**
|
||||
* A utility class for waiting for a condition to be true.
|
||||
*/
|
||||
public class Wait {
|
||||
|
||||
private static final long DEFAULT_SLEEP_MS = 200;
|
||||
|
||||
public static void atMost(String message, Condition condition, long timeout) {
|
||||
atMost(message, condition, timeout, DEFAULT_SLEEP_MS);
|
||||
}
|
||||
|
||||
public static void atMost(String message, Condition condition, long timeout, long sleepMillis) {
|
||||
long endTime = SystemClock.uptimeMillis() + timeout;
|
||||
while (SystemClock.uptimeMillis() < endTime) {
|
||||
try {
|
||||
if (condition.isTrue()) {
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
SystemClock.sleep(sleepMillis);
|
||||
}
|
||||
|
||||
// Check once more before returning false.
|
||||
try {
|
||||
if (condition.isTrue()) {
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
Assert.fail(message);
|
||||
}
|
||||
}
|
||||
59
tests/src/com/android/uiuios/util/rule/FailureWatcher.java
Normal file
59
tests/src/com/android/uiuios/util/rule/FailureWatcher.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.android.uiuios.util.rule;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
|
||||
import org.junit.rules.TestWatcher;
|
||||
import org.junit.runner.Description;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class FailureWatcher extends TestWatcher {
|
||||
private static final String TAG = "FailureWatcher";
|
||||
private static int sScreenshotCount = 0;
|
||||
final private UiDevice mDevice;
|
||||
|
||||
public FailureWatcher(UiDevice device) {
|
||||
mDevice = device;
|
||||
}
|
||||
|
||||
private void dumpViewHierarchy() {
|
||||
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
try {
|
||||
mDevice.dumpWindowHierarchy(stream);
|
||||
stream.flush();
|
||||
stream.close();
|
||||
for (String line : stream.toString().split("\\r?\\n")) {
|
||||
Log.e(TAG, line.trim());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "error dumping XML to logcat", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed(Throwable e, Description description) {
|
||||
if (mDevice == null) return;
|
||||
final String pathname = getInstrumentation().getTargetContext().
|
||||
getFilesDir().getPath() + "/TaplTestScreenshot" + sScreenshotCount++ + ".png";
|
||||
Log.e(TAG, "Failed test " + description.getMethodName() +
|
||||
", screenshot will be saved to " + pathname +
|
||||
", track trace is below, UI object dump is further below:\n" +
|
||||
Log.getStackTraceString(e));
|
||||
dumpViewHierarchy();
|
||||
|
||||
try {
|
||||
final String dumpsysResult = mDevice.executeShellCommand(
|
||||
"dumpsys activity service TouchInteractionService");
|
||||
Log.d(TAG, "TouchInteractionService: " + dumpsysResult);
|
||||
} catch (IOException ex) {
|
||||
}
|
||||
|
||||
mDevice.takeScreenshot(new File(pathname));
|
||||
}
|
||||
}
|
||||
124
tests/src/com/android/uiuios/util/rule/LauncherActivityRule.java
Normal file
124
tests/src/com/android/uiuios/util/rule/LauncherActivityRule.java
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.util.rule;
|
||||
|
||||
import static com.android.uiuios.tapl.TestHelpers.getHomeIntentInPackage;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
import static androidx.test.InstrumentationRegistry.getTargetContext;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.app.Application.ActivityLifecycleCallbacks;
|
||||
import android.os.Bundle;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
|
||||
import com.android.uiuios.Launcher;
|
||||
import com.android.uiuios.Workspace.ItemOperator;
|
||||
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* Test rule to get the current Launcher activity.
|
||||
*/
|
||||
public class LauncherActivityRule implements TestRule {
|
||||
|
||||
private Launcher mActivity;
|
||||
|
||||
@Override
|
||||
public Statement apply(Statement base, Description description) {
|
||||
return new MyStatement(base);
|
||||
}
|
||||
|
||||
public Launcher getActivity() {
|
||||
return mActivity;
|
||||
}
|
||||
|
||||
public Callable<Boolean> itemExists(final ItemOperator op) {
|
||||
return new Callable<Boolean>() {
|
||||
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
Launcher launcher = getActivity();
|
||||
if (launcher == null) {
|
||||
return false;
|
||||
}
|
||||
return launcher.getWorkspace().getFirstMatch(op) != null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the launcher activity in the target package.
|
||||
*/
|
||||
public void startLauncher() {
|
||||
getInstrumentation().startActivitySync(getHomeIntentInPackage(getTargetContext()));
|
||||
}
|
||||
|
||||
private class MyStatement extends Statement implements ActivityLifecycleCallbacks {
|
||||
|
||||
private final Statement mBase;
|
||||
|
||||
public MyStatement(Statement base) {
|
||||
mBase = base;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
Application app = (Application)
|
||||
InstrumentationRegistry.getTargetContext().getApplicationContext();
|
||||
app.registerActivityLifecycleCallbacks(this);
|
||||
try {
|
||||
mBase.evaluate();
|
||||
} finally {
|
||||
app.unregisterActivityLifecycleCallbacks(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity activity, Bundle bundle) {
|
||||
if (activity instanceof Launcher) {
|
||||
mActivity = (Launcher) activity;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity activity) { }
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) { }
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity activity) { }
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity activity) { }
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { }
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity activity) {
|
||||
if (activity == mActivity) {
|
||||
mActivity = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
tests/src/com/android/uiuios/util/rule/ShellCommandRule.java
Normal file
90
tests/src/com/android/uiuios/util/rule/ShellCommandRule.java
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.util.rule;
|
||||
|
||||
import static com.android.uiuios.tapl.TestHelpers.getLauncherInMyProcess;
|
||||
|
||||
import static androidx.test.InstrumentationRegistry.getInstrumentation;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.pm.ActivityInfo;
|
||||
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
|
||||
/**
|
||||
* Test rule which executes a shell command at the start of the test.
|
||||
*/
|
||||
public class ShellCommandRule implements TestRule {
|
||||
|
||||
private final String mCmd;
|
||||
private final String mRevertCommand;
|
||||
|
||||
public ShellCommandRule(String cmd, @Nullable String revertCommand) {
|
||||
mCmd = cmd;
|
||||
mRevertCommand = revertCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(Statement base, Description description) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
UiDevice.getInstance(getInstrumentation()).executeShellCommand(mCmd);
|
||||
try {
|
||||
base.evaluate();
|
||||
} finally {
|
||||
if (mRevertCommand != null) {
|
||||
UiDevice.getInstance(getInstrumentation()).executeShellCommand(mRevertCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants the launcher permission to bind widgets.
|
||||
*/
|
||||
public static ShellCommandRule grantWidgetBind() {
|
||||
return new ShellCommandRule("appwidget grantbind --package "
|
||||
+ InstrumentationRegistry.getTargetContext().getPackageName(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the target launcher as default launcher.
|
||||
*/
|
||||
public static ShellCommandRule setDefaultLauncher() {
|
||||
return new ShellCommandRule(getLauncherCommand(getLauncherInMyProcess()), null);
|
||||
}
|
||||
|
||||
public static String getLauncherCommand(ActivityInfo launcher) {
|
||||
return "cmd package set-home-activity " +
|
||||
new ComponentName(launcher.packageName, launcher.name).flattenToString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables heads up notification for the duration of the test
|
||||
*/
|
||||
public static ShellCommandRule disableHeadsUpNotification() {
|
||||
return new ShellCommandRule("settings put global heads_up_notifications_enabled 0",
|
||||
"settings put global heads_up_notifications_enabled 1");
|
||||
}
|
||||
}
|
||||
151
tests/src/com/android/uiuios/widget/WidgetsListAdapterTest.java
Normal file
151
tests/src/com/android/uiuios/widget/WidgetsListAdapterTest.java
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.uiuios.widget;
|
||||
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Matchers.isNull;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.appwidget.AppWidgetProviderInfo;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import com.android.uiuios.icons.IconCache;
|
||||
import com.android.uiuios.InvariantDeviceProfile;
|
||||
import com.android.uiuios.LauncherAppWidgetProviderInfo;
|
||||
import com.android.uiuios.WidgetPreviewLoader;
|
||||
import com.android.uiuios.compat.AppWidgetManagerCompat;
|
||||
import com.android.uiuios.model.PackageItemInfo;
|
||||
import com.android.uiuios.model.WidgetItem;
|
||||
import com.android.uiuios.util.MultiHashMap;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class WidgetsListAdapterTest {
|
||||
|
||||
@Mock private LayoutInflater mMockLayoutInflater;
|
||||
@Mock private WidgetPreviewLoader mMockWidgetCache;
|
||||
@Mock private RecyclerView.AdapterDataObserver mListener;
|
||||
@Mock private IconCache mIconCache;
|
||||
|
||||
private WidgetsListAdapter mAdapter;
|
||||
private InvariantDeviceProfile mTestProfile;
|
||||
private Context mContext;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = InstrumentationRegistry.getTargetContext();
|
||||
mTestProfile = new InvariantDeviceProfile();
|
||||
mTestProfile.numRows = 5;
|
||||
mTestProfile.numColumns = 5;
|
||||
mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache,
|
||||
mIconCache, null, null);
|
||||
mAdapter.registerAdapterDataObserver(mListener);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_notifyDataSetChanged() throws Exception {
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
verify(mListener, times(1)).onChanged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_notifyItemInserted() throws Exception {
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
mAdapter.setWidgets(generateSampleMap(2));
|
||||
verify(mListener, times(1)).onChanged();
|
||||
verify(mListener, times(1)).onItemRangeInserted(eq(1), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_notifyItemRemoved() throws Exception {
|
||||
mAdapter.setWidgets(generateSampleMap(2));
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
verify(mListener, times(1)).onChanged();
|
||||
verify(mListener, times(1)).onItemRangeRemoved(eq(1), eq(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyItemChanged_PackageIconDiff() throws Exception {
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
mAdapter.setWidgets(generateSampleMap(1));
|
||||
verify(mListener, times(1)).onChanged();
|
||||
verify(mListener, times(1)).onItemRangeChanged(eq(0), eq(1), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyItemChanged_widgetItemInfoDiff() throws Exception {
|
||||
// TODO: same package name but item number changed
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotifyItemInsertedRemoved_hodgepodge() throws Exception {
|
||||
// TODO: insert and remove combined. curMap
|
||||
// newMap [A, C, D] [A, B, E]
|
||||
// B - C < 0, removed B from index 1 [A, E]
|
||||
// E - C > 0, C inserted to index 1 [A, C, E]
|
||||
// E - D > 0, D inserted to index 2 [A, C, D, E]
|
||||
// E - null = -1, E deleted from index 3 [A, C, D]
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to generate the sample widget model map that can be used for the tests
|
||||
* @param num the number of WidgetItem the map should contain
|
||||
* @return
|
||||
*/
|
||||
private ArrayList<WidgetListRowEntry> generateSampleMap(int num) {
|
||||
ArrayList<WidgetListRowEntry> result = new ArrayList<>();
|
||||
if (num <= 0) return result;
|
||||
|
||||
MultiHashMap<PackageItemInfo, WidgetItem> newMap = new MultiHashMap();
|
||||
AppWidgetManagerCompat widgetManager = AppWidgetManagerCompat.getInstance(mContext);
|
||||
for (AppWidgetProviderInfo widgetInfo : widgetManager.getAllProviders(null)) {
|
||||
WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
|
||||
.fromProviderInfo(mContext, widgetInfo), mTestProfile, mIconCache);
|
||||
|
||||
PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
|
||||
pInfo.title = pInfo.packageName;
|
||||
pInfo.user = wi.user;
|
||||
pInfo.iconBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8);
|
||||
newMap.addToList(pInfo, wi);
|
||||
if (newMap.size() == num) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (Map.Entry<PackageItemInfo, ArrayList<WidgetItem>> entry : newMap.entrySet()) {
|
||||
result.add(new WidgetListRowEntry(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user