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