| /* |
| * 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.launcher3.ui.widget; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| 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.ContentValues; |
| 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 androidx.test.uiautomator.UiSelector; |
| |
| import com.android.launcher3.LauncherAppWidgetHost; |
| import com.android.launcher3.LauncherAppWidgetInfo; |
| import com.android.launcher3.LauncherAppWidgetProviderInfo; |
| import com.android.launcher3.LauncherModel; |
| import com.android.launcher3.LauncherSettings; |
| import com.android.launcher3.Workspace; |
| import com.android.launcher3.compat.AppWidgetManagerCompat; |
| import com.android.launcher3.compat.PackageInstallerCompat; |
| import com.android.launcher3.ui.AbstractLauncherUiTest; |
| import com.android.launcher3.util.ContentWriter; |
| import com.android.launcher3.util.LooperExecutor; |
| import com.android.launcher3.util.rule.ShellCommandRule; |
| import com.android.launcher3.widget.LauncherAppWidgetHostView; |
| import com.android.launcher3.widget.PendingAddWidgetInfo; |
| import com.android.launcher3.widget.PendingAppWidgetHostView; |
| import com.android.launcher3.widget.WidgetHostViewLoader; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Ignore; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.util.Set; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * 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.grandWidgetBind(); |
| |
| 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 { |
| if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS |
| && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { |
| android.util.Log.d("b/117332845", |
| android.util.Log.getStackTraceString(new Throwable())); |
| } |
| 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() throws Exception { |
| if (mCursor != null) { |
| mCursor.close(); |
| } |
| |
| if (mSessionId > -1) { |
| mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId); |
| } |
| |
| super.tearDown(); |
| if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS |
| && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { |
| android.util.Log.d("b/117332845", |
| android.util.Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| @Test |
| public void testBindNormalWidget_withConfig() { |
| LauncherAppWidgetProviderInfo info = findWidgetProvider(true); |
| LauncherAppWidgetInfo item = createWidgetInfo(info, true); |
| |
| setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label); |
| } |
| |
| @Test |
| public void testBindNormalWidget_withoutConfig() { |
| LauncherAppWidgetProviderInfo info = findWidgetProvider(false); |
| LauncherAppWidgetInfo item = createWidgetInfo(info, true); |
| |
| setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label); |
| } |
| |
| @Test @Ignore |
| 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()); |
| } |
| |
| @Test |
| public void testPendingWidget_autoRestored() { |
| if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { |
| android.util.Log.d("b/117332845", |
| "Test Started @ " + android.util.Log.getStackTraceString(new Throwable())); |
| } |
| // 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); |
| if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS |
| && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { |
| android.util.Log.d("b/117332845", |
| "Test Ended @ " + android.util.Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| @Test |
| public void testPendingWidget_withConfigScreen() throws Exception { |
| if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS |
| && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { |
| android.util.Log.d("b/117332845", |
| "Test Started @ " + android.util.Log.getStackTraceString(new Throwable())); |
| } |
| // 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(AppWidgetManager.getInstance(mTargetContext) |
| .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex( |
| LauncherSettings.Favorites.APPWIDGET_ID)))); |
| if (com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS |
| && com.android.launcher3.Utilities.IS_DEBUG_DEVICE) { |
| android.util.Log.d("b/117332845", |
| "Test Ended @ " + android.util.Log.getStackTraceString(new Throwable())); |
| } |
| } |
| |
| @Test @Ignore |
| 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()); |
| } |
| |
| @Test |
| 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); |
| } |
| |
| @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); |
| |
| 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) { |
| long screenId = Workspace.FIRST_SCREEN_ID; |
| // Update the screen id counter for the provider. |
| LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID); |
| |
| if (screenId > Workspace.FIRST_SCREEN_ID) { |
| screenId = Workspace.FIRST_SCREEN_ID; |
| } |
| 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 |
| ContentWriter writer = new ContentWriter(mTargetContext); |
| item.id = LauncherSettings.Settings.call( |
| mResolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID) |
| .getLong(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(); |
| // Verify UI |
| UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName()) |
| .className(widgetClass); |
| if (desc != null) { |
| selector = selector.description(desc); |
| } |
| assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT)); |
| } |
| |
| /** |
| * 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(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 = 1; |
| item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; |
| return item; |
| } |
| |
| /** |
| * Blocks the current thread until all the jobs in the main worker thread are complete. |
| */ |
| private void waitUntilLoaderIdle() throws Exception { |
| new LooperExecutor(LauncherModel.getWorkerLooper()) |
| .submit(new Runnable() { |
| @Override |
| public void run() { } |
| }).get(DEFAULT_WORKER_TIMEOUT_SECS, TimeUnit.SECONDS); |
| } |
| } |