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