diff options
3 files changed, 293 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/wm/SplashScreenExceptionList.java b/services/core/java/com/android/server/wm/SplashScreenExceptionList.java new file mode 100644 index 000000000000..e815a0e2682a --- /dev/null +++ b/services/core/java/com/android/server/wm/SplashScreenExceptionList.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2021 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.server.wm; + +import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.ApplicationInfo; +import android.os.Build; +import android.provider.DeviceConfig; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.HashSet; +import java.util.Locale; +import java.util.concurrent.Executor; +import java.util.function.Supplier; + +/** + * Handles filtering the list of package that don't use the system splash screen. + * The list is backed by a {@link DeviceConfig} property. + * <p> + * An application can manually opt-out of the exception list by setting the <meta-data + * {@value OPT_OUT_METADATA_FLAG}="true"/> in the <code><application></code> section of the + * manifest. + */ +class SplashScreenExceptionList { + + private static final boolean DEBUG = Build.isDebuggable(); + private static final String LOG_TAG = "SplashScreenExceptionList"; + private static final String KEY_SPLASH_SCREEN_EXCEPTION_LIST = "splash_screen_exception_list"; + private static final String NAMESPACE = NAMESPACE_WINDOW_MANAGER; + private static final String OPT_OUT_METADATA_FLAG = "android.splashscreen.exception_opt_out"; + + @GuardedBy("mLock") + private final HashSet<String> mDeviceConfigExcludedPackages = new HashSet<>(); + private final Object mLock = new Object(); + + @VisibleForTesting + final DeviceConfig.OnPropertiesChangedListener mOnPropertiesChangedListener; + + SplashScreenExceptionList(@NonNull Executor executor) { + updateDeviceConfig(DeviceConfig.getString(NAMESPACE, KEY_SPLASH_SCREEN_EXCEPTION_LIST, "")); + mOnPropertiesChangedListener = properties -> updateDeviceConfig( + properties.getString(KEY_SPLASH_SCREEN_EXCEPTION_LIST, "")); + DeviceConfig.addOnPropertiesChangedListener(NAMESPACE, executor, + mOnPropertiesChangedListener); + } + + private void updateDeviceConfig(String values) { + parseDeviceConfigPackageList(values); + } + + /** + * Returns true if the packageName is in the list and the target sdk is before S. + * + * @param packageName The package name of the application to check + * @param targetSdk The target sdk of the application + * @param infoSupplier A {@link Supplier} that returns an {@link ApplicationInfo} used to + * check if the application wants to opt-out of the exception list in the + * manifest metadata. Evaluated only if the application is in the exception + * list. + */ + @SuppressWarnings("AndroidFrameworkCompatChange") // Target sdk check + public boolean isException(@NonNull String packageName, int targetSdk, + @Nullable Supplier<ApplicationInfo> infoSupplier) { + if (targetSdk >= Build.VERSION_CODES.S) { + return false; + } + + synchronized (mLock) { + if (DEBUG) { + Slog.v(LOG_TAG, String.format(Locale.US, + "SplashScreen checking exception for package %s (target sdk:%d) -> %s", + packageName, targetSdk, + mDeviceConfigExcludedPackages.contains(packageName))); + } + if (!mDeviceConfigExcludedPackages.contains(packageName)) { + return false; + } + } + return !isOptedOut(infoSupplier); + } + + /** + * An application can manually opt-out of the exception list by setting the meta-data + * {@value OPT_OUT_METADATA_FLAG} = true in the <code>application</code> section of the manifest + */ + private static boolean isOptedOut(@Nullable Supplier<ApplicationInfo> infoProvider) { + if (infoProvider == null) { + return false; + } + ApplicationInfo info = infoProvider.get(); + return info != null && info.metaData != null && info.metaData.getBoolean( + OPT_OUT_METADATA_FLAG, false); + } + + private void parseDeviceConfigPackageList(String rawList) { + synchronized (mLock) { + mDeviceConfigExcludedPackages.clear(); + String[] packages = rawList.split(","); + for (String packageName : packages) { + String packageNameTrimmed = packageName.trim(); + if (!packageNameTrimmed.isEmpty()) { + mDeviceConfigExcludedPackages.add(packageNameTrimmed); + } + } + } + } +} diff --git a/services/core/java/com/android/server/wm/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java index 39ff94038024..da257477b1d8 100644 --- a/services/core/java/com/android/server/wm/StartingSurfaceController.java +++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java @@ -26,6 +26,9 @@ import static android.window.StartingWindowInfo.TYPE_PARAMETER_USE_EMPTY_SPLASH_ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.os.SystemProperties; @@ -34,6 +37,8 @@ import android.window.TaskSnapshot; import com.android.server.policy.WindowManagerPolicy.StartingSurface; +import java.util.function.Supplier; + /** * Managing to create and release a starting window surface. */ @@ -44,9 +49,11 @@ public class StartingSurfaceController { static final boolean DEBUG_ENABLE_SHELL_DRAWER = SystemProperties.getBoolean("persist.debug.shell_starting_surface", true); private final WindowManagerService mService; + private final SplashScreenExceptionList mSplashScreenExceptionsList; public StartingSurfaceController(WindowManagerService wm) { mService = wm; + mSplashScreenExceptionsList = new SplashScreenExceptionList(wm.mContext.getMainExecutor()); } StartingSurface createSplashScreenStartingSurface(ActivityRecord activity, String packageName, @@ -68,6 +75,14 @@ public class StartingSurfaceController { return null; } + /** + * @see SplashScreenExceptionList#isException(String, int, Supplier) + */ + boolean isExceptionApp(@NonNull String packageName, int targetSdk, + @Nullable Supplier<ApplicationInfo> infoProvider) { + return mSplashScreenExceptionsList.isException(packageName, targetSdk, infoProvider); + } + int makeStartingWindowTypeParameter(boolean newTask, boolean taskSwitch, boolean processRunning, boolean allowTaskSnapshot, boolean activityCreated, boolean useEmpty) { diff --git a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java new file mode 100644 index 000000000000..3714d9984a0c --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2021 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.server.wm; + +import static android.os.Build.VERSION_CODES; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.pm.ApplicationInfo; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.Looper; +import android.platform.test.annotations.Presubmit; +import android.provider.DeviceConfig; + +import androidx.test.filters.MediumTest; + +import junit.framework.Assert; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test for the splash screen exception list + * atest WmTests:SplashScreenExceptionListTest + */ +@MediumTest +@Presubmit +public class SplashScreenExceptionListTest { + + // Constant copied on purpose so it's not refactored by accident. + // If the key needs to be modified, the server side key also needs to be changed. + private static final String KEY_SPLASH_SCREEN_EXCEPTION_LIST = "splash_screen_exception_list"; + + private DeviceConfig.Properties mInitialWindowManagerProperties; + private final HandlerExecutor mExecutor = new HandlerExecutor( + new Handler(Looper.getMainLooper())); + private final SplashScreenExceptionList mList = new SplashScreenExceptionList(mExecutor); + + @Before + public void setUp() throws Exception { + mInitialWindowManagerProperties = DeviceConfig.getProperties( + DeviceConfig.NAMESPACE_WINDOW_MANAGER); + clearConstrainDisplayApisFlags(); + } + + private void clearConstrainDisplayApisFlags() { + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, + KEY_SPLASH_SCREEN_EXCEPTION_LIST, + null, /* makeDefault= */ false); + } + + @After + public void tearDown() throws Exception { + DeviceConfig.setProperties(mInitialWindowManagerProperties); + DeviceConfig.removeOnPropertiesChangedListener(mList.mOnPropertiesChangedListener); + } + + @Test + public void packageFromDeviceConfigIgnored() { + setExceptionListAndWaitForCallback("com.test.nosplashscreen1,com.test.nosplashscreen2"); + + assertIsException("com.test.nosplashscreen1", null); + assertIsException("com.test.nosplashscreen2", null); + + assertIsNotException("com.test.nosplashscreen1", VERSION_CODES.S, null); + assertIsNotException("com.test.nosplashscreen2", VERSION_CODES.S, null); + assertIsNotException("com.test.splashscreen", VERSION_CODES.S, null); + assertIsNotException("com.test.splashscreen", VERSION_CODES.R, null); + } + + private void setExceptionListAndWaitForCallback(String commaSeparatedList) { + CountDownLatch latch = new CountDownLatch(1); + DeviceConfig.OnPropertiesChangedListener onPropertiesChangedListener = (p) -> { + if (commaSeparatedList.equals(p.getString(KEY_SPLASH_SCREEN_EXCEPTION_LIST, null))) { + latch.countDown(); + } + }; + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_WINDOW_MANAGER, + mExecutor, onPropertiesChangedListener); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, + KEY_SPLASH_SCREEN_EXCEPTION_LIST, commaSeparatedList, false); + try { + assertTrue("Timed out waiting for DeviceConfig to be updated.", + latch.await(1, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + Assert.fail(e.getMessage()); + } finally { + DeviceConfig.removeOnPropertiesChangedListener(onPropertiesChangedListener); + } + } + + @Test + public void metaDataOptOut() { + String packageName = "com.test.nosplashscreen_opt_out"; + setExceptionListAndWaitForCallback(packageName); + + Bundle metaData = new Bundle(); + ApplicationInfo activityInfo = new ApplicationInfo(); + activityInfo.metaData = metaData; + + // No Exceptions + metaData.putBoolean("android.splashscreen.exception_opt_out", true); + assertIsNotException(packageName, VERSION_CODES.R, activityInfo); + assertIsNotException(packageName, VERSION_CODES.S, activityInfo); + + // Exception Pre S + metaData.putBoolean("android.splashscreen.exception_opt_out", false); + assertIsException(packageName, activityInfo); + assertIsNotException(packageName, VERSION_CODES.S, activityInfo); + + // Edge Cases + activityInfo.metaData = null; + assertIsException(packageName, activityInfo); + assertIsException(packageName, null); + } + + private void assertIsNotException(String packageName, int targetSdk, + ApplicationInfo activityInfo) { + assertFalse(String.format("%s (sdk=%d) should have not been considered as an exception", + packageName, targetSdk), + mList.isException(packageName, targetSdk, () -> activityInfo)); + } + + private void assertIsException(String packageName, + ApplicationInfo activityInfo) { + assertTrue(String.format("%s (sdk=%d) should have been considered as an exception", + packageName, VERSION_CODES.R), + mList.isException(packageName, VERSION_CODES.R, () -> activityInfo)); + } +} |