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