Allow Activities to handle configuration changes
Make CONFIG_ASSETS_PATHS to public to allow activity itself to handle
assets paths config changes.
Introduce CONFIG_RESOURCES_UNUSED for applications that do not use
Resources at all like games to disabled all activity restart triggered
by any kind of configuration change.
Adding aapt2 support to allow app to handle all kinds of config change
based on the targeting SDK for more general cases.
Adding Framework host tests to test against non activity restart after
building and enabling frro.
Bug: b/180625460
Flag: android.content.res.handle_all_config_changes
Test: Added unit and CTS tests and verified all affected tests passed
Change-Id: I816df3604149ed337974ace83aef20d3d3566338
diff --git a/core/api/current.txt b/core/api/current.txt
index 831cf01..4fd9722 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -12040,6 +12040,7 @@
field public static final int COLOR_MODE_DEFAULT = 0; // 0x0
field public static final int COLOR_MODE_HDR = 2; // 0x2
field public static final int COLOR_MODE_WIDE_COLOR_GAMUT = 1; // 0x1
+ field @FlaggedApi("android.content.res.handle_all_config_changes") public static final int CONFIG_ASSETS_PATHS = -2147483648; // 0x80000000
field public static final int CONFIG_COLOR_MODE = 16384; // 0x4000
field public static final int CONFIG_DENSITY = 4096; // 0x1000
field public static final int CONFIG_FONT_SCALE = 1073741824; // 0x40000000
@@ -12053,6 +12054,7 @@
field public static final int CONFIG_MNC = 2; // 0x2
field public static final int CONFIG_NAVIGATION = 64; // 0x40
field public static final int CONFIG_ORIENTATION = 128; // 0x80
+ field @FlaggedApi("android.content.res.handle_all_config_changes") public static final int CONFIG_RESOURCES_UNUSED = 134217728; // 0x8000000
field public static final int CONFIG_SCREEN_LAYOUT = 256; // 0x100
field public static final int CONFIG_SCREEN_SIZE = 1024; // 0x400
field public static final int CONFIG_SMALLEST_SCREEN_SIZE = 2048; // 0x800
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index b384326..7bea0489 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -30,6 +30,7 @@
import static android.app.servertransaction.ActivityLifecycleItem.PRE_ON_CREATE;
import static android.content.ContentResolver.DEPRECATE_DATA_COLUMNS;
import static android.content.ContentResolver.DEPRECATE_DATA_PREFIX;
+import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED;
import static android.content.res.Configuration.UI_MODE_TYPE_DESK;
import static android.content.res.Configuration.UI_MODE_TYPE_MASK;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -6520,6 +6521,13 @@
return true;
}
+ if (android.content.res.Flags.handleAllConfigChanges()) {
+ if ((handledConfigChanges & CONFIG_RESOURCES_UNUSED) != 0) {
+ // Report the change if activities claim they do not use resources at all.
+ return true;
+ }
+ }
+
final int diffWithBucket = SizeConfigurationBuckets.filterDiff(publicDiff, currentConfig,
newConfig, sizeBuckets);
// Compare to the diff which filter the change without crossing size buckets with
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 4f06209..55e4c7c 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -941,6 +941,8 @@
CONFIG_COLOR_MODE,
CONFIG_FONT_SCALE,
CONFIG_GRAMMATICAL_GENDER,
+ CONFIG_ASSETS_PATHS,
+ CONFIG_RESOURCES_UNUSED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Config {}
@@ -1060,8 +1062,8 @@
* can itself handle asset path changes. Set from the {@link android.R.attr#configChanges}
* attribute. This is not a core resource configuration, but a higher-level value, so its
* constant starts at the high bits.
- * @hide We do not want apps handling this yet, but we do need some kind of bit for diffs.
*/
+ @FlaggedApi(android.content.res.Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES)
public static final int CONFIG_ASSETS_PATHS = 0x80000000;
/**
* Bit in {@link #configChanges} that indicates that the activity
@@ -1088,6 +1090,30 @@
*/
public static final int CONFIG_FONT_WEIGHT_ADJUSTMENT = 0x10000000;
+ /**
+ * <p>This is probably not the constant you want, the resources compiler supports a less
+ * dangerous version of it, 'allKnown', that only suppresses all currently existing
+ * configuration change restarts depending on your target SDK rather than whatever the latest
+ * SDK supports, allowing the application to work with resources on future Platform versions.
+ *
+ * <p>Bit in {@link #configChanges} that indicates that the activity doesn't use Android
+ * Resources at all and doesn't need to be restarted on any configuration changes. This bit
+ * disables all restarts for configuration dimensions available in the current target SDK as
+ * well as dimensions introduced in future SDKs. Use it only if the activity doesn't need
+ * anything from its resources, and doesn't depend on any libraries that may provide resources
+ * and need to respond to configuration changes. When set,
+ * {@link Activity#onConfigurationChanged(Configuration)} will be called instead of a restart,
+ * and it’s up to the implementation to ensure that no stale resource values remain loaded
+ * anywhere in the code.
+ *
+ * <p>This overrides all other bits, and this is recommended to be used individually.
+ *
+ * <p>This is not a core resource configuration, but a higher-level value, so its constant
+ * starts at the high bits.
+ */
+ @FlaggedApi(android.content.res.Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES)
+ public static final int CONFIG_RESOURCES_UNUSED = 0x8000000;
+
/** @hide
* Unfortunately the constants for config changes in native code are
* different from ActivityInfo. :( Here are the values we should use for the
@@ -1640,7 +1666,8 @@
* {@link #CONFIG_KEYBOARD}, {@link #CONFIG_NAVIGATION},
* {@link #CONFIG_ORIENTATION}, {@link #CONFIG_SCREEN_LAYOUT},
* {@link #CONFIG_DENSITY}, {@link #CONFIG_LAYOUT_DIRECTION},
- * {@link #CONFIG_COLOR_MODE}, and {link #CONFIG_GRAMMATICAL_GENDER}.
+ * {@link #CONFIG_COLOR_MODE}, {@link #CONFIG_GRAMMATICAL_GENDER},
+ * {@link #CONFIG_ASSETS_PATHS}, and {@link #CONFIG_RESOURCES_UNUSED}.
* Set from the {@link android.R.attr#configChanges} attribute.
*/
public int configChanges;
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index a475cc8..a5f8199 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -56,3 +56,13 @@
# This flag is read in ResourcesImpl at boot time.
is_fixed_read_only: true
}
+
+flag {
+ name: "handle_all_config_changes"
+ is_exported: true
+ namespace: "resource_manager"
+ description: "Feature flag for allowing activities to handle all kinds of configuration changes"
+ bug: "180625460"
+ # This flag is read at boot time.
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/inputmethodservice/ImsConfigurationTracker.java b/core/java/android/inputmethodservice/ImsConfigurationTracker.java
index 30ef0a2..17a5ffc 100644
--- a/core/java/android/inputmethodservice/ImsConfigurationTracker.java
+++ b/core/java/android/inputmethodservice/ImsConfigurationTracker.java
@@ -16,6 +16,8 @@
package android.inputmethodservice;
+import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED;
+
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -88,12 +90,16 @@
if (!mInitialized) {
return;
}
+ // Don't restart InputMethodService that claims it does not use resources at all.
+ boolean neverReset = android.content.res.Flags.handleAllConfigChanges()
+ && (mHandledConfigChanges & CONFIG_RESOURCES_UNUSED) != 0;
+
final int diff = mLastKnownConfig != null
? mLastKnownConfig.diffPublicOnly(newConfig) : CONFIG_CHANGED;
// If the new config is the same as the config this Service is already running with,
// then don't bother calling resetStateForNewConfiguration.
final int unhandledDiff = (diff & ~mHandledConfigChanges);
- if (unhandledDiff != 0) {
+ if (unhandledDiff != 0 && !neverReset) {
resetStateForNewConfigurationRunner.run();
}
if (diff != 0) {
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index f94c8ab..2e3dbda 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -1052,6 +1052,19 @@
<!-- The font weight adjustment value has changed. Used to reflect the user increasing font
weight. -->
<flag name="fontWeightAdjustment" value="0x10000000" />
+ <!-- The assets paths have changed. For example a runtime overlay is installed and enabled.
+ Corresponds to {@link android.content.pm.ActivityInfo#CONFIG_ASSETS_PATHS}. -->
+ <flag name="assetsPaths" value="0x80000000" />
+ <!-- This is probably not the flag you want, the resources compiler supports a less
+ dangerous version of it, 'allKnown', that only suppresses all currently existing
+ configuration change restarts depending on your target SDK rather than whatever the
+ latest SDK supports, allowing the application to work with resources on future Platform
+ versions.
+ Activity doesn't use Android Resources at all and doesn't need to be restarted on any
+ configuration changes. This overrides all other flags, and this is recommended to be
+ used individually. Corresponds to
+ {@link android.content.pm.ActivityInfo#CONFIG_RESOURCES_UNUSED}. -->
+ <flag name="resourcesUnused" value="0x8000000" />
</attr>
<!-- Indicate that the activity can be launched as the embedded child of another
diff --git a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java
index 064439e..6998c32 100644
--- a/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java
+++ b/core/tests/coretests/src/android/inputmethodservice/ImsConfigurationTrackerTest.java
@@ -24,6 +24,7 @@
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -74,4 +75,33 @@
assertFalse("IME shouldn't restart since it handles configChanges",
didReset.get());
}
+
+ @Test
+ @RequiresFlagsEnabled(android.content.res.Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES)
+ public void testShouldImeRestart_handleResourceUnused() throws Exception {
+ Configuration config = mContext.getResources().getConfiguration();
+ mImsConfigTracker.onInitialize(0 /* handledConfigChanges */);
+ mImsConfigTracker.onBindInput(mContext.getResources());
+ Configuration newConfig = new Configuration(config);
+
+ final AtomicBoolean didReset = new AtomicBoolean();
+ Runnable resetStateRunner = () -> didReset.set(true);
+
+ mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner);
+ assertFalse("IME shouldn't restart if config hasn't changed",
+ didReset.get());
+
+ // Screen density changed but IME doesn't handle configChanges
+ newConfig.densityDpi = 99;
+ mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner);
+ assertTrue("IME should restart for unhandled configChanges",
+ didReset.get());
+
+ didReset.set(false);
+ // opt-in IME to handle all configuration changes.
+ mImsConfigTracker.setHandledConfigChanges(ActivityInfo.CONFIG_RESOURCES_UNUSED);
+ mImsConfigTracker.onConfigurationChanged(newConfig, resetStateRunner);
+ assertFalse("IME shouldn't restart since it handles configChanges",
+ didReset.get());
+ }
}
diff --git a/core/tests/overlaytests/handle_config_change/Android.bp b/core/tests/overlaytests/handle_config_change/Android.bp
new file mode 100644
index 0000000..2b31d0a
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/Android.bp
@@ -0,0 +1,45 @@
+// Copyright (C) 2024 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+ default_team: "trendy_team_android_resources",
+}
+
+java_test_host {
+ name: "HandleConfigChangeHostTests",
+ srcs: ["src/**/*.java"],
+ libs: [
+ "tradefed",
+ "compatibility-host-util",
+ ],
+ static_libs: [
+ "compatibility-host-util-axt",
+ "flag-junit-host",
+ "android.content.res.flags-aconfig-java-host",
+ ],
+ test_suites: [
+ "device-tests",
+ ],
+ // All APKs required by the tests
+ data: [
+ ":OverlayResApp",
+ ],
+ per_testcase_directory: true,
+}
diff --git a/core/tests/overlaytests/handle_config_change/AndroidTest.xml b/core/tests/overlaytests/handle_config_change/AndroidTest.xml
new file mode 100644
index 0000000..05ea036
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<configuration description="Config for the handle config change test cases">
+ <option name="test-tag" value="HandleConfigChangeHostTests" />
+ <option name="test-suite-tag" value="apct" />
+
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="force-skip-system-props" value="true" />
+ <option name="set-global-setting" key="verifier_engprod" value="1" />
+ <option name="set-global-setting" key="verifier_verify_adb_installs" value="0" />
+ <option name="restore-settings" value="true" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="OverlayResApp.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.HostTest">
+ <option name="class" value="com.android.overlaytest.HandleConfigChangeHostTests" />
+ </test>
+</configuration>
diff --git a/core/tests/overlaytests/handle_config_change/src/com/android/overlaytest/HandleConfigChangeHostTests.java b/core/tests/overlaytests/handle_config_change/src/com/android/overlaytest/HandleConfigChangeHostTests.java
new file mode 100644
index 0000000..e91716a
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/src/com/android/overlaytest/HandleConfigChangeHostTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.overlaytest;
+
+import android.content.res.Flags;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.host.HostFlagsValueProvider;
+
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(DeviceJUnit4ClassRunner.class)
+@AppModeFull
+public class HandleConfigChangeHostTests extends BaseHostJUnit4Test {
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ HostFlagsValueProvider.createCheckFlagsRule(this::getDevice);
+ private static final String DEVICE_TEST_PKG1 = "com.android.overlaytest.overlayresapp";
+ private static final String DEVICE_TEST_CLASS = "OverlayResTest";
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_HANDLE_ALL_CONFIG_CHANGES)
+ public void testOverlayRes() throws Exception {
+ runDeviceTests(DEVICE_TEST_PKG1, DEVICE_TEST_PKG1 + "." + DEVICE_TEST_CLASS,
+ "overlayRes_onConfigurationChanged");
+ }
+}
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/Android.bp b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/Android.bp
new file mode 100644
index 0000000..e0f1012
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/Android.bp
@@ -0,0 +1,43 @@
+// Copyright (C) 2024 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "OverlayResApp",
+ srcs: ["src/**/*.java"],
+ resource_dirs: ["res"],
+ platform_apis: true,
+ certificate: "platform",
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.rules",
+ "androidx.test.core",
+ "compatibility-device-util-axt",
+ "truth",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ test_suites: [
+ "device-tests",
+ ],
+
+ manifest: "AndroidManifest.xml",
+}
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/AndroidManifest.xml b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/AndroidManifest.xml
new file mode 100644
index 0000000..617e879
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.overlaytest.overlayresapp"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ <activity
+ android:name=".OverlayResActivity"
+ android:exported="false"
+ android:configChanges="assetsPaths">
+ </activity>
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.overlaytest.overlayresapp" />
+
+</manifest>
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/integers.xml b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/integers.xml
new file mode 100644
index 0000000..493333f
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/integers.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources>
+ <integer name="test_integer">0</integer>
+</resources>
\ No newline at end of file
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/strings.xml b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/strings.xml
new file mode 100644
index 0000000..72820d3
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 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.
+ -->
+
+<resources>
+ <string name="app_name">My Application</string>
+ <string name="test_string">Test String</string>
+</resources>
\ No newline at end of file
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResActivity.java b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResActivity.java
new file mode 100644
index 0000000..96143ae
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResActivity.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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.overlaytest.overlayresapp;
+
+import android.app.Activity;
+import android.content.res.Configuration;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * A test activity to verify that the assets paths configuration changes are received if the
+ * overlay targeting state is changed.
+ */
+public class OverlayResActivity extends Activity {
+ private Runnable mConfigurationChangedCallback;
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ final Runnable callback = mConfigurationChangedCallback;
+ if (callback != null) {
+ callback.run();
+ }
+ }
+
+ /** Registers the callback of onConfigurationChanged. */
+ public void setConfigurationChangedCallback(Runnable callback) {
+ mConfigurationChangedCallback = callback;
+ }
+}
diff --git a/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResTest.java b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResTest.java
new file mode 100644
index 0000000..1c37719
--- /dev/null
+++ b/core/tests/overlaytests/handle_config_change/test-apps/OverlayResApp/src/com/android/overlaytest/overlayresapp/OverlayResTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2024 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.overlaytest.overlayresapp;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static junit.framework.Assert.assertNotNull;
+
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.content.om.FabricatedOverlay;
+import android.content.om.OverlayInfo;
+import android.content.om.OverlayManager;
+import android.content.om.OverlayManagerTransaction;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.util.TypedValue;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class OverlayResTest {
+ // Default timeout value
+ private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5);
+ private static final String TEST_OVERLAY_NAME = "Test";
+ private static final String TEST_RESOURCE_INTEGER = "integer/test_integer";
+ private static final String TEST_RESOURCE_STRING = "string/test_string";
+ private static final int TEST_INTEGER = 0;
+ private static final int TEST_FRRO_INTEGER = 1;
+ private static final String TEST_STRING = "Test String";
+ private static final String TEST_FRRO_STRING = "FRRO Test String";
+ private OverlayResActivity mActivity;
+ private Context mContext;
+ private OverlayManager mOverlayManager;
+ private int mUserId;
+ private UserHandle mUserHandle;
+
+ @Rule
+ public ActivityScenarioRule<OverlayResActivity> mActivityScenarioRule =
+ new ActivityScenarioRule<>(OverlayResActivity.class);
+
+ @Before
+ public void setUp() {
+ mActivityScenarioRule.getScenario().onActivity(activity -> {
+ assertThat(activity).isNotNull();
+ mActivity = activity;
+ });
+ mContext = mActivity.getApplicationContext();
+ mOverlayManager = mContext.getSystemService(OverlayManager.class);
+ mUserId = UserHandle.myUserId();
+ mUserHandle = UserHandle.of(mUserId);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ final OverlayManagerTransaction.Builder cleanUp = new OverlayManagerTransaction.Builder();
+ mOverlayManager.getOverlayInfosForTarget(mContext.getPackageName(), mUserHandle).forEach(
+ info -> {
+ if (info.isFabricated()) {
+ cleanUp.unregisterFabricatedOverlay(info.getOverlayIdentifier());
+ }
+ });
+ mOverlayManager.commit(cleanUp.build());
+
+ }
+
+ @Test
+ public void overlayRes_onConfigurationChanged() throws Exception {
+ final CountDownLatch latch1 = new CountDownLatch(1);
+ mActivity.setConfigurationChangedCallback(() -> {
+ Resources r = mActivity.getApplicationContext().getResources();
+ assertThat(r.getInteger(R.integer.test_integer)).isEqualTo(TEST_FRRO_INTEGER);
+ assertThat(r.getString(R.string.test_string)).isEqualTo(TEST_FRRO_STRING);
+ latch1.countDown();
+ });
+
+ // Create and enable FRRO
+ final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
+ mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
+ .setResourceValue(TEST_RESOURCE_INTEGER, TypedValue.TYPE_INT_DEC, TEST_FRRO_INTEGER)
+ .setResourceValue(TEST_RESOURCE_STRING, TypedValue.TYPE_STRING, TEST_FRRO_STRING)
+ .build();
+
+ mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+ .registerFabricatedOverlay(overlay)
+ .build());
+
+ OverlayInfo info = mOverlayManager.getOverlayInfo(overlay.getIdentifier(), mUserHandle);
+ assertNotNull(info);
+ assertThat(info.isEnabled()).isFalse();
+
+ mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+ .setEnabled(overlay.getIdentifier(), true, mUserId)
+ .build());
+
+ info = mOverlayManager.getOverlayInfo(overlay.getIdentifier(), mUserHandle);
+ assertNotNull(info);
+ assertThat(info.isEnabled()).isTrue();
+
+ if (!latch1.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ fail("Fail to wait configuration changes for build and enabling frro, "
+ + "onConfigurationChanged() has not been invoked.");
+ }
+
+ final CountDownLatch latch2 = new CountDownLatch(1);
+ mActivity.setConfigurationChangedCallback(() -> {
+ Resources r = mActivity.getApplicationContext().getResources();
+ assertThat(r.getInteger(R.integer.test_integer)).isEqualTo(TEST_INTEGER);
+ assertThat(r.getString(R.string.test_string)).isEqualTo(TEST_STRING);
+ latch2.countDown();
+ });
+
+ // unregister FRRO
+ mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+ .unregisterFabricatedOverlay(overlay.getIdentifier())
+ .build());
+
+ if (!latch2.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ fail("Fail to wait configuration changes after unregister frro,"
+ + " onConfigurationChanged() has not been invoked.");
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5159fc4..982f9e0 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -63,6 +63,7 @@
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY;
import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
+import static android.content.pm.ActivityInfo.CONFIG_RESOURCES_UNUSED;
import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
import static android.content.pm.ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
@@ -10184,6 +10185,15 @@
*/
private boolean shouldRelaunchLocked(int changes, Configuration changesConfig) {
int configChanged = info.getRealConfigChanged();
+ if (android.content.res.Flags.handleAllConfigChanges()) {
+ if ((configChanged & CONFIG_RESOURCES_UNUSED) != 0) {
+ // Don't relaunch any activities that claim they do not use resources at all.
+ // If they still do, the onConfigurationChanged() callback will get called to
+ // let them know anyway.
+ return false;
+ }
+ }
+
boolean onlyVrUiModeChanged = onlyVrUiModeChanged(changes, changesConfig);
// Override for apps targeting pre-O sdks
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp
index 669cddb..cbf2c2f 100644
--- a/tools/aapt2/link/ManifestFixer.cpp
+++ b/tools/aapt2/link/ManifestFixer.cpp
@@ -18,9 +18,9 @@
#include <unordered_set>
-#include "android-base/logging.h"
-
#include "ResourceUtils.h"
+#include "android-base/logging.h"
+#include "process/SymbolTable.h"
#include "trace/TraceBuffer.h"
#include "util/Util.h"
#include "xml/XmlActionExecutor.h"
@@ -172,6 +172,58 @@
return NameIsJavaClassName(el, attr, diag);
}
+static bool UpdateConfigChangesIfNeeded(xml::Element* el, IAaptContext* context) {
+ xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "configChanges");
+ if (attr == nullptr) {
+ return true;
+ }
+
+ if (attr->value != "allKnown" && attr->value.find("allKnown") != std::string::npos) {
+ context->GetDiagnostics()->Error(
+ android::DiagMessage(el->line_number)
+ << "If you want to declare 'allKnown' in attribute 'android:configChanges' in <" << el->name
+ << ">, " << attr->value << " is not allowed', allKnown has to be used "
+ << "by itself, for example: 'android:configChanges=allKnown', it cannot be combined with "
+ << "the other flags");
+ return false;
+ }
+
+ if (attr->value == "allKnown") {
+ SymbolTable* symbol_table = context->GetExternalSymbols();
+ const SymbolTable::Symbol* symbol =
+ symbol_table->FindByName(ResourceName("android", ResourceType::kAttr, "configChanges"));
+
+ if (symbol == nullptr) {
+ context->GetDiagnostics()->Error(
+ android::DiagMessage(el->line_number)
+ << "Cannot find symbol for android:configChanges with min sdk: "
+ << context->GetMinSdkVersion());
+ }
+
+ std::stringstream new_value;
+
+ const auto& symbols = symbol->attribute->symbols;
+ for (auto it = symbols.begin(); it != symbols.end(); ++it) {
+ // Skip 'resourcesUnused' which is the flag to fully disable activity restart specifically
+ // for games.
+ if (it->symbol.name.value().entry == "resourcesUnused") {
+ continue;
+ }
+ if (it != symbols.begin()) {
+ new_value << "|";
+ }
+ new_value << it->symbol.name.value().entry;
+ }
+ const auto& old_value = attr->value;
+ auto new_value_str = new_value.str();
+ context->GetDiagnostics()->Note(android::DiagMessage(el->line_number)
+ << "Updating value of 'android:configChanges' from "
+ << old_value << " to " << new_value_str);
+ attr->value = std::move(new_value_str);
+ }
+ return true;
+}
+
static bool RequiredNameIsJavaPackage(xml::Element* el, android::SourcePathDiagnostics* diag) {
xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "name");
if (attr == nullptr) {
@@ -382,8 +434,9 @@
}
}
-bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagnostics* diag) {
+bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, IAaptContext* context) {
// First verify some options.
+ android::IDiagnostics* diag = context->GetDiagnostics();
if (options_.rename_manifest_package) {
if (!util::IsJavaPackageName(options_.rename_manifest_package.value())) {
diag->Error(android::DiagMessage() << "invalid manifest package override '"
@@ -432,6 +485,8 @@
// Common component actions.
xml::XmlNodeAction component_action;
component_action.Action(RequiredNameIsJavaClassName);
+ component_action.Action(
+ [context](xml::Element* el) -> bool { return UpdateConfigChangesIfNeeded(el, context); });
component_action["intent-filter"] = intent_filter_action;
component_action["preferred"] = intent_filter_action;
component_action["meta-data"] = meta_data_action;
@@ -778,7 +833,7 @@
}
xml::XmlActionExecutor executor;
- if (!BuildRules(&executor, context->GetDiagnostics())) {
+ if (!BuildRules(&executor, context)) {
return false;
}
diff --git a/tools/aapt2/link/ManifestFixer.h b/tools/aapt2/link/ManifestFixer.h
index df0ece6..748a828 100644
--- a/tools/aapt2/link/ManifestFixer.h
+++ b/tools/aapt2/link/ManifestFixer.h
@@ -110,7 +110,7 @@
private:
DISALLOW_COPY_AND_ASSIGN(ManifestFixer);
- bool BuildRules(xml::XmlActionExecutor* executor, android::IDiagnostics* diag);
+ bool BuildRules(xml::XmlActionExecutor* executor, IAaptContext* context);
ManifestFixerOptions options_;
};
diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp
index 3cfdf78..5008627 100644
--- a/tools/aapt2/link/ManifestFixer_test.cpp
+++ b/tools/aapt2/link/ManifestFixer_test.cpp
@@ -37,27 +37,30 @@
.SetCompilationPackage("android")
.SetPackageId(0x01)
.SetNameManglerPolicy(NameManglerPolicy{"android"})
- .AddSymbolSource(
- test::StaticSymbolSourceBuilder()
- .AddSymbol(
- "android:attr/package", ResourceId(0x01010000),
- test::AttributeBuilder()
- .SetTypeMask(android::ResTable_map::TYPE_STRING)
- .Build())
- .AddSymbol(
- "android:attr/minSdkVersion", ResourceId(0x01010001),
- test::AttributeBuilder()
- .SetTypeMask(android::ResTable_map::TYPE_STRING |
- android::ResTable_map::TYPE_INTEGER)
- .Build())
- .AddSymbol(
- "android:attr/targetSdkVersion", ResourceId(0x01010002),
- test::AttributeBuilder()
- .SetTypeMask(android::ResTable_map::TYPE_STRING |
- android::ResTable_map::TYPE_INTEGER)
- .Build())
- .AddSymbol("android:string/str", ResourceId(0x01060000))
- .Build())
+ .AddSymbolSource(test::StaticSymbolSourceBuilder()
+ .AddSymbol("android:attr/package", ResourceId(0x01010000),
+ test::AttributeBuilder()
+ .SetTypeMask(android::ResTable_map::TYPE_STRING)
+ .Build())
+ .AddSymbol("android:attr/minSdkVersion", ResourceId(0x01010001),
+ test::AttributeBuilder()
+ .SetTypeMask(android::ResTable_map::TYPE_STRING |
+ android::ResTable_map::TYPE_INTEGER)
+ .Build())
+ .AddSymbol("android:attr/targetSdkVersion", ResourceId(0x01010002),
+ test::AttributeBuilder()
+ .SetTypeMask(android::ResTable_map::TYPE_STRING |
+ android::ResTable_map::TYPE_INTEGER)
+ .Build())
+ .AddSymbol("android:string/str", ResourceId(0x01060000))
+ .AddSymbol("android:attr/configChanges", ResourceId(0x01010003),
+ test::AttributeBuilder()
+ .AddItem("testConfigChange1", 1)
+ .AddItem("testConfigChange2", 2)
+ .AddItem("resourcesUnused", 4)
+ .SetTypeMask(android::ResTable_map::TYPE_STRING)
+ .Build())
+ .Build())
.Build();
}
@@ -1591,4 +1594,72 @@
</manifest>)";
EXPECT_THAT(Verify(input), NotNull());
}
+
+TEST_F(ManifestFixerTest, AllKnownNotDeclaredProperly) {
+ std::string input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity"
+ android:configChanges="allKnown|testConfigChange1">
+ </activity>
+ </application>
+ </manifest>)";
+ auto doc = Verify(input);
+ EXPECT_THAT(doc, IsNull());
+}
+
+TEST_F(ManifestFixerTest, ModifyAttributeValueForAllKnown) {
+ std::string input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity"
+ android:configChanges="allKnown">
+ </activity>
+ </application>
+ </manifest>)";
+ auto doc = Verify(input);
+ EXPECT_THAT(doc, NotNull());
+
+ xml::Element* el;
+ xml::Attribute* attr;
+
+ el = doc->root.get();
+ ASSERT_THAT(el, NotNull());
+ el = el->FindChild({}, "application");
+ ASSERT_THAT(el, NotNull());
+ el = el->FindChild({}, "activity");
+ ASSERT_THAT(el, NotNull());
+
+ attr = el->FindAttribute(xml::kSchemaAndroid, "configChanges");
+ ASSERT_THAT(attr->value, "testConfigChange1|testConfigChange2");
+}
+
+TEST_F(ManifestFixerTest, DoNothingForOtherConfigChanges) {
+ std::string input = R"(
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android">
+ <application>
+ <activity android:name=".MainActivity"
+ android:configChanges="testConfigChange2">
+ </activity>
+ </application>
+ </manifest>)";
+ auto doc = Verify(input);
+ EXPECT_THAT(doc, NotNull());
+
+ xml::Element* el;
+ xml::Attribute* attr;
+
+ el = doc->root.get();
+ ASSERT_THAT(el, NotNull());
+ el = el->FindChild({}, "application");
+ ASSERT_THAT(el, NotNull());
+ el = el->FindChild({}, "activity");
+ ASSERT_THAT(el, NotNull());
+
+ attr = el->FindAttribute(xml::kSchemaAndroid, "configChanges");
+ ASSERT_THAT(attr->value, "testConfigChange2");
+}
} // namespace aapt