1 /* 2 * Copyright (C) 2019 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.util; 17 18 import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 20 21 import static com.android.launcher3.LauncherSettings.Favorites.CONTENT_URI; 22 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 23 24 import static org.mockito.ArgumentMatchers.anyInt; 25 import static org.mockito.ArgumentMatchers.eq; 26 import static org.mockito.Mockito.atLeast; 27 import static org.mockito.Mockito.doReturn; 28 import static org.mockito.Mockito.mock; 29 import static org.mockito.Mockito.spy; 30 import static org.mockito.Mockito.verify; 31 32 import android.content.ComponentName; 33 import android.content.ContentProvider; 34 import android.content.ContentResolver; 35 import android.content.ContentValues; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.pm.PackageManager; 39 import android.content.pm.ProviderInfo; 40 import android.content.res.Resources; 41 import android.database.sqlite.SQLiteDatabase; 42 import android.net.Uri; 43 import android.os.ParcelFileDescriptor; 44 import android.os.ParcelFileDescriptor.AutoCloseOutputStream; 45 import android.os.Process; 46 import android.provider.Settings; 47 import android.test.mock.MockContentResolver; 48 import android.util.ArrayMap; 49 50 import androidx.annotation.NonNull; 51 import androidx.test.core.app.ApplicationProvider; 52 import androidx.test.uiautomator.UiDevice; 53 54 import com.android.launcher3.InvariantDeviceProfile; 55 import com.android.launcher3.LauncherAppState; 56 import com.android.launcher3.LauncherModel; 57 import com.android.launcher3.LauncherModel.ModelUpdateTask; 58 import com.android.launcher3.LauncherPrefs; 59 import com.android.launcher3.LauncherProvider; 60 import com.android.launcher3.LauncherSettings; 61 import com.android.launcher3.model.AllAppsList; 62 import com.android.launcher3.model.BgDataModel; 63 import com.android.launcher3.model.BgDataModel.Callbacks; 64 import com.android.launcher3.model.ItemInstallQueue; 65 import com.android.launcher3.model.data.AppInfo; 66 import com.android.launcher3.model.data.ItemInfo; 67 import com.android.launcher3.pm.InstallSessionHelper; 68 import com.android.launcher3.pm.UserCache; 69 import com.android.launcher3.testing.TestInformationProvider; 70 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; 71 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; 72 import com.android.launcher3.util.window.WindowManagerProxy; 73 import com.android.launcher3.widget.custom.CustomWidgetManager; 74 75 import org.mockito.ArgumentCaptor; 76 77 import java.io.BufferedReader; 78 import java.io.ByteArrayOutputStream; 79 import java.io.File; 80 import java.io.FileNotFoundException; 81 import java.io.InputStreamReader; 82 import java.io.OutputStreamWriter; 83 import java.lang.reflect.Field; 84 import java.util.HashMap; 85 import java.util.List; 86 import java.util.UUID; 87 import java.util.concurrent.CountDownLatch; 88 import java.util.concurrent.ExecutionException; 89 import java.util.concurrent.Executor; 90 import java.util.function.Function; 91 92 /** 93 * Utility class to help manage Launcher Model and related objects for test. 94 */ 95 public class LauncherModelHelper { 96 97 public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP; 98 public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT; 99 100 public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 101 public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; 102 public static final int NO__ICON = -1; 103 104 public static final String TEST_PACKAGE = testContext().getPackageName(); 105 public static final String TEST_ACTIVITY = "com.android.launcher3.tests.Activity2"; 106 107 // Authority for providing a test default-workspace-layout data. 108 private static final String TEST_PROVIDER_AUTHORITY = 109 LauncherModelHelper.class.getName().toLowerCase(); 110 private static final int DEFAULT_BITMAP_SIZE = 10; 111 private static final int DEFAULT_GRID_SIZE = 4; 112 113 private final HashMap<Class, HashMap<String, Field>> mFieldCache = new HashMap<>(); 114 private final MockContentResolver mMockResolver = new MockContentResolver(); 115 public final TestLauncherProvider provider; 116 public final SanboxModelContext sandboxContext; 117 118 public final long defaultProfileId; 119 120 private BgDataModel mDataModel; 121 private AllAppsList mAllAppsList; 122 LauncherModelHelper()123 public LauncherModelHelper() { 124 Context context = getApplicationContext(); 125 // System settings cache content provider. Ensure that they are statically initialized 126 Settings.Secure.getString(context.getContentResolver(), "test"); 127 Settings.System.getString(context.getContentResolver(), "test"); 128 Settings.Global.getString(context.getContentResolver(), "test"); 129 130 provider = new TestLauncherProvider(); 131 sandboxContext = new SanboxModelContext(); 132 defaultProfileId = UserCache.INSTANCE.get(sandboxContext) 133 .getSerialNumberForUser(Process.myUserHandle()); 134 setupProvider(LauncherProvider.AUTHORITY, provider); 135 } 136 setupProvider(String authority, ContentProvider provider)137 public void setupProvider(String authority, ContentProvider provider) { 138 ProviderInfo providerInfo = new ProviderInfo(); 139 providerInfo.authority = authority; 140 providerInfo.applicationInfo = sandboxContext.getApplicationInfo(); 141 provider.attachInfo(sandboxContext, providerInfo); 142 mMockResolver.addProvider(providerInfo.authority, provider); 143 doReturn(providerInfo) 144 .when(sandboxContext.mPm) 145 .resolveContentProvider(eq(authority), anyInt()); 146 } 147 getModel()148 public LauncherModel getModel() { 149 return LauncherAppState.getInstance(sandboxContext).getModel(); 150 } 151 getBgDataModel()152 public synchronized BgDataModel getBgDataModel() { 153 if (mDataModel == null) { 154 mDataModel = ReflectionHelpers.getField(getModel(), "mBgDataModel"); 155 } 156 return mDataModel; 157 } 158 getAllAppsList()159 public synchronized AllAppsList getAllAppsList() { 160 if (mAllAppsList == null) { 161 mAllAppsList = ReflectionHelpers.getField(getModel(), "mBgAllAppsList"); 162 } 163 return mAllAppsList; 164 } 165 destroy()166 public void destroy() { 167 // When destroying the context, make sure that the model thread is blocked, so that no 168 // new jobs get posted while we are cleaning up 169 CountDownLatch l1 = new CountDownLatch(1); 170 CountDownLatch l2 = new CountDownLatch(1); 171 MODEL_EXECUTOR.execute(() -> { 172 l1.countDown(); 173 waitOrThrow(l2); 174 }); 175 waitOrThrow(l1); 176 sandboxContext.onDestroy(); 177 l2.countDown(); 178 } 179 waitOrThrow(CountDownLatch latch)180 private void waitOrThrow(CountDownLatch latch) { 181 try { 182 latch.await(); 183 } catch (Exception e) { 184 throw new RuntimeException(e); 185 } 186 } 187 188 /** 189 * Synchronously executes the task and returns all the UI callbacks posted. 190 */ executeTaskForTest(ModelUpdateTask task)191 public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception { 192 LauncherModel model = getModel(); 193 if (!model.isModelLoaded()) { 194 ReflectionHelpers.setField(model, "mModelLoaded", true); 195 } 196 Executor mockExecutor = mock(Executor.class); 197 model.enqueueModelUpdateTask(new ModelUpdateTask() { 198 @Override 199 public void init(@NonNull final LauncherAppState app, 200 @NonNull final LauncherModel model, @NonNull final BgDataModel dataModel, 201 @NonNull final AllAppsList allAppsList, @NonNull final Executor uiExecutor) { 202 task.init(app, model, dataModel, allAppsList, mockExecutor); 203 } 204 205 @Override 206 public void run() { 207 task.run(); 208 } 209 }); 210 MODEL_EXECUTOR.submit(() -> null).get(); 211 212 ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); 213 verify(mockExecutor, atLeast(0)).execute(captor.capture()); 214 return captor.getAllValues(); 215 } 216 217 /** 218 * Synchronously executes a task on the model 219 */ executeSimpleTask(Function<BgDataModel, T> task)220 public <T> T executeSimpleTask(Function<BgDataModel, T> task) throws Exception { 221 BgDataModel dataModel = getBgDataModel(); 222 return MODEL_EXECUTOR.submit(() -> task.apply(dataModel)).get(); 223 } 224 225 /** 226 * Initializes mock data for the test. 227 */ initializeData(String resourceName)228 public void initializeData(String resourceName) throws Exception { 229 BgDataModel bgDataModel = getBgDataModel(); 230 AllAppsList allAppsList = getAllAppsList(); 231 232 MODEL_EXECUTOR.submit(() -> { 233 // Copy apk from resources to a local file and install from there. 234 Resources resources = testContext().getResources(); 235 int resId = resources.getIdentifier( 236 resourceName, "raw", testContext().getPackageName()); 237 try (BufferedReader reader = new BufferedReader(new InputStreamReader( 238 resources.openRawResource(resId)))) { 239 String line; 240 HashMap<String, Class> classMap = new HashMap<>(); 241 while ((line = reader.readLine()) != null) { 242 line = line.trim(); 243 if (line.startsWith("#") || line.isEmpty()) { 244 continue; 245 } 246 String[] commands = line.split(" "); 247 switch (commands[0]) { 248 case "classMap": 249 classMap.put(commands[1], Class.forName(commands[2])); 250 break; 251 case "bgItem": 252 bgDataModel.addItem(sandboxContext, 253 (ItemInfo) initItem(classMap.get(commands[1]), commands, 2), 254 false); 255 break; 256 case "allApps": 257 allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null); 258 break; 259 } 260 } 261 } catch (Exception e) { 262 throw new RuntimeException(e); 263 } 264 }).get(); 265 } 266 initItem(Class clazz, String[] fieldDef, int startIndex)267 private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception { 268 HashMap<String, Field> cache = mFieldCache.get(clazz); 269 if (cache == null) { 270 cache = new HashMap<>(); 271 Class c = clazz; 272 while (c != null) { 273 for (Field f : c.getDeclaredFields()) { 274 f.setAccessible(true); 275 cache.put(f.getName(), f); 276 } 277 c = c.getSuperclass(); 278 } 279 mFieldCache.put(clazz, cache); 280 } 281 282 Object item = clazz.newInstance(); 283 for (int i = startIndex; i < fieldDef.length; i++) { 284 String[] fieldData = fieldDef[i].split("=", 2); 285 Field f = cache.get(fieldData[0]); 286 Class type = f.getType(); 287 if (type == int.class || type == long.class) { 288 f.set(item, Integer.parseInt(fieldData[1])); 289 } else if (type == CharSequence.class || type == String.class) { 290 f.set(item, fieldData[1]); 291 } else if (type == Intent.class) { 292 if (!fieldData[1].startsWith("#Intent")) { 293 fieldData[1] = "#Intent;" + fieldData[1] + ";end"; 294 } 295 f.set(item, Intent.parseUri(fieldData[1], 0)); 296 } else if (type == ComponentName.class) { 297 f.set(item, ComponentName.unflattenFromString(fieldData[1])); 298 } else { 299 throw new Exception("Added parsing logic for " 300 + f.getName() + " of type " + f.getType()); 301 } 302 } 303 return item; 304 } 305 addItem(int type, int screen, int container, int x, int y)306 public int addItem(int type, int screen, int container, int x, int y) { 307 return addItem(type, screen, container, x, y, defaultProfileId, TEST_PACKAGE); 308 } 309 addItem(int type, int screen, int container, int x, int y, long profileId)310 public int addItem(int type, int screen, int container, int x, int y, long profileId) { 311 return addItem(type, screen, container, x, y, profileId, TEST_PACKAGE); 312 } 313 addItem(int type, int screen, int container, int x, int y, String packageName)314 public int addItem(int type, int screen, int container, int x, int y, String packageName) { 315 return addItem(type, screen, container, x, y, defaultProfileId, packageName); 316 } 317 addItem(int type, int screen, int container, int x, int y, String packageName, int id, Uri contentUri)318 public int addItem(int type, int screen, int container, int x, int y, String packageName, 319 int id, Uri contentUri) { 320 addItem(type, screen, container, x, y, defaultProfileId, packageName, id, contentUri); 321 return id; 322 } 323 324 /** 325 * Adds a mock item in the DB. 326 * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for 327 * folder (where the type represents the number of items in the folder). 328 */ addItem(int type, int screen, int container, int x, int y, long profileId, String packageName)329 public int addItem(int type, int screen, int container, int x, int y, long profileId, 330 String packageName) { 331 int id = LauncherSettings.Settings.call(sandboxContext.getContentResolver(), 332 LauncherSettings.Settings.METHOD_NEW_ITEM_ID) 333 .getInt(LauncherSettings.Settings.EXTRA_VALUE); 334 addItem(type, screen, container, x, y, profileId, packageName, id, CONTENT_URI); 335 return id; 336 } 337 addItem(int type, int screen, int container, int x, int y, long profileId, String packageName, int id, Uri contentUri)338 public void addItem(int type, int screen, int container, int x, int y, long profileId, 339 String packageName, int id, Uri contentUri) { 340 ContentValues values = new ContentValues(); 341 values.put(LauncherSettings.Favorites._ID, id); 342 values.put(LauncherSettings.Favorites.CONTAINER, container); 343 values.put(LauncherSettings.Favorites.SCREEN, screen); 344 values.put(LauncherSettings.Favorites.CELLX, x); 345 values.put(LauncherSettings.Favorites.CELLY, y); 346 values.put(LauncherSettings.Favorites.SPANX, 1); 347 values.put(LauncherSettings.Favorites.SPANY, 1); 348 values.put(LauncherSettings.Favorites.PROFILE_ID, profileId); 349 350 if (type == APP_ICON || type == SHORTCUT) { 351 values.put(LauncherSettings.Favorites.ITEM_TYPE, type); 352 values.put(LauncherSettings.Favorites.INTENT, 353 new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0)); 354 } else { 355 values.put(LauncherSettings.Favorites.ITEM_TYPE, 356 LauncherSettings.Favorites.ITEM_TYPE_FOLDER); 357 // Add folder items. 358 for (int i = 0; i < type; i++) { 359 addItem(APP_ICON, 0, id, 0, 0, profileId); 360 } 361 } 362 363 sandboxContext.getContentResolver().insert(contentUri, values); 364 } 365 deleteItem(int itemId, @NonNull final String tableName)366 public void deleteItem(int itemId, @NonNull final String tableName) { 367 final Uri uri = Uri.parse("content://" 368 + LauncherProvider.AUTHORITY + "/" + tableName + "/" + itemId); 369 sandboxContext.getContentResolver().delete(uri, null, null); 370 } 371 createGrid(int[][][] typeArray)372 public int[][][] createGrid(int[][][] typeArray) { 373 return createGrid(typeArray, 1); 374 } 375 createGrid(int[][][] typeArray, int startScreen)376 public int[][][] createGrid(int[][][] typeArray, int startScreen) { 377 LauncherSettings.Settings.call(sandboxContext.getContentResolver(), 378 LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); 379 LauncherSettings.Settings.call(sandboxContext.getContentResolver(), 380 LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG); 381 return createGrid(typeArray, startScreen, defaultProfileId); 382 } 383 384 /** 385 * Initializes the DB with mock elements to represent the provided grid structure. 386 * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for 387 * type definitions. The first dimension represents the screens and the next 388 * two represent the workspace grid. 389 * @param startScreen First screen id from where the icons will be added. 390 * @return the same grid representation where each entry is the corresponding item id. 391 */ createGrid(int[][][] typeArray, int startScreen, long profileId)392 public int[][][] createGrid(int[][][] typeArray, int startScreen, long profileId) { 393 int[][][] ids = new int[typeArray.length][][]; 394 for (int i = 0; i < typeArray.length; i++) { 395 // Add screen to DB 396 int screenId = startScreen + i; 397 398 // Keep the screen id counter up to date 399 LauncherSettings.Settings.call(sandboxContext.getContentResolver(), 400 LauncherSettings.Settings.METHOD_NEW_SCREEN_ID); 401 402 ids[i] = new int[typeArray[i].length][]; 403 for (int y = 0; y < typeArray[i].length; y++) { 404 ids[i][y] = new int[typeArray[i][y].length]; 405 for (int x = 0; x < typeArray[i][y].length; x++) { 406 if (typeArray[i][y][x] < 0) { 407 // Empty cell 408 ids[i][y][x] = -1; 409 } else { 410 ids[i][y][x] = addItem( 411 typeArray[i][y][x], screenId, DESKTOP, x, y, profileId); 412 } 413 } 414 } 415 } 416 417 return ids; 418 } 419 420 /** 421 * Sets up a mock provider to load the provided layout by default, next time the layout loads 422 */ setupDefaultLayoutProvider(LauncherLayoutBuilder builder)423 public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder) 424 throws Exception { 425 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext); 426 idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE; 427 idp.iconBitmapSize = DEFAULT_BITMAP_SIZE; 428 429 UiDevice.getInstance(getInstrumentation()).executeShellCommand( 430 "settings put secure launcher3.layout.provider " + TEST_PROVIDER_AUTHORITY); 431 ContentProvider cp = new TestInformationProvider() { 432 433 @Override 434 public ParcelFileDescriptor openFile(Uri uri, String mode) 435 throws FileNotFoundException { 436 try { 437 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); 438 AutoCloseOutputStream outputStream = new AutoCloseOutputStream(pipe[1]); 439 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 440 builder.build(new OutputStreamWriter(bos)); 441 outputStream.write(bos.toByteArray()); 442 outputStream.flush(); 443 outputStream.close(); 444 return pipe[0]; 445 } catch (Exception e) { 446 throw new FileNotFoundException(e.getMessage()); 447 } 448 } 449 }; 450 setupProvider(TEST_PROVIDER_AUTHORITY, cp); 451 return this; 452 } 453 454 /** 455 * Loads the model in memory synchronously 456 */ loadModelSync()457 public void loadModelSync() throws ExecutionException, InterruptedException { 458 Callbacks mockCb = new Callbacks() { }; 459 Executors.MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get(); 460 461 Executors.MODEL_EXECUTOR.submit(() -> { }).get(); 462 Executors.MAIN_EXECUTOR.submit(() -> { }).get(); 463 Executors.MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get(); 464 } 465 466 /** 467 * An extension of LauncherProvider backed up by in-memory database. 468 */ 469 public static class TestLauncherProvider extends LauncherProvider { 470 471 @Override onCreate()472 public boolean onCreate() { 473 return true; 474 } 475 getDb()476 public SQLiteDatabase getDb() { 477 createDbIfNotExists(); 478 return mOpenHelper.getWritableDatabase(); 479 } 480 getHelper()481 public DatabaseHelper getHelper() { 482 return mOpenHelper; 483 } 484 } 485 deleteContents(File dir)486 public static boolean deleteContents(File dir) { 487 File[] files = dir.listFiles(); 488 boolean success = true; 489 if (files != null) { 490 for (File file : files) { 491 if (file.isDirectory()) { 492 success &= deleteContents(file); 493 } 494 if (!file.delete()) { 495 success = false; 496 } 497 } 498 } 499 return success; 500 } 501 502 public class SanboxModelContext extends SandboxContext { 503 504 private final ArrayMap<String, Object> mSpiedServices = new ArrayMap<>(); 505 private final PackageManager mPm; 506 private final File mDbDir; 507 SanboxModelContext()508 SanboxModelContext() { 509 super(ApplicationProvider.getApplicationContext(), 510 UserCache.INSTANCE, InstallSessionHelper.INSTANCE, LauncherPrefs.INSTANCE, 511 LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, 512 DisplayController.INSTANCE, CustomWidgetManager.INSTANCE, 513 SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE, LockedUserState.INSTANCE, 514 ItemInstallQueue.INSTANCE, WindowManagerProxy.INSTANCE); 515 mPm = spy(getBaseContext().getPackageManager()); 516 mDbDir = new File(getCacheDir(), UUID.randomUUID().toString()); 517 } 518 allow(MainThreadInitializedObject object)519 public SanboxModelContext allow(MainThreadInitializedObject object) { 520 mAllowedObjects.add(object); 521 return this; 522 } 523 524 @Override getDatabasePath(String name)525 public File getDatabasePath(String name) { 526 if (!mDbDir.exists()) { 527 mDbDir.mkdirs(); 528 } 529 return new File(mDbDir, name); 530 } 531 532 @Override getContentResolver()533 public ContentResolver getContentResolver() { 534 return mMockResolver; 535 } 536 537 @Override onDestroy()538 public void onDestroy() { 539 if (deleteContents(mDbDir)) { 540 mDbDir.delete(); 541 } 542 super.onDestroy(); 543 } 544 545 @Override getPackageManager()546 public PackageManager getPackageManager() { 547 return mPm; 548 } 549 550 @Override getSystemService(String name)551 public Object getSystemService(String name) { 552 Object service = mSpiedServices.get(name); 553 return service != null ? service : super.getSystemService(name); 554 } 555 spyService(Class<T> tClass)556 public <T> T spyService(Class<T> tClass) { 557 String name = getSystemServiceName(tClass); 558 Object service = mSpiedServices.get(name); 559 if (service != null) { 560 return (T) service; 561 } 562 563 T result = spy(getSystemService(tClass)); 564 mSpiedServices.put(name, result); 565 return result; 566 } 567 } 568 testContext()569 private static Context testContext() { 570 return getInstrumentation().getContext(); 571 } 572 } 573