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.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL;
19 
20 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
21 
22 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
23 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
24 import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
25 import static com.android.launcher3.util.TestUtil.grantWriteSecurePermission;
26 
27 import static org.mockito.ArgumentMatchers.anyInt;
28 import static org.mockito.ArgumentMatchers.eq;
29 import static org.mockito.Mockito.doReturn;
30 import static org.mockito.Mockito.spy;
31 
32 import android.content.ContentProvider;
33 import android.content.ContentResolver;
34 import android.content.Context;
35 import android.content.pm.PackageInstaller;
36 import android.content.pm.PackageInstaller.SessionParams;
37 import android.content.pm.PackageManager;
38 import android.content.pm.ProviderInfo;
39 import android.graphics.Bitmap;
40 import android.graphics.Bitmap.Config;
41 import android.graphics.Color;
42 import android.net.Uri;
43 import android.os.ParcelFileDescriptor;
44 import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
45 import android.provider.Settings;
46 import android.test.mock.MockContentResolver;
47 import android.util.ArrayMap;
48 
49 import androidx.test.core.app.ApplicationProvider;
50 
51 import com.android.launcher3.InvariantDeviceProfile;
52 import com.android.launcher3.LauncherAppState;
53 import com.android.launcher3.LauncherModel;
54 import com.android.launcher3.model.BgDataModel;
55 import com.android.launcher3.model.BgDataModel.Callbacks;
56 import com.android.launcher3.model.ModelDbController;
57 import com.android.launcher3.testing.TestInformationProvider;
58 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
59 
60 import java.io.ByteArrayInputStream;
61 import java.io.ByteArrayOutputStream;
62 import java.io.File;
63 import java.io.FileNotFoundException;
64 import java.io.IOException;
65 import java.io.OutputStreamWriter;
66 import java.util.Arrays;
67 import java.util.List;
68 import java.util.UUID;
69 import java.util.concurrent.CountDownLatch;
70 import java.util.concurrent.ExecutionException;
71 
72 /**
73  * Utility class to help manage Launcher Model and related objects for test.
74  */
75 public class LauncherModelHelper {
76 
77     public static final String TEST_PACKAGE = getInstrumentation().getContext().getPackageName();
78     public static final String TEST_ACTIVITY = "com.android.launcher3.tests.Activity2";
79     public static final String TEST_ACTIVITY2 = "com.android.launcher3.tests.Activity3";
80     public static final String TEST_ACTIVITY3 = "com.android.launcher3.tests.Activity4";
81     public static final String TEST_ACTIVITY4 = "com.android.launcher3.tests.Activity5";
82     public static final String TEST_ACTIVITY5 = "com.android.launcher3.tests.Activity6";
83     public static final String TEST_ACTIVITY6 = "com.android.launcher3.tests.Activity7";
84     public static final String TEST_ACTIVITY7 = "com.android.launcher3.tests.Activity8";
85     public static final String TEST_ACTIVITY8 = "com.android.launcher3.tests.Activity9";
86     public static final String TEST_ACTIVITY9 = "com.android.launcher3.tests.Activity10";
87     public static final String TEST_ACTIVITY10 = "com.android.launcher3.tests.Activity11";
88     public static final String TEST_ACTIVITY11 = "com.android.launcher3.tests.Activity12";
89     public static final String TEST_ACTIVITY12 = "com.android.launcher3.tests.Activity13";
90     public static final String TEST_ACTIVITY13 = "com.android.launcher3.tests.Activity14";
91     public static final String TEST_ACTIVITY14 = "com.android.launcher3.tests.Activity15";
92 
93     public static final List<String> ACTIVITY_LIST = Arrays.asList(
94             TEST_ACTIVITY,
95             TEST_ACTIVITY2,
96             TEST_ACTIVITY3,
97             TEST_ACTIVITY4,
98             TEST_ACTIVITY5,
99             TEST_ACTIVITY6,
100             TEST_ACTIVITY7,
101             TEST_ACTIVITY8,
102             TEST_ACTIVITY9,
103             TEST_ACTIVITY10,
104             TEST_ACTIVITY11,
105             TEST_ACTIVITY12,
106             TEST_ACTIVITY13,
107             TEST_ACTIVITY14
108     );
109 
110     // Authority for providing a test default-workspace-layout data.
111     private static final String TEST_PROVIDER_AUTHORITY =
112             LauncherModelHelper.class.getName().toLowerCase();
113     private static final int DEFAULT_BITMAP_SIZE = 10;
114     private static final int DEFAULT_GRID_SIZE = 4;
115 
116     public final SandboxModelContext sandboxContext;
117 
118     private final RunnableList mDestroyTask = new RunnableList();
119 
120     private BgDataModel mDataModel;
121 
LauncherModelHelper()122     public LauncherModelHelper() {
123         sandboxContext = new SandboxModelContext();
124     }
125 
setupProvider(String authority, ContentProvider provider)126     public void setupProvider(String authority, ContentProvider provider) {
127         sandboxContext.setupProvider(authority, provider);
128     }
129 
getModel()130     public LauncherModel getModel() {
131         return LauncherAppState.getInstance(sandboxContext).getModel();
132     }
133 
getBgDataModel()134     public synchronized BgDataModel getBgDataModel() {
135         if (mDataModel == null) {
136             getModel().enqueueModelUpdateTask((taskController, dataModel, apps) ->
137                     mDataModel = dataModel);
138             runOnExecutorSync(Executors.MODEL_EXECUTOR, () -> { });
139         }
140         return mDataModel;
141     }
142 
143     /**
144      * Creates a installer session for the provided package.
145      */
createInstallerSession(String pkg)146     public int createInstallerSession(String pkg) throws IOException {
147         SessionParams sp = new SessionParams(MODE_FULL_INSTALL);
148         sp.setAppPackageName(pkg);
149         Bitmap icon = Bitmap.createBitmap(100, 100, Config.ARGB_8888);
150         icon.eraseColor(Color.RED);
151         sp.setAppIcon(icon);
152         sp.setAppLabel(pkg);
153         sp.setInstallerPackageName(ApplicationProvider.getApplicationContext().getPackageName());
154         PackageInstaller pi = ApplicationProvider.getApplicationContext().getPackageManager()
155                 .getPackageInstaller();
156         int sessionId = pi.createSession(sp);
157         mDestroyTask.add(() -> pi.abandonSession(sessionId));
158         return sessionId;
159     }
160 
destroy()161     public void destroy() {
162         // When destroying the context, make sure that the model thread is blocked, so that no
163         // new jobs get posted while we are cleaning up
164         CountDownLatch l1 = new CountDownLatch(1);
165         CountDownLatch l2 = new CountDownLatch(1);
166         MODEL_EXECUTOR.execute(() -> {
167             l1.countDown();
168             waitOrThrow(l2);
169         });
170         waitOrThrow(l1);
171         sandboxContext.onDestroy();
172         l2.countDown();
173 
174         mDestroyTask.executeAllAndDestroy();
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      * Sets up a mock provider to load the provided layout by default, next time the layout loads
187      */
setupDefaultLayoutProvider(LauncherLayoutBuilder builder)188     public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder)
189             throws Exception {
190         grantWriteSecurePermission();
191 
192         InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext);
193         if (idp.numRows == 0 && idp.numColumns == 0) {
194             idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE;
195         }
196         if (idp.iconBitmapSize == 0) {
197             idp.iconBitmapSize = DEFAULT_BITMAP_SIZE;
198         }
199 
200         Settings.Secure.putString(sandboxContext.getContentResolver(), "launcher3.layout.provider",
201                 TEST_PROVIDER_AUTHORITY);
202 
203         // TODO: use a wrapper class to differentiate the behavior
204         ByteArrayOutputStream bos = new ByteArrayOutputStream();
205         builder.build(new OutputStreamWriter(bos));
206         ContentProvider cp = new TestInformationProvider() {
207 
208             @Override
209             public ParcelFileDescriptor openFile(Uri uri, String mode)
210                     throws FileNotFoundException {
211                 try {
212                     ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
213                     AutoCloseOutputStream outputStream = new AutoCloseOutputStream(pipe[1]);
214                     outputStream.write(bos.toByteArray());
215                     outputStream.flush();
216                     outputStream.close();
217                     return pipe[0];
218                 } catch (Exception e) {
219                     throw new FileNotFoundException(e.getMessage());
220                 }
221             }
222         };
223         setupProvider(TEST_PROVIDER_AUTHORITY, cp);
224         RoboApiWrapper.INSTANCE.registerInputStream(sandboxContext.getContentResolver(),
225                 ModelDbController.getLayoutUri(TEST_PROVIDER_AUTHORITY, sandboxContext),
226                 ()-> new ByteArrayInputStream(bos.toByteArray()));
227 
228         mDestroyTask.add(() -> runOnExecutorSync(MODEL_EXECUTOR, () ->
229                 Settings.Secure.putString(sandboxContext.getContentResolver(),
230                         "launcher3.layout.provider", "")));
231         return this;
232     }
233 
234     /**
235      * Loads the model in memory synchronously
236      */
loadModelSync()237     public void loadModelSync() throws ExecutionException, InterruptedException {
238         Callbacks mockCb = new Callbacks() { };
239         MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get();
240 
241         Executors.MODEL_EXECUTOR.submit(() -> { }).get();
242         getInstrumentation().waitForIdleSync();
243         MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get();
244     }
245 
246     public static class SandboxModelContext extends SandboxContext {
247 
248         private final MockContentResolver mMockResolver = new MockContentResolver();
249         private final ArrayMap<String, Object> mSpiedServices = new ArrayMap<>();
250         private final PackageManager mPm;
251         private final File mDbDir;
252 
SandboxModelContext()253         public SandboxModelContext() {
254             this(ApplicationProvider.getApplicationContext());
255         }
256 
SandboxModelContext(Context context)257         public SandboxModelContext(Context context) {
258             super(context);
259 
260             // System settings cache content provider. Ensure that they are statically initialized
261             Settings.Secure.getString(context.getContentResolver(), "test");
262             Settings.System.getString(context.getContentResolver(), "test");
263             Settings.Global.getString(context.getContentResolver(), "test");
264 
265             mPm = spy(getBaseContext().getPackageManager());
266             mDbDir = new File(getCacheDir(), UUID.randomUUID().toString());
267         }
268 
269         @Override
createObject(MainThreadInitializedObject<T> object)270         public <T extends SafeCloseable> T createObject(MainThreadInitializedObject<T> object) {
271             if (object == LauncherAppState.INSTANCE) {
272                 return (T) new LauncherAppState(this, null /* iconCacheFileName */);
273             }
274             return super.createObject(object);
275         }
276 
277         @Override
getDatabasePath(String name)278         public File getDatabasePath(String name) {
279             if (!mDbDir.exists()) {
280                 mDbDir.mkdirs();
281             }
282             return new File(mDbDir, name);
283         }
284 
285         @Override
getContentResolver()286         public ContentResolver getContentResolver() {
287             return mMockResolver;
288         }
289 
290         @Override
cleanUpObjects()291         protected void cleanUpObjects() {
292             if (deleteContents(mDbDir)) {
293                 mDbDir.delete();
294             }
295             super.cleanUpObjects();
296         }
297 
298         @Override
getPackageManager()299         public PackageManager getPackageManager() {
300             return mPm;
301         }
302 
303         @Override
getSystemService(String name)304         public Object getSystemService(String name) {
305             Object service = mSpiedServices.get(name);
306             return service != null ? service : super.getSystemService(name);
307         }
308 
spyService(Class<T> tClass)309         public <T> T spyService(Class<T> tClass) {
310             String name = getSystemServiceName(tClass);
311             Object service = mSpiedServices.get(name);
312             if (service != null) {
313                 return (T) service;
314             }
315 
316             T result = spy(getSystemService(tClass));
317             mSpiedServices.put(name, result);
318             return result;
319         }
320 
setupProvider(String authority, ContentProvider provider)321         public void setupProvider(String authority, ContentProvider provider) {
322             ProviderInfo providerInfo = new ProviderInfo();
323             providerInfo.authority = authority;
324             providerInfo.applicationInfo = getApplicationInfo();
325             provider.attachInfo(this, providerInfo);
326             mMockResolver.addProvider(providerInfo.authority, provider);
327             doReturn(providerInfo).when(mPm).resolveContentProvider(eq(authority), anyInt());
328         }
329 
deleteContents(File dir)330         private static boolean deleteContents(File dir) {
331             File[] files = dir.listFiles();
332             boolean success = true;
333             if (files != null) {
334                 for (File file : files) {
335                     if (file.isDirectory()) {
336                         success &= deleteContents(file);
337                     }
338                     if (!file.delete()) {
339                         success = false;
340                     }
341                 }
342             }
343             return success;
344         }
345     }
346 }
347