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