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 android.content.Intent.ACTION_CREATE_SHORTCUT; 19 20 import static com.android.launcher3.LauncherSettings.Favorites.CONTENT_URI; 21 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 22 23 import static org.mockito.Mockito.atLeast; 24 import static org.mockito.Mockito.mock; 25 import static org.mockito.Mockito.verify; 26 import static org.robolectric.Shadows.shadowOf; 27 28 import android.content.ComponentName; 29 import android.content.ContentValues; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.content.pm.PackageManager.NameNotFoundException; 34 import android.database.sqlite.SQLiteDatabase; 35 import android.net.Uri; 36 import android.os.Process; 37 import android.provider.Settings; 38 39 import com.android.launcher3.InvariantDeviceProfile; 40 import com.android.launcher3.LauncherAppState; 41 import com.android.launcher3.LauncherModel; 42 import com.android.launcher3.LauncherModel.ModelUpdateTask; 43 import com.android.launcher3.LauncherProvider; 44 import com.android.launcher3.LauncherSettings; 45 import com.android.launcher3.model.AllAppsList; 46 import com.android.launcher3.model.BgDataModel; 47 import com.android.launcher3.model.BgDataModel.Callbacks; 48 import com.android.launcher3.model.data.AppInfo; 49 import com.android.launcher3.model.data.ItemInfo; 50 import com.android.launcher3.pm.UserCache; 51 import com.android.launcher3.shadows.ShadowLooperExecutor; 52 53 import org.mockito.ArgumentCaptor; 54 import org.robolectric.Robolectric; 55 import org.robolectric.RuntimeEnvironment; 56 import org.robolectric.shadow.api.Shadow; 57 import org.robolectric.shadows.ShadowContentResolver; 58 import org.robolectric.shadows.ShadowPackageManager; 59 import org.robolectric.util.ReflectionHelpers; 60 61 import java.io.BufferedReader; 62 import java.io.ByteArrayInputStream; 63 import java.io.ByteArrayOutputStream; 64 import java.io.InputStreamReader; 65 import java.io.OutputStreamWriter; 66 import java.lang.reflect.Field; 67 import java.util.HashMap; 68 import java.util.List; 69 import java.util.concurrent.ExecutionException; 70 import java.util.concurrent.Executor; 71 import java.util.function.Function; 72 73 /** 74 * Utility class to help manage Launcher Model and related objects for test. 75 */ 76 public class LauncherModelHelper { 77 78 public static final int DESKTOP = LauncherSettings.Favorites.CONTAINER_DESKTOP; 79 public static final int HOTSEAT = LauncherSettings.Favorites.CONTAINER_HOTSEAT; 80 81 public static final int APP_ICON = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 82 public static final int SHORTCUT = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; 83 public static final int NO__ICON = -1; 84 public static final String TEST_PACKAGE = "com.android.launcher3.validpackage"; 85 86 // Authority for providing a test default-workspace-layout data. 87 private static final String TEST_PROVIDER_AUTHORITY = 88 LauncherModelHelper.class.getName().toLowerCase(); 89 private static final int DEFAULT_BITMAP_SIZE = 10; 90 private static final int DEFAULT_GRID_SIZE = 4; 91 92 private final HashMap<Class, HashMap<String, Field>> mFieldCache = new HashMap<>(); 93 public final TestLauncherProvider provider; 94 private final long mDefaultProfileId; 95 96 private BgDataModel mDataModel; 97 private AllAppsList mAllAppsList; 98 LauncherModelHelper()99 public LauncherModelHelper() { 100 provider = Robolectric.setupContentProvider(TestLauncherProvider.class); 101 mDefaultProfileId = UserCache.INSTANCE.get(RuntimeEnvironment.application) 102 .getSerialNumberForUser(Process.myUserHandle()); 103 ShadowContentResolver.registerProviderInternal(LauncherProvider.AUTHORITY, provider); 104 } 105 getModel()106 public LauncherModel getModel() { 107 return LauncherAppState.getInstance(RuntimeEnvironment.application).getModel(); 108 } 109 getBgDataModel()110 public synchronized BgDataModel getBgDataModel() { 111 if (mDataModel == null) { 112 mDataModel = ReflectionHelpers.getField(getModel(), "mBgDataModel"); 113 } 114 return mDataModel; 115 } 116 getAllAppsList()117 public synchronized AllAppsList getAllAppsList() { 118 if (mAllAppsList == null) { 119 mAllAppsList = ReflectionHelpers.getField(getModel(), "mBgAllAppsList"); 120 } 121 return mAllAppsList; 122 } 123 124 /** 125 * Synchronously executes the task and returns all the UI callbacks posted. 126 */ executeTaskForTest(ModelUpdateTask task)127 public List<Runnable> executeTaskForTest(ModelUpdateTask task) throws Exception { 128 LauncherModel model = getModel(); 129 if (!model.isModelLoaded()) { 130 ReflectionHelpers.setField(model, "mModelLoaded", true); 131 } 132 Executor mockExecutor = mock(Executor.class); 133 model.enqueueModelUpdateTask(new ModelUpdateTask() { 134 @Override 135 public void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel, 136 AllAppsList allAppsList, Executor uiExecutor) { 137 task.init(app, model, dataModel, allAppsList, mockExecutor); 138 } 139 140 @Override 141 public void run() { 142 task.run(); 143 } 144 }); 145 MODEL_EXECUTOR.submit(() -> null).get(); 146 147 ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); 148 verify(mockExecutor, atLeast(0)).execute(captor.capture()); 149 return captor.getAllValues(); 150 } 151 152 /** 153 * Synchronously executes a task on the model 154 */ executeSimpleTask(Function<BgDataModel, T> task)155 public <T> T executeSimpleTask(Function<BgDataModel, T> task) throws Exception { 156 BgDataModel dataModel = getBgDataModel(); 157 return MODEL_EXECUTOR.submit(() -> task.apply(dataModel)).get(); 158 } 159 160 /** 161 * Initializes mock data for the test. 162 */ initializeData(String resourceName)163 public void initializeData(String resourceName) throws Exception { 164 Context targetContext = RuntimeEnvironment.application; 165 BgDataModel bgDataModel = getBgDataModel(); 166 AllAppsList allAppsList = getAllAppsList(); 167 168 MODEL_EXECUTOR.submit(() -> { 169 try (BufferedReader reader = new BufferedReader(new InputStreamReader( 170 this.getClass().getResourceAsStream(resourceName)))) { 171 String line; 172 HashMap<String, Class> classMap = new HashMap<>(); 173 while ((line = reader.readLine()) != null) { 174 line = line.trim(); 175 if (line.startsWith("#") || line.isEmpty()) { 176 continue; 177 } 178 String[] commands = line.split(" "); 179 switch (commands[0]) { 180 case "classMap": 181 classMap.put(commands[1], Class.forName(commands[2])); 182 break; 183 case "bgItem": 184 bgDataModel.addItem(targetContext, 185 (ItemInfo) initItem(classMap.get(commands[1]), commands, 2), 186 false); 187 break; 188 case "allApps": 189 allAppsList.add((AppInfo) initItem(AppInfo.class, commands, 1), null); 190 break; 191 } 192 } 193 } catch (Exception e) { 194 throw new RuntimeException(e); 195 } 196 }).get(); 197 } 198 initItem(Class clazz, String[] fieldDef, int startIndex)199 private Object initItem(Class clazz, String[] fieldDef, int startIndex) throws Exception { 200 HashMap<String, Field> cache = mFieldCache.get(clazz); 201 if (cache == null) { 202 cache = new HashMap<>(); 203 Class c = clazz; 204 while (c != null) { 205 for (Field f : c.getDeclaredFields()) { 206 f.setAccessible(true); 207 cache.put(f.getName(), f); 208 } 209 c = c.getSuperclass(); 210 } 211 mFieldCache.put(clazz, cache); 212 } 213 214 Object item = clazz.newInstance(); 215 for (int i = startIndex; i < fieldDef.length; i++) { 216 String[] fieldData = fieldDef[i].split("=", 2); 217 Field f = cache.get(fieldData[0]); 218 Class type = f.getType(); 219 if (type == int.class || type == long.class) { 220 f.set(item, Integer.parseInt(fieldData[1])); 221 } else if (type == CharSequence.class || type == String.class) { 222 f.set(item, fieldData[1]); 223 } else if (type == Intent.class) { 224 if (!fieldData[1].startsWith("#Intent")) { 225 fieldData[1] = "#Intent;" + fieldData[1] + ";end"; 226 } 227 f.set(item, Intent.parseUri(fieldData[1], 0)); 228 } else if (type == ComponentName.class) { 229 f.set(item, ComponentName.unflattenFromString(fieldData[1])); 230 } else { 231 throw new Exception("Added parsing logic for " 232 + f.getName() + " of type " + f.getType()); 233 } 234 } 235 return item; 236 } 237 addItem(int type, int screen, int container, int x, int y)238 public int addItem(int type, int screen, int container, int x, int y) { 239 return addItem(type, screen, container, x, y, mDefaultProfileId, TEST_PACKAGE); 240 } 241 addItem(int type, int screen, int container, int x, int y, long profileId)242 public int addItem(int type, int screen, int container, int x, int y, long profileId) { 243 return addItem(type, screen, container, x, y, profileId, TEST_PACKAGE); 244 } 245 addItem(int type, int screen, int container, int x, int y, String packageName)246 public int addItem(int type, int screen, int container, int x, int y, String packageName) { 247 return addItem(type, screen, container, x, y, mDefaultProfileId, packageName); 248 } 249 addItem(int type, int screen, int container, int x, int y, String packageName, int id, Uri contentUri)250 public int addItem(int type, int screen, int container, int x, int y, String packageName, 251 int id, Uri contentUri) { 252 addItem(type, screen, container, x, y, mDefaultProfileId, packageName, id, contentUri); 253 return id; 254 } 255 256 /** 257 * Adds a mock item in the DB. 258 * @param type {@link #APP_ICON} or {@link #SHORTCUT} or >= 2 for 259 * folder (where the type represents the number of items in the folder). 260 */ addItem(int type, int screen, int container, int x, int y, long profileId, String packageName)261 public int addItem(int type, int screen, int container, int x, int y, long profileId, 262 String packageName) { 263 Context context = RuntimeEnvironment.application; 264 int id = LauncherSettings.Settings.call(context.getContentResolver(), 265 LauncherSettings.Settings.METHOD_NEW_ITEM_ID) 266 .getInt(LauncherSettings.Settings.EXTRA_VALUE); 267 addItem(type, screen, container, x, y, profileId, packageName, id, CONTENT_URI); 268 return id; 269 } 270 addItem(int type, int screen, int container, int x, int y, long profileId, String packageName, int id, Uri contentUri)271 public void addItem(int type, int screen, int container, int x, int y, long profileId, 272 String packageName, int id, Uri contentUri) { 273 Context context = RuntimeEnvironment.application; 274 275 ContentValues values = new ContentValues(); 276 values.put(LauncherSettings.Favorites._ID, id); 277 values.put(LauncherSettings.Favorites.CONTAINER, container); 278 values.put(LauncherSettings.Favorites.SCREEN, screen); 279 values.put(LauncherSettings.Favorites.CELLX, x); 280 values.put(LauncherSettings.Favorites.CELLY, y); 281 values.put(LauncherSettings.Favorites.SPANX, 1); 282 values.put(LauncherSettings.Favorites.SPANY, 1); 283 values.put(LauncherSettings.Favorites.PROFILE_ID, profileId); 284 285 if (type == APP_ICON || type == SHORTCUT) { 286 values.put(LauncherSettings.Favorites.ITEM_TYPE, type); 287 values.put(LauncherSettings.Favorites.INTENT, 288 new Intent(Intent.ACTION_MAIN).setPackage(packageName).toUri(0)); 289 } else { 290 values.put(LauncherSettings.Favorites.ITEM_TYPE, 291 LauncherSettings.Favorites.ITEM_TYPE_FOLDER); 292 // Add folder items. 293 for (int i = 0; i < type; i++) { 294 addItem(APP_ICON, 0, id, 0, 0, profileId); 295 } 296 } 297 298 context.getContentResolver().insert(contentUri, values); 299 } 300 createGrid(int[][][] typeArray)301 public int[][][] createGrid(int[][][] typeArray) { 302 return createGrid(typeArray, 1); 303 } 304 createGrid(int[][][] typeArray, int startScreen)305 public int[][][] createGrid(int[][][] typeArray, int startScreen) { 306 final Context context = RuntimeEnvironment.application; 307 LauncherSettings.Settings.call(context.getContentResolver(), 308 LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); 309 LauncherSettings.Settings.call(context.getContentResolver(), 310 LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG); 311 return createGrid(typeArray, startScreen, mDefaultProfileId); 312 } 313 314 /** 315 * Initializes the DB with mock elements to represent the provided grid structure. 316 * @param typeArray A 3d array of item types. {@see #addItem(int, long, long, int, int)} for 317 * type definitions. The first dimension represents the screens and the next 318 * two represent the workspace grid. 319 * @param startScreen First screen id from where the icons will be added. 320 * @return the same grid representation where each entry is the corresponding item id. 321 */ createGrid(int[][][] typeArray, int startScreen, long profileId)322 public int[][][] createGrid(int[][][] typeArray, int startScreen, long profileId) { 323 Context context = RuntimeEnvironment.application; 324 int[][][] ids = new int[typeArray.length][][]; 325 for (int i = 0; i < typeArray.length; i++) { 326 // Add screen to DB 327 int screenId = startScreen + i; 328 329 // Keep the screen id counter up to date 330 LauncherSettings.Settings.call(context.getContentResolver(), 331 LauncherSettings.Settings.METHOD_NEW_SCREEN_ID); 332 333 ids[i] = new int[typeArray[i].length][]; 334 for (int y = 0; y < typeArray[i].length; y++) { 335 ids[i][y] = new int[typeArray[i][y].length]; 336 for (int x = 0; x < typeArray[i][y].length; x++) { 337 if (typeArray[i][y][x] < 0) { 338 // Empty cell 339 ids[i][y][x] = -1; 340 } else { 341 ids[i][y][x] = addItem( 342 typeArray[i][y][x], screenId, DESKTOP, x, y, profileId); 343 } 344 } 345 } 346 } 347 348 return ids; 349 } 350 351 /** 352 * Sets up a mock provider to load the provided layout by default, next time the layout loads 353 */ setupDefaultLayoutProvider(LauncherLayoutBuilder builder)354 public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder) 355 throws Exception { 356 Context context = RuntimeEnvironment.application; 357 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(context); 358 idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE; 359 idp.iconBitmapSize = DEFAULT_BITMAP_SIZE; 360 361 Settings.Secure.putString(context.getContentResolver(), 362 "launcher3.layout.provider", TEST_PROVIDER_AUTHORITY); 363 364 shadowOf(context.getPackageManager()) 365 .addProviderIfNotPresent(new ComponentName("com.test", "Mock")).authority = 366 TEST_PROVIDER_AUTHORITY; 367 368 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 369 builder.build(new OutputStreamWriter(bos)); 370 Uri layoutUri = LauncherProvider.getLayoutUri(TEST_PROVIDER_AUTHORITY, context); 371 shadowOf(context.getContentResolver()).registerInputStream(layoutUri, 372 new ByteArrayInputStream(bos.toByteArray())); 373 return this; 374 } 375 376 /** 377 * Simulates an apk install with a default main activity with same class and package name 378 */ installApp(String component)379 public void installApp(String component) throws NameNotFoundException { 380 IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN); 381 filter.addCategory(Intent.CATEGORY_LAUNCHER); 382 installApp(component, component, filter); 383 } 384 385 /** 386 * Simulates a custom shortcut install 387 */ installCustomShortcut(String pkg, String clazz)388 public void installCustomShortcut(String pkg, String clazz) throws NameNotFoundException { 389 installApp(pkg, clazz, new IntentFilter(ACTION_CREATE_SHORTCUT)); 390 } 391 installApp(String pkg, String clazz, IntentFilter filter)392 private void installApp(String pkg, String clazz, IntentFilter filter) 393 throws NameNotFoundException { 394 ShadowPackageManager spm = shadowOf(RuntimeEnvironment.application.getPackageManager()); 395 ComponentName cn = new ComponentName(pkg, clazz); 396 spm.addActivityIfNotPresent(cn); 397 398 filter.addCategory(Intent.CATEGORY_DEFAULT); 399 spm.addIntentFilterForActivity(cn, filter); 400 } 401 402 /** 403 * Loads the model in memory synchronously 404 */ loadModelSync()405 public void loadModelSync() throws ExecutionException, InterruptedException { 406 // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread, 407 // so that we can wait appropriately for the loader to complete. 408 ShadowLooperExecutor sle = Shadow.extract(Executors.MAIN_EXECUTOR); 409 sle.setHandler(Executors.UI_HELPER_EXECUTOR.getHandler()); 410 411 Callbacks mockCb = mock(Callbacks.class); 412 getModel().addCallbacksAndLoad(mockCb); 413 414 Executors.MODEL_EXECUTOR.submit(() -> { }).get(); 415 Executors.UI_HELPER_EXECUTOR.submit(() -> { }).get(); 416 417 sle.setHandler(null); 418 getModel().removeCallbacks(mockCb); 419 } 420 421 /** 422 * An extension of LauncherProvider backed up by in-memory database. 423 */ 424 public static class TestLauncherProvider extends LauncherProvider { 425 426 @Override onCreate()427 public boolean onCreate() { 428 return true; 429 } 430 getDb()431 public SQLiteDatabase getDb() { 432 createDbIfNotExists(); 433 return mOpenHelper.getWritableDatabase(); 434 } 435 getHelper()436 public DatabaseHelper getHelper() { 437 return mOpenHelper; 438 } 439 } 440 } 441