• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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