1/* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16package com.android.launcher3.ui.widget; 17 18import android.appwidget.AppWidgetHost; 19import android.appwidget.AppWidgetManager; 20import android.content.ComponentName; 21import android.content.ContentResolver; 22import android.content.ContentValues; 23import android.content.pm.PackageInstaller; 24import android.content.pm.PackageInstaller.SessionParams; 25import android.content.pm.PackageManager; 26import android.database.Cursor; 27import android.os.Bundle; 28import android.support.test.filters.LargeTest; 29import android.support.test.runner.AndroidJUnit4; 30import android.support.test.uiautomator.UiSelector; 31 32import com.android.launcher3.LauncherAppWidgetHost; 33import com.android.launcher3.widget.LauncherAppWidgetHostView; 34import com.android.launcher3.LauncherAppWidgetInfo; 35import com.android.launcher3.LauncherAppWidgetProviderInfo; 36import com.android.launcher3.LauncherModel; 37import com.android.launcher3.LauncherSettings; 38import com.android.launcher3.widget.PendingAppWidgetHostView; 39import com.android.launcher3.Workspace; 40import com.android.launcher3.compat.AppWidgetManagerCompat; 41import com.android.launcher3.compat.PackageInstallerCompat; 42import com.android.launcher3.ui.AbstractLauncherUiTest; 43import com.android.launcher3.util.ContentWriter; 44import com.android.launcher3.util.LooperExecutor; 45import com.android.launcher3.util.rule.LauncherActivityRule; 46import com.android.launcher3.util.rule.ShellCommandRule; 47import com.android.launcher3.widget.PendingAddWidgetInfo; 48import com.android.launcher3.widget.WidgetHostViewLoader; 49 50import org.junit.After; 51import org.junit.Before; 52import org.junit.Rule; 53import org.junit.Test; 54import org.junit.runner.RunWith; 55 56import java.util.Set; 57import java.util.concurrent.Callable; 58import java.util.concurrent.TimeUnit; 59 60import static org.junit.Assert.assertEquals; 61import static org.junit.Assert.assertFalse; 62import static org.junit.Assert.assertNotNull; 63import static org.junit.Assert.assertTrue; 64 65/** 66 * Tests for bind widget flow. 67 * 68 * Note running these tests will clear the workspace on the device. 69 */ 70@LargeTest 71@RunWith(AndroidJUnit4.class) 72public class BindWidgetTest extends AbstractLauncherUiTest { 73 74 @Rule public LauncherActivityRule mActivityMonitor = new LauncherActivityRule(); 75 @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grandWidgetBind(); 76 77 private ContentResolver mResolver; 78 private AppWidgetManagerCompat mWidgetManager; 79 80 // Objects created during test, which should be cleaned up in the end. 81 private Cursor mCursor; 82 // App install session id. 83 private int mSessionId = -1; 84 85 @Override 86 @Before 87 public void setUp() throws Exception { 88 super.setUp(); 89 90 mResolver = mTargetContext.getContentResolver(); 91 mWidgetManager = AppWidgetManagerCompat.getInstance(mTargetContext); 92 93 // Clear all existing data 94 LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); 95 LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG); 96 } 97 98 @After 99 public void tearDown() throws Exception { 100 if (mCursor != null) { 101 mCursor.close(); 102 } 103 104 if (mSessionId > -1) { 105 mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId); 106 } 107 } 108 109 @Test 110 public void testBindNormalWidget_withConfig() { 111 LauncherAppWidgetProviderInfo info = findWidgetProvider(true); 112 LauncherAppWidgetInfo item = createWidgetInfo(info, true); 113 114 setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label); 115 } 116 117 @Test 118 public void testBindNormalWidget_withoutConfig() { 119 LauncherAppWidgetProviderInfo info = findWidgetProvider(false); 120 LauncherAppWidgetInfo item = createWidgetInfo(info, true); 121 122 setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label); 123 } 124 125 @Test 126 public void testUnboundWidget_removed() throws Exception { 127 LauncherAppWidgetProviderInfo info = findWidgetProvider(false); 128 LauncherAppWidgetInfo item = createWidgetInfo(info, false); 129 item.appWidgetId = -33; 130 131 // Since there is no widget to verify, just wait until the workspace is ready. 132 setupAndVerifyContents(item, Workspace.class, null); 133 134 waitUntilLoaderIdle(); 135 // Item deleted from db 136 mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), 137 null, null, null, null, null); 138 assertEquals(0, mCursor.getCount()); 139 140 // The view does not exist 141 assertFalse(mDevice.findObject(new UiSelector().description(info.label)).exists()); 142 } 143 144 @Test 145 public void testPendingWidget_autoRestored() { 146 // A non-restored widget with no config screen gets restored automatically. 147 LauncherAppWidgetProviderInfo info = findWidgetProvider(false); 148 149 // Do not bind the widget 150 LauncherAppWidgetInfo item = createWidgetInfo(info, false); 151 item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID; 152 153 setupAndVerifyContents(item, LauncherAppWidgetHostView.class, info.label); 154 } 155 156 @Test 157 public void testPendingWidget_withConfigScreen() throws Exception { 158 // A non-restored widget with config screen get bound and shows a 'Click to setup' UI. 159 LauncherAppWidgetProviderInfo info = findWidgetProvider(true); 160 161 // Do not bind the widget 162 LauncherAppWidgetInfo item = createWidgetInfo(info, false); 163 item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID; 164 165 setupAndVerifyContents(item, PendingAppWidgetHostView.class, null); 166 waitUntilLoaderIdle(); 167 // Item deleted from db 168 mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), 169 null, null, null, null, null); 170 mCursor.moveToNext(); 171 172 // Widget has a valid Id now. 173 assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED)) 174 & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID); 175 assertNotNull(AppWidgetManager.getInstance(mTargetContext) 176 .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex( 177 LauncherSettings.Favorites.APPWIDGET_ID)))); 178 } 179 180 @Test 181 public void testPendingWidget_notRestored_removed() throws Exception { 182 LauncherAppWidgetInfo item = getInvalidWidgetInfo(); 183 item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID 184 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; 185 186 setupAndVerifyContents(item, Workspace.class, null); 187 // The view does not exist 188 assertFalse(mDevice.findObject( 189 new UiSelector().className(PendingAppWidgetHostView.class)).exists()); 190 waitUntilLoaderIdle(); 191 // Item deleted from db 192 mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), 193 null, null, null, null, null); 194 assertEquals(0, mCursor.getCount()); 195 } 196 197 @Test 198 public void testPendingWidget_notRestored_brokenInstall() throws Exception { 199 // A widget which is was being installed once, even if its not being 200 // installed at the moment is not removed. 201 LauncherAppWidgetInfo item = getInvalidWidgetInfo(); 202 item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID 203 | LauncherAppWidgetInfo.FLAG_RESTORE_STARTED 204 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; 205 206 setupAndVerifyContents(item, PendingAppWidgetHostView.class, null); 207 // Verify item still exists in db 208 waitUntilLoaderIdle(); 209 mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), 210 null, null, null, null, null); 211 assertEquals(1, mCursor.getCount()); 212 213 // Widget still has an invalid id. 214 mCursor.moveToNext(); 215 assertEquals(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID, 216 mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED)) 217 & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID); 218 } 219 220 @Test 221 public void testPendingWidget_notRestored_activeInstall() throws Exception { 222 // A widget which is being installed is not removed 223 LauncherAppWidgetInfo item = getInvalidWidgetInfo(); 224 item.restoreStatus = LauncherAppWidgetInfo.FLAG_ID_NOT_VALID 225 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; 226 227 // Create an active installer session 228 SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL); 229 params.setAppPackageName(item.providerName.getPackageName()); 230 PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller(); 231 mSessionId = installer.createSession(params); 232 233 setupAndVerifyContents(item, PendingAppWidgetHostView.class, null); 234 // Verify item still exists in db 235 waitUntilLoaderIdle(); 236 mCursor = mResolver.query(LauncherSettings.Favorites.getContentUri(item.id), 237 null, null, null, null, null); 238 assertEquals(1, mCursor.getCount()); 239 240 // Widget still has an invalid id. 241 mCursor.moveToNext(); 242 assertEquals(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID, 243 mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED)) 244 & LauncherAppWidgetInfo.FLAG_ID_NOT_VALID); 245 } 246 247 /** 248 * Adds {@param item} on the homescreen on the 0th screen at 0,0, and verifies that the 249 * widget class is displayed on the homescreen. 250 * @param widgetClass the View class which is displayed on the homescreen 251 * @param desc the content description of the view or null. 252 */ 253 private void setupAndVerifyContents( 254 LauncherAppWidgetInfo item, Class<?> widgetClass, String desc) { 255 long screenId = Workspace.FIRST_SCREEN_ID; 256 // Update the screen id counter for the provider. 257 LauncherSettings.Settings.call(mResolver, LauncherSettings.Settings.METHOD_NEW_SCREEN_ID); 258 259 if (screenId > Workspace.FIRST_SCREEN_ID) { 260 screenId = Workspace.FIRST_SCREEN_ID; 261 } 262 ContentValues v = new ContentValues(); 263 v.put(LauncherSettings.WorkspaceScreens._ID, screenId); 264 v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, 0); 265 mResolver.insert(LauncherSettings.WorkspaceScreens.CONTENT_URI, v); 266 267 // Insert the item 268 ContentWriter writer = new ContentWriter(mTargetContext); 269 item.id = LauncherSettings.Settings.call( 270 mResolver, LauncherSettings.Settings.METHOD_NEW_ITEM_ID) 271 .getLong(LauncherSettings.Settings.EXTRA_VALUE); 272 item.screenId = screenId; 273 item.onAddToDatabase(writer); 274 writer.put(LauncherSettings.Favorites._ID, item.id); 275 mResolver.insert(LauncherSettings.Favorites.CONTENT_URI, writer.getValues(mTargetContext)); 276 resetLoaderState(); 277 278 // Launch the home activity 279 mActivityMonitor.startLauncher(); 280 // Verify UI 281 UiSelector selector = new UiSelector().packageName(mTargetContext.getPackageName()) 282 .className(widgetClass); 283 if (desc != null) { 284 selector = selector.description(desc); 285 } 286 assertTrue(mDevice.findObject(selector).waitForExists(DEFAULT_UI_TIMEOUT)); 287 } 288 289 /** 290 * Creates a LauncherAppWidgetInfo corresponding to {@param info} 291 * @param bindWidget if true the info is bound and a valid widgetId is assigned to 292 * the LauncherAppWidgetInfo 293 */ 294 private LauncherAppWidgetInfo createWidgetInfo( 295 LauncherAppWidgetProviderInfo info, boolean bindWidget) { 296 LauncherAppWidgetInfo item = new LauncherAppWidgetInfo( 297 LauncherAppWidgetInfo.NO_ID, info.provider); 298 item.spanX = info.minSpanX; 299 item.spanY = info.minSpanY; 300 item.minSpanX = info.minSpanX; 301 item.minSpanY = info.minSpanY; 302 item.user = info.getProfile(); 303 item.cellX = 0; 304 item.cellY = 1; 305 item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; 306 307 if (bindWidget) { 308 PendingAddWidgetInfo pendingInfo = new PendingAddWidgetInfo(info); 309 pendingInfo.spanX = item.spanX; 310 pendingInfo.spanY = item.spanY; 311 pendingInfo.minSpanX = item.minSpanX; 312 pendingInfo.minSpanY = item.minSpanY; 313 Bundle options = WidgetHostViewLoader.getDefaultOptionsForWidget(mTargetContext, pendingInfo); 314 315 AppWidgetHost host = new LauncherAppWidgetHost(mTargetContext); 316 int widgetId = host.allocateAppWidgetId(); 317 if (!mWidgetManager.bindAppWidgetIdIfAllowed(widgetId, info, options)) { 318 host.deleteAppWidgetId(widgetId); 319 throw new IllegalArgumentException("Unable to bind widget id"); 320 } 321 item.appWidgetId = widgetId; 322 } 323 return item; 324 } 325 326 /** 327 * Returns a LauncherAppWidgetInfo with package name which is not present on the device 328 */ 329 private LauncherAppWidgetInfo getInvalidWidgetInfo() { 330 String invalidPackage = "com.invalidpackage"; 331 int count = 0; 332 String pkg = invalidPackage; 333 334 Set<String> activePackage = getOnUiThread(new Callable<Set<String>>() { 335 @Override 336 public Set<String> call() throws Exception { 337 return PackageInstallerCompat.getInstance(mTargetContext) 338 .updateAndGetActiveSessionCache().keySet(); 339 } 340 }); 341 while(true) { 342 try { 343 mTargetContext.getPackageManager().getPackageInfo( 344 pkg, PackageManager.GET_UNINSTALLED_PACKAGES); 345 } catch (Exception e) { 346 if (!activePackage.contains(pkg)) { 347 break; 348 } 349 } 350 pkg = invalidPackage + count; 351 count ++; 352 } 353 LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(10, 354 new ComponentName(pkg, "com.test.widgetprovider")); 355 item.spanX = 2; 356 item.spanY = 2; 357 item.minSpanX = 2; 358 item.minSpanY = 2; 359 item.cellX = 0; 360 item.cellY = 1; 361 item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; 362 return item; 363 } 364 365 /** 366 * Blocks the current thread until all the jobs in the main worker thread are complete. 367 */ 368 private void waitUntilLoaderIdle() throws Exception { 369 new LooperExecutor(LauncherModel.getWorkerLooper()) 370 .submit(new Runnable() { 371 @Override 372 public void run() { } 373 }).get(DEFAULT_WORKER_TIMEOUT_SECS, TimeUnit.SECONDS); 374 } 375} 376