Merge "Creating database without localized collators on NYC and above" into ub-launcher3-calgary
diff --git a/res/drawable/bg_celllayout.xml b/res/drawable/bg_celllayout.xml
index 0ce083e..651ae52 100644
--- a/res/drawable/bg_celllayout.xml
+++ b/res/drawable/bg_celllayout.xml
@@ -22,13 +22,14 @@
<item>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
- <solid android:color="#3fffffff" />
+ <solid android:color="@color/spring_loaded_panel_color" />
</shape>
</item>
<item>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
- <solid android:color="#3fffffff" />
- <stroke android:width="1dp" android:color="#fff" />
+ <stroke
+ android:width="@dimen/spring_loaded_panel_border"
+ android:color="@color/spring_loaded_highlighted_panel_border_color" />
</shape>
</item>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index b679270..dc3d4fa 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -39,6 +39,9 @@
<color name="outline_color">#FFFFFFFF</color>
<color name="launcher_accent_color">#ff009688</color>
+ <color name="spring_loaded_panel_color">#40FFFFFF</color>
+ <color name="spring_loaded_highlighted_panel_border_color">#FFF</color>
+
<!-- Containers -->
<color name="container_fastscroll_thumb_inactive_color">#009688</color>
<color name="container_fastscroll_thumb_active_color">#009688</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index fee62dc..b06afc4 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -134,6 +134,8 @@
<dimen name="drag_flingToDeleteMinVelocity">-1500dp</dimen>
+ <dimen name="spring_loaded_panel_border">1dp</dimen>
+
<!-- Theme -->
<dimen name="quantum_panel_outer_padding">4dp</dimen>
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index 0ba9499..daeca3b 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -363,7 +363,7 @@
sTmpRect.right, sTmpRect.bottom);
}
- public static Rect getWidgetSizeRanges(Launcher launcher, int spanX, int spanY, Rect rect) {
+ public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) {
if (sCellSize == null) {
InvariantDeviceProfile inv = LauncherAppState.getInstance().getInvariantDeviceProfile();
@@ -376,7 +376,7 @@
if (rect == null) {
rect = new Rect();
}
- final float density = launcher.getResources().getDisplayMetrics().density;
+ final float density = context.getResources().getDisplayMetrics().density;
// Compute landscape size
int landWidth = (int) ((spanX * sCellSize[0].x) / density);
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 052f7ac..e691b48 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -55,7 +55,7 @@
mHasVerticalHotseat = mLauncher.getDeviceProfile().isVerticalBarLayout();
}
- CellLayout getLayout() {
+ public CellLayout getLayout() {
return mContent;
}
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index e411527..245b399 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -4013,7 +4013,7 @@
// Note: This assumes that the id remap broadcast is received before this step.
// If that is not the case, the id remap will be ignored and user may see the
// click to setup view.
- PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(this, appWidgetInfo, null);
+ PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(this, appWidgetInfo);
pendingInfo.spanX = item.spanX;
pendingInfo.spanY = item.spanY;
pendingInfo.minSpanX = item.minSpanX;
diff --git a/src/com/android/launcher3/LauncherAppWidgetHostView.java b/src/com/android/launcher3/LauncherAppWidgetHostView.java
index c789082..570607e 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java
@@ -27,6 +27,7 @@
import android.view.ViewConfiguration;
import android.view.ViewDebug;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.RemoteViews;
import com.android.launcher3.dragndrop.DragLayer;
@@ -291,4 +292,10 @@
});
}
}
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(info);
+ info.setClassName(getClass().getName());
+ }
}
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index fc8bb45..5606a24 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -625,17 +625,23 @@
// we moved this functionality to a helper function so SmoothPagedView can reuse it
protected boolean computeScrollHelper() {
+ return computeScrollHelper(true);
+ }
+
+ protected boolean computeScrollHelper(boolean shouldInvalidate) {
if (mScroller.computeScrollOffset()) {
// Don't bother scrolling if the page does not need to be moved
if (getScrollX() != mScroller.getCurrX()
- || getScrollY() != mScroller.getCurrY()) {
+ || getScrollY() != mScroller.getCurrY()) {
float scaleX = mFreeScroll ? getScaleX() : 1f;
int scrollX = (int) (mScroller.getCurrX() * (1 / scaleX));
scrollTo(scrollX, mScroller.getCurrY());
}
- invalidate();
+ if (shouldInvalidate) {
+ invalidate();
+ }
return true;
- } else if (mNextPage != INVALID_PAGE) {
+ } else if (mNextPage != INVALID_PAGE && shouldInvalidate) {
sendScrollAccessibilityEvent();
mCurrentPage = validateNewPage(mNextPage);
diff --git a/src/com/android/launcher3/PinchToOverviewListener.java b/src/com/android/launcher3/PinchToOverviewListener.java
index ef47485..02b6044 100644
--- a/src/com/android/launcher3/PinchToOverviewListener.java
+++ b/src/com/android/launcher3/PinchToOverviewListener.java
@@ -63,16 +63,24 @@
// Don't listen for the pinch gesture if we are already animating from a previous one.
return false;
}
+ if (mLauncher.isWorkspaceLocked()) {
+ // Don't listen for the pinch gesture if the workspace isn't ready.
+ return false;
+ }
if (mWorkspace == null) {
- mWorkspace = mLauncher.mWorkspace;
+ mWorkspace = mLauncher.getWorkspace();
mThresholdManager = new PinchThresholdManager(mWorkspace);
mAnimationManager = new PinchAnimationManager(mLauncher);
}
if (mWorkspace.isSwitchingState() || mWorkspace.mScrollInteractionBegan) {
- // Don't listen to pinches occurring while switching state, as it will cause a jump
+ // Don't listen for the pinch gesture while switching state, as it will cause a jump
// once the state switching animation is complete.
return false;
}
+ if (mWorkspace.getOpenFolder() != null) {
+ // Don't listen for the pinch gesture if a folder is open.
+ return false;
+ }
mPreviousProgress = mWorkspace.isInOverviewMode() ? OVERVIEW_PROGRESS : WORKSPACE_PROGRESS;
mPreviousTimeMillis = System.currentTimeMillis();
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 0b72ef7..44a17cc 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -149,7 +149,7 @@
*/
@Thunk CellLayout mDragTargetLayout = null;
/**
- * The CellLayout that we will show as glowing
+ * The CellLayout that we will show as highlighted
*/
private CellLayout mDragOverlappingLayout = null;
@@ -1421,6 +1421,10 @@
mWallpaperOffset.syncWithScroll();
}
+ public void computeScrollWithoutInvalidation() {
+ computeScrollHelper(false);
+ }
+
@Override
protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) {
if (!isSwitchingState()) {
@@ -2836,7 +2840,13 @@
if (mDragOverlappingLayout != null) {
mDragOverlappingLayout.setIsDragOverlapping(true);
}
- invalidate();
+ // Invalidating the scrim will also force this CellLayout
+ // to be invalidated so that it is highlighted if necessary.
+ mLauncher.getDragLayer().invalidateScrim();
+ }
+
+ public CellLayout getCurrentDragOverlappingLayout() {
+ return mDragOverlappingLayout;
}
void setCurrentDropOverCell(int x, int y) {
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 647ec5e..40b9179 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -26,6 +26,7 @@
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
+import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.DragEvent;
@@ -42,6 +43,7 @@
import com.android.launcher3.AppWidgetResizeFrame;
import com.android.launcher3.CellLayout;
+import com.android.launcher3.Hotseat;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
@@ -92,6 +94,7 @@
private boolean mHoverPointClosesFolder = false;
private final Rect mHitRect = new Rect();
+ private final Rect mHighlightRect = new Rect();
private TouchCompleteListener mTouchCompleteListener;
@@ -247,12 +250,9 @@
}
clearAllResizeFrames();
- Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
- if (currentFolder == null) {
- if (mPinchListener.onInterceptTouchEvent(ev)) {
- // Stop listening for scrolling etc. (onTouchEvent() handles the rest of the pinch.)
- return true;
- }
+ if (mPinchListener.onInterceptTouchEvent(ev)) {
+ // Stop listening for scrolling etc. (onTouchEvent() handles the rest of the pinch.)
+ return true;
}
return mDragController.onInterceptTouchEvent(ev);
}
@@ -922,12 +922,29 @@
invalidate();
}
+ public void invalidateScrim() {
+ if (mBackgroundAlpha > 0.0f) {
+ invalidate();
+ }
+ }
+
@Override
protected void dispatchDraw(Canvas canvas) {
// Draw the background below children.
if (mBackgroundAlpha > 0.0f) {
+ // Update the scroll position first to ensure scrim cutout is in the right place.
+ mLauncher.getWorkspace().computeScrollWithoutInvalidation();
+
int alpha = (int) (mBackgroundAlpha * 255);
+ CellLayout currCellLayout = mLauncher.getWorkspace().getCurrentDragOverlappingLayout();
+ canvas.save();
+ if (currCellLayout != null && currCellLayout != mLauncher.getHotseat().getLayout()) {
+ // Cut a hole in the darkening scrim on the page that should be highlighted, if any.
+ getDescendantRectRelativeToSelf(currCellLayout, mHighlightRect);
+ canvas.clipRect(mHighlightRect, Region.Op.DIFFERENCE);
+ }
canvas.drawColor((alpha << 24) | SCRIM_COLOR);
+ canvas.restore();
}
super.dispatchDraw(canvas);
diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
index fcb714f..de06ab6 100644
--- a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
+++ b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java
@@ -16,6 +16,7 @@
package com.android.launcher3.widget;
import android.appwidget.AppWidgetHostView;
+import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
@@ -37,14 +38,14 @@
public AppWidgetHostView boundWidget;
public Bundle bindOptions = null;
- public PendingAddWidgetInfo(Launcher launcher, LauncherAppWidgetProviderInfo i, Parcelable data) {
+ public PendingAddWidgetInfo(Context context, LauncherAppWidgetProviderInfo i) {
if (i.isCustomWidget) {
itemType = LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
} else {
itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
}
this.info = i;
- user = AppWidgetManagerCompat.getInstance(launcher).getUser(i);
+ user = AppWidgetManagerCompat.getInstance(context).getUser(i);
componentName = i.provider;
previewImage = i.previewImage;
icon = i.icon;
diff --git a/src/com/android/launcher3/widget/WidgetHostViewLoader.java b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
index b47ba84..297505b 100644
--- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java
+++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java
@@ -151,15 +151,15 @@
return true;
}
- public static Bundle getDefaultOptionsForWidget(Launcher launcher, PendingAddWidgetInfo info) {
+ public static Bundle getDefaultOptionsForWidget(Context context, PendingAddWidgetInfo info) {
Bundle options = null;
- Rect rect = new Rect();
if (Utilities.ATLEAST_JB_MR1) {
- AppWidgetResizeFrame.getWidgetSizeRanges(launcher, info.spanX, info.spanY, rect);
- Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(launcher,
+ Rect rect = new Rect();
+ AppWidgetResizeFrame.getWidgetSizeRanges(context, info.spanX, info.spanY, rect);
+ Rect padding = AppWidgetHostView.getDefaultPaddingForWidget(context,
info.componentName, null);
- float density = launcher.getResources().getDisplayMetrics().density;
+ float density = context.getResources().getDisplayMetrics().density;
int xPaddingDips = (int) ((padding.left + padding.right) / density);
int yPaddingDips = (int) ((padding.top + padding.bottom) / density);
diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/WidgetsListAdapter.java
index ac9d62e..5d8adf5 100644
--- a/src/com/android/launcher3/widget/WidgetsListAdapter.java
+++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java
@@ -138,7 +138,7 @@
WidgetCell widget = (WidgetCell) row.getChildAt(i);
if (infoList.get(i) instanceof LauncherAppWidgetProviderInfo) {
LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo) infoList.get(i);
- PendingAddWidgetInfo pawi = new PendingAddWidgetInfo(mLauncher, info, null);
+ PendingAddWidgetInfo pawi = new PendingAddWidgetInfo(mLauncher, info);
widget.setTag(pawi);
widget.applyFromAppWidgetProviderInfo(info, mWidgetPreviewLoader);
} else if (infoList.get(i) instanceof ResolveInfo) {
diff --git a/tests/src/com/android/launcher3/BindWidgetTest.java b/tests/src/com/android/launcher3/BindWidgetTest.java
new file mode 100644
index 0000000..06e1936
--- /dev/null
+++ b/tests/src/com/android/launcher3/BindWidgetTest.java
@@ -0,0 +1,428 @@
+package com.android.launcher3;
+
+import android.annotation.TargetApi;
+import android.app.SearchManager;
+import android.appwidget.AppWidgetHost;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.SessionParams;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiSelector;
+import android.test.InstrumentationTestCase;
+
+import com.android.launcher3.compat.AppWidgetManagerCompat;
+import com.android.launcher3.compat.PackageInstallerCompat;
+import com.android.launcher3.util.ManagedProfileHeuristic;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
+import com.android.launcher3.widget.WidgetHostViewLoader;
+
+import java.io.FileInputStream;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Tests for bind widget flow.
+ *
+ * Note running these tests will clear the workspace on the device.
+ */
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public class BindWidgetTest extends InstrumentationTestCase {
+
+ private static final long DEFAULT_TIMEOUT = 6000;
+
+ private UiDevice mDevice;
+ private Context mTargetContext;
+ 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
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mTargetContext = getInstrumentation().getTargetContext();
+ mResolver = mTargetContext.getContentResolver();
+ mWidgetManager = AppWidgetManagerCompat.getInstance(mTargetContext);
+
+ // Check bind widget permission
+ String pkg = mTargetContext.getPackageName();
+ if (mTargetContext.getPackageManager().checkPermission(
+ pkg, android.Manifest.permission.BIND_APPWIDGET)
+ != PackageManager.PERMISSION_GRANTED) {
+ ParcelFileDescriptor pfd = getInstrumentation().getUiAutomation().executeShellCommand(
+ "appwidget grantbind --package " + pkg);
+ // Read the input stream fully.
+ FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+ while (fis.read() != -1);
+ fis.close();
+ }
+
+ // 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);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ if (mCursor != null) {
+ mCursor.close();
+ }
+
+ if (mSessionId > -1) {
+ mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
+ }
+ }
+
+ public void testBindNormalWidget_withConfig() {
+ LauncherAppWidgetProviderInfo info = findWidgetProvider(true);
+ LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+
+ setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label);
+ }
+
+ public void testBindNormalWidget_withoutConfig() {
+ LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
+ LauncherAppWidgetInfo item = createWidgetInfo(info, true);
+
+ setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label);
+ }
+
+ public void testUnboundWidget_removed() throws Exception {
+ LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
+ LauncherAppWidgetInfo item = createWidgetInfo(info, false);
+ item.appWidgetId = 33;
+
+ // Since there is no widget to verify, just wait until the workspace is ready.
+ setupAndVerifyContents(item, Workspace.class, null);
+
+ waitUntilLoaderIdle();
+ // 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
+ assertFalse(mDevice.findObject(new UiSelector().description(info.label)).exists());
+ }
+
+ public void testPendingWidget_autoRestored() {
+ // A non-restored widget with no config screen gets restored automatically.
+ LauncherAppWidgetProviderInfo info = findWidgetProvider(false);
+
+ // Do not bind the widget
+ LauncherAppWidgetInfo item = createWidgetInfo(info, false);
+ item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
+
+ setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label);
+ }
+
+ public void testPendingWidget_withConfigScreen() throws Exception {
+ // A non-restored widget with config screen get bound and shows a 'Click to setup' UI.
+ LauncherAppWidgetProviderInfo info = findWidgetProvider(true);
+
+ // Do not bind the widget
+ LauncherAppWidgetInfo item = createWidgetInfo(info, false);
+ item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID;
+
+ setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
+ waitUntilLoaderIdle();
+ // 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(mWidgetManager.getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex(
+ LauncherSettings.Favorites.APPWIDGET_ID))));
+ }
+
+ public void testPendingWidget_notRestored_removed() throws Exception {
+ LauncherAppWidgetInfo item = getInvalidWidgetInfo();
+ item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
+ | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
+
+ setupAndVerifyContents(item, Workspace.class, null);
+ // The view does not exist
+ assertFalse(mDevice.findObject(
+ new UiSelector().className(PendingAppWidgetHostView.class)).exists());
+ waitUntilLoaderIdle();
+ // Item deleted from db
+ mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id),
+ null, null, null, null, null);
+ assertEquals(0, mCursor.getCount());
+ }
+
+ public void testPendingWidget_notRestored_brokenInstall() throws Exception {
+ // 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;
+
+ setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
+ // Verify item still exists in db
+ waitUntilLoaderIdle();
+ 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);
+ }
+
+ 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);
+
+ setupAndVerifyContents(item, PendingAppWidgetHostView.class, null);
+ // Verify item still exists in db
+ waitUntilLoaderIdle();
+ 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.
+ * @param widgetClass the View class which is displayed on the homescreen
+ * @param desc the content description of the view or null.
+ */
+ private void setupAndVerifyContents(
+ LauncherAppWidgetInfo item, Class<?> widgetClass, String desc) {
+ // Add new screen
+ long screenId = LauncherSettings.Settings.call(
+ mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID)
+ .getLong(LauncherSettings.Settings.EXTRA_VALUE);
+ ContentValues v = new ContentValues();
+ v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
+ v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, 0);
+ mResolver.insert(LauncherSettings.WorkspaceScreens.CONTENT_URI, v);
+
+ // Insert the item
+ v = new ContentValues();
+ item.id = LauncherSettings.Settings.call(
+ mResolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID)
+ .getLong(LauncherSettings.Settings.EXTRA_VALUE);
+ item.screenId = screenId;
+ item.onAddToDatabase(mTargetContext, v);
+ v.put(LauncherSettings.Favorites._ID, item.id);
+ mResolver.insert(LauncherSettings.Favorites.CONTENT_URI, v);
+
+ // Reset loader
+ try {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mTargetContext);
+ LauncherAppState.getInstance().getModel().resetLoadedState(true, true);
+ }
+ });
+ } catch (Throwable t) {
+ throw new IllegalArgumentException(t);
+ }
+ // Launch the home activity
+ getInstrumentation().getContext().startActivity(new Intent(Intent.ACTION_MAIN)
+ .addCategory(Intent.CATEGORY_HOME)
+ .setPackage(mTargetContext.getPackageName())
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+
+ // Verify UI
+ UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName())
+ .className(widgetClass);
+ if (desc != null) {
+ selector = selector.description(desc);
+ }
+ assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_TIMEOUT));
+ }
+
+ /**
+ * Finds a widget provider which can fit on the home screen.
+ * @param hasConfigureScreen if true, a provider with a config screen is returned.
+ */
+ private LauncherAppWidgetProviderInfo findWidgetProvider(final boolean hasConfigureScreen) {
+ LauncherAppWidgetProviderInfo info = getOnUiThread(new Callable<LauncherAppWidgetProviderInfo>() {
+ @Override
+ public LauncherAppWidgetProviderInfo call() throws Exception {
+ InvariantDeviceProfile idv =
+ LauncherAppState.getInstance().getInvariantDeviceProfile();
+
+ ComponentName searchComponent = ((SearchManager) mTargetContext
+ .getSystemService(Context.SEARCH_SERVICE)).getGlobalSearchActivity();
+ String searchPackage = searchComponent == null
+ ? null : searchComponent.getPackageName();
+
+ for (AppWidgetProviderInfo info :
+ AppWidgetManagerCompat.getInstance(mTargetContext).getAllProviders()) {
+ if ((info.configure != null) ^ hasConfigureScreen) {
+ continue;
+ }
+ // Exclude the widgets in search package, as Launcher already binds them in
+ // QSB, so they can cause conflicts.
+ if (info.provider.getPackageName().equals(searchPackage)) {
+ continue;
+ }
+ LauncherAppWidgetProviderInfo widgetInfo = LauncherAppWidgetProviderInfo
+ .fromProviderInfo(mTargetContext, info);
+ if (widgetInfo.minSpanX >= idv.numColumns
+ || widgetInfo.minSpanY >= idv.numRows) {
+ continue;
+ }
+ return widgetInfo;
+ }
+ return null;
+ }
+ });
+ if (info == null) {
+ throw new IllegalArgumentException("No valid widget provider");
+ }
+ return info;
+ }
+
+ /**
+ * 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 = mWidgetManager.getUser(info);
+ item.cellX = 0;
+ item.cellY = 0;
+ item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+
+ if (bindWidget) {
+ PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(mTargetContext, 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 AppWidgetHost(mTargetContext, Launcher.APPWIDGET_HOST_ID);
+ 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(new Callable<Set<String>>() {
+ @Override
+ public Set<String> call() throws Exception {
+ return 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 = 0;
+ item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP;
+ return item;
+ }
+
+ /**
+ * Runs the callback on the UI thread and returns the result.
+ */
+ private <T> T getOnUiThread(final Callable<T> callback) {
+ final AtomicReference<T> result = new AtomicReference<>(null);
+ try {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ result.set(callback.call());
+ } catch (Exception e) { }
+ }
+ });
+ } catch (Throwable t) { }
+ return result.get();
+ }
+
+ /**
+ * Blocks the current thread until all the jobs in the main worker thread are complete.
+ */
+ private void waitUntilLoaderIdle() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ LauncherModel.sWorker.post(new Runnable() {
+ @Override
+ public void run() {
+ latch.countDown();
+ }
+ });
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+}