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.annotation.NonNull; 48 import androidx.test.core.app.ApplicationProvider; 49 import androidx.test.uiautomator.UiDevice; 50 51 import com.android.launcher3.InvariantDeviceProfile; 52 import com.android.launcher3.LauncherAppState; 53 import com.android.launcher3.LauncherModel; 54 import com.android.launcher3.LauncherModel.ModelUpdateTask; 55 import com.android.launcher3.LauncherPrefs; 56 import com.android.launcher3.model.AllAppsList; 57 import com.android.launcher3.model.BgDataModel; 58 import com.android.launcher3.model.BgDataModel.Callbacks; 59 import com.android.launcher3.model.ItemInstallQueue; 60 import com.android.launcher3.pm.InstallSessionHelper; 61 import com.android.launcher3.pm.UserCache; 62 import com.android.launcher3.testing.TestInformationProvider; 63 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; 64 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; 65 import com.android.launcher3.util.window.WindowManagerProxy; 66 import com.android.launcher3.widget.custom.CustomWidgetManager; 67 68 import java.io.ByteArrayOutputStream; 69 import java.io.File; 70 import java.io.FileNotFoundException; 71 import java.io.IOException; 72 import java.io.OutputStreamWriter; 73 import java.util.UUID; 74 import java.util.concurrent.CountDownLatch; 75 import java.util.concurrent.ExecutionException; 76 import java.util.concurrent.Executor; 77 78 /** 79 * Utility class to help manage Launcher Model and related objects for test. 80 */ 81 public class LauncherModelHelper { 82 83 public static final String TEST_PACKAGE = getInstrumentation().getContext().getPackageName(); 84 public static final String TEST_ACTIVITY = "com.android.launcher3.tests.Activity2"; 85 public static final String TEST_ACTIVITY2 = "com.android.launcher3.tests.Activity3"; 86 public static final String TEST_ACTIVITY3 = "com.android.launcher3.tests.Activity4"; 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(new ModelUpdateTask() { 115 @Override 116 public void init(@NonNull LauncherAppState app, @NonNull LauncherModel model, 117 @NonNull BgDataModel dataModel, @NonNull AllAppsList allAppsList, 118 @NonNull Executor uiExecutor) { 119 mDataModel = dataModel; 120 } 121 122 @Override 123 public void run() { } 124 }); 125 } 126 return mDataModel; 127 } 128 129 /** 130 * Creates a installer session for the provided package. 131 */ createInstallerSession(String pkg)132 public int createInstallerSession(String pkg) throws IOException { 133 SessionParams sp = new SessionParams(MODE_FULL_INSTALL); 134 sp.setAppPackageName(pkg); 135 Bitmap icon = Bitmap.createBitmap(100, 100, Config.ARGB_8888); 136 icon.eraseColor(Color.RED); 137 sp.setAppIcon(icon); 138 sp.setAppLabel(pkg); 139 PackageInstaller pi = sandboxContext.getPackageManager().getPackageInstaller(); 140 int sessionId = pi.createSession(sp); 141 mDestroyTask.add(() -> pi.abandonSession(sessionId)); 142 return sessionId; 143 } 144 destroy()145 public void destroy() { 146 // When destroying the context, make sure that the model thread is blocked, so that no 147 // new jobs get posted while we are cleaning up 148 CountDownLatch l1 = new CountDownLatch(1); 149 CountDownLatch l2 = new CountDownLatch(1); 150 MODEL_EXECUTOR.execute(() -> { 151 l1.countDown(); 152 waitOrThrow(l2); 153 }); 154 waitOrThrow(l1); 155 sandboxContext.onDestroy(); 156 l2.countDown(); 157 158 mDestroyTask.executeAllAndDestroy(); 159 } 160 waitOrThrow(CountDownLatch latch)161 private void waitOrThrow(CountDownLatch latch) { 162 try { 163 latch.await(); 164 } catch (Exception e) { 165 throw new RuntimeException(e); 166 } 167 } 168 169 /** 170 * Sets up a mock provider to load the provided layout by default, next time the layout loads 171 */ setupDefaultLayoutProvider(LauncherLayoutBuilder builder)172 public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder) 173 throws Exception { 174 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext); 175 idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE; 176 idp.iconBitmapSize = DEFAULT_BITMAP_SIZE; 177 178 UiDevice.getInstance(getInstrumentation()).executeShellCommand( 179 "settings put secure launcher3.layout.provider " + TEST_PROVIDER_AUTHORITY); 180 ContentProvider cp = new TestInformationProvider() { 181 182 @Override 183 public ParcelFileDescriptor openFile(Uri uri, String mode) 184 throws FileNotFoundException { 185 try { 186 ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); 187 AutoCloseOutputStream outputStream = new AutoCloseOutputStream(pipe[1]); 188 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 189 builder.build(new OutputStreamWriter(bos)); 190 outputStream.write(bos.toByteArray()); 191 outputStream.flush(); 192 outputStream.close(); 193 return pipe[0]; 194 } catch (Exception e) { 195 throw new FileNotFoundException(e.getMessage()); 196 } 197 } 198 }; 199 setupProvider(TEST_PROVIDER_AUTHORITY, cp); 200 mDestroyTask.add(() -> runOnExecutorSync(MODEL_EXECUTOR, () -> 201 UiDevice.getInstance(getInstrumentation()).executeShellCommand( 202 "settings delete secure launcher3.layout.provider"))); 203 return this; 204 } 205 206 /** 207 * Loads the model in memory synchronously 208 */ loadModelSync()209 public void loadModelSync() throws ExecutionException, InterruptedException { 210 Callbacks mockCb = new Callbacks() { }; 211 MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get(); 212 213 Executors.MODEL_EXECUTOR.submit(() -> { }).get(); 214 MAIN_EXECUTOR.submit(() -> { }).get(); 215 MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get(); 216 } 217 218 public static class SandboxModelContext extends SandboxContext { 219 220 private final MockContentResolver mMockResolver = new MockContentResolver(); 221 private final ArrayMap<String, Object> mSpiedServices = new ArrayMap<>(); 222 private final PackageManager mPm; 223 private final File mDbDir; 224 SandboxModelContext()225 SandboxModelContext() { 226 super(ApplicationProvider.getApplicationContext(), 227 UserCache.INSTANCE, InstallSessionHelper.INSTANCE, LauncherPrefs.INSTANCE, 228 LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE, 229 DisplayController.INSTANCE, CustomWidgetManager.INSTANCE, 230 SettingsCache.INSTANCE, PluginManagerWrapper.INSTANCE, 231 LockedUserState.INSTANCE, WallpaperColorHints.INSTANCE, 232 ItemInstallQueue.INSTANCE, WindowManagerProxy.INSTANCE); 233 234 // System settings cache content provider. Ensure that they are statically initialized 235 Settings.Secure.getString( 236 ApplicationProvider.getApplicationContext().getContentResolver(), "test"); 237 Settings.System.getString( 238 ApplicationProvider.getApplicationContext().getContentResolver(), "test"); 239 Settings.Global.getString( 240 ApplicationProvider.getApplicationContext().getContentResolver(), "test"); 241 242 mPm = spy(getBaseContext().getPackageManager()); 243 mDbDir = new File(getCacheDir(), UUID.randomUUID().toString()); 244 } 245 246 @Override createObject(MainThreadInitializedObject<T> object)247 protected <T> T createObject(MainThreadInitializedObject<T> object) { 248 if (object == LauncherAppState.INSTANCE) { 249 return (T) new LauncherAppState(this, null /* iconCacheFileName */); 250 } 251 return super.createObject(object); 252 } 253 allow(MainThreadInitializedObject object)254 public SandboxModelContext allow(MainThreadInitializedObject object) { 255 mAllowedObjects.add(object); 256 return this; 257 } 258 259 @Override getDatabasePath(String name)260 public File getDatabasePath(String name) { 261 if (!mDbDir.exists()) { 262 mDbDir.mkdirs(); 263 } 264 return new File(mDbDir, name); 265 } 266 267 @Override getContentResolver()268 public ContentResolver getContentResolver() { 269 return mMockResolver; 270 } 271 272 @Override onDestroy()273 public void onDestroy() { 274 if (deleteContents(mDbDir)) { 275 mDbDir.delete(); 276 } 277 super.onDestroy(); 278 } 279 280 @Override getPackageManager()281 public PackageManager getPackageManager() { 282 return mPm; 283 } 284 285 @Override getSystemService(String name)286 public Object getSystemService(String name) { 287 Object service = mSpiedServices.get(name); 288 return service != null ? service : super.getSystemService(name); 289 } 290 spyService(Class<T> tClass)291 public <T> T spyService(Class<T> tClass) { 292 String name = getSystemServiceName(tClass); 293 Object service = mSpiedServices.get(name); 294 if (service != null) { 295 return (T) service; 296 } 297 298 T result = spy(getSystemService(tClass)); 299 mSpiedServices.put(name, result); 300 return result; 301 } 302 setupProvider(String authority, ContentProvider provider)303 public void setupProvider(String authority, ContentProvider provider) { 304 ProviderInfo providerInfo = new ProviderInfo(); 305 providerInfo.authority = authority; 306 providerInfo.applicationInfo = getApplicationInfo(); 307 provider.attachInfo(this, providerInfo); 308 mMockResolver.addProvider(providerInfo.authority, provider); 309 doReturn(providerInfo).when(mPm).resolveContentProvider(eq(authority), anyInt()); 310 } 311 deleteContents(File dir)312 private static boolean deleteContents(File dir) { 313 File[] files = dir.listFiles(); 314 boolean success = true; 315 if (files != null) { 316 for (File file : files) { 317 if (file.isDirectory()) { 318 success &= deleteContents(file); 319 } 320 if (!file.delete()) { 321 success = false; 322 } 323 } 324 } 325 return success; 326 } 327 } 328 } 329