Merge "Allow Activities to handle configuration changes" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 889b627..9ad8dd3 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -12047,6 +12047,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
@@ -12060,6 +12061,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 57ffed4..6952a09 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
@@ -1657,7 +1683,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 c8340a9..a1e71a8 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -64,6 +64,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;
@@ -9839,6 +9840,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