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