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