Merge "Trim leading src-dir from Ravenwood sources" into main
diff --git a/Android.bp b/Android.bp
index b5f7e99..49256dd 100644
--- a/Android.bp
+++ b/Android.bp
@@ -631,6 +631,7 @@
name: "android-non-updatable-stub-sources",
srcs: [
":framework-mime-sources", // mimemap builds separately but has no separate droidstubs.
+ ":framework-minus-apex-aconfig-srcjars",
":framework-non-updatable-sources",
":opt-telephony-srcs",
":opt-net-voip-srcs",
diff --git a/api/Android.bp b/api/Android.bp
index 222275f..d11ea7b 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -74,7 +74,6 @@
"framework-configinfrastructure",
"framework-connectivity",
"framework-connectivity-t",
- "framework-crashrecovery",
"framework-devicelock",
"framework-graphics",
"framework-healthfitness",
@@ -97,7 +96,6 @@
system_server_classpath: [
"service-art",
"service-configinfrastructure",
- "service-crashrecovery",
"service-healthfitness",
"service-media-s",
"service-permission",
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index d566552..5688b96 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -29,9 +29,6 @@
droidstubs {
name: "api-stubs-docs-non-updatable",
- srcs: [
- ":framework-minus-apex-aconfig-srcjars",
- ],
defaults: [
"android-non-updatable-stubs-defaults",
"module-classpath-stubs-defaults",
@@ -91,9 +88,6 @@
droidstubs {
name: "system-api-stubs-docs-non-updatable",
- srcs: [
- ":framework-minus-apex-aconfig-srcjars",
- ],
defaults: [
"android-non-updatable-stubs-defaults",
"module-classpath-stubs-defaults",
@@ -134,9 +128,6 @@
droidstubs {
name: "test-api-stubs-docs-non-updatable",
- srcs: [
- ":framework-minus-apex-aconfig-srcjars",
- ],
defaults: [
"android-non-updatable-stubs-defaults",
"module-classpath-stubs-defaults",
@@ -184,9 +175,6 @@
droidstubs {
name: "module-lib-api-stubs-docs-non-updatable",
- srcs: [
- ":framework-minus-apex-aconfig-srcjars",
- ],
defaults: [
"android-non-updatable-stubs-defaults",
"module-classpath-stubs-defaults",
diff --git a/boot/Android.bp b/boot/Android.bp
index b33fab6..8a3d35e 100644
--- a/boot/Android.bp
+++ b/boot/Android.bp
@@ -84,10 +84,6 @@
module: "com.android.conscrypt-bootclasspath-fragment",
},
{
- apex: "com.android.crashrecovery",
- module: "com.android.crashrecovery-bootclasspath-fragment",
- },
- {
apex: "com.android.devicelock",
module: "com.android.devicelock-bootclasspath-fragment",
},
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index e12181a..3b6ea14 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -135,6 +135,7 @@
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
+import android.os.IBinderCallback;
import android.os.ICancellationSignal;
import android.os.LocaleList;
import android.os.Looper;
@@ -7274,6 +7275,18 @@
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
+
+ // Set binder transaction callback after finishing bindApplication
+ Binder.setTransactionCallback(new IBinderCallback() {
+ @Override
+ public void onTransactionError(int pid, int code, int flags, int err) {
+ try {
+ mgr.frozenBinderTransactionDetected(pid, code, flags, err);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ });
}
@UnsupportedAppUsage
diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java
index d15c79f..24cb9ea 100644
--- a/core/java/android/app/ApplicationExitInfo.java
+++ b/core/java/android/app/ApplicationExitInfo.java
@@ -477,6 +477,16 @@
*/
public static final int SUBREASON_OOM_KILL = 30;
+ /**
+ * The process was killed because its async kernel binder buffer is running out
+ * while being frozen.
+ * this would be set only when the reason is {@link #REASON_FREEZER}.
+ *
+ * For internal use only.
+ * @hide
+ */
+ public static final int SUBREASON_FREEZER_BINDER_ASYNC_FULL = 31;
+
// If there is any OEM code which involves additional app kill reasons, it should
// be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000.
@@ -654,6 +664,7 @@
SUBREASON_UNDELIVERED_BROADCAST,
SUBREASON_EXCESSIVE_BINDER_OBJECTS,
SUBREASON_OOM_KILL,
+ SUBREASON_FREEZER_BINDER_ASYNC_FULL,
})
@Retention(RetentionPolicy.SOURCE)
public @interface SubReason {}
@@ -1383,6 +1394,8 @@
return "EXCESSIVE BINDER OBJECTS";
case SUBREASON_OOM_KILL:
return "OOM KILL";
+ case SUBREASON_FREEZER_BINDER_ASYNC_FULL:
+ return "FREEZER BINDER ASYNC FULL";
default:
return "UNKNOWN";
}
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 03baf26..520bf7d 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -939,4 +939,14 @@
int[] getUidFrozenState(in int[] uids);
int checkPermissionForDevice(in String permission, int pid, int uid, int deviceId);
+
+ /**
+ * Notify AMS about binder transactions to frozen apps.
+ *
+ * @param debugPid The binder transaction sender
+ * @param code The binder transaction code
+ * @param flags The binder transaction flags
+ * @param err The binder transaction error
+ */
+ oneway void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err);
}
diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig
index 55ae8ee..f380963 100644
--- a/core/java/android/companion/virtual/flags.aconfig
+++ b/core/java/android/companion/virtual/flags.aconfig
@@ -37,10 +37,17 @@
}
flag {
- name: "virtual_camera"
- namespace: "virtual_devices"
- description: "Enable Virtual Camera"
- bug: "270352264"
+ name: "virtual_camera"
+ namespace: "virtual_devices"
+ description: "Enable Virtual Camera"
+ bug: "270352264"
+}
+
+flag {
+ name: "stream_camera"
+ namespace: "virtual_devices"
+ description: "Enable streaming camera to Virtual Devices"
+ bug: "291740640"
}
flag {
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 218d4bb..3db1cb0 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -665,6 +665,32 @@
*/
public static final native void blockUntilThreadAvailable();
+
+ /**
+ * TODO (b/308179628): Move this to libbinder for non-Java usages.
+ */
+ private static IBinderCallback sBinderCallback = null;
+
+ /**
+ * Set callback function for unexpected binder transaction errors.
+ *
+ * @hide
+ */
+ public static final void setTransactionCallback(IBinderCallback callback) {
+ sBinderCallback = callback;
+ }
+
+ /**
+ * Execute the callback function if it's already set.
+ *
+ * @hide
+ */
+ public static final void transactionCallback(int pid, int code, int flags, int err) {
+ if (sBinderCallback != null) {
+ sBinderCallback.onTransactionError(pid, code, flags, err);
+ }
+ }
+
/**
* Default constructor just initializes the object.
*
diff --git a/core/java/android/os/IBinderCallback.java b/core/java/android/os/IBinderCallback.java
new file mode 100644
index 0000000..e4be5b0
--- /dev/null
+++ b/core/java/android/os/IBinderCallback.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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;
+
+/**
+ * Callback interface for binder transaction errors
+ *
+ * @hide
+ */
+public interface IBinderCallback {
+ /**
+ * Callback function for unexpected binder transaction errors.
+ *
+ * @param debugPid The binder transaction sender
+ * @param code The binder transaction code
+ * @param flags The binder transaction flags
+ * @param err The binder transaction error
+ */
+ void onTransactionError(int debugPid, int code, int flags, int err);
+}
diff --git a/core/java/com/android/internal/os/BinderfsStatsReader.java b/core/java/com/android/internal/os/BinderfsStatsReader.java
new file mode 100644
index 0000000..9cc4a35
--- /dev/null
+++ b/core/java/com/android/internal/os/BinderfsStatsReader.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 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.internal.os;
+
+import com.android.internal.util.ProcFileReader;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Reads and parses {@code binder_logs/stats} file in the {@code binderfs} filesystem.
+ * Reuse procFileReader as the contents are generated by Linux kernel in the same way.
+ *
+ * A typical example of binderfs stats log
+ *
+ * binder stats:
+ * BC_TRANSACTION: 378004
+ * BC_REPLY: 268352
+ * BC_FREE_BUFFER: 665854
+ * ...
+ * proc 12645
+ * context binder
+ * threads: 12
+ * requested threads: 0+5/15
+ * ready threads 0
+ * free async space 520192
+ * ...
+ */
+public class BinderfsStatsReader {
+ private final String mPath;
+
+ public BinderfsStatsReader() {
+ mPath = "/dev/binderfs/binder_logs/stats";
+ }
+
+ public BinderfsStatsReader(String path) {
+ mPath = path;
+ }
+
+ /**
+ * Read binderfs stats and call the consumer(pid, free) function for each valid process
+ *
+ * @param predicate Test if the pid is valid.
+ * @param biConsumer Callback function for each valid pid and its free async space
+ * @param consumer The error function to deal with exceptions
+ */
+ public void handleFreeAsyncSpace(Predicate<Integer> predicate,
+ BiConsumer<Integer, Integer> biConsumer, Consumer<Exception> consumer) {
+ try (ProcFileReader mReader = new ProcFileReader(new FileInputStream(mPath))) {
+ while (mReader.hasMoreData()) {
+ // find the next process
+ if (!mReader.nextString().equals("proc")) {
+ mReader.finishLine();
+ continue;
+ }
+
+ // read pid
+ int pid = mReader.nextInt();
+ mReader.finishLine();
+
+ // check if we have interest in this process
+ if (!predicate.test(pid)) {
+ continue;
+ }
+
+ // read free async space
+ mReader.finishLine(); // context binder
+ mReader.finishLine(); // threads:
+ mReader.finishLine(); // requested threads:
+ mReader.finishLine(); // ready threads
+ if (!mReader.nextString().equals("free")) {
+ mReader.finishLine();
+ continue;
+ }
+ if (!mReader.nextString().equals("async")) {
+ mReader.finishLine();
+ continue;
+ }
+ if (!mReader.nextString().equals("space")) {
+ mReader.finishLine();
+ continue;
+ }
+ int free = mReader.nextInt();
+ mReader.finishLine();
+ biConsumer.accept(pid, free);
+ }
+ } catch (IOException | NumberFormatException e) {
+ consumer.accept(e);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index 3e9458d..b462c21 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -27,6 +27,7 @@
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_SHOWN;
import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN;
@@ -215,6 +216,12 @@
*/
public static final int ACTION_SMARTSPACE_DOORBELL = 22;
+ /**
+ * Time it takes to lazy-load the image of a {@link android.app.Notification.BigPictureStyle}
+ * notification.
+ */
+ public static final int ACTION_NOTIFICATION_BIG_PICTURE_LOADED = 23;
+
private static final int[] ACTIONS_ALL = {
ACTION_EXPAND_PANEL,
ACTION_TOGGLE_RECENTS,
@@ -239,6 +246,7 @@
ACTION_REQUEST_IME_SHOWN,
ACTION_REQUEST_IME_HIDDEN,
ACTION_SMARTSPACE_DOORBELL,
+ ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
};
/** @hide */
@@ -266,6 +274,7 @@
ACTION_REQUEST_IME_SHOWN,
ACTION_REQUEST_IME_HIDDEN,
ACTION_SMARTSPACE_DOORBELL,
+ ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Action {
@@ -296,6 +305,7 @@
UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_SHOWN,
UIACTION_LATENCY_REPORTED__ACTION__ACTION_REQUEST_IME_HIDDEN,
UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL,
+ UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED,
};
private final Object mLock = new Object();
@@ -480,6 +490,8 @@
return "ACTION_REQUEST_IME_HIDDEN";
case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SMARTSPACE_DOORBELL:
return "ACTION_SMARTSPACE_DOORBELL";
+ case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED:
+ return "ACTION_NOTIFICATION_BIG_PICTURE_LOADED";
default:
throw new IllegalArgumentException("Invalid action");
}
diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index bfd80a9e..d2d5186 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -73,6 +73,7 @@
jclass mClass;
jmethodID mExecTransact;
jmethodID mGetInterfaceDescriptor;
+ jmethodID mTransactionCallback;
// Object state.
jfieldID mObject;
@@ -1173,6 +1174,8 @@
gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z");
gBinderOffsets.mGetInterfaceDescriptor = GetMethodIDOrDie(env, clazz, "getInterfaceDescriptor",
"()Ljava/lang/String;");
+ gBinderOffsets.mTransactionCallback =
+ GetStaticMethodIDOrDie(env, clazz, "transactionCallback", "(IIII)V");
gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J");
return RegisterMethodsOrDie(
@@ -1387,7 +1390,12 @@
if (err == NO_ERROR) {
return JNI_TRUE;
- } else if (err == UNKNOWN_TRANSACTION) {
+ }
+
+ env->CallStaticVoidMethod(gBinderOffsets.mClass, gBinderOffsets.mTransactionCallback, getpid(),
+ code, flags, err);
+
+ if (err == UNKNOWN_TRANSACTION) {
return JNI_FALSE;
}
diff --git a/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java b/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java
new file mode 100644
index 0000000..e9f6450
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/BinderfsStatsReaderTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2023 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.internal.os;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.content.Context;
+import android.os.FileUtils;
+import android.util.IntArray;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Files;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BinderfsStatsReaderTest {
+ private static final String BINDER_LOGS_STATS_HEADER = """
+ binder stats:
+ BC_TRANSACTION: 695756
+ BC_REPLY: 547779
+ BC_FREE_BUFFER: 1283223
+ BR_FAILED_REPLY: 4
+ BR_FROZEN_REPLY: 3
+ BR_ONEWAY_SPAM_SUSPECT: 1
+ proc: active 313 total 377
+ thread: active 3077 total 5227
+ """;
+ private static final String BINDER_LOGS_STATS_PROC1 = """
+ proc 14505
+ context binder
+ threads: 4
+ requested threads: 0+2/15
+ ready threads 0
+ free async space 520192
+ nodes: 9
+ refs: 29 s 29 w 29
+ buffers: 0
+ """;
+ private static final String BINDER_LOGS_STATS_PROC2 = """
+ proc 14461
+ context binder
+ threads: 8
+ requested threads: 0+2/15
+ ready threads 0
+ free async space 62
+ nodes: 30
+ refs: 51 s 51 w 51
+ buffers: 0
+ """;
+ private static final String BINDER_LOGS_STATS_PROC3 = """
+ proc 542
+ context binder
+ threads: 2
+ requested threads: 0+0/15
+ ready threads 0
+ free async space 519896
+ nodes: 1
+ refs: 2 s 3 w 2
+ buffers: 1
+ """;
+ private static final String BINDER_LOGS_STATS_PROC4 = """
+ proc 540
+ context binder
+ threads: 1
+ requested threads: 0+0/0
+ ready threads 1
+ free async space 44
+ nodes: 4
+ refs: 1 s 1 w 1
+ buffers: 0
+ """;
+ private File mStatsDirectory;
+ private int mFreezerBinderAsyncThreshold;
+ private IntArray mValidPids; // The pool of valid pids
+ private IntArray mStatsPids; // The pids read from binderfs stats that are also valid
+ private IntArray mStatsFree; // The free async space of the above pids
+ private boolean mHasError;
+
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getContext();
+ mStatsDirectory = context.getDir("binder_logs", Context.MODE_PRIVATE);
+ mFreezerBinderAsyncThreshold = 1024;
+ mValidPids = IntArray.fromArray(new int[]{14505, 14461, 542, 540}, 4);
+ mStatsPids = new IntArray();
+ mStatsFree = new IntArray();
+ mHasError = false;
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ FileUtils.deleteContents(mStatsDirectory);
+ }
+
+ @Test
+ public void testNoneProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER);
+ assertFalse(mHasError);
+ assertEquals(0, mStatsPids.size());
+ assertEquals(0, mStatsFree.size());
+ }
+
+ @Test
+ public void testOneProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1);
+ assertFalse(mHasError);
+ assertEquals(0, mStatsPids.size());
+ assertEquals(0, mStatsFree.size());
+ }
+
+ @Test
+ public void testTwoProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+ + BINDER_LOGS_STATS_PROC2);
+ assertFalse(mHasError);
+ assertArrayEquals(mStatsPids.toArray(), new int[]{14461});
+ assertArrayEquals(mStatsFree.toArray(), new int[]{62});
+ }
+
+ @Test
+ public void testThreeProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+ + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3);
+ assertFalse(mHasError);
+ assertArrayEquals(mStatsPids.toArray(), new int[]{14461});
+ assertArrayEquals(mStatsFree.toArray(), new int[]{62});
+ }
+
+ @Test
+ public void testFourProc() throws Exception {
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+ + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3 + BINDER_LOGS_STATS_PROC4);
+ assertFalse(mHasError);
+ assertArrayEquals(mStatsPids.toArray(), new int[]{14461, 540});
+ assertArrayEquals(mStatsFree.toArray(), new int[]{62, 44});
+ }
+
+ @Test
+ public void testInvalidProc() throws Exception {
+ mValidPids = new IntArray();
+ runHandleBlockingFileLocks(BINDER_LOGS_STATS_HEADER + BINDER_LOGS_STATS_PROC1
+ + BINDER_LOGS_STATS_PROC2 + BINDER_LOGS_STATS_PROC3 + BINDER_LOGS_STATS_PROC4);
+ assertFalse(mHasError);
+ assertEquals(0, mStatsPids.size());
+ assertEquals(0, mStatsFree.size());
+ }
+
+ private void runHandleBlockingFileLocks(String fileContents) throws Exception {
+ File tempFile = File.createTempFile("stats", null, mStatsDirectory);
+ Files.write(tempFile.toPath(), fileContents.getBytes());
+ new BinderfsStatsReader(tempFile.toString()).handleFreeAsyncSpace(
+ // Check if the current process is a valid one
+ mValidPids::contains,
+
+ // Check if the current process is running out of async binder space
+ (pid, free) -> {
+ if (free < mFreezerBinderAsyncThreshold) {
+ mStatsPids.add(pid);
+ mStatsFree.add(free);
+ }
+ },
+
+ // Log the error if binderfs stats can't be accesses or correctly parsed
+ exception -> {
+ mHasError = true;
+ });
+ Files.delete(tempFile.toPath());
+ }
+}
diff --git a/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto b/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
index c82a70c..5c58158 100644
--- a/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
+++ b/libs/WindowManager/Shell/proto/wm_shell_transition_trace.proto
@@ -48,7 +48,7 @@
optional int32 handler = 3;
optional int64 merge_time_ns = 4;
optional int64 merge_request_time_ns = 5;
- optional int32 merged_into = 6;
+ optional int32 merge_target = 6;
optional int64 abort_time_ns = 7;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
index e27e4f9..5919aad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java
@@ -129,13 +129,12 @@
* Adds an entry in the trace to log that a request to merge a transition was made.
*
* @param mergeRequestedTransitionId The id of the transition we are requesting to be merged.
- * @param playingTransitionId The id of the transition we was to merge the transition into.
*/
public void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) {
com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
proto.id = mergeRequestedTransitionId;
proto.mergeRequestTimeNs = SystemClock.elapsedRealtimeNanos();
- proto.mergedInto = playingTransitionId;
+ proto.mergeTarget = playingTransitionId;
mTraceBuffer.add(proto);
}
@@ -150,7 +149,7 @@
com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition();
proto.id = mergedTransitionId;
proto.mergeTimeNs = SystemClock.elapsedRealtimeNanos();
- proto.mergedInto = playingTransitionId;
+ proto.mergeTarget = playingTransitionId;
mTraceBuffer.add(proto);
}
diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp
index 009407a..eaeda3c 100644
--- a/packages/SettingsLib/SpaPrivileged/Android.bp
+++ b/packages/SettingsLib/SpaPrivileged/Android.bp
@@ -38,7 +38,6 @@
static_libs: [
"androidx.compose.runtime_runtime",
"SpaPrivilegedLib",
- "android.content.pm.flags-aconfig-java",
],
kotlincflags: ["-Xjvm-default=all"],
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
index 92fd0cd..95e678f 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/PackageManagers.kt
@@ -35,7 +35,7 @@
fun ApplicationInfo.hasGrantPermission(permission: String): Boolean
suspend fun getAppOpPermissionPackages(userId: Int, permission: String): Set<String>
- fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo?
+ fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo?
}
object PackageManagers : IPackageManagers by PackageManagersImpl(PackageManagerWrapperImpl)
@@ -72,14 +72,16 @@
?: false
override fun ApplicationInfo.hasRequestPermission(permission: String): Boolean {
- val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
+ val packageInfo =
+ getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
return packageInfo?.requestedPermissions?.let {
permission in it
} ?: false
}
override fun ApplicationInfo.hasGrantPermission(permission: String): Boolean {
- val packageInfo = getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS, userId)
+ val packageInfo =
+ getPackageInfoAsUser(packageName, PackageManager.GET_PERMISSIONS.toLong(), userId)
val index = packageInfo?.requestedPermissions?.indexOf(permission) ?: return false
return index >= 0 &&
checkNotNull(packageInfo.requestedPermissionsFlags)[index]
@@ -91,8 +93,8 @@
iPackageManager.isPackageAvailable(it, userId)
}.toSet()
- override fun getPackageInfoAsUser(packageName: String, flags: Int, userId: Int): PackageInfo? =
- packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags.toLong(), userId)
+ override fun getPackageInfoAsUser(packageName: String, flags: Long, userId: Int): PackageInfo? =
+ packageManagerWrapper.getPackageInfoAsUserCached(packageName, flags, userId)
private fun Int.hasFlag(flag: Int) = (this and flag) > 0
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index c67df71..b0832e3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -16,6 +16,7 @@
package com.android.settingslib.bluetooth;
+import android.annotation.CallbackExecutor;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
@@ -39,6 +40,7 @@
import android.util.LruCache;
import android.util.Pair;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
@@ -52,8 +54,12 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
import java.util.stream.Stream;
/**
@@ -101,6 +107,8 @@
private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>();
+ private final Map<Callback, Executor> mCallbackExecutorMap = new ConcurrentHashMap<>();
+
/**
* Last time a bt profile auto-connect was attempted.
* If an ACTION_UUID intent comes in within
@@ -992,18 +1000,39 @@
return new ArrayList<>(mRemovedProfiles);
}
+ /**
+ * @deprecated Use {@link #registerCallback(Executor, Callback)}.
+ */
+ @Deprecated
public void registerCallback(Callback callback) {
mCallbacks.add(callback);
}
+ /**
+ * Registers a {@link Callback} that will be invoked when the bluetooth device attribute is
+ * changed.
+ *
+ * @param executor an {@link Executor} to execute given callback
+ * @param callback user implementation of the {@link Callback}
+ */
+ public void registerCallback(
+ @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) {
+ Objects.requireNonNull(executor, "executor cannot be null");
+ Objects.requireNonNull(callback, "callback cannot be null");
+ mCallbackExecutorMap.put(callback, executor);
+ }
+
public void unregisterCallback(Callback callback) {
mCallbacks.remove(callback);
+ mCallbackExecutorMap.remove(callback);
}
void dispatchAttributesChanged() {
for (Callback callback : mCallbacks) {
callback.onDeviceAttributesChanged();
}
+ mCallbackExecutorMap.forEach((callback, executor) ->
+ executor.execute(callback::onDeviceAttributesChanged));
}
@Override
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
deleted file mode 100644
index 82d4239..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2023 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.compose.animation.scene
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-
-interface EdgeDetector {
- /**
- * Return the [Edge] associated to [position] inside a layout of size [layoutSize], given
- * [density] and [orientation].
- */
- fun edge(
- layoutSize: IntSize,
- position: IntOffset,
- density: Density,
- orientation: Orientation,
- ): Edge?
-}
-
-val DefaultEdgeDetector = FixedSizeEdgeDetector(40.dp)
-
-/** An [EdgeDetector] that detects edges assuming a fixed edge size of [size]. */
-class FixedSizeEdgeDetector(val size: Dp) : EdgeDetector {
- override fun edge(
- layoutSize: IntSize,
- position: IntOffset,
- density: Density,
- orientation: Orientation,
- ): Edge? {
- val axisSize: Int
- val axisPosition: Int
- val topOrLeft: Edge
- val bottomOrRight: Edge
- when (orientation) {
- Orientation.Horizontal -> {
- axisSize = layoutSize.width
- axisPosition = position.x
- topOrLeft = Edge.Left
- bottomOrRight = Edge.Right
- }
- Orientation.Vertical -> {
- axisSize = layoutSize.height
- axisPosition = position.y
- topOrLeft = Edge.Top
- bottomOrRight = Edge.Bottom
- }
- }
-
- val sizePx = with(density) { size.toPx() }
- return when {
- axisPosition <= sizePx -> topOrLeft
- axisPosition >= axisSize - sizePx -> bottomOrRight
- else -> null
- }
- }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
index ae7d8f5..d005413 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt
@@ -2,6 +2,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import kotlinx.coroutines.CoroutineScope
interface GestureHandler {
val draggable: DraggableHandler
@@ -9,9 +10,9 @@
}
interface DraggableHandler {
- fun onDragStarted(startedPosition: Offset, pointersDown: Int = 1)
+ suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset)
fun onDelta(pixels: Float)
- fun onDragStopped(velocity: Float)
+ suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float)
}
interface NestedScrollHandler {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
deleted file mode 100644
index 97d3fff..0000000
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright (C) 2023 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.compose.animation.scene
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
-import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
-import androidx.compose.foundation.gestures.horizontalDrag
-import androidx.compose.foundation.gestures.verticalDrag
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.util.VelocityTracker
-import androidx.compose.ui.input.pointer.util.addPointerInputChange
-import androidx.compose.ui.platform.LocalViewConfiguration
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.util.fastForEach
-
-/**
- * Make an element draggable in the given [orientation].
- *
- * The main difference with [multiPointerDraggable] and
- * [androidx.compose.foundation.gestures.draggable] is that [onDragStarted] also receives the number
- * of pointers that are down when the drag is started. If you don't need this information, you
- * should use `draggable` instead.
- *
- * Note that the current implementation is trivial: we wait for the touch slope on the *first* down
- * pointer, then we count the number of distinct pointers that are down right before calling
- * [onDragStarted]. This means that the drag won't start when a first pointer is down (but not
- * dragged) and a second pointer is down and dragged. This is an implementation detail that might
- * change in the future.
- */
-// TODO(b/291055080): Migrate to the Modifier.Node API.
-@Composable
-internal fun Modifier.multiPointerDraggable(
- orientation: Orientation,
- enabled: Boolean,
- startDragImmediately: Boolean,
- onDragStarted: (startedPosition: Offset, pointersDown: Int) -> Unit,
- onDragDelta: (Float) -> Unit,
- onDragStopped: (velocity: Float) -> Unit,
-): Modifier {
- val onDragStarted by rememberUpdatedState(onDragStarted)
- val onDragStopped by rememberUpdatedState(onDragStopped)
- val onDragDelta by rememberUpdatedState(onDragDelta)
- val startDragImmediately by rememberUpdatedState(startDragImmediately)
-
- val velocityTracker = remember { VelocityTracker() }
- val maxFlingVelocity =
- LocalViewConfiguration.current.maximumFlingVelocity.let { max ->
- val maxF = max.toFloat()
- Velocity(maxF, maxF)
- }
-
- return this.pointerInput(enabled, orientation, maxFlingVelocity) {
- if (!enabled) {
- return@pointerInput
- }
-
- val onDragStart: (Offset, Int) -> Unit = { startedPosition, pointersDown ->
- velocityTracker.resetTracking()
- onDragStarted(startedPosition, pointersDown)
- }
-
- val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) }
-
- val onDragEnd: () -> Unit = {
- val velocity = velocityTracker.calculateVelocity(maxFlingVelocity)
- onDragStopped(
- when (orientation) {
- Orientation.Horizontal -> velocity.x
- Orientation.Vertical -> velocity.y
- }
- )
- }
-
- val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = { change, amount ->
- velocityTracker.addPointerInputChange(change)
- onDragDelta(amount)
- }
-
- detectDragGestures(
- orientation = orientation,
- startDragImmediately = { startDragImmediately },
- onDragStart = onDragStart,
- onDragEnd = onDragEnd,
- onDragCancel = onDragCancel,
- onDrag = onDrag,
- )
- }
-}
-
-/**
- * Detect drag gestures in the given [orientation].
- *
- * This function is a mix of [androidx.compose.foundation.gestures.awaitDownAndSlop] and
- * [androidx.compose.foundation.gestures.detectVerticalDragGestures] to add support for:
- * 1) starting the gesture immediately without requiring a drag >= touch slope;
- * 2) passing the number of pointers down to [onDragStart].
- */
-private suspend fun PointerInputScope.detectDragGestures(
- orientation: Orientation,
- startDragImmediately: () -> Boolean,
- onDragStart: (startedPosition: Offset, pointersDown: Int) -> Unit,
- onDragEnd: () -> Unit,
- onDragCancel: () -> Unit,
- onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
-) {
- awaitEachGesture {
- val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
- var overSlop = 0f
- val drag =
- if (startDragImmediately()) {
- initialDown.consume()
- initialDown
- } else {
- val down = awaitFirstDown(requireUnconsumed = false)
- val onSlopReached = { change: PointerInputChange, over: Float ->
- change.consume()
- overSlop = over
- }
-
- // TODO(b/291055080): Replace by await[Orientation]PointerSlopOrCancellation once
- // it is public.
- when (orientation) {
- Orientation.Horizontal ->
- awaitHorizontalTouchSlopOrCancellation(down.id, onSlopReached)
- Orientation.Vertical ->
- awaitVerticalTouchSlopOrCancellation(down.id, onSlopReached)
- }
- }
-
- if (drag != null) {
- // Count the number of pressed pointers.
- val pressed = mutableSetOf<PointerId>()
- currentEvent.changes.fastForEach { change ->
- if (change.pressed) {
- pressed.add(change.id)
- }
- }
-
- onDragStart(drag.position, pressed.size)
- onDrag(drag, overSlop)
-
- val successful =
- when (orientation) {
- Orientation.Horizontal ->
- horizontalDrag(drag.id) {
- onDrag(it, it.positionChange().x)
- it.consume()
- }
- Orientation.Vertical ->
- verticalDrag(drag.id) {
- onDrag(it, it.positionChange().y)
- it.consume()
- }
- }
-
- if (successful) {
- onDragEnd()
- } else {
- onDragCancel()
- }
- }
- }
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
index 3fd6828..9c799b28 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt
@@ -16,6 +16,7 @@
package com.android.compose.animation.scene
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
@@ -100,3 +101,19 @@
MovableElement(layoutImpl, scene, key, modifier, content)
}
}
+
+/** The destination scene when swiping up or left from [upOrLeft]. */
+internal fun Scene.upOrLeft(orientation: Orientation): SceneKey? {
+ return when (orientation) {
+ Orientation.Vertical -> userActions[Swipe.Up]
+ Orientation.Horizontal -> userActions[Swipe.Left]
+ }
+}
+
+/** The destination scene when swiping down or right from [downOrRight]. */
+internal fun Scene.downOrRight(orientation: Orientation): SceneKey? {
+ return when (orientation) {
+ Orientation.Vertical -> userActions[Swipe.Down]
+ Orientation.Horizontal -> userActions[Swipe.Right]
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 1f38e70..74e66d2 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -16,7 +16,6 @@
package com.android.compose.animation.scene
-import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
@@ -38,7 +37,6 @@
* instance by triggering back navigation or by swiping to a new scene.
* @param transitions the definition of the transitions used to animate a change of scene.
* @param state the observable state of this layout.
- * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
* @param scenes the configuration of the different scenes of this layout.
*/
@Composable
@@ -48,7 +46,6 @@
transitions: SceneTransitions,
modifier: Modifier = Modifier,
state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
- edgeDetector: EdgeDetector = DefaultEdgeDetector,
scenes: SceneTransitionLayoutScope.() -> Unit,
) {
val density = LocalDensity.current
@@ -59,17 +56,15 @@
transitions,
state,
density,
- edgeDetector,
)
}
layoutImpl.onChangeScene = onChangeScene
layoutImpl.transitions = transitions
layoutImpl.density = density
- layoutImpl.edgeDetector = edgeDetector
-
layoutImpl.setScenes(scenes)
layoutImpl.setCurrentScene(currentScene)
+
layoutImpl.Content(modifier)
}
@@ -196,9 +191,9 @@
}
}
-enum class SwipeDirection(val orientation: Orientation) {
- Up(Orientation.Vertical),
- Down(Orientation.Vertical),
- Left(Orientation.Horizontal),
- Right(Orientation.Horizontal),
+enum class SwipeDirection {
+ Up,
+ Down,
+ Left,
+ Right,
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index a803a47..a40b299 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -47,7 +47,6 @@
transitions: SceneTransitions,
internal val state: SceneTransitionLayoutState,
density: Density,
- edgeDetector: EdgeDetector,
) {
internal val scenes = SnapshotStateMap<SceneKey, Scene>()
internal val elements = SnapshotStateMap<ElementKey, Element>()
@@ -58,7 +57,6 @@
internal var onChangeScene by mutableStateOf(onChangeScene)
internal var transitions by mutableStateOf(transitions)
internal var density: Density by mutableStateOf(density)
- internal var edgeDetector by mutableStateOf(edgeDetector)
/**
* The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index ee1f133..2dc53ab 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -22,6 +22,8 @@
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
@@ -35,7 +37,6 @@
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.round
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
@@ -54,7 +55,7 @@
/** Whether swipe should be enabled in the given [orientation]. */
fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean =
- userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
+ upOrLeft(orientation) != null || downOrRight(orientation) != null
val currentScene = gestureHandler.currentScene
val canSwipe = currentScene.shouldEnableSwipes(orientation)
@@ -67,7 +68,8 @@
)
return nestedScroll(connection = gestureHandler.nestedScroll.connection)
- .multiPointerDraggable(
+ .draggable(
+ state = rememberDraggableState(onDelta = gestureHandler.draggable::onDelta),
orientation = orientation,
enabled = gestureHandler.isDrivingTransition || canSwipe,
// Immediately start the drag if this our [transition] is currently animating to a scene
@@ -78,7 +80,6 @@
gestureHandler.isAnimatingOffset &&
!canOppositeSwipe,
onDragStarted = gestureHandler.draggable::onDragStarted,
- onDragDelta = gestureHandler.draggable::onDelta,
onDragStopped = gestureHandler.draggable::onDragStopped,
)
}
@@ -158,7 +159,7 @@
internal var gestureWithPriority: Any? = null
- internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?) {
+ internal fun onDragStarted() {
if (isDrivingTransition) {
// This [transition] was already driving the animation: simply take over it.
// Stop animating and start from where the current offset.
@@ -198,48 +199,6 @@
Orientation.Vertical -> layoutImpl.size.height
}.toFloat()
- val fromEdge =
- startedPosition?.let { position ->
- layoutImpl.edgeDetector.edge(
- layoutImpl.size,
- position.round(),
- layoutImpl.density,
- orientation,
- )
- }
-
- swipeTransition.actionUpOrLeft =
- Swipe(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Left
- Orientation.Vertical -> SwipeDirection.Up
- },
- pointerCount = pointersDown,
- fromEdge = fromEdge,
- )
-
- swipeTransition.actionDownOrRight =
- Swipe(
- direction =
- when (orientation) {
- Orientation.Horizontal -> SwipeDirection.Right
- Orientation.Vertical -> SwipeDirection.Down
- },
- pointerCount = pointersDown,
- fromEdge = fromEdge,
- )
-
- if (fromEdge == null) {
- swipeTransition.actionUpOrLeftNoEdge = null
- swipeTransition.actionDownOrRightNoEdge = null
- } else {
- swipeTransition.actionUpOrLeftNoEdge =
- (swipeTransition.actionUpOrLeft as Swipe).copy(fromEdge = null)
- swipeTransition.actionDownOrRightNoEdge =
- (swipeTransition.actionDownOrRight as Swipe).copy(fromEdge = null)
- }
-
if (swipeTransition.absoluteDistance > 0f) {
transitionState = swipeTransition
}
@@ -287,11 +246,11 @@
// to the next screen or go back to the previous one.
val offset = swipeTransition.dragOffset
val absoluteDistance = swipeTransition.absoluteDistance
- if (offset <= -absoluteDistance && swipeTransition.upOrLeft(fromScene) == toScene.key) {
+ if (offset <= -absoluteDistance && fromScene.upOrLeft(orientation) == toScene.key) {
swipeTransition.dragOffset += absoluteDistance
swipeTransition._fromScene = toScene
} else if (
- offset >= absoluteDistance && swipeTransition.downOrRight(fromScene) == toScene.key
+ offset >= absoluteDistance && fromScene.downOrRight(orientation) == toScene.key
) {
swipeTransition.dragOffset -= absoluteDistance
swipeTransition._fromScene = toScene
@@ -313,8 +272,8 @@
Orientation.Vertical -> layoutImpl.size.height
}.toFloat()
- val upOrLeft = swipeTransition.upOrLeft(this)
- val downOrRight = swipeTransition.downOrRight(this)
+ val upOrLeft = upOrLeft(orientation)
+ val downOrRight = downOrRight(orientation)
// Compute the target scene depending on the current offset.
return when {
@@ -557,22 +516,6 @@
var _distance by mutableFloatStateOf(0f)
val distance: Float
get() = _distance
-
- /** The [UserAction]s associated to this swipe. */
- var actionUpOrLeft: UserAction = Back
- var actionDownOrRight: UserAction = Back
- var actionUpOrLeftNoEdge: UserAction? = null
- var actionDownOrRightNoEdge: UserAction? = null
-
- fun upOrLeft(scene: Scene): SceneKey? {
- return scene.userActions[actionUpOrLeft]
- ?: actionUpOrLeftNoEdge?.let { scene.userActions[it] }
- }
-
- fun downOrRight(scene: Scene): SceneKey? {
- return scene.userActions[actionDownOrRight]
- ?: actionDownOrRightNoEdge?.let { scene.userActions[it] }
- }
}
companion object {
@@ -583,9 +526,9 @@
private class SceneDraggableHandler(
private val gestureHandler: SceneGestureHandler,
) : DraggableHandler {
- override fun onDragStarted(startedPosition: Offset, pointersDown: Int) {
+ override suspend fun onDragStarted(coroutineScope: CoroutineScope, startedPosition: Offset) {
gestureHandler.gestureWithPriority = this
- gestureHandler.onDragStarted(pointersDown, startedPosition)
+ gestureHandler.onDragStarted()
}
override fun onDelta(pixels: Float) {
@@ -594,7 +537,7 @@
}
}
- override fun onDragStopped(velocity: Float) {
+ override suspend fun onDragStopped(coroutineScope: CoroutineScope, velocity: Float) {
if (gestureHandler.gestureWithPriority == this) {
gestureHandler.gestureWithPriority = null
gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true)
@@ -637,31 +580,11 @@
// moving on to the next scene.
var gestureStartedOnNestedChild = false
- val actionUpOrLeft =
- Swipe(
- direction =
- when (gestureHandler.orientation) {
- Orientation.Horizontal -> SwipeDirection.Left
- Orientation.Vertical -> SwipeDirection.Up
- },
- pointerCount = 1,
- )
-
- val actionDownOrRight =
- Swipe(
- direction =
- when (gestureHandler.orientation) {
- Orientation.Horizontal -> SwipeDirection.Right
- Orientation.Vertical -> SwipeDirection.Down
- },
- pointerCount = 1,
- )
-
fun findNextScene(amount: Float): SceneKey? {
val fromScene = gestureHandler.currentScene
return when {
- amount < 0f -> fromScene.userActions[actionUpOrLeft]
- amount > 0f -> fromScene.userActions[actionDownOrRight]
+ amount < 0f -> fromScene.upOrLeft(gestureHandler.orientation)
+ amount > 0f -> fromScene.downOrRight(gestureHandler.orientation)
else -> null
}
}
@@ -702,7 +625,7 @@
onStart = {
gestureHandler.gestureWithPriority = this
priorityScene = nextScene
- gestureHandler.onDragStarted(pointersDown = 1, startedPosition = null)
+ gestureHandler.onDragStarted()
},
onScroll = { offsetAvailable ->
if (gestureHandler.gestureWithPriority != this) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt
deleted file mode 100644
index a68282a..0000000
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2023 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.compose.animation.scene
-
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class FixedSizeEdgeDetectorTest {
- private val detector = FixedSizeEdgeDetector(30.dp)
- private val layoutSize = IntSize(100, 100)
- private val density = Density(1f)
-
- @Test
- fun horizontalEdges() {
- fun horizontalEdge(position: Int): Edge? =
- detector.edge(
- layoutSize,
- position = IntOffset(position, 0),
- density,
- Orientation.Horizontal,
- )
-
- assertThat(horizontalEdge(0)).isEqualTo(Edge.Left)
- assertThat(horizontalEdge(30)).isEqualTo(Edge.Left)
- assertThat(horizontalEdge(31)).isEqualTo(null)
- assertThat(horizontalEdge(69)).isEqualTo(null)
- assertThat(horizontalEdge(70)).isEqualTo(Edge.Right)
- assertThat(horizontalEdge(100)).isEqualTo(Edge.Right)
- }
-
- @Test
- fun verticalEdges() {
- fun verticalEdge(position: Int): Edge? =
- detector.edge(
- layoutSize,
- position = IntOffset(0, position),
- density,
- Orientation.Vertical,
- )
-
- assertThat(verticalEdge(0)).isEqualTo(Edge.Top)
- assertThat(verticalEdge(30)).isEqualTo(Edge.Top)
- assertThat(verticalEdge(31)).isEqualTo(null)
- assertThat(verticalEdge(69)).isEqualTo(null)
- assertThat(verticalEdge(70)).isEqualTo(Edge.Bottom)
- assertThat(verticalEdge(100)).isEqualTo(Edge.Bottom)
- }
-}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
index 1eb3392..6791a85 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt
@@ -55,8 +55,7 @@
builder = scenesBuilder,
transitions = EmptyTestTransitions,
state = layoutState,
- density = Density(1f),
- edgeDetector = DefaultEdgeDetector,
+ density = Density(1f)
)
.also { it.size = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) },
orientation = Orientation.Vertical,
@@ -105,13 +104,13 @@
@Test
fun onDragStarted_shouldStartATransition() = runGestureTest {
- draggable.onDragStarted(startedPosition = Offset.Zero)
+ draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
}
@Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
- draggable.onDragStarted(startedPosition = Offset.Zero)
+ draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
val transition = transitionState as Transition
@@ -124,13 +123,14 @@
@Test
fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
- draggable.onDragStarted(startedPosition = Offset.Zero)
+ draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDelta(pixels = deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDragStopped(
+ coroutineScope = coroutineScope,
velocity = velocityThreshold - 0.01f,
)
assertScene(currentScene = SceneA, isIdle = false)
@@ -142,13 +142,14 @@
@Test
fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
- draggable.onDragStarted(startedPosition = Offset.Zero)
+ draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDelta(pixels = deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDragStopped(
+ coroutineScope = coroutineScope,
velocity = velocityThreshold,
)
assertScene(currentScene = SceneC, isIdle = false)
@@ -160,22 +161,23 @@
@Test
fun onDragStoppedAfterStarted_returnImmediatelyToIdle() = runGestureTest {
- draggable.onDragStarted(startedPosition = Offset.Zero)
+ draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
- draggable.onDragStopped(velocity = 0f)
+ draggable.onDragStopped(coroutineScope = coroutineScope, velocity = 0f)
assertScene(currentScene = SceneA, isIdle = true)
}
@Test
fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
- draggable.onDragStarted(startedPosition = Offset.Zero)
+ draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDelta(pixels = deltaInPixels10)
assertScene(currentScene = SceneA, isIdle = false)
draggable.onDragStopped(
+ coroutineScope = coroutineScope,
velocity = velocityThreshold,
)
@@ -189,7 +191,7 @@
assertScene(currentScene = SceneC, isIdle = false)
// Start a new gesture while the offset is animating
- draggable.onDragStarted(startedPosition = Offset.Zero)
+ draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
}
@@ -318,7 +320,7 @@
}
@Test
fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest {
- draggable.onDragStopped(velocityThreshold)
+ draggable.onDragStopped(coroutineScope, velocityThreshold)
assertScene(currentScene = SceneA, isIdle = true)
}
@@ -330,7 +332,7 @@
@Test
fun startNestedScrollWhileDragging() = runGestureTest {
- draggable.onDragStarted(Offset.Zero)
+ draggable.onDragStarted(coroutineScope, Offset.Zero)
assertScene(currentScene = SceneA, isIdle = false)
val transition = transitionState as Transition
@@ -342,7 +344,7 @@
assertThat(transition.progress).isEqualTo(0.2f)
// this should be ignored, we are scrolling now!
- draggable.onDragStopped(velocityThreshold)
+ draggable.onDragStopped(coroutineScope, velocityThreshold)
assertScene(currentScene = SceneA, isIdle = false)
nestedScrollEvents(available = offsetY10)
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 4a6066f..df3b72a 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -48,14 +48,6 @@
/** The middle of the layout, in pixels. */
private val Density.middle: Offset
get() = Offset((LayoutWidth / 2).toPx(), (LayoutHeight / 2).toPx())
-
- /** The middle-top of the layout, in pixels. */
- private val Density.middleTop: Offset
- get() = Offset((LayoutWidth / 2).toPx(), 0f)
-
- /** The middle-left of the layout, in pixels. */
- private val Density.middleLeft: Offset
- get() = Offset(0f, (LayoutHeight / 2).toPx())
}
private var currentScene by mutableStateOf(TestScenes.SceneA)
@@ -91,13 +83,7 @@
}
scene(
TestScenes.SceneC,
- userActions =
- mapOf(
- Swipe.Down to TestScenes.SceneA,
- Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB,
- Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TestScenes.SceneB,
- Swipe(SwipeDirection.Down, fromEdge = Edge.Top) to TestScenes.SceneB,
- ),
+ userActions = mapOf(Swipe.Down to TestScenes.SceneA),
) {
Box(Modifier.fillMaxSize())
}
@@ -256,100 +242,4 @@
assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
}
-
- @Test
- fun multiPointerSwipe() {
- // Start at scene C.
- currentScene = TestScenes.SceneC
-
- // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
- // detected as a drag event.
- var touchSlop = 0f
- rule.setContent {
- touchSlop = LocalViewConfiguration.current.touchSlop
- TestContent()
- }
-
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
-
- // Swipe down with two fingers.
- rule.onRoot().performTouchInput {
- repeat(2) { i -> down(pointerId = i, middle) }
- repeat(2) { i ->
- moveBy(pointerId = i, Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000)
- }
- }
-
- // We are transitioning to B because we used 2 fingers.
- val transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneC)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
-
- // Release the fingers and wait for the animation to end. We are back to C because we only
- // swiped 10dp.
- rule.onRoot().performTouchInput { repeat(2) { i -> up(pointerId = i) } }
- rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
- }
-
- @Test
- fun defaultEdgeSwipe() {
- // Start at scene C.
- currentScene = TestScenes.SceneC
-
- // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
- // detected as a drag event.
- var touchSlop = 0f
- rule.setContent {
- touchSlop = LocalViewConfiguration.current.touchSlop
- TestContent()
- }
-
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
-
- // Swipe down from the top edge.
- rule.onRoot().performTouchInput {
- down(middleTop)
- moveBy(Offset(0f, touchSlop + 10.dp.toPx()), delayMillis = 1_000)
- }
-
- // We are transitioning to B (and not A) because we started from the top edge.
- var transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneC)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
-
- // Release the fingers and wait for the animation to end. We are back to C because we only
- // swiped 10dp.
- rule.onRoot().performTouchInput { up() }
- rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
-
- // Swipe right from the left edge.
- rule.onRoot().performTouchInput {
- down(middleLeft)
- moveBy(Offset(touchSlop + 10.dp.toPx(), 0f), delayMillis = 1_000)
- }
-
- // We are transitioning to B (and not A) because we started from the left edge.
- transition = layoutState.transitionState
- assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
- assertThat((transition as TransitionState.Transition).fromScene)
- .isEqualTo(TestScenes.SceneC)
- assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
-
- // Release the fingers and wait for the animation to end. We are back to C because we only
- // swiped 10dp.
- rule.onRoot().performTouchInput { up() }
- rule.waitForIdle()
- assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
- assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
- }
}
diff --git a/packages/SystemUI/res/layout/connected_display_chip.xml b/packages/SystemUI/res/layout/connected_display_chip.xml
index d9df91e..f9a183d 100644
--- a/packages/SystemUI/res/layout/connected_display_chip.xml
+++ b/packages/SystemUI/res/layout/connected_display_chip.xml
@@ -41,6 +41,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="10dp"
+ android:layout_marginVertical="3dp"
android:scaleType="centerInside"
android:src="@drawable/stat_sys_connected_display"
android:tint="@android:color/black" />
diff --git a/packages/SystemUI/res/layout/connected_display_dialog.xml b/packages/SystemUI/res/layout/connected_display_dialog.xml
index 8cfcb68..3f65aa7 100644
--- a/packages/SystemUI/res/layout/connected_display_dialog.xml
+++ b/packages/SystemUI/res/layout/connected_display_dialog.xml
@@ -30,11 +30,11 @@
android:layout_width="@dimen/connected_display_dialog_logo_size"
android:layout_height="@dimen/connected_display_dialog_logo_size"
android:background="@drawable/circular_background"
- android:backgroundTint="?androidprv:attr/materialColorPrimary"
+ android:backgroundTint="?androidprv:attr/materialColorSecondary"
android:importantForAccessibility="no"
android:padding="6dp"
android:src="@drawable/stat_sys_connected_display"
- android:tint="?androidprv:attr/materialColorOnPrimary" />
+ android:tint="?androidprv:attr/materialColorOnSecondary" />
<TextView
android:id="@+id/connected_display_dialog_title"
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
index b2bc06f..48d3742 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/SharedNotificationContainerPosition.kt
@@ -20,4 +20,7 @@
data class SharedNotificationContainerPosition(
val top: Float = 0f,
val bottom: Float = 0f,
+
+ /** Whether any modifications to top/bottom are smoothly animated */
+ val animate: Boolean = false,
)
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 04b2852d..a41bb2f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -54,6 +54,7 @@
import com.android.systemui.dreams.dagger.DreamModule;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.FlagDependenciesModule;
import com.android.systemui.flags.FlagsModule;
import com.android.systemui.keyboard.KeyboardModule;
import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
@@ -182,6 +183,7 @@
DreamModule.class,
FalsingModule.class,
FlagsModule.class,
+ FlagDependenciesModule.class,
FooterActionsModule.class,
KeyEventRepositoryModule.class,
KeyboardModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index e96e318..e872d13 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 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.deviceentry.domain.interactor
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
@@ -5,6 +21,8 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
import javax.inject.Inject
@@ -14,6 +32,7 @@
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn
/**
@@ -27,9 +46,11 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val repository: DeviceEntryRepository,
+ repository: DeviceEntryRepository,
private val authenticationInteractor: AuthenticationInteractor,
sceneInteractor: SceneInteractor,
+ deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository,
+ trustRepository: TrustRepository,
) {
/**
* Whether the device is unlocked.
@@ -73,6 +94,13 @@
initialValue = false,
)
+ // Authenticated by a TrustAgent like trusted device, location, etc or by face auth.
+ private val passivelyAuthenticated =
+ merge(
+ trustRepository.isCurrentUserTrusted,
+ deviceEntryFaceAuthRepository.isAuthenticated,
+ )
+
/**
* Whether it's currently possible to swipe up to enter the device without requiring
* authentication. This returns `false` whenever the lockscreen has been dismissed.
@@ -81,10 +109,14 @@
* UI.
*/
val canSwipeToEnter =
- combine(authenticationInteractor.authenticationMethod, isDeviceEntered) {
- authenticationMethod,
- isDeviceEntered ->
- authenticationMethod is AuthenticationMethodModel.Swipe && !isDeviceEntered
+ combine(
+ authenticationInteractor.authenticationMethod.map {
+ it == AuthenticationMethodModel.Swipe
+ },
+ passivelyAuthenticated,
+ isDeviceEntered
+ ) { isSwipeAuthMethod, passivelyAuthenticated, isDeviceEntered ->
+ (isSwipeAuthMethod || passivelyAuthenticated) && !isDeviceEntered
}
.stateIn(
scope = applicationScope,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
new file mode 100644
index 0000000..730a7a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 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.flags
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.Flags as Classic
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import javax.inject.Inject
+
+/** A class in which engineers can define flag dependencies */
+@SysUISingleton
+class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, handler: Handler) :
+ FlagDependenciesBase(featureFlags, handler) {
+ override fun defineDependencies() {
+ FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
+ NotificationIconContainerRefactor.token dependsOn Classic.NOTIFICATION_SHELF_REFACTOR
+
+ // These two flags are effectively linked. We should migrate them to a single aconfig flag.
+ Classic.MIGRATE_NSSL dependsOn Classic.MIGRATE_KEYGUARD_STATUS_VIEW
+ Classic.MIGRATE_KEYGUARD_STATUS_VIEW dependsOn Classic.MIGRATE_NSSL
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt
new file mode 100644
index 0000000..ae3b501
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2023 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.flags
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.util.Log
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.Compile
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.withIncreasedIndent
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * This base class provides the helpers necessary to define dependencies between flags from the
+ * different flagging systems; classic and aconfig. This class is to be extended
+ */
+abstract class FlagDependenciesBase(
+ private val featureFlags: FeatureFlagsClassic,
+ private val handler: Handler
+) : CoreStartable {
+ protected abstract fun defineDependencies()
+
+ private val workingDependencies = mutableListOf<Dependency>()
+ private var allDependencies = emptyList<Dependency>()
+ private var unmetDependencies = emptyList<Dependency>()
+
+ override fun start() {
+ defineDependencies()
+ allDependencies = workingDependencies.toList()
+ unmetDependencies = workingDependencies.filter { !it.isMet }
+ workingDependencies.clear()
+ if (unmetDependencies.isNotEmpty()) {
+ handler.warnAboutBadFlagConfiguration(all = allDependencies, unmet = unmetDependencies)
+ }
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.asIndenting().run {
+ println("allDependencies: ${allDependencies.size}")
+ withIncreasedIndent { allDependencies.forEach(::println) }
+ println("unmetDependencies: ${unmetDependencies.size}")
+ withIncreasedIndent { unmetDependencies.forEach(::println) }
+ }
+ }
+
+ /** A dependency where enabling the `alpha` feature depends on enabling the `beta` feature */
+ class Dependency(
+ private val alphaName: String,
+ private val alphaEnabled: Boolean,
+ private val betaName: String,
+ private val betaEnabled: Boolean
+ ) {
+ val isMet = !alphaEnabled || betaEnabled
+ override fun toString(): String {
+ val isMetBullet = if (isMet) "+" else "-"
+ return "$isMetBullet $alphaName ($alphaEnabled) DEPENDS ON $betaName ($betaEnabled)"
+ }
+ }
+
+ protected infix fun UnreleasedFlag.dependsOn(other: UnreleasedFlag) =
+ addDependency(this.token, other.token)
+ protected infix fun ReleasedFlag.dependsOn(other: UnreleasedFlag) =
+ addDependency(this.token, other.token)
+ protected infix fun ReleasedFlag.dependsOn(other: ReleasedFlag) =
+ addDependency(this.token, other.token)
+ protected infix fun FlagToken.dependsOn(other: UnreleasedFlag) =
+ addDependency(this, other.token)
+ protected infix fun FlagToken.dependsOn(other: ReleasedFlag) = addDependency(this, other.token)
+ protected infix fun FlagToken.dependsOn(other: FlagToken) = addDependency(this, other)
+
+ private val UnreleasedFlag.token
+ get() = FlagToken("classic.$name", featureFlags.isEnabled(this))
+ private val ReleasedFlag.token
+ get() = FlagToken("classic.$name", featureFlags.isEnabled(this))
+
+ /** Add a dependency to the working list */
+ private fun addDependency(first: FlagToken, second: FlagToken) {
+ if (!Compile.IS_DEBUG) return // `user` builds should omit all this code
+ workingDependencies.add(
+ Dependency(first.name, first.isEnabled, second.name, second.isEnabled)
+ )
+ }
+
+ /** An interface which handles a warning about a bad flag configuration. */
+ interface Handler {
+ fun warnAboutBadFlagConfiguration(all: List<Dependency>, unmet: List<Dependency>)
+ }
+}
+
+/**
+ * A flag dependencies handler which posts a notification and logs to logcat that the configuration
+ * is invalid.
+ */
+@SysUISingleton
+class FlagDependenciesNotifier
+@Inject
+constructor(
+ private val context: Context,
+ private val notifManager: NotificationManager,
+) : FlagDependenciesBase.Handler {
+ override fun warnAboutBadFlagConfiguration(
+ all: List<FlagDependenciesBase.Dependency>,
+ unmet: List<FlagDependenciesBase.Dependency>
+ ) {
+ val title = "Invalid flag dependencies: ${unmet.size} of ${all.size}"
+ val details = unmet.joinToString("\n")
+ Log.e("FlagDependencies", "$title:\n$details")
+ val channel = NotificationChannel("FLAGS", "Flags", NotificationManager.IMPORTANCE_DEFAULT)
+ val notification =
+ Notification.Builder(context, channel.id)
+ .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
+ .setContentTitle(title)
+ .setContentText(details)
+ .setStyle(Notification.BigTextStyle().bigText(details))
+ .build()
+ notifManager.createNotificationChannel(channel)
+ notifManager.notify("flags", 0, notification)
+ }
+}
+
+@Module
+abstract class FlagDependenciesModule {
+
+ /** Inject into FlagDependencies. */
+ @Binds
+ @IntoMap
+ @ClassKey(FlagDependencies::class)
+ abstract fun bindFlagDependencies(sysui: FlagDependencies): CoreStartable
+
+ /** Bind the flag dependencies handler */
+ @Binds
+ abstract fun bindFlagDependenciesHandler(
+ handler: FlagDependenciesNotifier
+ ): FlagDependenciesBase.Handler
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/OWNERS b/packages/SystemUI/src/com/android/systemui/flags/OWNERS
index c9d2db1..57ebccb 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/flags/OWNERS
@@ -10,3 +10,6 @@
alexflo@google.com
dsandler@android.com
adamcohen@google.com
+
+# Anyone in System UI can declare dependencies between flags
+per-file FlagDependencies.kt = file:../../../../../OWNERS
diff --git a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt
index 2aa397f..ae67e60 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt
@@ -16,6 +16,7 @@
package com.android.systemui.flags
+import android.os.Build
import android.util.Log
/**
@@ -25,6 +26,7 @@
* ```
* object SomeRefactor {
* const val FLAG_NAME = Flags.SOME_REFACTOR
+ * val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled)
* @JvmStatic inline val isEnabled get() = Flags.someRefactor()
* @JvmStatic inline fun isUnexpectedlyInLegacyMode() =
* RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
@@ -32,6 +34,11 @@
* RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
* }
* ```
+ *
+ * Legacy mode crashes can be disabled with the command:
+ * ```
+ * adb shell setprop log.tag.RefactorFlagAssert silent
+ * ```
*/
@Suppress("NOTHING_TO_INLINE")
object RefactorFlagUtils {
@@ -51,8 +58,7 @@
inline fun isUnexpectedlyInLegacyMode(isEnabled: Boolean, flagName: Any): Boolean {
val inLegacyMode = !isEnabled
if (inLegacyMode) {
- val message = "New code path expects $flagName to be enabled."
- Log.wtf("RefactorFlag", message, IllegalStateException(message))
+ assertOnEngBuild("New code path expects $flagName to be enabled.")
}
return inLegacyMode
}
@@ -71,4 +77,37 @@
*/
inline fun assertInLegacyMode(isEnabled: Boolean, flagName: Any) =
check(!isEnabled) { "Legacy code path not supported when $flagName is enabled." }
+
+ /**
+ * This will [Log.wtf] with the given message, assuming [ASSERT_TAG] is loggable at that level.
+ * This means an engineer can prevent this from crashing by running the command:
+ * ```
+ * adb shell setprop log.tag.RefactorFlagAssert silent
+ * ```
+ */
+ fun assertOnEngBuild(message: String) {
+ if (Log.isLoggable(ASSERT_TAG, Log.ASSERT)) {
+ val exception = if (Build.isDebuggable()) IllegalStateException(message) else null
+ Log.wtf(ASSERT_TAG, message, exception)
+ } else if (Log.isLoggable(STANDARD_TAG, Log.WARN)) {
+ Log.w(STANDARD_TAG, message)
+ }
+ }
+
+ /**
+ * Tag used to determine if an incorrect flag guard should crash System UI running an eng build.
+ * This is enabled by default. To disable, run:
+ * ```
+ * adb shell setprop log.tag.RefactorFlagAssert silent
+ * ```
+ */
+ private const val ASSERT_TAG = "RefactorFlagAssert"
+
+ /** Tag used for non-crashing logs or when the [ASSERT_TAG] has been silenced. */
+ private const val STANDARD_TAG = "RefactorFlag"
+}
+
+/** An object which allows dependency tracking */
+data class FlagToken(val name: String, val isEnabled: Boolean) {
+ override fun toString(): String = "$name (${if (isEnabled) "enabled" else "disabled"})"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 4d5c503..67a12b0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -22,6 +22,8 @@
import android.view.View.OnLayoutChangeListener
import android.view.ViewGroup
import android.view.ViewGroup.OnHierarchyChangeListener
+import android.view.WindowInsets
+import android.view.WindowInsets.Type
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.internal.jank.InteractionJankMonitor
@@ -242,11 +244,18 @@
}
)
+ view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
+ val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
+ viewModel.topInset = insets.getInsetsIgnoringVisibility(insetTypes).top
+ insets
+ }
+
return object : DisposableHandle {
override fun dispose() {
disposableHandle.dispose()
view.removeOnLayoutChangeListener(onLayoutChangeListener)
view.setOnHierarchyChangeListener(null)
+ view.setOnApplyWindowInsetsListener(null)
childViews.clear()
}
}
@@ -288,7 +297,6 @@
oldBottom: Int
) {
val nsslPlaceholder = v.findViewById(R.id.nssl_placeholder) as View?
-
if (nsslPlaceholder != null) {
// After layout, ensure the notifications are positioned correctly
viewModel.onSharedNotificationContainerPositionChanged(
@@ -296,6 +304,11 @@
nsslPlaceholder.bottom.toFloat(),
)
}
+
+ val ksv = v.findViewById(R.id.keyguard_status_view) as View?
+ if (ksv != null) {
+ viewModel.statusViewTop = ksv.top
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 1f98082..e12da53 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -65,7 +65,12 @@
*/
private val previewMode = MutableStateFlow(PreviewMode())
- public var clockControllerProvider: Provider<ClockController>? = null
+ var clockControllerProvider: Provider<ClockController>? = null
+
+ /** System insets that keyguard needs to stay out of */
+ var topInset: Int = 0
+ /** Status view top, without translation added in */
+ var statusViewTop: Int = 0
val burnInLayerVisibility: Flow<Int> =
keyguardTransitionInteractor.startedKeyguardState
@@ -102,9 +107,12 @@
scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
)
} else {
+ // Ensure the desired translation doesn't encroach on the top inset
+ val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolation).toInt()
+ val translationY = -(statusViewTop - Math.max(topInset, statusViewTop + burnInY))
BurnInModel(
translationX = MathUtils.lerp(0, burnIn.translationX, interpolation).toInt(),
- translationY = MathUtils.lerp(0, burnIn.translationY, interpolation).toInt(),
+ translationY = translationY,
scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolation),
scaleClockOnly = true,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
index 94e70e5..7e6044e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt
@@ -17,13 +17,19 @@
package com.android.systemui.statusbar.notification.footer.shared
import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
/** Helper for reading or using the FooterView refactor flag state. */
@Suppress("NOTHING_TO_INLINE")
object FooterViewRefactor {
+ /** The aconfig flag name */
const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
/** Is the refactor enabled */
@JvmStatic
inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt
index 79bdd1f..e6deb8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BigPictureIconManager.kt
@@ -117,11 +117,6 @@
@WorkerThread
override fun updateIcon(drawableConsumer: NotificationDrawableConsumer, icon: Icon?): Runnable {
- if (this.drawableConsumer != null && this.drawableConsumer != drawableConsumer) {
- Log.wtf(TAG, "A consumer is already set for this iconManager.")
- return Runnable {}
- }
-
this.drawableConsumer = drawableConsumer
this.lastLoadingJob?.cancel()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
index dee609c..a08af75 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt
@@ -16,13 +16,19 @@
package com.android.systemui.statusbar.notification.shared
import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
/** Helper for reading or using the NotificationIconContainer refactor flag state. */
@Suppress("NOTHING_TO_INLINE")
object NotificationIconContainerRefactor {
+ /** The aconfig flag name */
const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
/** Is the refactor enabled? */
@JvmStatic
inline val isEnabled
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 2af7181..6785da4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -59,10 +59,8 @@
launch {
viewModel.position.collect {
- controller.updateTopPadding(
- it.top,
- controller.isAddOrRemoveAnimationPending()
- )
+ val animate = it.animate || controller.isAddOrRemoveAnimationPending()
+ controller.updateTopPadding(it.top, animate)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 1229cb9..b86b5dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -25,6 +25,7 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -118,8 +119,15 @@
}
}
} else {
- interactor.topPosition.map { top ->
- keyguardInteractor.sharedNotificationContainerPosition.value.copy(top = top)
+ interactor.topPosition.sample(shadeInteractor.qsExpansion, ::Pair).map {
+ (top, qsExpansion) ->
+ // When QS expansion > 0, it should directly set the top padding so do not
+ // animate it
+ val animate = qsExpansion == 0f
+ keyguardInteractor.sharedNotificationContainerPosition.value.copy(
+ top = top,
+ animate = animate
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
index dd32434..649a4ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java
@@ -512,7 +512,7 @@
}
public void showIconIsolated(StatusBarIconView icon, boolean animated) {
- mNotificationIcons.showIconIsolated(icon, animated);
+ mNotificationIcons.showIconIsolatedLegacy(icon, animated);
}
public void setIsolatedIconLocation(@NotNull Rect iconDrawingRect, boolean requireStateUpdate) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index efb8e2c..75a697f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -701,13 +701,13 @@
}
@Deprecated
- public void showIconIsolated(StatusBarIconView icon, boolean animated) {
+ public void showIconIsolatedLegacy(StatusBarIconView icon, boolean animated) {
NotificationIconContainerRefactor.assertInLegacyMode();
if (animated) {
- showIconIsolatedAnimated(icon, null);
- } else {
- showIconIsolated(icon);
+ mIsolatedIconForAnimation = icon != null ? icon : mIsolatedIcon;
}
+ mIsolatedIcon = icon;
+ updateState();
}
public void showIconIsolatedAnimated(StatusBarIconView icon,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index c13fde7..aebadc5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2023 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.deviceentry.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -6,6 +22,8 @@
import com.android.systemui.authentication.data.model.AuthenticationMethodModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
@@ -24,6 +42,8 @@
private val utils = SceneTestUtils(this)
private val testScope = utils.testScope
private val repository: FakeDeviceEntryRepository = utils.deviceEntryRepository
+ private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
+ private val trustRepository = FakeTrustRepository()
private val sceneInteractor = utils.sceneInteractor()
private val authenticationInteractor = utils.authenticationInteractor()
private val underTest =
@@ -31,6 +51,8 @@
repository = repository,
authenticationInteractor = authenticationInteractor,
sceneInteractor = sceneInteractor,
+ faceAuthRepository = faceAuthRepository,
+ trustRepository = trustRepository,
)
@Test
@@ -171,6 +193,40 @@
}
@Test
+ fun canSwipeToEnter_whenTrustedByTrustManager_isTrue() =
+ testScope.runTest {
+ val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter)
+ utils.authenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Password
+ )
+ switchToScene(SceneKey.Lockscreen)
+ assertThat(canSwipeToEnter).isFalse()
+
+ trustRepository.setCurrentUserTrusted(true)
+ runCurrent()
+ faceAuthRepository.isAuthenticated.value = false
+
+ assertThat(canSwipeToEnter).isTrue()
+ }
+
+ @Test
+ fun canSwipeToEnter_whenAuthenticatedByFace_isTrue() =
+ testScope.runTest {
+ val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter)
+ utils.authenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Password
+ )
+ switchToScene(SceneKey.Lockscreen)
+ assertThat(canSwipeToEnter).isFalse()
+
+ faceAuthRepository.isAuthenticated.value = true
+ runCurrent()
+ trustRepository.setCurrentUserTrusted(false)
+
+ assertThat(canSwipeToEnter).isTrue()
+ }
+
+ @Test
fun isAuthenticationRequired_lockedAndSecured_true() =
testScope.runTest {
utils.deviceEntryRepository.setUnlocked(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt
new file mode 100644
index 0000000..936aa8b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 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.flags
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import java.io.PrintWriter
+import kotlin.test.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class FlagDependenciesTest : SysuiTestCase() {
+ @Test
+ fun testRelease() {
+ testFlagDependencies(teamfood = false).start()
+ }
+
+ @Test
+ fun testTeamfood() {
+ testFlagDependencies(teamfood = true).start()
+ }
+
+ private fun testFlagDependencies(teamfood: Boolean) =
+ FlagDependencies(TestFeatureFlags(teamfood = teamfood), TestHandler())
+
+ private class TestHandler : FlagDependenciesBase.Handler {
+ override fun warnAboutBadFlagConfiguration(
+ all: List<FlagDependenciesBase.Dependency>,
+ unmet: List<FlagDependenciesBase.Dependency>
+ ) {
+ val title = "${unmet.size} invalid of ${all.size} flag dependencies"
+ val details = unmet.joinToString("\n")
+ fail("$title:\n$details")
+ }
+ }
+
+ private class TestFeatureFlags(val teamfood: Boolean) : FeatureFlagsClassic {
+ private val unsupported: Nothing
+ get() = fail("Unsupported")
+
+ override fun isEnabled(flag: ReleasedFlag): Boolean = true
+ override fun isEnabled(flag: UnreleasedFlag): Boolean = teamfood && flag.teamfood
+ override fun isEnabled(flag: ResourceBooleanFlag): Boolean = unsupported
+ override fun isEnabled(flag: SysPropBooleanFlag): Boolean = unsupported
+ override fun getString(flag: StringFlag): String = unsupported
+ override fun getString(flag: ResourceStringFlag): String = unsupported
+ override fun getInt(flag: IntFlag): Int = unsupported
+ override fun getInt(flag: ResourceIntFlag): Int = unsupported
+ override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) = unsupported
+ override fun removeListener(listener: FlagListenable.Listener) = unsupported
+ override fun dump(writer: PrintWriter, args: Array<out String>?) = unsupported
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 4f545cb..b80771f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -179,6 +179,8 @@
val translationY by collectLastValue(underTest.translationY)
val scale by collectLastValue(underTest.scale)
+ underTest.statusViewTop = 100
+
// Set to dozing (on AOD)
dozeAmountTransitionStep.emit(TransitionStep(value = 1f))
// Trigger a change to the burn-in model
@@ -200,6 +202,37 @@
}
@Test
+ fun translationAndScaleFromBurnFullyDozingStaysOutOfTopInset() =
+ testScope.runTest {
+ val translationX by collectLastValue(underTest.translationX)
+ val translationY by collectLastValue(underTest.translationY)
+ val scale by collectLastValue(underTest.scale)
+
+ underTest.statusViewTop = 100
+ underTest.topInset = 80
+
+ // Set to dozing (on AOD)
+ dozeAmountTransitionStep.emit(TransitionStep(value = 1f))
+ // Trigger a change to the burn-in model
+ burnInFlow.value =
+ BurnInModel(
+ translationX = 20,
+ translationY = -30,
+ scale = 0.5f,
+ )
+ assertThat(translationX).isEqualTo(20)
+ // -20 instead of -30, due to inset of 80
+ assertThat(translationY).isEqualTo(-20)
+ assertThat(scale).isEqualTo(Pair(0.5f, true /* scaleClockOnly */))
+
+ // Set to the beginning of GONE->AOD transition
+ goneToAodTransitionStep.emit(TransitionStep(value = 0f))
+ assertThat(translationX).isEqualTo(0)
+ assertThat(translationY).isEqualTo(0)
+ assertThat(scale).isEqualTo(Pair(1f, true /* scaleClockOnly */))
+ }
+
+ @Test
fun translationAndScaleFromBurnInUseScaleOnly() =
testScope.runTest {
whenever(clockController.config.useAlternateSmartspaceAODTransition).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
index 1bb7b61..2bad9f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/BigPictureIconManagerTest.kt
@@ -143,18 +143,44 @@
}
@Test
- fun onIconUpdated_consumerAlreadySet_nothingHappens() =
+ fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithPlaceholder() =
testScope.runTest {
// GIVEN a consumer is set
- val otherConsumer: NotificationDrawableConsumer = mock()
iconManager.updateIcon(mockConsumer, supportedIcon).run()
clearInvocations(mockConsumer)
// WHEN a new consumer is set
- iconManager.updateIcon(otherConsumer, unsupportedIcon).run()
+ val newConsumer: NotificationDrawableConsumer = mock()
+ iconManager.updateIcon(newConsumer, supportedIcon).run()
- // THEN nothing happens
- verifyZeroInteractions(mockConsumer, otherConsumer)
+ // THEN the new consumer is updated
+ verify(newConsumer).setImageDrawable(drawableCaptor.capture())
+ assertIsPlaceHolder(drawableCaptor.value)
+ assertSize(drawableCaptor.value)
+ // AND nothing happens on the old consumer
+ verifyZeroInteractions(mockConsumer)
+ }
+
+ @Test
+ fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithFullImage() =
+ testScope.runTest {
+ // GIVEN a consumer is set
+ iconManager.updateIcon(mockConsumer, supportedIcon).run()
+ // AND an icon is loaded
+ iconManager.onViewShown(true)
+ runCurrent()
+ clearInvocations(mockConsumer)
+
+ // WHEN a new consumer is set
+ val newConsumer: NotificationDrawableConsumer = mock()
+ iconManager.updateIcon(newConsumer, supportedIcon).run()
+
+ // THEN the new consumer is updated
+ verify(newConsumer).setImageDrawable(drawableCaptor.capture())
+ assertIsFullImage(drawableCaptor.value)
+ assertSize(drawableCaptor.value)
+ // AND nothing happens on the old consumer
+ verifyZeroInteractions(mockConsumer)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 60421c98..978fafe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -314,7 +314,26 @@
sharedNotificationContainerInteractor.setTopPosition(10f)
assertThat(position)
- .isEqualTo(SharedNotificationContainerPosition(top = 10f, bottom = 0f))
+ .isEqualTo(
+ SharedNotificationContainerPosition(top = 10f, bottom = 0f, animate = true)
+ )
+ }
+
+ @Test
+ fun positionOnQS() =
+ testScope.runTest {
+ val position by collectLastValue(underTest.position)
+
+ // Start on lockscreen with shade expanded
+ showLockscreenWithQSExpanded()
+
+ // When not in split shade
+ sharedNotificationContainerInteractor.setTopPosition(10f)
+
+ assertThat(position)
+ .isEqualTo(
+ SharedNotificationContainerPosition(top = 10f, bottom = 0f, animate = false)
+ )
}
@Test
@@ -390,6 +409,18 @@
)
}
+ private suspend fun showLockscreenWithQSExpanded() {
+ shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeRepository.setQsExpansion(1f)
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ to = KeyguardState.LOCKSCREEN,
+ transitionState = TransitionState.FINISHED
+ )
+ )
+ }
+
@SysUISingleton
@Component(
modules =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
index ef02bdd..44286b7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/FakeDeviceEntryDataLayerModule.kt
@@ -16,6 +16,16 @@
package com.android.systemui.deviceentry.data
import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepositoryModule
+import com.android.systemui.keyguard.data.repository.FakeTrustRepositoryModule
import dagger.Module
-@Module(includes = [FakeDeviceEntryRepositoryModule::class]) object FakeDeviceEntryDataLayerModule
+@Module(
+ includes =
+ [
+ FakeDeviceEntryRepositoryModule::class,
+ FakeTrustRepositoryModule::class,
+ FakeDeviceEntryFaceAuthRepositoryModule::class,
+ ]
+)
+object FakeDeviceEntryDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
index 6710072..abf72af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/FakeKeyguardDataLayerModule.kt
@@ -16,7 +16,6 @@
package com.android.systemui.keyguard.data
import com.android.systemui.keyguard.data.repository.FakeCommandQueueModule
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepositoryModule
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepositoryModule
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepositoryModule
import dagger.Module
@@ -25,7 +24,6 @@
includes =
[
FakeCommandQueueModule::class,
- FakeDeviceEntryFaceAuthRepositoryModule::class,
FakeKeyguardRepositoryModule::class,
FakeKeyguardTransitionRepositoryModule::class,
]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
index 482126d..cd83c2f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeTrustRepository.kt
@@ -18,13 +18,18 @@
package com.android.systemui.keyguard.data.repository
import com.android.keyguard.TrustGrantFlags
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.TrustModel
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-class FakeTrustRepository : TrustRepository {
+@SysUISingleton
+class FakeTrustRepository @Inject constructor() : TrustRepository {
private val _isTrustUsuallyManaged = MutableStateFlow(false)
override val isCurrentUserTrustUsuallyManaged: StateFlow<Boolean>
get() = _isTrustUsuallyManaged
@@ -63,3 +68,8 @@
_isTrustUsuallyManaged.value = value
}
}
+
+@Module
+interface FakeTrustRepositoryModule {
+ @Binds fun bindFake(fake: FakeTrustRepository): TrustRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 17384351..bdddc04 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -42,9 +42,13 @@
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeTrustRepository
import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.power.data.repository.FakePowerRepository
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
@@ -155,12 +159,16 @@
repository: DeviceEntryRepository = deviceEntryRepository,
authenticationInteractor: AuthenticationInteractor,
sceneInteractor: SceneInteractor,
+ faceAuthRepository: DeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository(),
+ trustRepository: TrustRepository = FakeTrustRepository(),
): DeviceEntryInteractor {
return DeviceEntryInteractor(
applicationScope = applicationScope(),
repository = repository,
authenticationInteractor = authenticationInteractor,
sceneInteractor = sceneInteractor,
+ deviceEntryFaceAuthRepository = faceAuthRepository,
+ trustRepository = trustRepository,
)
}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 70c449f..a036383 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -326,7 +326,9 @@
mSensorController = new SensorController(this, mDeviceId,
mParams.getVirtualSensorCallback(), mParams.getVirtualSensorConfigs());
mCameraAccessController = cameraAccessController;
- mCameraAccessController.startObservingIfNeeded();
+ if (mCameraAccessController != null) {
+ mCameraAccessController.startObservingIfNeeded();
+ }
if (!Flags.streamPermissions()) {
mPermissionDialogComponent = getPermissionDialogComponent();
} else {
@@ -566,7 +568,9 @@
}
mAppToken.unlinkToDeath(this, 0);
- mCameraAccessController.stopObservingIfNeeded();
+ if (mCameraAccessController != null) {
+ mCameraAccessController.stopObservingIfNeeded();
+ }
mInputController.close();
mSensorController.close();
@@ -586,7 +590,9 @@
@Override
@RequiresPermission(android.Manifest.permission.CAMERA_INJECT_EXTERNAL_CAMERA)
public void onRunningAppsChanged(ArraySet<Integer> runningUids) {
- mCameraAccessController.blockCameraAccessIfNeeded(runningUids);
+ if (mCameraAccessController != null) {
+ mCameraAccessController.blockCameraAccessIfNeeded(runningUids);
+ }
mRunningAppsChangedCallback.accept(runningUids);
}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 3031a84..959f69e 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -187,6 +187,9 @@
}
CameraAccessController getCameraAccessController(UserHandle userHandle) {
+ if (Flags.streamCamera()) {
+ return null;
+ }
int userId = userHandle.getIdentifier();
synchronized (mVirtualDeviceManagerLock) {
for (int i = 0; i < mVirtualDevices.size(); i++) {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index c220528..7dbf61b 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -192,7 +192,6 @@
"com.android.sysprop.watchdog",
"ImmutabilityAnnotation",
"securebox",
- "android.content.pm.flags-aconfig-java",
"apache-commons-math",
"backstage_power_flags_lib",
"notification_flags_lib",
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index e88d0c6..ae79f19 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -20305,7 +20305,7 @@
final long token = Binder.clearCallingIdentity();
try {
- return mOomAdjuster.mCachedAppOptimizer.isFreezerSupported();
+ return CachedAppOptimizer.isFreezerSupported();
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -20433,4 +20433,21 @@
return index >= 0 && !mMediaProjectionTokenMap.valueAt(index).isEmpty();
}
}
+
+ /**
+ * Deal with binder transactions to frozen apps.
+ *
+ * @param debugPid The binder transaction sender
+ * @param code The binder transaction code
+ * @param flags The binder transaction flags
+ * @param err The binder transaction error
+ */
+ @Override
+ public void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err) {
+ final ProcessRecord app;
+ synchronized (mPidsSelfLocked) {
+ app = mPidsSelfLocked.get(debugPid);
+ }
+ mOomAdjuster.mCachedAppOptimizer.binderError(debugPid, app, code, flags, err);
+ }
}
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 6005b64..68af626 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -52,6 +52,8 @@
import android.app.ActivityManagerInternal.OomAdjReason;
import android.app.ActivityThread;
import android.app.ApplicationExitInfo;
+import android.app.ApplicationExitInfo.Reason;
+import android.app.ApplicationExitInfo.SubReason;
import android.app.IApplicationThread;
import android.database.ContentObserver;
import android.net.Uri;
@@ -67,6 +69,7 @@
import android.provider.DeviceConfig.Properties;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.EventLog;
import android.util.IntArray;
import android.util.Pair;
@@ -75,6 +78,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BinderfsStatsReader;
import com.android.internal.os.ProcLocksReader;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.ServiceThread;
@@ -131,6 +135,12 @@
"freeze_binder_offset";
@VisibleForTesting static final String KEY_FREEZER_BINDER_THRESHOLD =
"freeze_binder_threshold";
+ @VisibleForTesting static final String KEY_FREEZER_BINDER_CALLBACK_ENABLED =
+ "freeze_binder_callback_enabled";
+ @VisibleForTesting static final String KEY_FREEZER_BINDER_CALLBACK_THROTTLE =
+ "freeze_binder_callback_throttle";
+ @VisibleForTesting static final String KEY_FREEZER_BINDER_ASYNC_THRESHOLD =
+ "freeze_binder_async_threshold";
static final int UNFREEZE_REASON_NONE =
FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_NONE;
@@ -270,6 +280,9 @@
@VisibleForTesting static final long DEFAULT_FREEZER_BINDER_DIVISOR = 4;
@VisibleForTesting static final int DEFAULT_FREEZER_BINDER_OFFSET = 500;
@VisibleForTesting static final long DEFAULT_FREEZER_BINDER_THRESHOLD = 1_000;
+ @VisibleForTesting static final boolean DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED = true;
+ @VisibleForTesting static final long DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE = 10_000L;
+ @VisibleForTesting static final int DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD = 1_024;
@VisibleForTesting static final Uri CACHED_APP_FREEZER_ENABLED_URI = Settings.Global.getUriFor(
Settings.Global.CACHED_APPS_FREEZER_ENABLED);
@@ -312,6 +325,7 @@
static final int COMPACT_NATIVE_MSG = 5;
static final int UID_FROZEN_STATE_CHANGED_MSG = 6;
static final int DEADLOCK_WATCHDOG_MSG = 7;
+ static final int BINDER_ERROR_MSG = 8;
// When free swap falls below this percentage threshold any full (file + anon)
// compactions will be downgraded to file only compactions to reduce pressure
@@ -408,7 +422,10 @@
} else if (KEY_FREEZER_BINDER_ENABLED.equals(name)
|| KEY_FREEZER_BINDER_DIVISOR.equals(name)
|| KEY_FREEZER_BINDER_THRESHOLD.equals(name)
- || KEY_FREEZER_BINDER_OFFSET.equals(name)) {
+ || KEY_FREEZER_BINDER_OFFSET.equals(name)
+ || KEY_FREEZER_BINDER_CALLBACK_ENABLED.equals(name)
+ || KEY_FREEZER_BINDER_CALLBACK_THROTTLE.equals(name)
+ || KEY_FREEZER_BINDER_ASYNC_THRESHOLD.equals(name)) {
updateFreezerBinderState();
}
}
@@ -480,7 +497,15 @@
@VisibleForTesting volatile int mFreezerBinderOffset = DEFAULT_FREEZER_BINDER_OFFSET;
@GuardedBy("mPhenotypeFlagLock")
@VisibleForTesting volatile long mFreezerBinderThreshold = DEFAULT_FREEZER_BINDER_THRESHOLD;
-
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting volatile boolean mFreezerBinderCallbackEnabled =
+ DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED;
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting volatile long mFreezerBinderCallbackThrottle =
+ DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE;
+ @GuardedBy("mPhenotypeFlagLock")
+ @VisibleForTesting volatile int mFreezerBinderAsyncThreshold =
+ DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD;
// Handler on which compaction runs.
@VisibleForTesting
@@ -488,6 +513,7 @@
private Handler mFreezeHandler;
@GuardedBy("mProcLock")
private boolean mFreezerOverride = false;
+ private long mFreezerBinderCallbackLast = -1;
@VisibleForTesting volatile long mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
@VisibleForTesting volatile boolean mFreezerExemptInstPkg = DEFAULT_FREEZER_EXEMPT_INST_PKG;
@@ -790,6 +816,12 @@
pw.println(" " + KEY_FREEZER_BINDER_THRESHOLD + "=" + mFreezerBinderThreshold);
pw.println(" " + KEY_FREEZER_BINDER_DIVISOR + "=" + mFreezerBinderDivisor);
pw.println(" " + KEY_FREEZER_BINDER_OFFSET + "=" + mFreezerBinderOffset);
+ pw.println(" " + KEY_FREEZER_BINDER_CALLBACK_ENABLED + "="
+ + mFreezerBinderCallbackEnabled);
+ pw.println(" " + KEY_FREEZER_BINDER_CALLBACK_THROTTLE + "="
+ + mFreezerBinderCallbackThrottle);
+ pw.println(" " + KEY_FREEZER_BINDER_ASYNC_THRESHOLD + "="
+ + mFreezerBinderAsyncThreshold);
synchronized (mProcLock) {
int size = mFrozenProcesses.size();
pw.println(" Apps frozen: " + size);
@@ -1309,10 +1341,22 @@
mFreezerBinderThreshold = DeviceConfig.getLong(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
KEY_FREEZER_BINDER_THRESHOLD, DEFAULT_FREEZER_BINDER_THRESHOLD);
+ mFreezerBinderCallbackEnabled = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+ KEY_FREEZER_BINDER_CALLBACK_ENABLED, DEFAULT_FREEZER_BINDER_CALLBACK_ENABLED);
+ mFreezerBinderCallbackThrottle = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+ KEY_FREEZER_BINDER_CALLBACK_THROTTLE, DEFAULT_FREEZER_BINDER_CALLBACK_THROTTLE);
+ mFreezerBinderAsyncThreshold = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+ KEY_FREEZER_BINDER_ASYNC_THRESHOLD, DEFAULT_FREEZER_BINDER_ASYNC_THRESHOLD);
Slog.d(TAG_AM, "Freezer binder state set to enabled=" + mFreezerBinderEnabled
+ ", divisor=" + mFreezerBinderDivisor
+ ", offset=" + mFreezerBinderOffset
- + ", threshold=" + mFreezerBinderThreshold);
+ + ", threshold=" + mFreezerBinderThreshold
+ + ", callback enabled=" + mFreezerBinderCallbackEnabled
+ + ", callback throttle=" + mFreezerBinderCallbackThrottle
+ + ", async threshold=" + mFreezerBinderAsyncThreshold);
}
private boolean parseProcStateThrottle(String procStateThrottleString) {
@@ -2182,6 +2226,21 @@
Slog.w(TAG_AM, "Unable to check file locks");
}
} break;
+ case BINDER_ERROR_MSG: {
+ IntArray pids = new IntArray();
+ // Copy the frozen pids to a local array to release mProcLock ASAP
+ synchronized (mProcLock) {
+ int size = mFrozenProcesses.size();
+ for (int i = 0; i < size; i++) {
+ pids.add(mFrozenProcesses.keyAt(i));
+ }
+ }
+
+ // Check binder errors to frozen processes with a local freezer lock
+ synchronized (mFreezerLock) {
+ binderErrorLocked(pids);
+ }
+ } break;
default:
return;
}
@@ -2487,4 +2546,115 @@
return UNFREEZE_REASON_NONE;
}
}
+
+ /**
+ * Kill a frozen process with a specified reason
+ */
+ public void killProcess(int pid, String reason, @Reason int reasonCode,
+ @SubReason int subReason) {
+ mAm.mHandler.post(() -> {
+ synchronized (mAm) {
+ synchronized (mProcLock) {
+ ProcessRecord proc = mFrozenProcesses.get(pid);
+ // The process might have been killed or unfrozen by others
+ if (proc != null && proc.getThread() != null && !proc.isKilledByAm()) {
+ proc.killLocked(reason, reasonCode, subReason, true);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Sending binder transactions to frozen apps most likely indicates there's a bug. Log it and
+ * kill the frozen apps if they 1) receive sync binder transactions while frozen, or 2) miss
+ * async binder transactions due to kernel binder buffer running out.
+ *
+ * @param debugPid The binder transaction sender
+ * @param app The ProcessRecord of the sender
+ * @param code The binder transaction code
+ * @param flags The binder transaction flags
+ * @param err The binder transaction error
+ */
+ public void binderError(int debugPid, ProcessRecord app, int code, int flags, int err) {
+ Slog.w(TAG_AM, "pid " + debugPid + " " + (app == null ? "null" : app.processName)
+ + " sent binder code " + code + " with flags " + flags
+ + " to frozen apps and got error " + err);
+
+ // Do nothing if the binder error callback is not enabled.
+ // That means the frozen apps in a wrong state will be killed when they are unfrozen later.
+ if (!mFreezerBinderCallbackEnabled) {
+ return;
+ }
+
+ final long now = SystemClock.uptimeMillis();
+ if (now < mFreezerBinderCallbackLast + mFreezerBinderCallbackThrottle) {
+ Slog.d(TAG_AM, "Too many transaction errors, throttling freezer binder callback.");
+ return;
+ }
+ mFreezerBinderCallbackLast = now;
+
+ // Check all frozen processes in Freezer handler
+ mFreezeHandler.sendEmptyMessage(BINDER_ERROR_MSG);
+ }
+
+ private void binderErrorLocked(IntArray pids) {
+ // PIDs that run out of async binder buffer when being frozen
+ ArraySet<Integer> pidsAsync = (mFreezerBinderAsyncThreshold < 0) ? null : new ArraySet<>();
+
+ for (int i = 0; i < pids.size(); i++) {
+ int current = pids.get(i);
+ try {
+ int freezeInfo = getBinderFreezeInfo(current);
+
+ if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {
+ killProcess(current, "Sync transaction while frozen",
+ ApplicationExitInfo.REASON_FREEZER,
+ ApplicationExitInfo.SUBREASON_FREEZER_BINDER_TRANSACTION);
+
+ // No need to check async transactions in this case
+ continue;
+ }
+
+ if ((freezeInfo & ASYNC_RECEIVED_WHILE_FROZEN) != 0) {
+ if (pidsAsync != null) {
+ pidsAsync.add(current);
+ }
+ if (DEBUG_FREEZER) {
+ Slog.w(TAG_AM, "pid " + current
+ + " received async transactions while frozen");
+ }
+ }
+ } catch (Exception e) {
+ // The process has died. No need to kill it again.
+ Slog.w(TAG_AM, "Unable to query binder frozen stats for pid " + current);
+ }
+ }
+
+ // TODO: when kernel binder driver supports, poll the binder status directly.
+ // Binderfs stats, like other debugfs files, is not a reliable interface. But it's the
+ // only true source for now. The following code checks all frozen PIDs. If any of them
+ // is running out of async binder buffer, kill it. Otherwise it will be killed at a
+ // later time when AMS unfreezes it, which causes race issues.
+ if (pidsAsync == null || pidsAsync.size() == 0) {
+ return;
+ }
+ new BinderfsStatsReader().handleFreeAsyncSpace(
+ // Check if the frozen process has pending async calls
+ pidsAsync::contains,
+
+ // Kill the current process if it's running out of async binder space
+ (current, free) -> {
+ if (free < mFreezerBinderAsyncThreshold) {
+ Slog.w(TAG_AM, "pid " + current
+ + " has " + free + " free async space, killing");
+ killProcess(current, "Async binder space running out while frozen",
+ ApplicationExitInfo.REASON_FREEZER,
+ ApplicationExitInfo.SUBREASON_FREEZER_BINDER_ASYNC_FULL);
+ }
+ },
+
+ // Log the error if binderfs stats can't be accesses or correctly parsed
+ exception -> Slog.e(TAG_AM, "Unable to parse binderfs stats"));
+ }
}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 9bbfc8f..16f55e8 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -125,6 +125,7 @@
"arc_next",
"bluetooth",
"build",
+ "biometrics",
"biometrics_framework",
"biometrics_integration",
"camera_platform",
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 8e767e7..8bf903a 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.CONTROL_KEYGUARD;
import static android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS;
+import static android.content.pm.Flags.sdkLibIndependence;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
@@ -187,7 +188,9 @@
List<VersionedPackage> libClientPackages =
computer.getPackagesUsingSharedLibrary(libraryInfo,
MATCH_KNOWN_PACKAGES, Process.SYSTEM_UID, currUserId);
- if (!ArrayUtils.isEmpty(libClientPackages)) {
+ boolean allowSdkLibIndependence =
+ (pkg.getSdkLibraryName() != null) && sdkLibIndependence();
+ if (!ArrayUtils.isEmpty(libClientPackages) && !allowSdkLibIndependence) {
Slog.w(TAG, "Not removing package " + pkg.getManifestPackageName()
+ " hosting lib " + libraryInfo.getName() + " version "
+ libraryInfo.getLongVersion() + " used by " + libClientPackages
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 0d024d6..56e385d 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -65,6 +65,7 @@
import android.os.FactoryTest;
import android.os.FileUtils;
import android.os.IBinder;
+import android.os.IBinderCallback;
import android.os.IIncidentManager;
import android.os.Looper;
import android.os.Message;
@@ -985,6 +986,14 @@
}
}
+ // Set binder transaction callback after starting system services
+ Binder.setTransactionCallback(new IBinderCallback() {
+ @Override
+ public void onTransactionError(int pid, int code, int flags, int err) {
+ mActivityManagerService.frozenBinderTransactionDetected(pid, code, flags, err);
+ }
+ });
+
// Loop forever.
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");