/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.util; import static android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; import static com.android.launcher3.util.TestUtil.runOnExecutorSync; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionParams; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Color; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.AutoCloseOutputStream; import android.provider.Settings; import android.test.mock.MockContentResolver; import android.util.ArrayMap; import androidx.test.core.app.ApplicationProvider; import androidx.test.uiautomator.UiDevice; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherModel; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.testing.TestInformationProvider; import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; /** * Utility class to help manage Launcher Model and related objects for test. */ public class LauncherModelHelper { public static final String TEST_PACKAGE = getInstrumentation().getContext().getPackageName(); public static final String TEST_ACTIVITY = "com.android.launcher3.tests.Activity2"; public static final String TEST_ACTIVITY2 = "com.android.launcher3.tests.Activity3"; public static final String TEST_ACTIVITY3 = "com.android.launcher3.tests.Activity4"; public static final String TEST_ACTIVITY4 = "com.android.launcher3.tests.Activity5"; public static final String TEST_ACTIVITY5 = "com.android.launcher3.tests.Activity6"; public static final String TEST_ACTIVITY6 = "com.android.launcher3.tests.Activity7"; public static final String TEST_ACTIVITY7 = "com.android.launcher3.tests.Activity8"; public static final String TEST_ACTIVITY8 = "com.android.launcher3.tests.Activity9"; public static final String TEST_ACTIVITY9 = "com.android.launcher3.tests.Activity10"; public static final String TEST_ACTIVITY10 = "com.android.launcher3.tests.Activity11"; public static final String TEST_ACTIVITY11 = "com.android.launcher3.tests.Activity12"; public static final String TEST_ACTIVITY12 = "com.android.launcher3.tests.Activity13"; public static final String TEST_ACTIVITY13 = "com.android.launcher3.tests.Activity14"; public static final String TEST_ACTIVITY14 = "com.android.launcher3.tests.Activity15"; // Authority for providing a test default-workspace-layout data. private static final String TEST_PROVIDER_AUTHORITY = LauncherModelHelper.class.getName().toLowerCase(); private static final int DEFAULT_BITMAP_SIZE = 10; private static final int DEFAULT_GRID_SIZE = 4; public final SandboxModelContext sandboxContext; private final RunnableList mDestroyTask = new RunnableList(); private BgDataModel mDataModel; public LauncherModelHelper() { sandboxContext = new SandboxModelContext(); } public void setupProvider(String authority, ContentProvider provider) { sandboxContext.setupProvider(authority, provider); } public LauncherModel getModel() { return LauncherAppState.getInstance(sandboxContext).getModel(); } public synchronized BgDataModel getBgDataModel() { if (mDataModel == null) { getModel().enqueueModelUpdateTask((taskController, dataModel, apps) -> mDataModel = dataModel); runOnExecutorSync(Executors.MODEL_EXECUTOR, () -> { }); } return mDataModel; } /** * Creates a installer session for the provided package. */ public int createInstallerSession(String pkg) throws IOException { SessionParams sp = new SessionParams(MODE_FULL_INSTALL); sp.setAppPackageName(pkg); Bitmap icon = Bitmap.createBitmap(100, 100, Config.ARGB_8888); icon.eraseColor(Color.RED); sp.setAppIcon(icon); sp.setAppLabel(pkg); PackageInstaller pi = sandboxContext.getPackageManager().getPackageInstaller(); int sessionId = pi.createSession(sp); mDestroyTask.add(() -> pi.abandonSession(sessionId)); return sessionId; } public void destroy() { // When destroying the context, make sure that the model thread is blocked, so that no // new jobs get posted while we are cleaning up CountDownLatch l1 = new CountDownLatch(1); CountDownLatch l2 = new CountDownLatch(1); MODEL_EXECUTOR.execute(() -> { l1.countDown(); waitOrThrow(l2); }); waitOrThrow(l1); sandboxContext.onDestroy(); l2.countDown(); mDestroyTask.executeAllAndDestroy(); } private void waitOrThrow(CountDownLatch latch) { try { latch.await(); } catch (Exception e) { throw new RuntimeException(e); } } /** * Sets up a mock provider to load the provided layout by default, next time the layout loads */ public LauncherModelHelper setupDefaultLayoutProvider(LauncherLayoutBuilder builder) throws Exception { InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(sandboxContext); idp.numRows = idp.numColumns = idp.numDatabaseHotseatIcons = DEFAULT_GRID_SIZE; idp.iconBitmapSize = DEFAULT_BITMAP_SIZE; UiDevice.getInstance(getInstrumentation()).executeShellCommand( "settings put secure launcher3.layout.provider " + TEST_PROVIDER_AUTHORITY); ContentProvider cp = new TestInformationProvider() { @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { try { ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); AutoCloseOutputStream outputStream = new AutoCloseOutputStream(pipe[1]); ByteArrayOutputStream bos = new ByteArrayOutputStream(); builder.build(new OutputStreamWriter(bos)); outputStream.write(bos.toByteArray()); outputStream.flush(); outputStream.close(); return pipe[0]; } catch (Exception e) { throw new FileNotFoundException(e.getMessage()); } } }; setupProvider(TEST_PROVIDER_AUTHORITY, cp); mDestroyTask.add(() -> runOnExecutorSync(MODEL_EXECUTOR, () -> UiDevice.getInstance(getInstrumentation()).executeShellCommand( "settings delete secure launcher3.layout.provider"))); return this; } /** * Loads the model in memory synchronously */ public void loadModelSync() throws ExecutionException, InterruptedException { Callbacks mockCb = new Callbacks() { }; MAIN_EXECUTOR.submit(() -> getModel().addCallbacksAndLoad(mockCb)).get(); Executors.MODEL_EXECUTOR.submit(() -> { }).get(); MAIN_EXECUTOR.submit(() -> { }).get(); MAIN_EXECUTOR.submit(() -> getModel().removeCallbacks(mockCb)).get(); } public static class SandboxModelContext extends SandboxContext { private final MockContentResolver mMockResolver = new MockContentResolver(); private final ArrayMap mSpiedServices = new ArrayMap<>(); private final PackageManager mPm; private final File mDbDir; public SandboxModelContext() { super(ApplicationProvider.getApplicationContext()); // System settings cache content provider. Ensure that they are statically initialized Settings.Secure.getString( ApplicationProvider.getApplicationContext().getContentResolver(), "test"); Settings.System.getString( ApplicationProvider.getApplicationContext().getContentResolver(), "test"); Settings.Global.getString( ApplicationProvider.getApplicationContext().getContentResolver(), "test"); mPm = spy(getBaseContext().getPackageManager()); mDbDir = new File(getCacheDir(), UUID.randomUUID().toString()); } @Override public T createObject(MainThreadInitializedObject object) { if (object == LauncherAppState.INSTANCE) { return (T) new LauncherAppState(this, null /* iconCacheFileName */); } return super.createObject(object); } @Override public File getDatabasePath(String name) { if (!mDbDir.exists()) { mDbDir.mkdirs(); } return new File(mDbDir, name); } @Override public ContentResolver getContentResolver() { return mMockResolver; } @Override public void onDestroy() { if (deleteContents(mDbDir)) { mDbDir.delete(); } super.onDestroy(); } @Override public PackageManager getPackageManager() { return mPm; } @Override public Object getSystemService(String name) { Object service = mSpiedServices.get(name); return service != null ? service : super.getSystemService(name); } public T spyService(Class tClass) { String name = getSystemServiceName(tClass); Object service = mSpiedServices.get(name); if (service != null) { return (T) service; } T result = spy(getSystemService(tClass)); mSpiedServices.put(name, result); return result; } public void setupProvider(String authority, ContentProvider provider) { ProviderInfo providerInfo = new ProviderInfo(); providerInfo.authority = authority; providerInfo.applicationInfo = getApplicationInfo(); provider.attachInfo(this, providerInfo); mMockResolver.addProvider(providerInfo.authority, provider); doReturn(providerInfo).when(mPm).resolveContentProvider(eq(authority), anyInt()); } private static boolean deleteContents(File dir) { File[] files = dir.listFiles(); boolean success = true; if (files != null) { for (File file : files) { if (file.isDirectory()) { success &= deleteContents(file); } if (!file.delete()) { success = false; } } } return success; } } }