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