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 */ 16 package com.android.launcher3.ui.widget; 17 18 import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME; 19 import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID; 20 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_ID_NOT_VALID; 21 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; 22 import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; 23 import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch; 24 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 25 import static com.android.launcher3.util.WidgetUtils.createWidgetInfo; 26 import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; 27 import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; 28 29 import static org.junit.Assert.assertEquals; 30 import static org.junit.Assert.assertNotNull; 31 import static org.junit.Assert.assertNull; 32 import static org.junit.Assert.assertTrue; 33 34 import android.appwidget.AppWidgetManager; 35 import android.content.ComponentName; 36 import android.content.pm.PackageInstaller; 37 import android.content.pm.PackageInstaller.SessionParams; 38 import android.content.pm.PackageManager; 39 import android.database.Cursor; 40 import android.os.Bundle; 41 import android.widget.RemoteViews; 42 43 import androidx.test.ext.junit.runners.AndroidJUnit4; 44 import androidx.test.filters.LargeTest; 45 46 import com.android.launcher3.Launcher; 47 import com.android.launcher3.LauncherAppState; 48 import com.android.launcher3.LauncherModel; 49 import com.android.launcher3.LauncherSettings; 50 import com.android.launcher3.R; 51 import com.android.launcher3.celllayout.FavoriteItemsTransaction; 52 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 53 import com.android.launcher3.pm.InstallSessionHelper; 54 import com.android.launcher3.tapl.Widget; 55 import com.android.launcher3.tapl.Workspace; 56 import com.android.launcher3.ui.AbstractLauncherUiTest; 57 import com.android.launcher3.ui.TestViewHelpers; 58 import com.android.launcher3.util.rule.ShellCommandRule; 59 import com.android.launcher3.util.rule.TestStabilityRule; 60 import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; 61 import com.android.launcher3.widget.WidgetManagerHelper; 62 63 import org.junit.After; 64 import org.junit.Before; 65 import org.junit.Rule; 66 import org.junit.Test; 67 import org.junit.runner.RunWith; 68 69 import java.util.HashSet; 70 import java.util.Set; 71 import java.util.function.Consumer; 72 73 /** 74 * Tests for bind widget flow. 75 * 76 * Note running these tests will clear the workspace on the device. 77 */ 78 @LargeTest 79 @RunWith(AndroidJUnit4.class) 80 public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> { 81 82 @Rule 83 public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind(); 84 85 // Objects created during test, which should be cleaned up in the end. 86 private Cursor mCursor; 87 // App install session id. 88 private int mSessionId = -1; 89 90 private LauncherModel mModel; 91 92 @Override 93 @Before setUp()94 public void setUp() throws Exception { 95 super.setUp(); 96 mModel = LauncherAppState.getInstance(mTargetContext).getModel(); 97 } 98 99 @After tearDown()100 public void tearDown() { 101 if (mCursor != null) { 102 mCursor.close(); 103 } 104 105 if (mSessionId > -1) { 106 mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId); 107 } 108 } 109 110 @Test testBindNormalWidget_withConfig()111 public void testBindNormalWidget_withConfig() { 112 LauncherAppWidgetProviderInfo info = addWidgetToScreen(true, true, i -> { }); 113 verifyWidgetPresent(info); 114 } 115 116 @Test testBindNormalWidget_withoutConfig()117 public void testBindNormalWidget_withoutConfig() { 118 LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, true, i -> { }); 119 verifyWidgetPresent(info); 120 } 121 122 @Test testUnboundWidget_removed()123 public void testUnboundWidget_removed() { 124 LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false, 125 item -> item.appWidgetId = -33); 126 127 final Workspace workspace = mLauncher.getWorkspace(); 128 // Item deleted from db 129 mCursor = queryItem(); 130 assertEquals(0, mCursor.getCount()); 131 132 // The view does not exist 133 assertTrue("Widget exists", workspace.tryGetWidget(info.label, 0) == null); 134 } 135 136 @Test testPendingWidget_autoRestored()137 public void testPendingWidget_autoRestored() { 138 // A non-restored widget with no config screen gets restored automatically. 139 // Do not bind the widget 140 LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false, 141 item -> item.restoreStatus = FLAG_ID_NOT_VALID); 142 verifyWidgetPresent(info); 143 } 144 145 @Test 146 @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/310242894 testPendingWidget_withConfigScreen()147 public void testPendingWidget_withConfigScreen() { 148 // A non-restored widget with config screen get bound and shows a 'Click to setup' UI. 149 // Do not bind the widget 150 LauncherAppWidgetProviderInfo info = addWidgetToScreen(true, false, 151 item -> item.restoreStatus = FLAG_ID_NOT_VALID); 152 verifyPendingWidgetPresent(); 153 154 mCursor = queryItem(); 155 mCursor.moveToNext(); 156 157 // Widget has a valid Id now. 158 assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED)) 159 & FLAG_ID_NOT_VALID); 160 assertNotNull(AppWidgetManager.getInstance(mTargetContext) 161 .getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex( 162 LauncherSettings.Favorites.APPWIDGET_ID)))); 163 164 // send OPTION_APPWIDGET_RESTORE_COMPLETED 165 int appWidgetId = mCursor.getInt( 166 mCursor.getColumnIndex(LauncherSettings.Favorites.APPWIDGET_ID)); 167 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mTargetContext); 168 169 Bundle b = new Bundle(); 170 b.putBoolean(WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED, true); 171 RemoteViews remoteViews = new RemoteViews(mTargetPackage, R.layout.appwidget_not_ready); 172 appWidgetManager.updateAppWidgetOptions(appWidgetId, b); 173 appWidgetManager.updateAppWidget(appWidgetId, remoteViews); 174 175 // verify changes are reflected 176 waitForLauncherCondition("App widget options did not update", 177 l -> appWidgetManager.getAppWidgetOptions(appWidgetId).getBoolean( 178 WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED)); 179 executeOnLauncher(l -> l.getAppWidgetHolder().startListening()); 180 verifyWidgetPresent(info); 181 assertNull(mLauncher.getWorkspace().tryGetPendingWidget(100)); 182 } 183 184 @Test testPendingWidget_notRestored_removed()185 public void testPendingWidget_notRestored_removed() { 186 addPendingItemToScreen(getInvalidWidgetInfo(), FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY); 187 188 assertTrue("Pending widget exists", 189 mLauncher.getWorkspace().tryGetPendingWidget(0) == null); 190 // Item deleted from db 191 mCursor = queryItem(); 192 assertEquals(0, mCursor.getCount()); 193 } 194 195 @Test 196 @TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/310242894 testPendingWidget_notRestored_brokenInstall()197 public void testPendingWidget_notRestored_brokenInstall() { 198 // A widget which is was being installed once, even if its not being 199 // installed at the moment is not removed. 200 addPendingItemToScreen(getInvalidWidgetInfo(), 201 FLAG_ID_NOT_VALID | FLAG_RESTORE_STARTED | FLAG_PROVIDER_NOT_READY); 202 verifyPendingWidgetPresent(); 203 204 // Verify item still exists in db 205 mCursor = queryItem(); 206 assertEquals(1, mCursor.getCount()); 207 208 // Widget still has an invalid id. 209 mCursor.moveToNext(); 210 assertEquals(FLAG_ID_NOT_VALID, 211 mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED)) 212 & FLAG_ID_NOT_VALID); 213 } 214 215 @Test testPendingWidget_notRestored_activeInstall()216 public void testPendingWidget_notRestored_activeInstall() throws Exception { 217 // A widget which is being installed is not removed 218 LauncherAppWidgetInfo item = getInvalidWidgetInfo(); 219 220 // Create an active installer session 221 SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL); 222 params.setAppPackageName(item.providerName.getPackageName()); 223 PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller(); 224 mSessionId = installer.createSession(params); 225 226 addPendingItemToScreen(item, FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY); 227 verifyPendingWidgetPresent(); 228 229 // Verify item still exists in db 230 mCursor = queryItem(); 231 assertEquals(1, mCursor.getCount()); 232 233 // Widget still has an invalid id. 234 mCursor.moveToNext(); 235 assertEquals(FLAG_ID_NOT_VALID, 236 mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED)) 237 & FLAG_ID_NOT_VALID); 238 } 239 verifyWidgetPresent(LauncherAppWidgetProviderInfo info)240 private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) { 241 final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label, DEFAULT_UI_TIMEOUT); 242 assertTrue("Widget is not present", 243 widget != null); 244 } 245 verifyPendingWidgetPresent()246 private void verifyPendingWidgetPresent() { 247 final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(DEFAULT_UI_TIMEOUT); 248 assertTrue("Pending widget is not present", 249 widget != null); 250 } 251 addPendingItemToScreen(LauncherAppWidgetInfo item, int restoreStatus)252 private void addPendingItemToScreen(LauncherAppWidgetInfo item, int restoreStatus) { 253 item.restoreStatus = restoreStatus; 254 item.screenId = FIRST_SCREEN_ID; 255 commitTransactionAndLoadHome( 256 new FavoriteItemsTransaction(mTargetContext).addItem(() -> item)); 257 } 258 addWidgetToScreen(boolean hasConfigureScreen, boolean bindWidget, Consumer<LauncherAppWidgetInfo> itemOverride)259 private LauncherAppWidgetProviderInfo addWidgetToScreen(boolean hasConfigureScreen, 260 boolean bindWidget, Consumer<LauncherAppWidgetInfo> itemOverride) { 261 LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(hasConfigureScreen); 262 commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext) 263 .addItem(() -> { 264 LauncherAppWidgetInfo item = createWidgetInfo(info, mTargetContext, bindWidget); 265 item.screenId = FIRST_SCREEN_ID; 266 itemOverride.accept(item); 267 return item; 268 })); 269 return info; 270 } 271 272 /** 273 * Returns a LauncherAppWidgetInfo with package name which is not present on the device 274 */ getInvalidWidgetInfo()275 private LauncherAppWidgetInfo getInvalidWidgetInfo() { 276 String invalidPackage = "com.invalidpackage"; 277 int count = 0; 278 String pkg = invalidPackage; 279 280 Set<String> activePackage = getOnUiThread(() -> { 281 Set<String> packages = new HashSet<>(); 282 InstallSessionHelper.INSTANCE.get(mTargetContext).getActiveSessions() 283 .keySet().forEach(packageUserKey -> packages.add(packageUserKey.mPackageName)); 284 return packages; 285 }); 286 while (true) { 287 try { 288 mTargetContext.getPackageManager().getPackageInfo( 289 pkg, PackageManager.GET_UNINSTALLED_PACKAGES); 290 } catch (Exception e) { 291 if (!activePackage.contains(pkg)) { 292 break; 293 } 294 } 295 pkg = invalidPackage + count; 296 count++; 297 } 298 LauncherAppWidgetInfo item = new LauncherAppWidgetInfo(10, 299 new ComponentName(pkg, "com.test.widgetprovider")); 300 item.spanX = 2; 301 item.spanY = 2; 302 item.minSpanX = 2; 303 item.minSpanY = 2; 304 item.cellX = 0; 305 item.cellY = 1; 306 item.container = LauncherSettings.Favorites.CONTAINER_DESKTOP; 307 return item; 308 } 309 queryItem()310 private Cursor queryItem() { 311 try { 312 return MODEL_EXECUTOR.submit(() -> 313 mModel.getModelDbController().query( 314 TABLE_NAME, null, itemIdMatch(0), null, null)).get(); 315 } catch (Exception e) { 316 throw new RuntimeException(e); 317 } 318 } 319 } 320