diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index 0ba94995b1..daeca3bef0 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -363,7 +363,7 @@ public class AppWidgetResizeFrame extends FrameLayout implements View.OnKeyListe 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 @@ public class AppWidgetResizeFrame extends FrameLayout implements View.OnKeyListe 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/Launcher.java b/src/com/android/launcher3/Launcher.java index e411527522..245b399098 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -4013,7 +4013,7 @@ public class Launcher extends Activity // 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 c789082737..570607ec7e 100644 --- a/src/com/android/launcher3/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/LauncherAppWidgetHostView.java @@ -27,6 +27,7 @@ import android.view.View; 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 @@ public class LauncherAppWidgetHostView extends AppWidgetHostView implements Touc }); } } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(getClass().getName()); + } } diff --git a/src/com/android/launcher3/widget/PendingAddWidgetInfo.java b/src/com/android/launcher3/widget/PendingAddWidgetInfo.java index fcb714ff19..de06ab664a 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 class PendingAddWidgetInfo extends PendingAddItemInfo { 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 b47ba84fc4..297505be2f 100644 --- a/src/com/android/launcher3/widget/WidgetHostViewLoader.java +++ b/src/com/android/launcher3/widget/WidgetHostViewLoader.java @@ -151,15 +151,15 @@ public class WidgetHostViewLoader implements DragController.DragListener { 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 ac9d62e9a0..5d8adf504c 100644 --- a/src/com/android/launcher3/widget/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/WidgetsListAdapter.java @@ -138,7 +138,7 @@ public class WidgetsListAdapter extends Adapter { 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 0000000000..06e193640a --- /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() { + @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 activePackage = getOnUiThread(new Callable>() { + @Override + public Set 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 getOnUiThread(final Callable callback) { + final AtomicReference 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)); + } +}