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