Merge "Update IpcDataCache documentation" into main
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index daa15f0..9be928f 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -213,19 +213,17 @@
* <a href="{@docRoot}guide/topics/admin/device-admin.html">Device Administration</a>
* developer guide.
*
- * <p id="devicepolicycontroller">Through <a href="#managed_provisioning">Managed Provisioning</a>,
- * Device Administrator apps can also be recognised as <b>
- Device Policy Controllers</b>. Device Policy Controllers can be one of
+ * <p id="devicepolicycontroller">Device Administrator apps can also be recognised as <b>
+ * Device Policy Controllers</b>. Device Policy Controllers can be one of
* two types:
* <ul>
* <li>A <i id="deviceowner">Device Owner</i>, which only ever exists on the
- * {@link UserManager#isSystemUser System User} or {@link UserManager#isMainUser Main User}, is
+ * {@link UserManager#isSystemUser System User} or Main User, is
* the most powerful type of Device Policy Controller and can affect policy across the device.
* <li>A <i id="profileowner">Profile Owner<i>, which can exist on any user, can
* affect policy on the user it is on, and when it is running on
* {@link UserManager#isProfile a profile} has
- * <a href="#profile-on-parent">limited</a> ability to affect policy on its
- * {@link UserManager#getProfileParent parent}.
+ * <a href="#profile-on-parent">limited</a> ability to affect policy on its parent.
* </ul>
*
* <p>Additional capabilities can be provided to Device Policy Controllers in
@@ -233,7 +231,7 @@
* <ul>
* <li>A Profile Owner on an <a href="#organization-owned">organization owned</a> device has access
* to additional abilities, both <a href="#profile-on-parent-organization-owned">affecting policy on the profile's</a>
- * {@link UserManager#getProfileParent parent} and also the profile itself.
+ * parent and also the profile itself.
* <li>A Profile Owner running on the {@link UserManager#isSystemUser System User} has access to
* additional capabilities which affect the {@link UserManager#isSystemUser System User} and
* also the whole device.
@@ -245,13 +243,12 @@
* Controller</a>.
*
* <p><a href="#permissions">Permissions</a> are generally only given to apps
- * fulfilling particular key roles on the device (such as managing {@link DeviceLockManager
-device locks}).
+ * fulfilling particular key roles on the device (such as managing
+ * {@link android.devicelock.DeviceLockManager device locks}).
*
* <p id="roleholder"><b>Device Policy Management Role Holder</b>
- * <p>One app on the device fulfills the {@link RoleManager#ROLE_DEVICE_POLICY_MANAGEMENT Device
-Policy Management Role} and is trusted with managing the overall state of
- * Device Policy. This has access to much more powerful methods than
+ * <p>One app on the device fulfills the Device Policy Management Role and is trusted with managing
+ * the overall state of Device Policy. This has access to much more powerful methods than
* <a href="#managingapps">managing apps</a>.
*
* <p id="querying"><b>Querying Device Policy</b>
@@ -273,7 +270,7 @@
*
* <p id="managed_profile">A <b>Managed Profile</b> enables data separation. For example to use
* a device both for personal and corporate usage. The managed profile and its
- * {@link UserManager#getProfileParent parent} share a launcher.
+ * parent share a launcher.
*
* <p id="affiliated"><b>Affiliation</b>
* <p>Using the {@link #setAffiliationIds} method, a
@@ -6643,7 +6640,7 @@
* @param flags May be 0 or {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY}.
* @throws SecurityException if the calling application does not own an active administrator
* that uses {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} and the does not hold
- * the {@link android.Manifest.permission#LOCK_DEVICE} permission, or
+ * the LOCK_DEVICE permission, or
* the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is passed by an
* application that is not a profile owner of a managed profile.
* @throws IllegalArgumentException if the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 031380d..044178c 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -20,6 +20,7 @@
import static android.content.ContentProvider.maybeAddUserId;
import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
import static android.security.Flags.FLAG_FRP_ENFORCEMENT;
+import static android.security.Flags.preventIntentRedirect;
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
@@ -7687,9 +7688,17 @@
/** @hide */
public static final int LOCAL_FLAG_FROM_SYSTEM = 1 << 5;
+ /**
+ * This flag indicates the creator token of this intent has been verified.
+ *
+ * @hide
+ */
+ public static final int LOCAL_FLAG_CREATOR_TOKEN_VERIFIED = 1 << 6;
+
/** @hide */
@IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = {
EXTENDED_FLAG_FILTER_MISMATCH,
+ EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ExtendedFlags {}
@@ -7703,6 +7712,13 @@
@TestApi
public static final int EXTENDED_FLAG_FILTER_MISMATCH = 1 << 0;
+ /**
+ * This flag indicates the creator token of this intent is either missing or invalid.
+ *
+ * @hide
+ */
+ public static final int EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN = 1 << 1;
+
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// toUri() and parseUri() options.
@@ -7870,6 +7886,7 @@
this.mPackage = o.mPackage;
this.mComponent = o.mComponent;
this.mOriginalIntent = o.mOriginalIntent;
+ this.mCreatorTokenInfo = o.mCreatorTokenInfo;
if (o.mCategories != null) {
this.mCategories = new ArraySet<>(o.mCategories);
@@ -12176,6 +12193,60 @@
return (mExtras != null) ? mExtras.describeContents() : 0;
}
+ private static class CreatorTokenInfo {
+ // Stores a creator token for an intent embedded as an extra intent in a top level intent,
+ private IBinder mCreatorToken;
+ // Stores all extra keys whose values are intents for a top level intent.
+ private ArraySet<String> mExtraIntentKeys;
+ }
+
+ private @Nullable CreatorTokenInfo mCreatorTokenInfo;
+
+ /** @hide */
+ public void removeCreatorTokenInfo() {
+ mCreatorTokenInfo = null;
+ }
+
+ /** @hide */
+ public @Nullable IBinder getCreatorToken() {
+ return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mCreatorToken;
+ }
+
+ /** @hide */
+ public Set<String> getExtraIntentKeys() {
+ return mCreatorTokenInfo == null ? null : mCreatorTokenInfo.mExtraIntentKeys;
+ }
+
+ /** @hide */
+ public void setCreatorToken(@NonNull IBinder creatorToken) {
+ if (mCreatorTokenInfo == null) {
+ mCreatorTokenInfo = new CreatorTokenInfo();
+ }
+ mCreatorTokenInfo.mCreatorToken = creatorToken;
+ }
+
+ /**
+ * Collects keys in the extra bundle whose value are intents.
+ * @hide
+ */
+ public void collectExtraIntentKeys() {
+ if (!preventIntentRedirect()) return;
+
+ if (mExtras != null && !mExtras.isParcelled() && !mExtras.isEmpty()) {
+ for (String key : mExtras.keySet()) {
+ if (mExtras.get(key) instanceof Intent) {
+ if (mCreatorTokenInfo == null) {
+ mCreatorTokenInfo = new CreatorTokenInfo();
+ }
+ if (mCreatorTokenInfo.mExtraIntentKeys == null) {
+ mCreatorTokenInfo.mExtraIntentKeys = new ArraySet<>();
+ }
+ mCreatorTokenInfo.mExtraIntentKeys.add(key);
+ }
+ }
+ }
+ }
+
public void writeToParcel(Parcel out, int flags) {
out.writeString8(mAction);
Uri.writeToParcel(out, mData);
@@ -12225,6 +12296,16 @@
} else {
out.writeInt(0);
}
+
+ if (preventIntentRedirect()) {
+ if (mCreatorTokenInfo == null) {
+ out.writeInt(0);
+ } else {
+ out.writeInt(1);
+ out.writeStrongBinder(mCreatorTokenInfo.mCreatorToken);
+ out.writeArraySet(mCreatorTokenInfo.mExtraIntentKeys);
+ }
+ }
}
public static final @android.annotation.NonNull Parcelable.Creator<Intent> CREATOR
@@ -12282,6 +12363,14 @@
if (in.readInt() != 0) {
mOriginalIntent = new Intent(in);
}
+
+ if (preventIntentRedirect()) {
+ if (in.readInt() != 0) {
+ mCreatorTokenInfo = new CreatorTokenInfo();
+ mCreatorTokenInfo.mCreatorToken = in.readStrongBinder();
+ mCreatorTokenInfo.mExtraIntentKeys = (ArraySet<String>) in.readArraySet(null);
+ }
+ }
}
/**
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index a2d24f6..73b5d94 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -432,6 +432,11 @@
public abstract IntArray getDisplayGroupIds();
/**
+ * Get all available display ids.
+ */
+ public abstract IntArray getDisplayIds();
+
+ /**
* Called upon presentation started/ended on the display.
* @param displayId the id of the display where presentation started.
* @param isShown whether presentation is shown.
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 97e9f34..ed75491 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -1111,6 +1111,21 @@
}
/**
+ * Called whenever the stub implementation throws an exception which isn't propagated to the
+ * remote caller by the binder. If this method isn't overridden, this exception is swallowed,
+ * and some default return values are propagated to the caller.
+ *
+ * <br> <b> This should not throw. </b> Doing so would defeat the purpose of this handler, and
+ * suppress the exception it is handling.
+ *
+ * @param code The transaction code being handled
+ * @param e The exception which was thrown.
+ * @hide
+ */
+ protected void onUnhandledException(int code, int flags, Exception e) {
+ }
+
+ /**
* @param in The raw file descriptor that an input data stream can be read from.
* @param out The raw file descriptor that normal command messages should be written to.
* @param err The raw file descriptor that command error messages should be written to.
@@ -1408,10 +1423,15 @@
} else {
Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e);
}
+ onUnhandledException(code, flags, e);
} else {
// Clear the parcel before writing the exception.
reply.setDataSize(0);
reply.setDataPosition(0);
+ // The writeException below won't do anything useful if this is the case.
+ if (Parcel.getExceptionCode(e) == 0) {
+ onUnhandledException(code, flags, e);
+ }
reply.writeException(e);
}
res = true;
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 56d3669..45e9def 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -52,3 +52,11 @@
description: "Opt the system into enforcement of BAL"
bug: "339403750"
}
+
+flag {
+ name: "prevent_intent_redirect"
+ namespace: "responsible_apis"
+ description: "Prevent intent redirect attacks"
+ bug: "361143368"
+ is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index d98836f..9821d43 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -25,6 +25,7 @@
"BinderProxyCountingTestApp/src/**/*.java",
"BinderProxyCountingTestService/src/**/*.java",
"BinderDeathRecipientHelperApp/src/**/*.java",
+ "AppThatCallsBinderMethods/src/**/*.kt",
],
visibility: ["//visibility:private"],
}
@@ -104,6 +105,7 @@
"mockito-target-extended-minus-junit4",
"TestParameterInjector",
"android.content.res.flags-aconfig-java",
+ "android.security.flags-aconfig-java",
],
libs: [
@@ -143,6 +145,7 @@
":BinderProxyCountingTestApp",
":BinderProxyCountingTestService",
":AppThatUsesAppOps",
+ ":AppThatCallsBinderMethods",
],
}
diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml
index b1f1e2c..05ab783 100644
--- a/core/tests/coretests/AndroidTest.xml
+++ b/core/tests/coretests/AndroidTest.xml
@@ -26,6 +26,7 @@
<option name="test-file-name" value="BinderProxyCountingTestApp.apk" />
<option name="test-file-name" value="BinderProxyCountingTestService.apk" />
<option name="test-file-name" value="AppThatUsesAppOps.apk" />
+ <option name="test-file-name" value="AppThatCallsBinderMethods.apk" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/Android.bp b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp
new file mode 100644
index 0000000..dcc0d4f
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp
@@ -0,0 +1,20 @@
+// 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.
+
+android_test_helper_app {
+ name: "AppThatCallsBinderMethods",
+ srcs: ["src/**/*.kt"],
+ platform_apis: true,
+ static_libs: ["coretests-aidl"],
+}
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml
new file mode 100644
index 0000000..b2f6d78
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?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.frameworks.coretests.methodcallerhelperapp">
+ <application>
+ <receiver android:name="com.android.frameworks.coretests.methodcallerhelperapp.CallMethodsReceiver"
+ android:exported="true"/>
+ </application>
+
+</manifest>
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt
new file mode 100644
index 0000000..638cc3b
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.frameworks.coretests.methodcallerhelperapp
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+
+import com.android.frameworks.coretests.aidl.ITestInterface
+
+/**
+ * Receiver used to call methods when a binder is received
+ * {@link android.os.BinderUncaughtExceptionHandlerTest}.
+ */
+class CallMethodsReceiver : BroadcastReceiver() {
+ private val TAG = "CallMethodsReceiver"
+
+ override fun onReceive(context: Context, intent: Intent) {
+ try {
+ when (intent.getAction()) {
+ ACTION_CALL_METHOD -> intent.getExtras()!!.let {
+ Log.i(TAG, "Received ACTION_CALL_METHOD with extras: $it")
+ val iface = it.getBinder(EXTRA_BINDER)!!.let(ITestInterface.Stub::asInterface)!!
+ val name = it.getString(EXTRA_METHOD_NAME)!!
+ try {
+ when (name) {
+ "foo" -> iface.foo(5)
+ "onewayFoo" -> iface.onewayFoo(5)
+ "bar" -> iface.bar(5)
+ else -> Log.e(TAG, "Unknown method name")
+ }
+ } catch (e: Exception) {
+ // Exceptions expected
+ }
+ }
+ else -> Log.e(TAG, "Unknown action " + intent.getAction())
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception: ", e)
+ }
+ }
+}
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt
new file mode 100644
index 0000000..37c6268
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.frameworks.coretests.methodcallerhelperapp
+
+const val PACKAGE_NAME = "com.android.frameworks.coretests.methodcallerhelperapp"
+const val RECEIVER_NAME = "CallMethodsReceiver"
+const val ACTION_CALL_METHOD = PACKAGE_NAME + ".ACTION_CALL_METHOD"
+const val EXTRA_METHOD_NAME = PACKAGE_NAME + ".EXTRA_METHOD_NAME"
+const val EXTRA_BINDER = PACKAGE_NAME + ".EXTRA_BINDER"
diff --git a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl
new file mode 100644
index 0000000..ffcf178
--- /dev/null
+++ b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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.frameworks.coretests.aidl;
+
+/**
+ * Just an interface with a oneway, void and non-oneway method.
+ */
+interface ITestInterface {
+ // Method order matters, since we verify transaction codes
+ int foo(int a);
+ oneway void onewayFoo(int a);
+ void bar(int a);
+}
diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java
new file mode 100644
index 0000000..d169ce3
--- /dev/null
+++ b/core/tests/coretests/src/android/content/IntentTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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 android.content;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.security.Flags;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest FrameworksCoreTests:IntentTest
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class IntentTest {
+ private static final String TEST_ACTION = "android.content.IntentTest_test";
+ private static final String TEST_EXTRA_NAME = "testExtraName";
+ private static final Uri TEST_URI = Uri.parse("content://com.example/people");
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT)
+ public void testReadFromParcelWithExtraIntentKeys() {
+ Intent intent = new Intent("TEST_ACTION");
+ intent.putExtra(TEST_EXTRA_NAME, new Intent(TEST_ACTION));
+ intent.putExtra(TEST_EXTRA_NAME + "2", 1);
+
+ intent.collectExtraIntentKeys();
+ final Parcel parcel = Parcel.obtain();
+ intent.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ final Intent target = new Intent();
+ target.readFromParcel(parcel);
+
+ assertEquals(intent.getAction(), target.getAction());
+ assertEquals(intent.getExtraIntentKeys(), target.getExtraIntentKeys());
+ assertThat(intent.getExtraIntentKeys()).hasSize(1);
+ }
+
+ @Test
+ public void testCreatorTokenInfo() {
+ Intent intent = new Intent(TEST_ACTION);
+ IBinder creatorToken = new Binder();
+
+ intent.setCreatorToken(creatorToken);
+ assertThat(intent.getCreatorToken()).isEqualTo(creatorToken);
+
+ intent.removeCreatorTokenInfo();
+ assertThat(intent.getCreatorToken()).isNull();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_PREVENT_INTENT_REDIRECT)
+ public void testCollectExtraIntentKeys() {
+ Intent intent = new Intent(TEST_ACTION);
+ Intent extraIntent = new Intent(TEST_ACTION, TEST_URI);
+ intent.putExtra(TEST_EXTRA_NAME, extraIntent);
+
+ intent.collectExtraIntentKeys();
+
+ assertThat(intent.getExtraIntentKeys()).hasSize(1);
+ assertThat(intent.getExtraIntentKeys()).contains(TEST_EXTRA_NAME);
+ }
+
+}
diff --git a/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt
new file mode 100644
index 0000000..791c209
--- /dev/null
+++ b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt
@@ -0,0 +1,247 @@
+/*
+ * 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 android.os
+
+import android.content.Intent
+import android.platform.test.annotations.DisabledOnRavenwood
+import android.platform.test.annotations.Presubmit
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+
+import com.android.frameworks.coretests.aidl.ITestInterface
+import com.android.frameworks.coretests.methodcallerhelperapp.*
+
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.mockito.ArgumentMatcher
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.intThat
+import org.mockito.Mockito.after
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.quality.Strictness.STRICT_STUBS
+
+private const val TIMEOUT_DURATION_MS = 2000L
+private const val FALSE_NEG_DURATION_MS = 500L
+private const val FLAG_ONEWAY = 1
+// From ITestInterface.Stub class, these values are package private
+private const val TRANSACTION_foo = 1
+private const val TRANSACTION_onewayFoo = 2
+private const val TRANSACTION_bar = 3
+
+/** Tests functionality of {@link android.os.Binder.onUnhandledException}. */
+@DisabledOnRavenwood(reason = "multi-app")
+@Presubmit
+@RunWith(AndroidJUnit4::class)
+class BinderUncaughtExceptionHandlerTest {
+
+ val mContext = InstrumentationRegistry.getInstrumentation().getTargetContext()
+
+ @Rule @JvmField val rule = MockitoJUnit.rule().strictness(STRICT_STUBS)
+
+ @Spy var mInterfaceImpl: ITestImpl = ITestImpl()
+
+ // This subclass is needed for visibility issues (via protected), since the method we are
+ // verifying lives on the boot classpath, it is not enough to be in the same package.
+ open class ITestImpl : ITestInterface.Stub() {
+ override fun onUnhandledException(code: Int, flags: Int, e: Exception?) =
+ onUnhandledExceptionVisible(code, flags, e)
+
+ public open fun onUnhandledExceptionVisible(code: Int, flags: Int, e: Exception?) {}
+
+ @Throws(RemoteException::class)
+ override open fun foo(x: Int): Int = throw UnsupportedOperationException()
+
+ @Throws(RemoteException::class)
+ override open fun onewayFoo(x: Int): Unit = throw UnsupportedOperationException()
+
+ @Throws(RemoteException::class)
+ override open fun bar(x: Int): Unit = throw UnsupportedOperationException()
+ }
+
+ class OnewayMatcher(private val isOneway: Boolean) : ArgumentMatcher<Int> {
+ override fun matches(argument: Int?) =
+ (argument!! and FLAG_ONEWAY) == if (isOneway) 1 else 0
+
+ override fun toString() = "Expected oneway: $isOneway"
+ }
+
+ @Test
+ fun testRegularMethod_ifThrowsRuntimeException_HandlerCalled() {
+ val myException = RuntimeException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+ dispatchActionCall("foo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_foo),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testRegularMethod_ifThrowsRemoteException_HandlerCalled() {
+ val myException = RemoteException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+ dispatchActionCall("foo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_foo),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testRegularMethod_ifThrowsSecurityException_HandlerNotCalled() {
+ val myException = SecurityException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+ dispatchActionCall("foo")
+
+ // No unexpected calls
+ verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never())
+ .onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testVoidMethod_ifThrowsRuntimeException_HandlerCalled() {
+ val myException = RuntimeException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+ dispatchActionCall("bar")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_bar),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testVoidMethod_ifThrowsRemoteException_HandlerCalled() {
+ val myException = RemoteException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+ dispatchActionCall("bar")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_bar),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testVoidMethod_ifThrowsSecurityException_HandlerNotCalled() {
+ val myException = SecurityException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+ dispatchActionCall("bar")
+
+ // No unexpected calls
+ verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never())
+ .onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testOnewayMethod_ifThrowsRuntimeException_HandlerCalled() {
+ val myException = RuntimeException("Test exception")
+ doThrow(myException).doNothing().`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+ dispatchActionCall("onewayFoo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+ /* flags= */ intThat(OnewayMatcher(true)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testOnewayMethod_ifThrowsRemoteException_HandlerCalled() {
+ val myException = RemoteException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+ dispatchActionCall("onewayFoo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+ /* flags= */ intThat(OnewayMatcher(true)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ // All exceptions are uncaught for oneway
+ @Test
+ fun testOnewayMethod_ifThrowsSecurityException_HandlerCalled() {
+ val myException = SecurityException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+ dispatchActionCall("onewayFoo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+ /* flags= */ intThat(OnewayMatcher(true)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ private fun dispatchActionCall(methodName: String) =
+ Intent(ACTION_CALL_METHOD).apply {
+ putExtras(
+ Bundle().apply {
+ putBinder(EXTRA_BINDER, mInterfaceImpl as IBinder)
+ putString(EXTRA_METHOD_NAME, methodName)
+ }
+ )
+ setClassName(PACKAGE_NAME, CallMethodsReceiver::class.java.getName())
+ }.let { mContext.sendBroadcast(it) }
+}
diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
index 005538a..89c7582 100644
--- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
+++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
@@ -78,6 +78,16 @@
}
@Test
+ public void testNegatedDisabledFlag() {
+ assertThat(mResources.getBoolean(R.bool.bool5)).isTrue();
+ }
+
+ @Test
+ public void testNegatedEnabledFlag() {
+ assertThat(mResources.getBoolean(R.bool.bool6)).isTrue();
+ }
+
+ @Test
public void testFlagEnabledDifferentCompilationUnit() {
assertThat(mResources.getBoolean(R.bool.bool3)).isTrue();
}
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index b6476c9..ae46a99 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -50,6 +50,10 @@
constexpr bool resample_gainmap_regions() {
return false;
}
+
+constexpr bool query_global_priority() {
+ return false;
+}
} // namespace hwui_flags
#endif
@@ -110,6 +114,7 @@
bool Properties::hdr10bitPlus = false;
bool Properties::skipTelemetry = false;
bool Properties::resampleGainmapRegions = false;
+bool Properties::queryGlobalPriority = false;
int Properties::timeoutMultiplier = 1;
@@ -187,6 +192,7 @@
hdr10bitPlus = hwui_flags::hdr_10bit_plus();
resampleGainmapRegions = base::GetBoolProperty("debug.hwui.resample_gainmap_regions",
hwui_flags::resample_gainmap_regions());
+ queryGlobalPriority = hwui_flags::query_global_priority();
timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1);
skipTelemetry = base::GetBoolProperty(PROPERTY_SKIP_EGLMANAGER_TELEMETRY,
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index db47152..6f84796 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -346,6 +346,7 @@
static bool hdr10bitPlus;
static bool skipTelemetry;
static bool resampleGainmapRegions;
+ static bool queryGlobalPriority;
static int timeoutMultiplier;
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index ab052b9..93df478 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -129,3 +129,13 @@
description: "APIs that expose gainmap metadata corresponding to those defined in ISO 21496-1"
bug: "349357636"
}
+
+flag {
+ name: "query_global_priority"
+ namespace: "core_graphics"
+ description: "Attempt to query whether the vulkan driver supports the requested global priority before queue creation."
+ bug: "343986434"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index e302393..6571d92 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -44,7 +44,7 @@
namespace renderthread {
// Not all of these are strictly required, but are all enabled if present.
-static std::array<std::string_view, 21> sEnableExtensions{
+static std::array<std::string_view, 23> sEnableExtensions{
VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME,
@@ -65,6 +65,8 @@
VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME,
VK_KHR_ANDROID_SURFACE_EXTENSION_NAME,
VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME,
+ VK_EXT_GLOBAL_PRIORITY_QUERY_EXTENSION_NAME,
+ VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME,
VK_EXT_DEVICE_FAULT_EXTENSION_NAME,
};
@@ -206,7 +208,7 @@
GET_INST_PROC(GetPhysicalDeviceFeatures2);
GET_INST_PROC(GetPhysicalDeviceImageFormatProperties2);
GET_INST_PROC(GetPhysicalDeviceProperties);
- GET_INST_PROC(GetPhysicalDeviceQueueFamilyProperties);
+ GET_INST_PROC(GetPhysicalDeviceQueueFamilyProperties2);
uint32_t gpuCount;
LOG_ALWAYS_FATAL_IF(mEnumeratePhysicalDevices(mInstance, &gpuCount, nullptr));
@@ -225,21 +227,30 @@
// query to get the initial queue props size
uint32_t queueCount = 0;
- mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, nullptr);
+ mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, nullptr);
LOG_ALWAYS_FATAL_IF(!queueCount);
// now get the actual queue props
- std::unique_ptr<VkQueueFamilyProperties[]> queueProps(new VkQueueFamilyProperties[queueCount]);
- mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, queueProps.get());
+ std::unique_ptr<VkQueueFamilyProperties2[]>
+ queueProps(new VkQueueFamilyProperties2[queueCount]);
+ // query the global priority, this ignored if VK_EXT_global_priority isn't supported
+ std::vector<VkQueueFamilyGlobalPriorityPropertiesEXT> queuePriorityProps(queueCount);
+ for (uint32_t i = 0; i < queueCount; i++) {
+ queuePriorityProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_GLOBAL_PRIORITY_PROPERTIES_EXT;
+ queuePriorityProps[i].pNext = nullptr;
+ queueProps[i].pNext = &queuePriorityProps[i];
+ }
+ mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, queueProps.get());
constexpr auto kRequestedQueueCount = 2;
// iterate to find the graphics queue
mGraphicsQueueIndex = queueCount;
for (uint32_t i = 0; i < queueCount; i++) {
- if (queueProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
+ if (queueProps[i].queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
mGraphicsQueueIndex = i;
- LOG_ALWAYS_FATAL_IF(queueProps[i].queueCount < kRequestedQueueCount);
+ LOG_ALWAYS_FATAL_IF(
+ queueProps[i].queueFamilyProperties.queueCount < kRequestedQueueCount);
break;
}
}
@@ -327,6 +338,15 @@
tailPNext = &formatFeatures->pNext;
}
+ VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT* globalPriorityQueryFeatures =
+ new VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT;
+ globalPriorityQueryFeatures->sType =
+ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GLOBAL_PRIORITY_QUERY_FEATURES_EXT;
+ globalPriorityQueryFeatures->pNext = nullptr;
+ globalPriorityQueryFeatures->globalPriorityQuery = false;
+ *tailPNext = globalPriorityQueryFeatures;
+ tailPNext = &globalPriorityQueryFeatures->pNext;
+
// query to get the physical device features
mGetPhysicalDeviceFeatures2(mPhysicalDevice, &features);
// this looks like it would slow things down,
@@ -341,24 +361,59 @@
if (Properties::contextPriority != 0 &&
grExtensions.hasExtension(VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME, 2)) {
- memset(&queuePriorityCreateInfo, 0, sizeof(VkDeviceQueueGlobalPriorityCreateInfoEXT));
- queuePriorityCreateInfo.sType =
- VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT;
- queuePriorityCreateInfo.pNext = nullptr;
+ VkQueueGlobalPriorityEXT globalPriority;
switch (Properties::contextPriority) {
case EGL_CONTEXT_PRIORITY_LOW_IMG:
- queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_LOW_EXT;
+ globalPriority = VK_QUEUE_GLOBAL_PRIORITY_LOW_EXT;
break;
case EGL_CONTEXT_PRIORITY_MEDIUM_IMG:
- queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_MEDIUM_EXT;
+ globalPriority = VK_QUEUE_GLOBAL_PRIORITY_MEDIUM_EXT;
break;
case EGL_CONTEXT_PRIORITY_HIGH_IMG:
- queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_EXT;
+ globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_EXT;
break;
default:
LOG_ALWAYS_FATAL("Unsupported context priority");
}
- queueNextPtr = &queuePriorityCreateInfo;
+
+ // check if the requested priority is reported by the query
+ bool attachGlobalPriority = false;
+ if (uirenderer::Properties::queryGlobalPriority &&
+ globalPriorityQueryFeatures->globalPriorityQuery) {
+ for (uint32_t i = 0; i < queuePriorityProps[mGraphicsQueueIndex].priorityCount; i++) {
+ if (queuePriorityProps[mGraphicsQueueIndex].priorities[i] == globalPriority) {
+ attachGlobalPriority = true;
+ break;
+ }
+ }
+ } else {
+ // Querying is not supported so attempt queue creation with requested priority anyways
+ // If the priority turns out not to be supported, the driver *may* fail with
+ // VK_ERROR_NOT_PERMITTED_KHR
+ attachGlobalPriority = true;
+ }
+
+ if (attachGlobalPriority) {
+ memset(&queuePriorityCreateInfo, 0, sizeof(VkDeviceQueueGlobalPriorityCreateInfoEXT));
+ queuePriorityCreateInfo.sType =
+ VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT;
+ queuePriorityCreateInfo.pNext = nullptr;
+ queuePriorityCreateInfo.globalPriority = globalPriority;
+ queueNextPtr = &queuePriorityCreateInfo;
+ } else {
+ // If globalPriorityQuery is enabled, attempting queue creation with an unsupported
+ // priority will return VK_ERROR_INITIALIZATION_FAILED.
+ //
+ // SysUI and Launcher will request HIGH when SF has RT but it is a known issue that
+ // upstream drm drivers currently lack a way to grant them the granular privileges
+ // they need for HIGH (but not RT) so they will fail queue creation.
+ // For now, drop the unsupported global priority request so that queue creation
+ // succeeds.
+ //
+ // Once that is fixed, this should probably be a fatal error indicating an improper
+ // request or an app needs to get the correct privileges.
+ ALOGW("Requested context priority is not supported by the queue");
+ }
}
const VkDeviceQueueCreateInfo queueInfo = {
diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h
index f042571..a593ec6 100644
--- a/libs/hwui/renderthread/VulkanManager.h
+++ b/libs/hwui/renderthread/VulkanManager.h
@@ -152,7 +152,7 @@
VkPtr<PFN_vkDestroyInstance> mDestroyInstance;
VkPtr<PFN_vkEnumeratePhysicalDevices> mEnumeratePhysicalDevices;
VkPtr<PFN_vkGetPhysicalDeviceProperties> mGetPhysicalDeviceProperties;
- VkPtr<PFN_vkGetPhysicalDeviceQueueFamilyProperties> mGetPhysicalDeviceQueueFamilyProperties;
+ VkPtr<PFN_vkGetPhysicalDeviceQueueFamilyProperties2> mGetPhysicalDeviceQueueFamilyProperties2;
VkPtr<PFN_vkGetPhysicalDeviceFeatures2> mGetPhysicalDeviceFeatures2;
VkPtr<PFN_vkGetPhysicalDeviceImageFormatProperties2> mGetPhysicalDeviceImageFormatProperties2;
VkPtr<PFN_vkCreateDevice> mCreateDevice;
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
index bfeaf92..0f6e6a7 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
@@ -28,6 +28,7 @@
import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -35,12 +36,7 @@
import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi
-@Module(
- includes =
- [
- LockscreenSceneBlueprintModule::class,
- ],
-)
+@Module(includes = [LockscreenSceneBlueprintModule::class])
interface LockscreenSceneModule {
@Binds @IntoSet fun lockscreenScene(scene: LockscreenScene): Scene
@@ -51,9 +47,7 @@
@Provides
@SysUISingleton
@KeyguardRootView
- fun viewProvider(
- configurator: Provider<KeyguardViewConfigurator>,
- ): () -> View {
+ fun viewProvider(configurator: Provider<KeyguardViewConfigurator>): () -> View {
return { configurator.get().getKeyguardRootView() }
}
@@ -67,10 +61,16 @@
@Provides
fun providesLockscreenContent(
viewModelFactory: LockscreenContentViewModel.Factory,
+ notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
clockInteractor: KeyguardClockInteractor,
): LockscreenContent {
- return LockscreenContent(viewModelFactory, blueprints, clockInteractor)
+ return LockscreenContent(
+ viewModelFactory,
+ notificationScrimViewModelFactory,
+ blueprints,
+ clockInteractor,
+ )
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index dbe7538..5c5514a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -30,6 +30,8 @@
import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.notifications.ui.composable.NotificationLockscreenScrim
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
/**
* Renders the content of the lockscreen.
@@ -39,6 +41,7 @@
*/
class LockscreenContent(
private val viewModelFactory: LockscreenContentViewModel.Factory,
+ private val notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
private val clockInteractor: KeyguardClockInteractor,
) {
@@ -47,10 +50,13 @@
}
@Composable
- fun SceneScope.Content(
- modifier: Modifier = Modifier,
- ) {
- val viewModel = rememberViewModel("LockscreenContent") { viewModelFactory.create() }
+ fun SceneScope.Content(modifier: Modifier = Modifier) {
+ val viewModel =
+ rememberViewModel("LockscreenContent-viewModel") { viewModelFactory.create() }
+ val notificationLockscreenScrimViewModel =
+ rememberViewModel("LockscreenContent-scrimViewModel") {
+ notificationScrimViewModelFactory.create()
+ }
val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle()
if (!isContentVisible) {
// If the content isn't supposed to be visible, show a large empty box as it's needed
@@ -71,6 +77,9 @@
}
val blueprint = blueprintByBlueprintId[blueprintId] ?: return
- with(blueprint) { Content(viewModel, modifier.sysuiResTag("keyguard_root_view")) }
+ with(blueprint) {
+ Content(viewModel, modifier.sysuiResTag("keyguard_root_view"))
+ NotificationLockscreenScrim(notificationLockscreenScrimViewModel)
+ }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt
new file mode 100644
index 0000000..4279be3
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.systemui.notifications.ui.composable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
+import kotlinx.coroutines.launch
+
+/**
+ * A full-screen notifications scrim that is only visible after transitioning from Shade scene to
+ * Lockscreen Scene and ending user input, at which point it fades out, visually completing the
+ * transition.
+ */
+@Composable
+fun SceneScope.NotificationLockscreenScrim(
+ viewModel: NotificationLockscreenScrimViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val shadeMode = viewModel.shadeMode.collectAsStateWithLifecycle()
+
+ // Important: Make sure that shouldShowScrimFadeOut() is checked the first time the Lockscreen
+ // scene is composed.
+ val useFadeOutOnComposition =
+ remember(shadeMode.value) {
+ layoutState.currentTransition?.let { currentTransition ->
+ shouldShowScrimFadeOut(currentTransition, shadeMode.value)
+ } ?: false
+ }
+
+ val alphaAnimatable = remember { Animatable(1f) }
+
+ LaunchedEffect(
+ alphaAnimatable,
+ layoutState.currentTransition,
+ useFadeOutOnComposition,
+ shadeMode,
+ ) {
+ val currentTransition = layoutState.currentTransition
+ if (
+ useFadeOutOnComposition &&
+ currentTransition != null &&
+ shouldShowScrimFadeOut(currentTransition, shadeMode.value) &&
+ currentTransition.isUserInputOngoing
+ ) {
+ // keep scrim visible until user lifts their finger.
+ viewModel.setAlphaForLockscreenFadeIn(0f)
+ alphaAnimatable.snapTo(1f)
+ } else if (
+ useFadeOutOnComposition &&
+ (currentTransition == null ||
+ (shouldShowScrimFadeOut(currentTransition, shadeMode.value) &&
+ !currentTransition.isUserInputOngoing))
+ ) {
+ // we no longer want to keep the scrim from fading out, so animate the scrim fade-out
+ // and pipe the progress to the view model as well, so NSSL can fade-in the stack in
+ // tandem.
+ viewModel.setAlphaForLockscreenFadeIn(0f)
+ coroutineScope.launch {
+ snapshotFlow { alphaAnimatable.value }
+ .collect { viewModel.setAlphaForLockscreenFadeIn(1 - it) }
+ }
+ alphaAnimatable.animateTo(0f, tween())
+ } else {
+ // disable the scrim fade logic.
+ viewModel.setAlphaForLockscreenFadeIn(1f)
+ alphaAnimatable.snapTo(0f)
+ }
+ }
+
+ Box(
+ modifier
+ .fillMaxSize()
+ .element(Notifications.Elements.NotificationScrim)
+ .graphicsLayer { alpha = alphaAnimatable.value }
+ .background(MaterialTheme.colorScheme.surface)
+ )
+}
+
+private fun shouldShowScrimFadeOut(
+ currentTransition: TransitionState.Transition,
+ shadeMode: ShadeMode,
+): Boolean {
+ return shadeMode == ShadeMode.Single &&
+ currentTransition.isInitiatedByUserInput &&
+ (currentTransition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) ||
+ currentTransition.isTransitioning(from = Scenes.Bouncer, to = Scenes.Lockscreen))
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index fe4a65b..2066c93 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -86,6 +86,8 @@
import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.SceneTransitionLayoutState
+import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.res.R
@@ -101,7 +103,7 @@
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
object Notifications {
@@ -251,6 +253,7 @@
NotificationPlaceholder(
stackScrollView = stackScrollView,
viewModel = viewModel,
+ useStackBounds = { shouldUseLockscreenStackBounds(layoutState.transitionState) },
modifier = Modifier.fillMaxSize(),
)
HeadsUpNotificationSpace(
@@ -363,7 +366,6 @@
snapshotFlow { syntheticScroll.value }
.collect { delta ->
scrollNotificationStack(
- scope = coroutineScope,
delta = delta,
animate = false,
scrimOffset = scrimOffset,
@@ -383,7 +385,6 @@
// composed at least once), and our remote input row overlaps with the ime bounds.
if (isRemoteInputActive && imeTopValue > 0f && remoteInputRowBottom > imeTopValue) {
scrollNotificationStack(
- scope = coroutineScope,
delta = remoteInputRowBottom - imeTopValue,
animate = true,
scrimOffset = scrimOffset,
@@ -450,7 +451,10 @@
scrimCornerRadius,
screenCornerRadius,
{ expansionFraction },
- shouldPunchHoleBehindScrim,
+ shouldAnimateScrimCornerRadius(
+ layoutState,
+ shouldPunchHoleBehindScrim,
+ ),
)
.let { scrimRounding.value.toRoundedCornerShape(it) }
clip = true
@@ -514,6 +518,9 @@
NotificationPlaceholder(
stackScrollView = stackScrollView,
viewModel = viewModel,
+ useStackBounds = {
+ !shouldUseLockscreenStackBounds(layoutState.transitionState)
+ },
modifier =
Modifier.notificationStackHeight(
view = stackScrollView,
@@ -600,6 +607,7 @@
private fun SceneScope.NotificationPlaceholder(
stackScrollView: NotificationScrollView,
viewModel: NotificationsPlaceholderViewModel,
+ useStackBounds: () -> Boolean,
modifier: Modifier = Modifier,
) {
Box(
@@ -609,21 +617,26 @@
.debugBackground(viewModel, DEBUG_STACK_COLOR)
.onSizeChanged { size -> debugLog(viewModel) { "STACK onSizeChanged: size=$size" } }
.onGloballyPositioned { coordinates: LayoutCoordinates ->
- val positionInWindow = coordinates.positionInWindow()
- debugLog(viewModel) {
- "STACK onGloballyPositioned:" +
- " size=${coordinates.size}" +
- " position=$positionInWindow" +
- " bounds=${coordinates.boundsInWindow()}"
+ // This element is opted out of the shared element system, so there can be
+ // multiple instances of it during a transition. Thus we need to determine which
+ // instance should feed its bounds to NSSL to avoid providing conflicting values
+ val useBounds = useStackBounds()
+ if (useBounds) {
+ // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top won't
+ val positionInWindow = coordinates.positionInWindow()
+ debugLog(viewModel) {
+ "STACK onGloballyPositioned:" +
+ " size=${coordinates.size}" +
+ " position=$positionInWindow" +
+ " bounds=${coordinates.boundsInWindow()}"
+ }
+ stackScrollView.setStackTop(positionInWindow.y)
}
- // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top will not
- stackScrollView.setStackTop(positionInWindow.y)
}
)
}
private suspend fun scrollNotificationStack(
- scope: CoroutineScope,
delta: Float,
animate: Boolean,
scrimOffset: Animatable<Float, AnimationVector1D>,
@@ -638,7 +651,7 @@
if (animate) {
// launch a new coroutine for the remainder animation so that it doesn't suspend the
// scrim animation, allowing both to play simultaneously.
- scope.launch { scrollState.animateScrollTo(remainingDelta) }
+ coroutineScope { launch { scrollState.animateScrollTo(remainingDelta) } }
} else {
scrollState.scrollTo(remainingDelta)
}
@@ -658,6 +671,18 @@
}
}
+private fun shouldUseLockscreenStackBounds(state: TransitionState): Boolean {
+ return state is TransitionState.Idle && state.currentScene == Scenes.Lockscreen
+}
+
+private fun shouldAnimateScrimCornerRadius(
+ state: SceneTransitionLayoutState,
+ shouldPunchHoleBehindScrim: Boolean,
+): Boolean {
+ return shouldPunchHoleBehindScrim ||
+ state.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)
+}
+
private fun calculateCornerRadius(
scrimCornerRadius: Dp,
screenCornerRadius: Dp,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 58fbf43..8b772d8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -82,6 +82,10 @@
sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false)
}
from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() }
+ from(Scenes.Shade, to = Scenes.Lockscreen) {
+ reversed { lockscreenToShadeTransition() }
+ sharedElement(Notifications.Elements.NotificationStackPlaceholder, enabled = false)
+ }
// Overlay transitions
@@ -94,6 +98,7 @@
// Scene overscroll
overscrollDisabled(Scenes.Gone, Orientation.Vertical)
+ overscrollDisabled(Scenes.Lockscreen, Orientation.Vertical)
overscroll(Scenes.Bouncer, Orientation.Vertical) {
translate(Bouncer.Elements.Content, y = { absoluteDistance })
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
index ac54896..4c0efd2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
@@ -4,13 +4,19 @@
import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.UserActionDistance
import com.android.systemui.bouncer.ui.composable.Bouncer
const val FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION = 0.5f
+const val FROM_LOCK_SCREEN_TO_BOUNCER_SWIPE_DISTANCE_FRACTION = 0.5f
fun TransitionBuilder.lockscreenToBouncerTransition() {
spec = tween(durationMillis = 500)
+ distance = UserActionDistance { fromSceneSize, _ ->
+ fromSceneSize.height * FROM_LOCK_SCREEN_TO_BOUNCER_SWIPE_DISTANCE_FRACTION
+ }
+
translate(Bouncer.Elements.Content, y = 300.dp)
fractionRange(end = FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION) {
fade(Bouncer.Elements.Background)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 7f2ee2a..db0fe3e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -296,7 +296,7 @@
val shouldPunchHoleBehindScrim =
layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ||
- layoutState.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade)
+ layoutState.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade)
// Media is visible and we are in landscape on a small height screen
val mediaInRow = isMediaVisible && isLandscape()
val mediaOffset by
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index a0cafcb..c9e958d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -341,7 +341,7 @@
bouncerActionButton?.onClick?.invoke()
runCurrent()
- // TODO(b/298026988): Assert that an activity was started once we use ActivityStarter.
+ // TODO(b/369765704): Assert that an activity was started once we use ActivityStarter.
}
@Test
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
index 8b5a09b..2c026c0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
@@ -103,7 +103,7 @@
prepareToPerformAction()
returnToCall()
},
- onLongClick = null
+ onLongClick = null,
)
}
@@ -115,15 +115,15 @@
dozeLogger.logEmergencyCall()
startEmergencyDialerActivity()
},
- // TODO(b/308001302): The long click detector doesn't work properly, investigate.
+ // TODO(b/369767936): The long click detector doesn't work properly, investigate.
onLongClick = {
if (emergencyAffordanceManager.needsEmergencyAffordance()) {
prepareToPerformAction()
- // TODO(b/298026988): Check that !longPressWasDragged before invoking.
+ // TODO(b/369767936): Check that !longPressWasDragged before invoking.
emergencyAffordanceManager.performEmergencyCall()
}
- }
+ },
)
}
@@ -143,7 +143,7 @@
applicationContext.startActivityAsUser(
this,
ActivityOptions.makeCustomAnimation(applicationContext, 0, 0).toBundle(),
- UserHandle(selectedUserInteractor.getSelectedUserId())
+ UserHandle(selectedUserInteractor.getSelectedUserId()),
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 416eaba..063adc8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -63,6 +63,7 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -102,6 +103,7 @@
private val keyguardClockViewModel: KeyguardClockViewModel,
private val smartspaceViewModel: KeyguardSmartspaceViewModel,
private val lockscreenContentViewModelFactory: LockscreenContentViewModel.Factory,
+ private val notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>,
private val clockInteractor: KeyguardClockInteractor,
private val keyguardViewMediator: KeyguardViewMediator,
@@ -207,6 +209,7 @@
private fun createLockscreen(
context: Context,
viewModelFactory: LockscreenContentViewModel.Factory,
+ notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>,
): View {
val sceneBlueprints =
@@ -222,6 +225,8 @@
with(
LockscreenContent(
viewModelFactory = viewModelFactory,
+ notificationScrimViewModelFactory =
+ notificationScrimViewModelFactory,
blueprints = sceneBlueprints,
clockInteractor = clockInteractor,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index fe59d73..5ff5d2d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -45,17 +45,16 @@
/**
* How long to wait before auto-dismissing a notification that was kept for active remote input, and
- * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel
- * these given that they technically don't exist anymore. We wait a bit in case the app issues
- * an update, and to also give the other lifetime extenders a beat to decide they want it.
+ * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel these given
+ * that they technically don't exist anymore. We wait a bit in case the app issues an update, and to
+ * also give the other lifetime extenders a beat to decide they want it.
*/
private const val REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY: Long = 500
/**
* How long to wait before releasing a lifetime extension when requested to do so due to a user
- * interaction (such as tapping another action).
- * We wait a bit in case the app issues an update in response to the action, but not too long or we
- * risk appearing unresponsive to the user.
+ * interaction (such as tapping another action). We wait a bit in case the app issues an update in
+ * response to the action, but not too long or we risk appearing unresponsive to the user.
*/
private const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200
@@ -63,22 +62,21 @@
private val DEBUG: Boolean by lazy { Log.isLoggable(TAG, Log.DEBUG) }
@CoordinatorScope
-class RemoteInputCoordinator @Inject constructor(
+class RemoteInputCoordinator
+@Inject
+constructor(
dumpManager: DumpManager,
private val mRebuilder: RemoteInputNotificationRebuilder,
private val mNotificationRemoteInputManager: NotificationRemoteInputManager,
@Main private val mMainHandler: Handler,
- private val mSmartReplyController: SmartReplyController
+ private val mSmartReplyController: SmartReplyController,
) : Coordinator, RemoteInputListener, Dumpable {
@VisibleForTesting val mRemoteInputHistoryExtender = RemoteInputHistoryExtender()
@VisibleForTesting val mSmartReplyHistoryExtender = SmartReplyHistoryExtender()
@VisibleForTesting val mRemoteInputActiveExtender = RemoteInputActiveExtender()
- private val mRemoteInputLifetimeExtenders = listOf(
- mRemoteInputHistoryExtender,
- mSmartReplyHistoryExtender,
- mRemoteInputActiveExtender
- )
+ private val mRemoteInputLifetimeExtenders =
+ listOf(mRemoteInputHistoryExtender, mSmartReplyHistoryExtender, mRemoteInputActiveExtender)
private lateinit var mNotifUpdater: InternalNotifUpdater
@@ -93,9 +91,7 @@
if (lifetimeExtensionRefactor()) {
pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender)
} else {
- mRemoteInputLifetimeExtenders.forEach {
- pipeline.addNotificationLifetimeExtender(it)
- }
+ mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) }
}
mNotifUpdater = pipeline.getInternalNotifUpdater(TAG)
pipeline.addCollectionListener(mCollectionListener)
@@ -105,65 +101,86 @@
* Listener that updates the appearance of the notification if it has been lifetime extended
* by a a direct reply or a smart reply, and cancelled.
*/
- val mCollectionListener = object : NotifCollectionListener {
- override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
- if (DEBUG) {
- Log.d(TAG, "mCollectionListener.onEntryUpdated(entry=${entry.key}," +
- " fromSystem=$fromSystem)")
- }
- if (fromSystem) {
- if (lifetimeExtensionRefactor()) {
- if ((entry.getSbn().getNotification().flags
- and FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
- if (mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
- entry)) {
- val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
- entry.onRemoteInputInserted()
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Extending lifetime of notification with remote input")
- } else if (mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
- entry)) {
- val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
- mSmartReplyController.stopSending(entry)
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Extending lifetime of notification with smart reply")
- } else {
- // The app may have re-cancelled a notification after it had already
- // been lifetime extended.
- // Rebuild the notification with the replies it already had to ensure
- // those replies continue to be displayed.
- val newSbn = mRebuilder.rebuildWithExistingReplies(entry)
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
+ val mCollectionListener =
+ object : NotifCollectionListener {
+ override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "mCollectionListener.onEntryUpdated(entry=${entry.key}," +
+ " fromSystem=$fromSystem)",
+ )
+ }
+ if (fromSystem) {
+ if (lifetimeExtensionRefactor()) {
+ if (
+ (entry.getSbn().getNotification().flags and
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
+ ) {
+ if (
+ mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
+ entry
+ )
+ ) {
+ val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
+ entry.onRemoteInputInserted()
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
+ "Extending lifetime of notification with remote input",
+ )
+ } else if (
+ mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
+ entry
+ )
+ ) {
+ val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
+ mSmartReplyController.stopSending(entry)
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
+ "Extending lifetime of notification with smart reply",
+ )
+ } else {
+ // The app may have re-cancelled a notification after it had already
+ // been lifetime extended.
+ // Rebuild the notification with the replies it already had to
+ // ensure
+ // those replies continue to be displayed.
+ val newSbn = mRebuilder.rebuildWithExistingReplies(entry)
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
"Extending lifetime of notification that has already been " +
- "lifetime extended.")
+ "lifetime extended.",
+ )
+ }
+ } else {
+ // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
+ // should have their remote inputs list cleared.
+ entry.remoteInputs = null
}
} else {
- // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
- // should have their remote inputs list cleared.
- entry.remoteInputs = null
+ // Mark smart replies as sent whenever a notification is updated by the app,
+ // otherwise the smart replies are never marked as sent.
+ mSmartReplyController.stopSending(entry)
}
- } else {
- // Mark smart replies as sent whenever a notification is updated by the app,
- // otherwise the smart replies are never marked as sent.
- mSmartReplyController.stopSending(entry)
+ }
+ }
+
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})")
+ // We're removing the notification, the smart reply controller can forget about it.
+ // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear
+ // it.
+ mSmartReplyController.stopSending(entry)
+
+ // When we know the entry will not be lifetime extended, clean up the remote input
+ // view
+ // TODO: Share code with NotifCollection.cannotBeLifetimeExtended
+ if (reason == REASON_CANCEL || reason == REASON_CLICK) {
+ mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry)
}
}
}
- override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
- if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})")
- // We're removing the notification, the smart reply controller can forget about it.
- // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it.
- mSmartReplyController.stopSending(entry)
-
- // When we know the entry will not be lifetime extended, clean up the remote input view
- // TODO: Share code with NotifCollection.cannotBeLifetimeExtended
- if (reason == REASON_CANCEL || reason == REASON_CLICK) {
- mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry)
- }
- }
- }
-
override fun dump(pw: PrintWriter, args: Array<out String>) {
mRemoteInputLifetimeExtenders.forEach { it.dump(pw, args) }
}
@@ -183,22 +200,25 @@
// view it is already canceled, so we'll need to cancel it on the apps behalf
// now that a reply has been sent. However, delay so that the app has time to posts an
// update in the mean time, and to give another lifetime extender time to pick it up.
- mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+ mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY,
+ )
}
private fun onSmartReplySent(entry: NotificationEntry, reply: CharSequence) {
if (DEBUG) Log.d(TAG, "onSmartReplySent(entry=${entry.key})")
val newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply)
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Adding smart reply spinner for sent")
+ mNotifUpdater.onInternalNotificationUpdate(newSbn, "Adding smart reply spinner for sent")
// If we're extending for remote input being active, then from the apps point of
// view it is already canceled, so we'll need to cancel it on the apps behalf
// now that a reply has been sent. However, delay so that the app has time to posts an
// update in the mean time, and to give another lifetime extender time to pick it up.
- mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+ mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY,
+ )
}
override fun onPanelCollapsed() {
@@ -208,19 +228,25 @@
override fun isNotificationKeptForRemoteInputHistory(key: String) =
if (!lifetimeExtensionRefactor()) {
mRemoteInputHistoryExtender.isExtending(key) ||
- mSmartReplyHistoryExtender.isExtending(key)
+ mSmartReplyHistoryExtender.isExtending(key)
} else false
override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
if (!lifetimeExtensionRefactor()) {
- mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
- mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
+ )
+ mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
+ )
}
- mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
+ )
}
override fun setRemoteInputController(remoteInputController: RemoteInputController) {
@@ -229,32 +255,36 @@
@VisibleForTesting
inner class RemoteInputHistoryExtender :
- SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) {
+ SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) {
override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
- mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry)
+ mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry)
override fun onStartedLifetimeExtension(entry: NotificationEntry) {
val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
entry.onRemoteInputInserted()
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Extending lifetime of notification with remote input")
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
+ "Extending lifetime of notification with remote input",
+ )
// TODO: Check if the entry was removed due perhaps to an inflation exception?
}
}
@VisibleForTesting
inner class SmartReplyHistoryExtender :
- SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) {
+ SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) {
override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
- mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry)
+ mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry)
override fun onStartedLifetimeExtension(entry: NotificationEntry) {
val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
mSmartReplyController.stopSending(entry)
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Extending lifetime of notification with smart reply")
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
+ "Extending lifetime of notification with smart reply",
+ )
// TODO: Check if the entry was removed due perhaps to an inflation exception?
}
@@ -266,9 +296,9 @@
@VisibleForTesting
inner class RemoteInputActiveExtender :
- SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) {
+ SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) {
override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
- mNotificationRemoteInputManager.isRemoteInputActive(entry)
+ mNotificationRemoteInputManager.isRemoteInputActive(entry)
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 1431b28..d246b04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -88,6 +88,8 @@
private ExpandableView mLastVisibleBackgroundChild;
private float mCurrentScrollVelocity;
private int mStatusBarState;
+ private boolean mShowingStackOnLockscreen;
+ private float mLockscreenStackFadeInProgress;
private float mExpandingVelocity;
private boolean mPanelTracking;
private boolean mExpansionChanging;
@@ -624,6 +626,26 @@
return mStatusBarState == StatusBarState.KEYGUARD;
}
+ public boolean isShowingStackOnLockscreen() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return false;
+ return mShowingStackOnLockscreen;
+ }
+
+ public void setShowingStackOnLockscreen(boolean showingStackOnLockscreen) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mShowingStackOnLockscreen = showingStackOnLockscreen;
+ }
+
+ public float getLockscreenStackFadeInProgress() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ return mLockscreenStackFadeInProgress;
+ }
+
+ public void setLockscreenStackFadeInProgress(float lockscreenStackFadeInProgress) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mLockscreenStackFadeInProgress = lockscreenStackFadeInProgress;
+ }
+
public void setStatusBarState(int statusBarState) {
if (mStatusBarState != StatusBarState.KEYGUARD) {
mIsFlingRequiredAfterLockScreenSwipeUp = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index b2b2c2a..0a44a2b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -5342,6 +5342,19 @@
updateDismissBehavior();
}
+ @Override
+ public void setShowingStackOnLockscreen(boolean showingStackOnLockscreen) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mAmbientState.setShowingStackOnLockscreen(showingStackOnLockscreen);
+ }
+
+ @Override
+ public void setAlphaForLockscreenFadeIn(float alphaForLockscreenFadeIn) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mAmbientState.setLockscreenStackFadeInProgress(alphaForLockscreenFadeIn);
+ requestChildrenUpdate();
+ }
+
void setUpcomingStatusBarState(int upcomingStatusBarState) {
FooterViewRefactor.assertInLegacyMode();
mUpcomingStatusBarState = upcomingStatusBarState;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index dad6894..9c5fecf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -380,7 +380,7 @@
new StatusBarStateController.StateListener() {
@Override
public void onStatePreChange(int oldState, int newState) {
- if (oldState == StatusBarState.SHADE_LOCKED
+ if (!SceneContainerFlag.isEnabled() && oldState == StatusBarState.SHADE_LOCKED
&& newState == KEYGUARD) {
mView.requestAnimateEverything();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 9c0fd0e..e0b0ccd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -148,12 +148,18 @@
if (isHunGoingToShade) {
// Keep 100% opacity for heads up notification going to shade.
viewState.setAlpha(1f);
- } else if (ambientState.isOnKeyguard()) {
+ } else if ((!SceneContainerFlag.isEnabled() && ambientState.isOnKeyguard())
+ || ambientState.isShowingStackOnLockscreen()) {
// Adjust alpha for wakeup to lockscreen.
if (view.isHeadsUpState()) {
// Pulsing HUN should be visible on AOD and stay visible during
// AOD=>lockscreen transition
viewState.setAlpha(1f - ambientState.getHideAmount());
+ } else if (SceneContainerFlag.isEnabled()) {
+ // Take into account scene container-specific Lockscreen fade-in progress
+ float fadeAlpha = ambientState.getLockscreenStackFadeInProgress();
+ float dozeAlpha = 1f - ambientState.getDozeAmount();
+ viewState.setAlpha(Math.min(dozeAlpha, fadeAlpha));
} else {
// Normal notifications are hidden on AOD and should fade in during
// AOD=>lockscreen transition
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
index f6722a4..c0f1a56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
@@ -31,6 +31,9 @@
/** The alpha of the shade in order to show brightness. */
val alphaForBrightnessMirror = MutableStateFlow(1f)
+ /** The alpha of the Notification Stack for lockscreen fade-in */
+ val alphaForLockscreenFadeIn = MutableStateFlow(0f)
+
/**
* The bounds of the notification shade scrim / container in the current scene.
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 756cd87..32e092b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -56,10 +56,9 @@
/** The rounding of the notification stack. */
val shadeScrimRounding: Flow<ShadeScrimRounding> =
- combine(
- shadeInteractor.shadeMode,
- isExpandingFromHeadsUp,
- ) { shadeMode, isExpandingFromHeadsUp ->
+ combine(shadeInteractor.shadeMode, isExpandingFromHeadsUp) {
+ shadeMode,
+ isExpandingFromHeadsUp ->
ShadeScrimRounding(
isTopRounded = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp),
isBottomRounded = shadeMode != ShadeMode.Single,
@@ -71,6 +70,10 @@
val alphaForBrightnessMirror: StateFlow<Float> =
placeholderRepository.alphaForBrightnessMirror.asStateFlow()
+ /** The alpha of the Notification Stack for lockscreen fade-in */
+ val alphaForLockscreenFadeIn: StateFlow<Float> =
+ placeholderRepository.alphaForLockscreenFadeIn.asStateFlow()
+
/** The height of the keyguard's available space bounds */
val constrainedAvailableSpace: StateFlow<Int> =
placeholderRepository.constrainedAvailableSpace.asStateFlow()
@@ -99,7 +102,7 @@
val shouldCloseGuts: Flow<Boolean> =
combine(
sceneInteractor.isSceneContainerUserInputOngoing,
- viewHeightRepository.isCurrentGestureInGuts
+ viewHeightRepository.isCurrentGestureInGuts,
) { isUserInputOngoing, isCurrentGestureInGuts ->
isUserInputOngoing && !isCurrentGestureInGuts
}
@@ -109,6 +112,11 @@
placeholderRepository.alphaForBrightnessMirror.value = alpha
}
+ /** Sets the alpha to apply to the NSSL for fade-in on lockscreen */
+ fun setAlphaForLockscreenFadeIn(alpha: Float) {
+ placeholderRepository.alphaForLockscreenFadeIn.value = alpha
+ }
+
/** Sets the position of the notification stack in the current scene. */
fun setShadeScrimBounds(bounds: ShadeScrimBounds?) {
check(bounds == null || bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 41c0293..0113e36 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -89,6 +89,12 @@
/** sets the current QS expand fraction */
fun setQsExpandFraction(expandFraction: Float)
+ /** set whether we are idle on the lockscreen scene */
+ fun setShowingStackOnLockscreen(showingStackOnLockscreen: Boolean)
+
+ /** set the alpha from 0-1f of stack fade-in on lockscreen */
+ fun setAlphaForLockscreenFadeIn(alphaForLockscreenFadeIn: Float)
+
/** Sets whether the view is displayed in doze mode. */
fun setDozing(dozing: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 2e37dea..99ff678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -88,6 +88,14 @@
viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) }
}
launch { viewModel.qsExpandFraction.collect { view.setQsExpandFraction(it) } }
+ launch {
+ viewModel.isShowingStackOnLockscreen.collect {
+ view.setShowingStackOnLockscreen(it)
+ }
+ }
+ launch {
+ viewModel.alphaForLockscreenFadeIn.collect { view.setAlphaForLockscreenFadeIn(it) }
+ }
launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
launch {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt
new file mode 100644
index 0000000..84aa997
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.systemui.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.util.kotlin.ActivatableFlowDumper
+import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class NotificationLockscreenScrimViewModel
+@AssistedInject
+constructor(
+ dumpManager: DumpManager,
+ shadeInteractor: ShadeInteractor,
+ private val stackAppearanceInteractor: NotificationStackAppearanceInteractor,
+) :
+ ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"),
+ ExclusiveActivatable() {
+
+ val shadeMode = shadeInteractor.shadeMode
+
+ /** Sets the alpha to apply to the NSSL for fade-in on lockscreen */
+ fun setAlphaForLockscreenFadeIn(alpha: Float) {
+ stackAppearanceInteractor.setAlphaForLockscreenFadeIn(alpha)
+ }
+
+ override suspend fun onActivated(): Nothing {
+ activateFlowDumper()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): NotificationLockscreenScrimViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 5b2e02d..cd9c07e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -82,8 +82,13 @@
private fun fullyExpandedDuringSceneChange(change: ChangeScene): Boolean {
// The lockscreen stack is visible during all transitions away from the lockscreen, so keep
// the stack expanded until those transitions finish.
- return (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) ||
- change.isBetween({ it == Scenes.Lockscreen }, { true })
+ return if (change.isFrom({ it == Scenes.Lockscreen }, to = { true })) {
+ true
+ } else if (change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen })) {
+ false
+ } else {
+ (expandedInScene(change.fromScene) && expandedInScene(change.toScene))
+ }
}
private fun expandFractionDuringSceneChange(
@@ -93,7 +98,10 @@
): Float {
return if (fullyExpandedDuringSceneChange(change)) {
1f
- } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade })) {
+ } else if (
+ change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade }) ||
+ change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen })
+ ) {
shadeExpansion
} else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.QuickSettings })) {
// during QS expansion, increase fraction at same rate as scrim alpha,
@@ -178,6 +186,18 @@
.distinctUntilChanged()
.dumpWhileCollecting("shouldResetStackTop")
+ /** Whether the Notification Stack is visibly on the lockscreen scene. */
+ val isShowingStackOnLockscreen: Flow<Boolean> =
+ sceneInteractor.transitionState
+ .mapNotNull { state ->
+ state.isIdle(Scenes.Lockscreen) ||
+ state.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade)
+ }
+ .distinctUntilChanged()
+
+ /** The alpha of the Notification Stack for lockscreen fade-in */
+ val alphaForLockscreenFadeIn = stackAppearanceInteractor.alphaForLockscreenFadeIn
+
private operator fun SceneKey.contains(scene: SceneKey) =
sceneInteractor.isSceneInFamily(scene, this)
@@ -298,3 +318,6 @@
private fun ChangeScene.isBetween(a: (SceneKey) -> Boolean, b: (SceneKey) -> Boolean): Boolean =
(a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene))
+
+private fun ChangeScene.isFrom(from: (SceneKey) -> Boolean, to: (SceneKey) -> Boolean): Boolean =
+ from(fromScene) && to(toScene)
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 7580b69..1145412 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -4962,9 +4962,14 @@
if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
&& android.security.Flags.extendEcmToAllSettings()) {
try {
- return !mContext.getSystemService(EnhancedConfirmationManager.class)
- .isRestricted(packageName,
- AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
+ final EnhancedConfirmationManager userContextEcm =
+ mContext.createContextAsUser(UserHandle.of(userId), /* flags = */ 0)
+ .getSystemService(EnhancedConfirmationManager.class);
+ if (userContextEcm != null) {
+ return !userContextEcm.isRestricted(packageName,
+ AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
+ }
+ return false;
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOG_TAG, "Exception when retrieving package:" + packageName, e);
return false;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index ad72941..35323d6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -262,6 +262,7 @@
import android.content.AttributionSource;
import android.content.AutofillOptions;
import android.content.BroadcastReceiver;
+import android.content.ClipData;
import android.content.ComponentCallbacks2;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
@@ -419,7 +420,6 @@
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.MemInfoReader;
import com.android.internal.util.Preconditions;
-import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.AlarmManagerInternal;
import com.android.server.BootReceiver;
import com.android.server.DeviceIdleInternal;
@@ -439,6 +439,7 @@
import com.android.server.appop.AppOpsService;
import com.android.server.compat.PlatformCompat;
import com.android.server.contentcapture.ContentCaptureManagerInternal;
+import com.android.server.crashrecovery.CrashRecoveryHelper;
import com.android.server.criticalevents.CriticalEventLog;
import com.android.server.firewall.IntentFirewall;
import com.android.server.graphics.fonts.FontManagerInternal;
@@ -483,6 +484,7 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -500,6 +502,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -19097,4 +19100,87 @@
Freezer getFreezer() {
return mFreezer;
}
+
+ // Set of IntentCreatorToken objects that are currently active.
+ private static final Map<IntentCreatorToken.Key, WeakReference<IntentCreatorToken>>
+ sIntentCreatorTokenCache = new ConcurrentHashMap<>();
+
+ /**
+ * A binder token used to keep track of which app created the intent. This token can be used to
+ * defend against intent redirect attacks. It stores uid of the intent creator and key fields of
+ * the intent to make it impossible for attacker to fake uid with a malicious intent.
+ *
+ * @hide
+ */
+ public static final class IntentCreatorToken extends Binder {
+ @NonNull
+ private final Key mKeyFields;
+
+ public IntentCreatorToken(int creatorUid, Intent intent) {
+ super();
+ this.mKeyFields = new Key(creatorUid, intent);
+ }
+
+ public int getCreatorUid() {
+ return mKeyFields.mCreatorUid;
+ }
+
+ /** {@hide} */
+ public static boolean isValid(@NonNull Intent intent) {
+ IBinder binder = intent.getCreatorToken();
+ IntentCreatorToken token = null;
+ if (binder instanceof IntentCreatorToken) {
+ token = (IntentCreatorToken) binder;
+ }
+ return token != null && token.mKeyFields.equals(
+ new Key(token.mKeyFields.mCreatorUid, intent));
+ }
+
+ private static class Key {
+ private Key(int creatorUid, Intent intent) {
+ this.mCreatorUid = creatorUid;
+ this.mAction = intent.getAction();
+ this.mData = intent.getData();
+ this.mType = intent.getType();
+ this.mPackage = intent.getPackage();
+ this.mComponent = intent.getComponent();
+ this.mFlags = intent.getFlags() & Intent.IMMUTABLE_FLAGS;
+ ClipData clipData = intent.getClipData();
+ if (clipData != null) {
+ this.mClipDataUris = new ArrayList<>(clipData.getItemCount());
+ for (int i = 0; i < clipData.getItemCount(); i++) {
+ this.mClipDataUris.add(clipData.getItemAt(i).getUri());
+ }
+ }
+ }
+
+ private final int mCreatorUid;
+ private final String mAction;
+ private final Uri mData;
+ private final String mType;
+ private final String mPackage;
+ private final ComponentName mComponent;
+ private final int mFlags;
+ private List<Uri> mClipDataUris;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Key key = (Key) o;
+ return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags && Objects.equals(
+ mAction, key.mAction) && Objects.equals(mData, key.mData)
+ && Objects.equals(mType, key.mType) && Objects.equals(mPackage,
+ key.mPackage) && Objects.equals(mComponent, key.mComponent)
+ && Objects.equals(mClipDataUris, key.mClipDataUris);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCreatorUid, mAction, mData, mType, mPackage, mComponent,
+ mFlags,
+ mClipDataUris);
+ }
+ }
+ }
}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index ae33b83..88907e3 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -5293,6 +5293,17 @@
}
@Override
+ public IntArray getDisplayIds() {
+ IntArray displayIds = new IntArray();
+ synchronized (mSyncRoot) {
+ mLogicalDisplayMapper.forEachLocked((logicalDisplay -> {
+ displayIds.add(logicalDisplay.getDisplayIdLocked());
+ }), /* includeDisabled= */ false);
+ }
+ return displayIds;
+ }
+
+ @Override
public DisplayManagerInternal.DisplayOffloadSession registerDisplayOffloader(
int displayId, @NonNull DisplayManagerInternal.DisplayOffloader displayOffloader) {
if (!mFlags.isDisplayOffloadEnabled()) {
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 9281267..99f7f12 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -23,6 +23,7 @@
import android.hardware.display.DisplayViewport;
import android.hardware.input.KeyGestureEvent;
import android.os.IBinder;
+import android.util.SparseBooleanArray;
import android.view.InputChannel;
import android.view.inputmethod.InputMethodSubtype;
@@ -45,9 +46,11 @@
/**
* Called by the power manager to tell the input manager whether it should start
- * watching for wake events.
+ * watching for wake events on given displays.
+ *
+ * @param displayInteractivities Map of display ids to their current interactive state.
*/
- public abstract void setInteractive(boolean interactive);
+ public abstract void setDisplayInteractivities(SparseBooleanArray displayInteractivities);
/**
* Toggles Caps Lock state for input device with specific id.
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 909c47b..f045576 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -96,6 +96,7 @@
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
@@ -3337,10 +3338,22 @@
}
@Override
- public void setInteractive(boolean interactive) {
- mNative.setInteractive(interactive);
- mBatteryController.onInteractiveChanged(interactive);
- mKeyboardBacklightController.onInteractiveChanged(interactive);
+ public void setDisplayInteractivities(SparseBooleanArray displayInteractivities) {
+ boolean globallyInteractive = false;
+ ArraySet<Integer> nonInteractiveDisplays = new ArraySet<>();
+ for (int i = 0; i < displayInteractivities.size(); i++) {
+ final int displayId = displayInteractivities.keyAt(i);
+ final boolean displayInteractive = displayInteractivities.get(displayId);
+ if (displayInteractive) {
+ globallyInteractive = true;
+ } else {
+ nonInteractiveDisplays.add(displayId);
+ }
+ }
+ mNative.setNonInteractiveDisplays(
+ nonInteractiveDisplays.stream().mapToInt(Integer::intValue).toArray());
+ mBatteryController.onInteractiveChanged(globallyInteractive);
+ mKeyboardBacklightController.onInteractiveChanged(globallyInteractive);
}
// TODO(b/358569822): Remove this method from InputManagerInternal after key gesture
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index d17e256..4404d63 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -141,7 +141,7 @@
void setShowTouches(boolean enabled);
- void setInteractive(boolean interactive);
+ void setNonInteractiveDisplays(int[] displayIds);
void reloadCalibration();
@@ -409,7 +409,7 @@
public native void setShowTouches(boolean enabled);
@Override
- public native void setInteractive(boolean interactive);
+ public native void setNonInteractiveDisplays(int[] displayIds);
@Override
public native void reloadCalibration();
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index df9f7fb..5fc3e33 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1015,8 +1015,7 @@
permission, attributionSource, message, forDataDelivery, startDataDelivery,
fromDatasource, attributedOp);
// Finish any started op if some step in the attribution chain failed.
- if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED
- && result != PermissionChecker.PERMISSION_SOFT_DENIED) {
+ if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED) {
if (attributedOp == AppOpsManager.OP_NONE) {
finishDataDelivery(AppOpsManager.permissionToOpCode(permission),
attributionSource.asState(), fromDatasource);
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 303828f..0cdf537 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -53,6 +53,7 @@
import android.util.EventLog;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.view.WindowManagerPolicyConstants;
import com.android.internal.annotations.VisibleForTesting;
@@ -512,8 +513,17 @@
}
// Start input as soon as we start waking up or going to sleep.
- mInputManagerInternal.setInteractive(interactive);
mInputMethodManagerInternal.setInteractive(interactive);
+ if (!mFlags.isPerDisplayWakeByTouchEnabled()) {
+ // Since wakefulness is a global property in original logic, all displays should
+ // be set to the same interactive state, matching system's global wakefulness
+ SparseBooleanArray displayInteractivities = new SparseBooleanArray();
+ int[] displayIds = mDisplayManagerInternal.getDisplayIds().toArray();
+ for (int displayId : displayIds) {
+ displayInteractivities.put(displayId, interactive);
+ }
+ mInputManagerInternal.setDisplayInteractivities(displayInteractivities);
+ }
// Notify battery stats.
try {
diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
index c6ef89d..fd60e06 100644
--- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
+++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
@@ -42,6 +42,11 @@
Flags::improveWakelockLatency
);
+ private final FlagState mPerDisplayWakeByTouch = new FlagState(
+ Flags.FLAG_PER_DISPLAY_WAKE_BY_TOUCH,
+ Flags::perDisplayWakeByTouch
+ );
+
/** Returns whether early-screen-timeout-detector is enabled on not. */
public boolean isEarlyScreenTimeoutDetectorEnabled() {
return mEarlyScreenTimeoutDetectorFlagState.isEnabled();
@@ -55,6 +60,13 @@
}
/**
+ * @return Whether per-display wake by touch is enabled or not.
+ */
+ public boolean isPerDisplayWakeByTouchEnabled() {
+ return mPerDisplayWakeByTouch.isEnabled();
+ }
+
+ /**
* dumps all flagstates
* @param pw printWriter
*/
@@ -62,6 +74,7 @@
pw.println("PowerManagerFlags:");
pw.println(" " + mEarlyScreenTimeoutDetectorFlagState);
pw.println(" " + mImproveWakelockLatency);
+ pw.println(" " + mPerDisplayWakeByTouch);
}
private static class FlagState {
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
index 3581b2f..9cf3bb6 100644
--- a/services/core/java/com/android/server/power/feature/power_flags.aconfig
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -17,4 +17,12 @@
description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock."
bug: "339590565"
is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+ name: "per_display_wake_by_touch"
+ namespace: "power"
+ description: "Feature flag to enable per-display wake by touch"
+ bug: "343295183"
+ is_fixed_read_only: true
+}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 5cd117b..efca902 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -56,6 +56,7 @@
#include <nativehelper/ScopedPrimitiveArray.h>
#include <nativehelper/ScopedUtfChars.h>
#include <server_configurable_flags/get_flags.h>
+#include <ui/LogicalDisplayId.h>
#include <ui/Region.h>
#include <utils/Log.h>
#include <utils/Looper.h>
@@ -64,6 +65,7 @@
#include <atomic>
#include <cinttypes>
+#include <map>
#include <vector>
#include "android_hardware_display_DisplayViewport.h"
@@ -343,7 +345,7 @@
void setTouchpadRightClickZoneEnabled(bool enabled);
void setInputDeviceEnabled(uint32_t deviceId, bool enabled);
void setShowTouches(bool enabled);
- void setInteractive(bool interactive);
+ void setNonInteractiveDisplays(const std::set<ui::LogicalDisplayId>& displayIds);
void reloadCalibration();
void reloadPointerIcons();
void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled);
@@ -508,9 +510,11 @@
// Keycodes to be remapped.
std::map<int32_t /* fromKeyCode */, int32_t /* toKeyCode */> keyRemapping{};
+
+ // Displays which are non-interactive.
+ std::set<ui::LogicalDisplayId> nonInteractiveDisplays;
} mLocked GUARDED_BY(mLock);
- std::atomic<bool> mInteractive;
void updateInactivityTimeoutLocked();
void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags);
void ensureSpriteControllerLocked();
@@ -524,12 +528,13 @@
void forEachPointerControllerLocked(std::function<void(PointerController&)> apply)
REQUIRES(mLock);
PointerIcon loadPointerIcon(JNIEnv* env, ui::LogicalDisplayId displayId, PointerIconStyle type);
+ bool isDisplayInteractive(ui::LogicalDisplayId displayId);
static inline JNIEnv* jniEnv() { return AndroidRuntime::getJNIEnv(); }
};
NativeInputManager::NativeInputManager(jobject serviceObj, const sp<Looper>& looper)
- : mLooper(looper), mInteractive(true) {
+ : mLooper(looper) {
JNIEnv* env = jniEnv();
mServiceObj = env->NewGlobalRef(serviceObj);
@@ -547,9 +552,13 @@
void NativeInputManager::dump(std::string& dump) {
dump += "Input Manager State:\n";
- dump += StringPrintf(INDENT "Interactive: %s\n", toString(mInteractive.load()));
{ // acquire lock
std::scoped_lock _l(mLock);
+ auto logicalDisplayIdToString = [](const ui::LogicalDisplayId& displayId) {
+ return std::to_string(displayId.val());
+ };
+ dump += StringPrintf(INDENT "Display not interactive: %s\n",
+ dumpSet(mLocked.nonInteractiveDisplays, streamableToString).c_str());
dump += StringPrintf(INDENT "System UI Lights Out: %s\n",
toString(mLocked.systemUiLightsOut));
dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed);
@@ -1476,8 +1485,10 @@
mInputManager->getDispatcher().requestPointerCapture(windowToken, enabled);
}
-void NativeInputManager::setInteractive(bool interactive) {
- mInteractive = interactive;
+void NativeInputManager::setNonInteractiveDisplays(
+ const std::set<ui::LogicalDisplayId>& displayIds) {
+ std::scoped_lock _l(mLock);
+ mLocked.nonInteractiveDisplays = displayIds;
}
void NativeInputManager::reloadCalibration() {
@@ -1606,7 +1617,7 @@
// - Ignore untrusted events and pass them along.
// - Ask the window manager what to do with normal events and trusted injected events.
// - For normal events wake and brighten the screen if currently off or dim.
- const bool interactive = mInteractive.load();
+ const bool interactive = isDisplayInteractive(keyEvent.getDisplayId());
if (interactive) {
policyFlags |= POLICY_FLAG_INTERACTIVE;
}
@@ -1644,7 +1655,7 @@
// - No special filtering for injected events required at this time.
// - Filter normal events based on screen state.
// - For normal events brighten (but do not wake) the screen if currently dim.
- const bool interactive = mInteractive.load();
+ const bool interactive = isDisplayInteractive(displayId);
if (interactive) {
policyFlags |= POLICY_FLAG_INTERACTIVE;
}
@@ -1683,6 +1694,24 @@
}
}
+bool NativeInputManager::isDisplayInteractive(ui::LogicalDisplayId displayId) {
+ // If an input event doesn't have an associated id, use the default display id
+ if (displayId == ui::LogicalDisplayId::INVALID) {
+ displayId = ui::LogicalDisplayId::DEFAULT;
+ }
+
+ { // acquire lock
+ std::scoped_lock _l(mLock);
+
+ auto it = mLocked.nonInteractiveDisplays.find(displayId);
+ if (it != mLocked.nonInteractiveDisplays.end()) {
+ return false;
+ }
+ } // release lock
+
+ return true;
+}
+
nsecs_t NativeInputManager::interceptKeyBeforeDispatching(const sp<IBinder>& token,
const KeyEvent& keyEvent,
uint32_t policyFlags) {
@@ -2372,10 +2401,17 @@
im->setShowTouches(enabled);
}
-static void nativeSetInteractive(JNIEnv* env, jobject nativeImplObj, jboolean interactive) {
+static void nativeSetNonInteractiveDisplays(JNIEnv* env, jobject nativeImplObj,
+ jintArray displayIds) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
- im->setInteractive(interactive);
+ const std::vector displayIdsVec = getIntArray(env, displayIds);
+ std::set<ui::LogicalDisplayId> logicalDisplayIds;
+ for (int displayId : displayIdsVec) {
+ logicalDisplayIds.emplace(ui::LogicalDisplayId{displayId});
+ }
+
+ im->setNonInteractiveDisplays(logicalDisplayIds);
}
static void nativeReloadCalibration(JNIEnv* env, jobject nativeImplObj) {
@@ -3021,7 +3057,7 @@
(void*)nativeSetShouldNotifyTouchpadHardwareState},
{"setTouchpadRightClickZoneEnabled", "(Z)V", (void*)nativeSetTouchpadRightClickZoneEnabled},
{"setShowTouches", "(Z)V", (void*)nativeSetShowTouches},
- {"setInteractive", "(Z)V", (void*)nativeSetInteractive},
+ {"setNonInteractiveDisplays", "([I)V", (void*)nativeSetNonInteractiveDisplays},
{"reloadCalibration", "()V", (void*)nativeReloadCalibration},
{"vibrate", "(I[J[III)V", (void*)nativeVibrate},
{"vibrateCombined", "(I[JLandroid/util/SparseArray;II)V", (void*)nativeVibrateCombined},
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
index b21c349..2144785 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
@@ -94,6 +94,7 @@
ParsedService::getIntents,
ParsedService::getProperties,
Intent::getCategories,
+ Intent::getExtraIntentKeys,
PackageUserState::getDisabledComponents,
PackageUserState::getEnabledComponents,
PackageUserState::getSharedLibraryOverlayPaths,
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index fc4d8d8..0702926 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -16,6 +16,9 @@
package com.android.server.power;
+import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
+import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
+
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
@@ -31,11 +34,13 @@
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
+import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.SensorManager;
import android.hardware.display.AmbientDisplayConfiguration;
+import android.hardware.display.DisplayManagerInternal;
import android.os.BatteryStats;
import android.os.Handler;
import android.os.IWakeLockCallback;
@@ -48,11 +53,18 @@
import android.os.test.TestLooper;
import android.provider.Settings;
import android.testing.TestableContext;
+import android.util.IntArray;
+import android.util.SparseBooleanArray;
+import android.view.Display;
+import android.view.DisplayAddress;
+import android.view.DisplayInfo;
import androidx.test.InstrumentationRegistry;
import com.android.internal.app.IBatteryStats;
import com.android.server.LocalServices;
+import com.android.server.input.InputManagerInternal;
+import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.power.batterysaver.BatterySaverStateMachine;
import com.android.server.power.feature.PowerManagerFlags;
@@ -71,6 +83,8 @@
public class NotifierTest {
private static final String SYSTEM_PROPERTY_QUIESCENT = "ro.boot.quiescent";
private static final int USER_ID = 0;
+ private static final int DISPLAY_PORT = 0xFF;
+ private static final long DISPLAY_MODEL = 0xEEEEEEEEL;
@Mock private BatterySaverStateMachine mBatterySaverStateMachineMock;
@Mock private PowerManagerService.NativeWrapper mNativeWrapperMock;
@@ -81,10 +95,16 @@
@Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
@Mock private Vibrator mVibrator;
@Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+ @Mock private InputManagerInternal mInputManagerInternal;
+ @Mock private InputMethodManagerInternal mInputMethodManagerInternal;
+ @Mock private DisplayManagerInternal mDisplayManagerInternal;
+ @Mock private ActivityManagerInternal mActivityManagerInternal;
@Mock private WakeLockLog mWakeLockLog;
@Mock private IBatteryStats mBatteryStats;
+ @Mock private WindowManagerPolicy mPolicy;
+
@Mock private PowerManagerFlags mPowerManagerFlags;
@Mock private AppOpsManager mAppOpsManager;
@@ -96,6 +116,8 @@
private FakeExecutor mTestExecutor = new FakeExecutor();
private Notifier mNotifier;
+ private DisplayInfo mDefaultDisplayInfo = new DisplayInfo();
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -103,11 +125,25 @@
LocalServices.removeServiceForTest(StatusBarManagerInternal.class);
LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal);
+ LocalServices.removeServiceForTest(InputManagerInternal.class);
+ LocalServices.addService(InputManagerInternal.class, mInputManagerInternal);
+ LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
+ LocalServices.addService(InputMethodManagerInternal.class, mInputMethodManagerInternal);
+
+ LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+ LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInternal);
+
+ mDefaultDisplayInfo.address = DisplayAddress.fromPortAndModel(DISPLAY_PORT, DISPLAY_MODEL);
+ LocalServices.removeServiceForTest(DisplayManagerInternal.class);
+ LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal);
+
mContextSpy = spy(new TestableContext(InstrumentationRegistry.getContext()));
mResourcesSpy = spy(mContextSpy.getResources());
when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
when(mSystemPropertiesMock.get(eq(SYSTEM_PROPERTY_QUIESCENT), anyString())).thenReturn("");
when(mContextSpy.getSystemService(Vibrator.class)).thenReturn(mVibrator);
+ when(mDisplayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn(
+ mDefaultDisplayInfo);
mService = new PowerManagerService(mContextSpy, mInjector);
}
@@ -232,6 +268,32 @@
}
@Test
+ public void testOnGlobalWakefulnessChangeStarted() throws Exception {
+ createNotifier();
+ // GIVEN system is currently non-interactive
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
+ final int displayId1 = 101;
+ final int displayId2 = 102;
+ final int[] displayIds = new int[]{displayId1, displayId2};
+ when(mDisplayManagerInternal.getDisplayIds()).thenReturn(IntArray.wrap(displayIds));
+ mNotifier.onGlobalWakefulnessChangeStarted(WAKEFULNESS_ASLEEP,
+ PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, /* eventTime= */ 1000);
+ mTestLooper.dispatchAll();
+
+ // WHEN a global wakefulness change to interactive starts
+ mNotifier.onGlobalWakefulnessChangeStarted(WAKEFULNESS_AWAKE,
+ PowerManager.WAKE_REASON_TAP, /* eventTime= */ 2000);
+ mTestLooper.dispatchAll();
+
+ // THEN input is notified of all displays being interactive
+ final SparseBooleanArray expectedDisplayInteractivities = new SparseBooleanArray();
+ expectedDisplayInteractivities.put(displayId1, true);
+ expectedDisplayInteractivities.put(displayId2, true);
+ verify(mInputManagerInternal).setDisplayInteractivities(expectedDisplayInteractivities);
+ verify(mInputMethodManagerInternal).setInteractive(/* interactive= */ true);
+ }
+
+ @Test
public void testOnWakeLockListener_RemoteException_NoRethrow() throws RemoteException {
when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
createNotifier();
@@ -551,7 +613,7 @@
mContextSpy,
mBatteryStats,
mInjector.createSuspendBlocker(mService, "testBlocker"),
- null,
+ mPolicy,
null,
null,
mTestExecutor, mPowerManagerFlags, injector);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 566feb7..7481fc8 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -63,8 +63,11 @@
import android.Manifest;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
+import android.annotation.NonNull;
import android.app.PendingIntent;
import android.app.RemoteAction;
+import android.app.admin.DevicePolicyManager;
+import android.app.ecm.EnhancedConfirmationManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -212,6 +215,7 @@
@Mock private FullScreenMagnificationController mMockFullScreenMagnificationController;
@Mock private ProxyManager mProxyManager;
@Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+ @Mock private DevicePolicyManager mDevicePolicyManager;
@Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback;
@Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor;
private IAccessibilityManager mA11yManagerServiceOnDevice;
@@ -241,6 +245,7 @@
UserManagerInternal.class, mMockUserManagerInternal);
LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal);
mInputFilter = mock(FakeInputFilter.class);
+ mTestableContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager);
when(mMockMagnificationController.getMagnificationConnectionManager()).thenReturn(
mMockMagnificationConnectionManager);
@@ -2160,6 +2165,24 @@
.isEqualTo(SOFTWARE);
}
+ @Test
+ @EnableFlags({android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED,
+ android.security.Flags.FLAG_EXTEND_ECM_TO_ALL_SETTINGS})
+ public void isAccessibilityTargetAllowed_nonSystemUserId_useEcmWithNonSystemUserId() {
+ String fakePackageName = "FAKE_PACKAGE_NAME";
+ int uid = 0; // uid is not used in the actual implementation when flags are on
+ int userId = mTestableContext.getUserId() + 1234;
+ when(mDevicePolicyManager.getPermittedAccessibilityServices(userId)).thenReturn(
+ List.of(fakePackageName));
+ Context mockUserContext = mock(Context.class);
+ mTestableContext.addMockUserContext(userId, mockUserContext);
+
+ mA11yms.isAccessibilityTargetAllowed(fakePackageName, uid, userId);
+
+ verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class);
+ }
+
+
private Set<String> readStringsFromSetting(String setting) {
final Set<String> result = new ArraySet<>();
mA11yms.readColonDelimitedSettingToSet(
@@ -2280,6 +2303,7 @@
private final Context mMockContext;
private final Map<String, List<BroadcastReceiver>> mBroadcastReceivers = new ArrayMap<>();
+ private ArrayMap<Integer, Context> mMockUserContexts = new ArrayMap<>();
A11yTestableContext(Context base) {
super(base);
@@ -2317,6 +2341,19 @@
return mMockContext;
}
+ public void addMockUserContext(int userId, Context context) {
+ mMockUserContexts.put(userId, context);
+ }
+
+ @Override
+ @NonNull
+ public Context createContextAsUser(UserHandle user, int flags) {
+ if (mMockUserContexts.containsKey(user.getIdentifier())) {
+ return mMockUserContexts.get(user.getIdentifier());
+ }
+ return super.createContextAsUser(user, flags);
+ }
+
Map<String, List<BroadcastReceiver>> getBroadcastReceivers() {
return mBroadcastReceivers;
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 63983437..92205f39 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -230,7 +230,7 @@
mDisplayContent.setIgnoreOrientationRequest(enabled);
}
- void setTopOrganizedTaskAsTopTask() {
+ void setTopActivityOrganizedTask() {
doReturn(mTaskStack.top()).when(mActivityStack.top()).getOrganizedTask();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
index 3f34b81..d8373c5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
@@ -150,7 +150,7 @@
robot.applyOnActivity((a) -> {
a.createActivityWithComponentInNewTask();
a.setIgnoreOrientationRequest(true);
- a.setTopOrganizedTaskAsTopTask();
+ a.setTopActivityOrganizedTask();
a.setTopActivityInSizeCompatMode(true);
a.setTopActivityVisible(true);
});
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index df1d51e..064b461 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -346,6 +346,21 @@
value->value->Accept(&body_printer);
printer->Undent();
}
+ printer->Println("Flag disabled values:");
+ for (const auto& value : entry.flag_disabled_values) {
+ printer->Print("(");
+ printer->Print(value->config.to_string());
+ printer->Print(") ");
+ value->value->Accept(&headline_printer);
+ if (options.show_sources && !value->value->GetSource().path.empty()) {
+ printer->Print(" src=");
+ printer->Print(value->value->GetSource().to_string());
+ }
+ printer->Println();
+ printer->Indent();
+ value->value->Accept(&body_printer);
+ printer->Undent();
+ }
printer->Undent();
}
}
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index a274f04..446fdd4 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -71,6 +71,17 @@
enum class FlagStatus { NoFlag = 0, Disabled = 1, Enabled = 2 };
+struct FeatureFlagAttribute {
+ std::string name;
+ bool negated = false;
+
+ std::string ToString() {
+ return (negated ? "!" : "") + name;
+ }
+
+ bool operator==(const FeatureFlagAttribute& o) const = default;
+};
+
android::StringPiece to_string(ResourceType type);
/**
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index a5aecc8..773edc3 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -107,9 +107,10 @@
Visibility::Level visibility_level = Visibility::Level::kUndefined;
bool staged_api = false;
bool allow_new = false;
- FlagStatus flag_status = FlagStatus::NoFlag;
std::optional<OverlayableItem> overlayable_item;
std::optional<StagedId> staged_alias;
+ std::optional<FeatureFlagAttribute> flag;
+ FlagStatus flag_status;
std::string comment;
std::unique_ptr<Value> value;
@@ -151,6 +152,7 @@
}
if (res->value != nullptr) {
+ res->value->SetFlag(res->flag);
res->value->SetFlagStatus(res->flag_status);
// Attach the comment, source and config to the value.
res->value->SetComment(std::move(res->comment));
@@ -162,8 +164,6 @@
res_builder.SetStagedId(res->staged_alias.value());
}
- res_builder.SetFlagStatus(res->flag_status);
-
bool error = false;
if (!res->name.entry.empty()) {
if (!table->AddResource(res_builder.Build(), diag)) {
@@ -547,11 +547,15 @@
});
std::string resource_type = parser->element_name();
- auto flag_status = GetFlagStatus(parser);
- if (!flag_status) {
+ out_resource->flag = GetFlag(parser);
+ std::string error;
+ auto flag_status = GetFlagStatus(out_resource->flag, options_.feature_flag_values, &error);
+ if (flag_status) {
+ out_resource->flag_status = flag_status.value();
+ } else {
+ diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << error);
return false;
}
- out_resource->flag_status = flag_status.value();
// The value format accepted for this resource.
uint32_t resource_format = 0u;
@@ -733,31 +737,20 @@
return false;
}
-std::optional<FlagStatus> ResourceParser::GetFlagStatus(xml::XmlPullParser* parser) {
- auto flag_status = FlagStatus::NoFlag;
-
- std::optional<StringPiece> flag = xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag");
- if (flag) {
- auto flag_it = options_.feature_flag_values.find(flag.value());
- if (flag_it == options_.feature_flag_values.end()) {
- diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
- << "Resource flag value undefined");
- return {};
+std::optional<FeatureFlagAttribute> ResourceParser::GetFlag(xml::XmlPullParser* parser) {
+ auto name = xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag");
+ if (name) {
+ FeatureFlagAttribute flag;
+ if (name->starts_with('!')) {
+ flag.negated = true;
+ flag.name = name->substr(1);
+ } else {
+ flag.name = name.value();
}
- const auto& flag_properties = flag_it->second;
- if (!flag_properties.read_only) {
- diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
- << "Only read only flags may be used with resources");
- return {};
- }
- if (!flag_properties.enabled.has_value()) {
- diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
- << "Only flags with a value may be used with resources");
- return {};
- }
- flag_status = flag_properties.enabled.value() ? FlagStatus::Enabled : FlagStatus::Disabled;
+ return flag;
+ } else {
+ return {};
}
- return flag_status;
}
bool ResourceParser::ParseItem(xml::XmlPullParser* parser,
@@ -1666,21 +1659,25 @@
const std::string& element_namespace = parser->element_namespace();
const std::string& element_name = parser->element_name();
if (element_namespace.empty() && element_name == "item") {
- auto flag_status = GetFlagStatus(parser);
- if (!flag_status) {
- error = true;
- continue;
- }
+ auto flag = GetFlag(parser);
std::unique_ptr<Item> item = ParseXml(parser, typeMask, kNoRawString);
if (!item) {
diag_->Error(android::DiagMessage(item_source) << "could not parse array item");
error = true;
continue;
}
- item->SetFlagStatus(flag_status.value());
+ item->SetFlag(flag);
+ std::string err;
+ auto status = GetFlagStatus(flag, options_.feature_flag_values, &err);
+ if (status) {
+ item->SetFlagStatus(status.value());
+ } else {
+ diag_->Error(android::DiagMessage(item_source) << err);
+ error = true;
+ continue;
+ }
item->SetSource(item_source);
array->elements.emplace_back(std::move(item));
-
} else if (!ShouldIgnoreElement(element_namespace, element_name)) {
diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
<< "unknown tag <" << element_namespace << ":" << element_name << ">");
diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h
index 442dea8..a789d3e 100644
--- a/tools/aapt2/ResourceParser.h
+++ b/tools/aapt2/ResourceParser.h
@@ -85,7 +85,7 @@
private:
DISALLOW_COPY_AND_ASSIGN(ResourceParser);
- std::optional<FlagStatus> GetFlagStatus(xml::XmlPullParser* parser);
+ std::optional<FeatureFlagAttribute> GetFlag(xml::XmlPullParser* parser);
std::optional<FlattenedXmlSubTree> CreateFlattenSubTree(xml::XmlPullParser* parser);
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 9751459..5435cba2 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -101,6 +101,21 @@
}
};
+struct ConfigFlagKey {
+ const ConfigDescription* config;
+ StringPiece product;
+ const FeatureFlagAttribute& flag;
+};
+
+struct lt_config_flag_key_ref {
+ template <typename T>
+ bool operator()(const T& lhs, const ConfigFlagKey& rhs) const noexcept {
+ return std::tie(lhs->config, lhs->product, lhs->value->GetFlag()->name,
+ lhs->value->GetFlag()->negated) <
+ std::tie(*rhs.config, rhs.product, rhs.flag.name, rhs.flag.negated);
+ }
+};
+
} // namespace
ResourceTable::ResourceTable(ResourceTable::Validation validation) : validation_(validation) {
@@ -213,6 +228,25 @@
return results;
}
+ResourceConfigValue* ResourceEntry::FindOrCreateFlagDisabledValue(
+ const FeatureFlagAttribute& flag, const android::ConfigDescription& config,
+ android::StringPiece product) {
+ auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(),
+ ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref());
+ if (iter != flag_disabled_values.end()) {
+ ResourceConfigValue* value = iter->get();
+ const auto value_flag = value->value->GetFlag().value();
+ if (value_flag.name == flag.name && value_flag.negated == flag.negated &&
+ value->config == config && value->product == product) {
+ return value;
+ }
+ }
+ ResourceConfigValue* newValue =
+ flag_disabled_values.insert(iter, util::make_unique<ResourceConfigValue>(config, product))
+ ->get();
+ return newValue;
+}
+
bool ResourceEntry::HasDefaultValue() const {
// The default config should be at the top of the list, since the list is sorted.
return !values.empty() && values.front()->config == ConfigDescription::DefaultConfig();
@@ -375,13 +409,14 @@
}
};
-void InsertEntryIntoTableView(ResourceTableView& table, const ResourceTablePackage* package,
- const ResourceTableType* type, const std::string& entry_name,
- const std::optional<ResourceId>& id, const Visibility& visibility,
- const std::optional<AllowNew>& allow_new,
- const std::optional<OverlayableItem>& overlayable_item,
- const std::optional<StagedId>& staged_id,
- const std::vector<std::unique_ptr<ResourceConfigValue>>& values) {
+void InsertEntryIntoTableView(
+ ResourceTableView& table, const ResourceTablePackage* package, const ResourceTableType* type,
+ const std::string& entry_name, const std::optional<ResourceId>& id,
+ const Visibility& visibility, const std::optional<AllowNew>& allow_new,
+ const std::optional<OverlayableItem>& overlayable_item,
+ const std::optional<StagedId>& staged_id,
+ const std::vector<std::unique_ptr<ResourceConfigValue>>& values,
+ const std::vector<std::unique_ptr<ResourceConfigValue>>& flag_disabled_values) {
SortedVectorInserter<ResourceTablePackageView, PackageViewComparer> package_inserter;
SortedVectorInserter<ResourceTableTypeView, TypeViewComparer> type_inserter;
SortedVectorInserter<ResourceTableEntryView, EntryViewComparer> entry_inserter;
@@ -408,6 +443,9 @@
for (auto& value : values) {
new_entry.values.emplace_back(value.get());
}
+ for (auto& value : flag_disabled_values) {
+ new_entry.flag_disabled_values.emplace_back(value.get());
+ }
entry_inserter.Insert(view_type->entries, std::move(new_entry));
}
@@ -426,6 +464,21 @@
return nullptr;
}
+const ResourceConfigValue* ResourceTableEntryView::FindFlagDisabledValue(
+ const FeatureFlagAttribute& flag, const ConfigDescription& config,
+ android::StringPiece product) const {
+ auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(),
+ ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref());
+ if (iter != values.end()) {
+ const ResourceConfigValue* value = *iter;
+ if (value->value->GetFlag() == flag && value->config == config &&
+ StringPiece(value->product) == product) {
+ return value;
+ }
+ }
+ return nullptr;
+}
+
ResourceTableView ResourceTable::GetPartitionedView(const ResourceTableViewOptions& options) const {
ResourceTableView view;
for (const auto& package : packages) {
@@ -433,13 +486,13 @@
for (const auto& entry : type->entries) {
InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, entry->id,
entry->visibility, entry->allow_new, entry->overlayable_item,
- entry->staged_id, entry->values);
+ entry->staged_id, entry->values, entry->flag_disabled_values);
if (options.create_alias_entries && entry->staged_id) {
auto alias_id = entry->staged_id.value().id;
InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, alias_id,
entry->visibility, entry->allow_new, entry->overlayable_item, {},
- entry->values);
+ entry->values, entry->flag_disabled_values);
}
}
}
@@ -587,6 +640,25 @@
entry->staged_id = res.staged_id.value();
}
+ if (res.value != nullptr && res.value->GetFlagStatus() == FlagStatus::Disabled) {
+ auto disabled_config_value =
+ entry->FindOrCreateFlagDisabledValue(res.value->GetFlag().value(), res.config, res.product);
+ if (!disabled_config_value->value) {
+ // Resource does not exist, add it now.
+ // Must clone the value since it might be in the values vector as well
+ CloningValueTransformer cloner(&string_pool);
+ disabled_config_value->value = res.value->Transform(cloner);
+ } else {
+ diag->Error(android::DiagMessage(source)
+ << "duplicate value for resource '" << res.name << "' " << "with config '"
+ << res.config << "' and flag '"
+ << (res.value->GetFlag().value().negated ? "!" : "")
+ << res.value->GetFlag().value().name << "'");
+ diag->Error(android::DiagMessage(source) << "resource previously defined here");
+ return false;
+ }
+ }
+
if (res.value != nullptr) {
auto config_value = entry->FindOrCreateValue(res.config, res.product);
if (!config_value->value) {
@@ -595,9 +667,9 @@
} else {
// When validation is enabled, ensure that a resource cannot have multiple values defined for
// the same configuration unless protected by flags.
- auto result =
- validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(), res.flag_status)
- : CollisionResult::kKeepBoth;
+ auto result = validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(),
+ res.value->GetFlagStatus())
+ : CollisionResult::kKeepBoth;
if (result == CollisionResult::kConflict) {
result = ResolveValueCollision(config_value->value.get(), res.value.get());
}
@@ -771,11 +843,6 @@
return *this;
}
-NewResourceBuilder& NewResourceBuilder::SetFlagStatus(FlagStatus flag_status) {
- res_.flag_status = flag_status;
- return *this;
-}
-
NewResource NewResourceBuilder::Build() {
return std::move(res_);
}
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index cba6b70..b0e1855 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -136,6 +136,9 @@
// The resource's values for each configuration.
std::vector<std::unique_ptr<ResourceConfigValue>> values;
+ // The resource's values that are behind disabled flags.
+ std::vector<std::unique_ptr<ResourceConfigValue>> flag_disabled_values;
+
explicit ResourceEntry(android::StringPiece name) : name(name) {
}
@@ -148,6 +151,13 @@
android::StringPiece product);
std::vector<ResourceConfigValue*> FindAllValues(const android::ConfigDescription& config);
+ // Either returns the existing ResourceConfigValue in the disabled list with the given flag,
+ // config, and product or creates a new one and returns that. In either case the returned value
+ // does not have the flag set on the value so it must be set by the caller.
+ ResourceConfigValue* FindOrCreateFlagDisabledValue(const FeatureFlagAttribute& flag,
+ const android::ConfigDescription& config,
+ android::StringPiece product = {});
+
template <typename Func>
std::vector<ResourceConfigValue*> FindValuesIf(Func f) {
std::vector<ResourceConfigValue*> results;
@@ -215,9 +225,14 @@
std::optional<OverlayableItem> overlayable_item;
std::optional<StagedId> staged_id;
std::vector<const ResourceConfigValue*> values;
+ std::vector<const ResourceConfigValue*> flag_disabled_values;
const ResourceConfigValue* FindValue(const android::ConfigDescription& config,
android::StringPiece product = {}) const;
+
+ const ResourceConfigValue* FindFlagDisabledValue(const FeatureFlagAttribute& flag,
+ const android::ConfigDescription& config,
+ android::StringPiece product = {}) const;
};
struct ResourceTableTypeView {
@@ -269,7 +284,6 @@
std::optional<AllowNew> allow_new;
std::optional<StagedId> staged_id;
bool allow_mangled = false;
- FlagStatus flag_status = FlagStatus::NoFlag;
};
struct NewResourceBuilder {
@@ -283,7 +297,6 @@
NewResourceBuilder& SetAllowNew(AllowNew allow_new);
NewResourceBuilder& SetStagedId(StagedId id);
NewResourceBuilder& SetAllowMangled(bool allow_mangled);
- NewResourceBuilder& SetFlagStatus(FlagStatus flag_status);
NewResource Build();
private:
diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp
index b75e87c..723cfc0 100644
--- a/tools/aapt2/ResourceValues.cpp
+++ b/tools/aapt2/ResourceValues.cpp
@@ -1102,6 +1102,7 @@
std::unique_ptr<T> CopyValueFields(std::unique_ptr<T> new_value, const T* value) {
new_value->SetSource(value->GetSource());
new_value->SetComment(value->GetComment());
+ new_value->SetFlag(value->GetFlag());
new_value->SetFlagStatus(value->GetFlagStatus());
return new_value;
}
diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h
index a1b1839..e000c65 100644
--- a/tools/aapt2/ResourceValues.h
+++ b/tools/aapt2/ResourceValues.h
@@ -65,10 +65,21 @@
return translatable_;
}
+ void SetFlag(std::optional<FeatureFlagAttribute> val) {
+ flag_ = val;
+ }
+
+ std::optional<FeatureFlagAttribute> GetFlag() const {
+ return flag_;
+ }
+
void SetFlagStatus(FlagStatus val) {
flag_status_ = val;
}
+ // If the value is behind a flag this returns whether that flag was enabled when the value was
+ // parsed by comparing it to the flags passed on the command line to aapt2 (taking into account
+ // negation if necessary). If there was no flag, FlagStatus::NoFlag is returned instead.
FlagStatus GetFlagStatus() const {
return flag_status_;
}
@@ -128,6 +139,7 @@
std::string comment_;
bool weak_ = false;
bool translatable_ = true;
+ std::optional<FeatureFlagAttribute> flag_;
FlagStatus flag_status_ = FlagStatus::NoFlag;
private:
diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto
index 5c64089..a0f60b6 100644
--- a/tools/aapt2/Resources.proto
+++ b/tools/aapt2/Resources.proto
@@ -240,6 +240,9 @@
// The staged resource ID of this finalized resource.
StagedId staged_id = 7;
+
+ // The set of values defined for this entry which are behind disabled flags
+ repeated ConfigValue flag_disabled_config_value = 8;
}
// A Configuration/Value pair.
@@ -283,6 +286,8 @@
// The status of the flag the value is behind if any
uint32 flag_status = 8;
+ bool flag_negated = 9;
+ string flag_name = 10;
}
// A CompoundValue is an abstract type. It represents a value that is a made of other values.
diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp
index 6da3176..d3750a6 100644
--- a/tools/aapt2/cmd/Diff.cpp
+++ b/tools/aapt2/cmd/Diff.cpp
@@ -138,6 +138,22 @@
}
}
+ for (const ResourceConfigValue* config_value_a : entry_a.flag_disabled_values) {
+ auto config_value_b = entry_b.FindFlagDisabledValue(config_value_a->value->GetFlag().value(),
+ config_value_a->config);
+ if (!config_value_b) {
+ std::stringstream str_stream;
+ str_stream << "missing disabled value " << pkg_a.name << ":" << type_a.named_type << "/"
+ << entry_a.name << " config=" << config_value_a->config
+ << " flag=" << config_value_a->value->GetFlag()->ToString();
+ EmitDiffLine(apk_b->GetSource(), str_stream.str());
+ diff = true;
+ } else {
+ diff |= EmitResourceConfigValueDiff(context, apk_a, pkg_a, type_a, entry_a, config_value_a,
+ apk_b, pkg_b, type_b, entry_b, config_value_b);
+ }
+ }
+
// Check for any newly added config values.
for (const ResourceConfigValue* config_value_b : entry_b.values) {
auto config_value_a = entry_a.FindValue(config_value_b->config);
@@ -149,6 +165,18 @@
diff = true;
}
}
+ for (const ResourceConfigValue* config_value_b : entry_b.flag_disabled_values) {
+ auto config_value_a = entry_a.FindFlagDisabledValue(config_value_b->value->GetFlag().value(),
+ config_value_b->config);
+ if (!config_value_a) {
+ std::stringstream str_stream;
+ str_stream << "new disabled config " << pkg_b.name << ":" << type_b.named_type << "/"
+ << entry_b.name << " config=" << config_value_b->config
+ << " flag=" << config_value_b->value->GetFlag()->ToString();
+ EmitDiffLine(apk_b->GetSource(), str_stream.str());
+ diff = true;
+ }
+ }
return diff;
}
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index 7739171..2177c34 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -34,6 +34,30 @@
namespace aapt {
+std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag,
+ const FeatureFlagValues& feature_flag_values,
+ std::string* out_err) {
+ if (!flag) {
+ return FlagStatus::NoFlag;
+ }
+ auto flag_it = feature_flag_values.find(flag->name);
+ if (flag_it == feature_flag_values.end()) {
+ *out_err = "Resource flag value undefined: " + flag->name;
+ return {};
+ }
+ const auto& flag_properties = flag_it->second;
+ if (!flag_properties.read_only) {
+ *out_err = "Only read only flags may be used with resources: " + flag->name;
+ return {};
+ }
+ if (!flag_properties.enabled.has_value()) {
+ *out_err = "Only flags with a value may be used with resources: " + flag->name;
+ return {};
+ }
+ return (flag_properties.enabled.value() != flag->negated) ? FlagStatus::Enabled
+ : FlagStatus::Disabled;
+}
+
std::optional<uint16_t> ParseTargetDensityParameter(StringPiece arg, android::IDiagnostics* diag) {
ConfigDescription preferred_density_config;
if (!ConfigDescription::Parse(arg, &preferred_density_config)) {
diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h
index 6b8813b..f8e44b7 100644
--- a/tools/aapt2/cmd/Util.h
+++ b/tools/aapt2/cmd/Util.h
@@ -49,6 +49,10 @@
using FeatureFlagValues = std::map<std::string, FeatureFlagProperties, std::less<>>;
+std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag,
+ const FeatureFlagValues& feature_flag_values,
+ std::string* out_err);
+
// Parses a configuration density (ex. hdpi, xxhdpi, 234dpi, anydpi, etc).
// Returns Nothing and logs a human friendly error message if the string was not legal.
std::optional<uint16_t> ParseTargetDensityParameter(android::StringPiece arg,
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 55f5e56..c6bd6dd 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -536,6 +536,34 @@
config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config,
&out_table->string_pool, files, out_error);
+
+ if (config_value->value == nullptr) {
+ return false;
+ }
+ }
+
+ // flag disabled
+ for (const pb::ConfigValue& pb_config_value : pb_entry.flag_disabled_config_value()) {
+ const pb::Configuration& pb_config = pb_config_value.config();
+
+ ConfigDescription config;
+ if (!DeserializeConfigFromPb(pb_config, &config, out_error)) {
+ return false;
+ }
+
+ FeatureFlagAttribute flag;
+ flag.name = pb_config_value.value().item().flag_name();
+ flag.negated = pb_config_value.value().item().flag_negated();
+ ResourceConfigValue* config_value =
+ entry->FindOrCreateFlagDisabledValue(std::move(flag), config, pb_config.product());
+ if (config_value->value != nullptr) {
+ *out_error = "duplicate configuration in resource table";
+ return false;
+ }
+
+ config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config,
+ &out_table->string_pool, files, out_error);
+
if (config_value->value == nullptr) {
return false;
}
@@ -748,7 +776,6 @@
if (value == nullptr) {
return {};
}
-
} else if (pb_value.has_compound_value()) {
const pb::CompoundValue& pb_compound_value = pb_value.compound_value();
switch (pb_compound_value.value_case()) {
@@ -1018,6 +1045,12 @@
DeserializeItemFromPbInternal(pb_item, src_pool, config, value_pool, files, out_error);
if (item) {
item->SetFlagStatus((FlagStatus)pb_item.flag_status());
+ if (!pb_item.flag_name().empty()) {
+ FeatureFlagAttribute flag;
+ flag.name = pb_item.flag_name();
+ flag.negated = pb_item.flag_negated();
+ item->SetFlag(std::move(flag));
+ }
}
return item;
}
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index 5772b3b..9c28780 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -427,6 +427,14 @@
SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(),
source_pool.get());
}
+
+ for (const ResourceConfigValue* config_value : entry.flag_disabled_values) {
+ pb::ConfigValue* pb_config_value = pb_entry->add_flag_disabled_config_value();
+ SerializeConfig(config_value->config, pb_config_value->mutable_config());
+ pb_config_value->mutable_config()->set_product(config_value->product);
+ SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(),
+ source_pool.get());
+ }
}
}
}
@@ -721,6 +729,11 @@
}
if (out_value->has_item()) {
out_value->mutable_item()->set_flag_status((uint32_t)value.GetFlagStatus());
+ if (value.GetFlag()) {
+ const auto& flag = value.GetFlag();
+ out_value->mutable_item()->set_flag_negated(flag->negated);
+ out_value->mutable_item()->set_flag_name(flag->name);
+ }
}
}
@@ -730,6 +743,11 @@
item.Accept(&serializer);
out_item->MergeFrom(value.item());
out_item->set_flag_status((uint32_t)item.GetFlagStatus());
+ if (item.GetFlag()) {
+ const auto& flag = item.GetFlag();
+ out_item->set_flag_negated(flag->negated);
+ out_item->set_flag_name(flag->name);
+ }
}
void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledFile* out_file) {
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
index 1ed0c8a..7837e17 100644
--- a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
@@ -9,4 +9,10 @@
<bool name="bool3">false</bool>
<bool name="bool4" android:featureFlag="test.package.falseFlag">true</bool>
+
+ <bool name="bool5">false</bool>
+ <bool name="bool5" android:featureFlag="!test.package.falseFlag">true</bool>
+
+ <bool name="bool6">true</bool>
+ <bool name="bool6" android:featureFlag="!test.package.trueFlag">false</bool>
</resources>
\ No newline at end of file
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index 3db37c2..67d0c41 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -17,6 +17,7 @@
#include "LoadedApk.h"
#include "cmd/Dump.h"
#include "io/StringStream.h"
+#include "test/Common.h"
#include "test/Test.h"
#include "text/Printer.h"
@@ -98,4 +99,47 @@
ASSERT_NE(r_contents.find("public static final int str1"), std::string::npos);
}
+TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlag) {
+ test::TestDiagnosticsImpl diag;
+ const std::string compiled_files_dir = GetTestPath("compiled");
+ ASSERT_FALSE(CompileFile(
+ GetTestPath("res/values/values.xml"),
+ R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool>
+ <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool>
+ </resources>)",
+ compiled_files_dir, &diag,
+ {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"}));
+ ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool/bool1'"));
+}
+
+TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlagDifferentFiles) {
+ test::TestDiagnosticsImpl diag;
+ const std::string compiled_files_dir = GetTestPath("compiled");
+ ASSERT_TRUE(CompileFile(
+ GetTestPath("res/values/values1.xml"),
+ R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool>
+ </resources>)",
+ compiled_files_dir, &diag,
+ {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"}));
+ ASSERT_TRUE(CompileFile(
+ GetTestPath("res/values/values2.xml"),
+ R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool>
+ </resources>)",
+ compiled_files_dir, &diag,
+ {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"}));
+ const std::string out_apk = GetTestPath("out.apk");
+ std::vector<std::string> link_args = {
+ "--manifest",
+ GetDefaultManifest(),
+ "-o",
+ out_apk,
+ };
+
+ ASSERT_FALSE(Link(link_args, compiled_files_dir, &diag));
+ ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool1'"));
+}
+
} // namespace aapt
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 37a039e..d216979 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -321,6 +321,30 @@
}
}
}
+
+ // disabled values
+ for (auto& src_config_value : src_entry->flag_disabled_values) {
+ auto dst_config_value = dst_entry->FindOrCreateFlagDisabledValue(
+ src_config_value->value->GetFlag().value(), src_config_value->config,
+ src_config_value->product);
+ if (!dst_config_value->value) {
+ // Resource does not exist, add it now.
+ // Must clone the value since it might be in the values vector as well
+ CloningValueTransformer cloner(&main_table_->string_pool);
+ dst_config_value->value = src_config_value->value->Transform(cloner);
+ } else {
+ error = true;
+ context_->GetDiagnostics()->Error(
+ android::DiagMessage(src_config_value->value->GetSource())
+ << "duplicate value for resource '" << src_entry->name << "' " << "with config '"
+ << src_config_value->config << "' and flag '"
+ << (src_config_value->value->GetFlag()->negated ? "!" : "")
+ << src_config_value->value->GetFlag()->name << "'");
+ context_->GetDiagnostics()->Note(
+ android::DiagMessage(dst_config_value->value->GetSource())
+ << "resource previously defined here");
+ }
+ }
}
}
return !error;
diff --git a/tools/aapt2/test/Common.cpp b/tools/aapt2/test/Common.cpp
index cdf24534..c7dd4c9 100644
--- a/tools/aapt2/test/Common.cpp
+++ b/tools/aapt2/test/Common.cpp
@@ -21,23 +21,6 @@
namespace aapt {
namespace test {
-struct TestDiagnosticsImpl : public android::IDiagnostics {
- void Log(Level level, android::DiagMessageActual& actual_msg) override {
- switch (level) {
- case Level::Note:
- return;
-
- case Level::Warn:
- std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl;
- break;
-
- case Level::Error:
- std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl;
- break;
- }
- }
-};
-
android::IDiagnostics* GetDiagnostics() {
static TestDiagnosticsImpl diag;
return &diag;
diff --git a/tools/aapt2/test/Common.h b/tools/aapt2/test/Common.h
index 0437980..b06c432 100644
--- a/tools/aapt2/test/Common.h
+++ b/tools/aapt2/test/Common.h
@@ -37,6 +37,32 @@
namespace aapt {
namespace test {
+struct TestDiagnosticsImpl : public android::IDiagnostics {
+ void Log(Level level, android::DiagMessageActual& actual_msg) override {
+ switch (level) {
+ case Level::Note:
+ return;
+
+ case Level::Warn:
+ std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl;
+ log << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl;
+ break;
+
+ case Level::Error:
+ std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl;
+ log << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl;
+ break;
+ }
+ }
+
+ std::string GetLog() {
+ return log.str();
+ }
+
+ private:
+ std::ostringstream log;
+};
+
android::IDiagnostics* GetDiagnostics();
inline ResourceName ParseNameOrDie(android::StringPiece str) {
diff --git a/tools/aapt2/test/Fixture.cpp b/tools/aapt2/test/Fixture.cpp
index b91abe5..570bcf1 100644
--- a/tools/aapt2/test/Fixture.cpp
+++ b/tools/aapt2/test/Fixture.cpp
@@ -91,10 +91,13 @@
}
bool CommandTestFixture::CompileFile(const std::string& path, const std::string& contents,
- android::StringPiece out_dir, android::IDiagnostics* diag) {
+ android::StringPiece out_dir, android::IDiagnostics* diag,
+ const std::vector<android::StringPiece>& additional_args) {
WriteFile(path, contents);
CHECK(file::mkdirs(out_dir.data()));
- return CompileCommand(diag).Execute({path, "-o", out_dir, "-v"}, &std::cerr) == 0;
+ std::vector<android::StringPiece> args = {path, "-o", out_dir, "-v"};
+ args.insert(args.end(), additional_args.begin(), additional_args.end());
+ return CompileCommand(diag).Execute(args, &std::cerr) == 0;
}
bool CommandTestFixture::Link(const std::vector<std::string>& args, android::IDiagnostics* diag) {
diff --git a/tools/aapt2/test/Fixture.h b/tools/aapt2/test/Fixture.h
index 14298d16..178d011 100644
--- a/tools/aapt2/test/Fixture.h
+++ b/tools/aapt2/test/Fixture.h
@@ -73,7 +73,8 @@
// Wries the contents of the file to the specified path. The file is compiled and the flattened
// file is written to the out directory.
bool CompileFile(const std::string& path, const std::string& contents,
- android::StringPiece flat_out_dir, android::IDiagnostics* diag);
+ android::StringPiece flat_out_dir, android::IDiagnostics* diag,
+ const std::vector<android::StringPiece>& additional_args = {});
// Executes the link command with the specified arguments.
bool Link(const std::vector<std::string>& args, android::IDiagnostics* diag);