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