update:2021.03.19

fix:重新提交
add:
This commit is contained in:
FHT
2021-03-19 18:14:04 +08:00
parent 23e93b0e31
commit d4399ffa61
1189 changed files with 0 additions and 149104 deletions

View File

@@ -1,98 +0,0 @@
/*
* 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;
}
}

View File

@@ -1,211 +0,0 @@
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;
}
}

View File

@@ -1,99 +0,0 @@
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() { }
}
}

View File

@@ -1,23 +0,0 @@
/*
* 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 { }

View File

@@ -1,26 +0,0 @@
/*
* 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 {
}

View File

@@ -1,23 +0,0 @@
/*
* 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 {
}

View File

@@ -1,128 +0,0 @@
/*
* 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);
}
}

View File

@@ -1,105 +0,0 @@
/*
* 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);
}
}

View File

@@ -1,130 +0,0 @@
/*
* 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);
}
}

View File

@@ -1,34 +0,0 @@
/*
* 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);
}
}

View File

@@ -1,275 +0,0 @@
/*
* 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;
}
}

View File

@@ -1,44 +0,0 @@
/*
* 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();
}
}

View File

@@ -1,165 +0,0 @@
/*
* 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());
}
}

View File

@@ -1,390 +0,0 @@
/*
* 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();
}
}

View File

@@ -1,129 +0,0 @@
/**
* 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();
}
}

View File

@@ -1,63 +0,0 @@
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();
}
};
}
}

View File

@@ -1,353 +0,0 @@
/*
* 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();
}
}

View File

@@ -1,187 +0,0 @@
/*
* 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;
}
}

View File

@@ -1,67 +0,0 @@
/*
* 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()));
*/
}
}

View File

@@ -1,184 +0,0 @@
/*
* 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;
}
}
}

View File

@@ -1,90 +0,0 @@
/*
* 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());
}
}

View File

@@ -1,355 +0,0 @@
/*
* 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;
}
}

View File

@@ -1,193 +0,0 @@
/*
* 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();
}
}
}

View File

@@ -1,43 +0,0 @@
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;
}
}

View File

@@ -1,172 +0,0 @@
/**
* 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;
}
}
}

View File

@@ -1,488 +0,0 @@
/*
* 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 cant 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;
}
}

View File

@@ -1,203 +0,0 @@
/*
* 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());
}
}

View File

@@ -1,58 +0,0 @@
/*
* 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);
}
}

View File

@@ -1,41 +0,0 @@
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);
}
}

View File

@@ -1,59 +0,0 @@
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));
}
}

View File

@@ -1,124 +0,0 @@
/*
* 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;
}
}
}
}

View File

@@ -1,90 +0,0 @@
/*
* 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");
}
}

View File

@@ -1,151 +0,0 @@
/*
* 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;
}
}