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