• 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.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.grantWriteSecurePermission;
25 import static com.android.launcher3.util.TestUtil.runOnExecutorSync;
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.dagger.LauncherBaseAppComponent;
55 import com.android.launcher3.model.BgDataModel;
56 import com.android.launcher3.model.BgDataModel.Callbacks;
57 import com.android.launcher3.model.ModelDbController;
58 import com.android.launcher3.testing.TestInformationProvider;
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
getDatabasePath(String name)270         public File getDatabasePath(String name) {
271             if (!mDbDir.exists()) {
272                 mDbDir.mkdirs();
273             }
274             return new File(mDbDir, name);
275         }
276 
277         @Override
getContentResolver()278         public ContentResolver getContentResolver() {
279             return mMockResolver;
280         }
281 
282         @Override
cleanUpObjects()283         protected void cleanUpObjects() {
284             if (deleteContents(mDbDir)) {
285                 mDbDir.delete();
286             }
287             super.cleanUpObjects();
288         }
289 
290         @Override
getPackageManager()291         public PackageManager getPackageManager() {
292             return mPm;
293         }
294 
295         @Override
getSystemService(String name)296         public Object getSystemService(String name) {
297             Object service = mSpiedServices.get(name);
298             return service != null ? service : super.getSystemService(name);
299         }
300 
spyService(Class<T> tClass)301         public <T> T spyService(Class<T> tClass) {
302             String name = getSystemServiceName(tClass);
303             Object service = mSpiedServices.get(name);
304             if (service != null) {
305                 return (T) service;
306             }
307 
308             T result = spy(getSystemService(tClass));
309             mSpiedServices.put(name, result);
310             return result;
311         }
312 
setupProvider(String authority, ContentProvider provider)313         public void setupProvider(String authority, ContentProvider provider) {
314             ProviderInfo providerInfo = new ProviderInfo();
315             providerInfo.authority = authority;
316             providerInfo.applicationInfo = getApplicationInfo();
317             provider.attachInfo(this, providerInfo);
318             mMockResolver.addProvider(providerInfo.authority, provider);
319             doReturn(providerInfo).when(mPm).resolveContentProvider(eq(authority), anyInt());
320         }
321 
deleteContents(File dir)322         private static boolean deleteContents(File dir) {
323             File[] files = dir.listFiles();
324             boolean success = true;
325             if (files != null) {
326                 for (File file : files) {
327                     if (file.isDirectory()) {
328                         success &= deleteContents(file);
329                     }
330                     if (!file.delete()) {
331                         success = false;
332                     }
333                 }
334             }
335             return success;
336         }
337 
338         @Override
initDaggerComponent(LauncherBaseAppComponent.Builder componentBuilder)339         public void initDaggerComponent(LauncherBaseAppComponent.Builder componentBuilder) {
340             super.initDaggerComponent(componentBuilder.iconsDbName(null));
341         }
342     }
343 }
344