Merge "Helper API for: QAS/suspension precedence validation." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 16389b3..941d842 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -755,6 +755,13 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+java_aconfig_library {
+ name: "android.hardware.usb.flags-aconfig-java-host",
+ aconfig_declarations: "android.hardware.usb.flags-aconfig",
+ host_supported: true,
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// WindowingTools
aconfig_declarations {
name: "android.tracing.flags-aconfig",
diff --git a/apct-tests/perftests/windowmanager/src/android/wm/InternalWindowOperationPerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/InternalWindowOperationPerfTest.java
index 3a11417..d3b4f23 100644
--- a/apct-tests/perftests/windowmanager/src/android/wm/InternalWindowOperationPerfTest.java
+++ b/apct-tests/perftests/windowmanager/src/android/wm/InternalWindowOperationPerfTest.java
@@ -52,9 +52,8 @@
private final TraceMarkParser mTraceMarkParser = new TraceMarkParser(
"applyPostLayoutPolicy",
"applySurfaceChanges",
- "AppTransitionReady",
- "closeSurfaceTransaction",
- "openSurfaceTransaction",
+ "onTransactionReady",
+ "applyTransaction",
"performLayout",
"performSurfacePlacement",
"prepareSurfaces",
diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
index ec2b1f4..a78a465 100644
--- a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
+++ b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
@@ -96,9 +96,9 @@
return env;
}
-std::unique_ptr<UinputDevice> UinputDevice::open(int32_t id, const char* name, int32_t vid,
- int32_t pid, uint16_t bus, uint32_t ffEffectsMax,
- const char* port,
+std::unique_ptr<UinputDevice> UinputDevice::open(int32_t id, const char* name, int32_t vendorId,
+ int32_t productId, int32_t versionId, uint16_t bus,
+ uint32_t ffEffectsMax, const char* port,
std::unique_ptr<DeviceCallback> callback) {
android::base::unique_fd fd(::open(UINPUT_PATH, O_RDWR | O_NONBLOCK | O_CLOEXEC));
if (!fd.ok()) {
@@ -118,8 +118,9 @@
strlcpy(setupDescriptor.name, name, UINPUT_MAX_NAME_SIZE);
setupDescriptor.id.version = 1;
setupDescriptor.id.bustype = bus;
- setupDescriptor.id.vendor = vid;
- setupDescriptor.id.product = pid;
+ setupDescriptor.id.vendor = vendorId;
+ setupDescriptor.id.product = productId;
+ setupDescriptor.id.version = versionId;
setupDescriptor.ff_effects_max = ffEffectsMax;
// Request device configuration.
@@ -242,9 +243,9 @@
return data;
}
-static jlong openUinputDevice(JNIEnv* env, jclass /* clazz */, jstring rawName, jint id, jint vid,
- jint pid, jint bus, jint ffEffectsMax, jstring rawPort,
- jobject callback) {
+static jlong openUinputDevice(JNIEnv* env, jclass /* clazz */, jstring rawName, jint id,
+ jint vendorId, jint productId, jint versionId, jint bus,
+ jint ffEffectsMax, jstring rawPort, jobject callback) {
ScopedUtfChars name(env, rawName);
if (name.c_str() == nullptr) {
return 0;
@@ -255,8 +256,8 @@
std::make_unique<uinput::DeviceCallback>(env, callback);
std::unique_ptr<uinput::UinputDevice> d =
- uinput::UinputDevice::open(id, name.c_str(), vid, pid, bus, ffEffectsMax, port.c_str(),
- std::move(cb));
+ uinput::UinputDevice::open(id, name.c_str(), vendorId, productId, versionId, bus,
+ ffEffectsMax, port.c_str(), std::move(cb));
return reinterpret_cast<jlong>(d.release());
}
@@ -326,7 +327,7 @@
static JNINativeMethod sMethods[] = {
{"nativeOpenUinputDevice",
- "(Ljava/lang/String;IIIIILjava/lang/String;"
+ "(Ljava/lang/String;IIIIIILjava/lang/String;"
"Lcom/android/commands/uinput/Device$DeviceCallback;)J",
reinterpret_cast<void*>(openUinputDevice)},
{"nativeInjectEvent", "(JIII)V", reinterpret_cast<void*>(injectEvent)},
diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.h b/cmds/uinput/jni/com_android_commands_uinput_Device.h
index 6da3d79..9769a75 100644
--- a/cmds/uinput/jni/com_android_commands_uinput_Device.h
+++ b/cmds/uinput/jni/com_android_commands_uinput_Device.h
@@ -46,9 +46,9 @@
class UinputDevice {
public:
- static std::unique_ptr<UinputDevice> open(int32_t id, const char* name, int32_t vid,
- int32_t pid, uint16_t bus, uint32_t ff_effects_max,
- const char* port,
+ static std::unique_ptr<UinputDevice> open(int32_t id, const char* name, int32_t vendorId,
+ int32_t productId, int32_t versionId, uint16_t bus,
+ uint32_t ff_effects_max, const char* port,
std::unique_ptr<DeviceCallback> callback);
virtual ~UinputDevice();
diff --git a/cmds/uinput/src/com/android/commands/uinput/Device.java b/cmds/uinput/src/com/android/commands/uinput/Device.java
index b0fa34c..787055c 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Device.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Device.java
@@ -61,8 +61,9 @@
System.loadLibrary("uinputcommand_jni");
}
- private static native long nativeOpenUinputDevice(String name, int id, int vid, int pid,
- int bus, int ffEffectsMax, String port, DeviceCallback callback);
+ private static native long nativeOpenUinputDevice(String name, int id, int vendorId,
+ int productId, int versionId, int bus, int ffEffectsMax, String port,
+ DeviceCallback callback);
private static native void nativeCloseUinputDevice(long ptr);
private static native void nativeInjectEvent(long ptr, int type, int code, int value);
private static native void nativeConfigure(int handle, int code, int[] configs);
@@ -71,7 +72,7 @@
private static native int nativeGetEvdevEventCodeByLabel(int type, String label);
private static native int nativeGetEvdevInputPropByLabel(String label);
- public Device(int id, String name, int vid, int pid, int bus,
+ public Device(int id, String name, int vendorId, int productId, int versionId, int bus,
SparseArray<int[]> configuration, int ffEffectsMax,
SparseArray<InputAbsInfo> absInfo, String port) {
mId = id;
@@ -83,19 +84,20 @@
mOutputStream = System.out;
SomeArgs args = SomeArgs.obtain();
args.argi1 = id;
- args.argi2 = vid;
- args.argi3 = pid;
- args.argi4 = bus;
- args.argi5 = ffEffectsMax;
+ args.argi2 = vendorId;
+ args.argi3 = productId;
+ args.argi4 = versionId;
+ args.argi5 = bus;
+ args.argi6 = ffEffectsMax;
if (name != null) {
args.arg1 = name;
} else {
- args.arg1 = id + ":" + vid + ":" + pid;
+ args.arg1 = id + ":" + vendorId + ":" + productId;
}
if (port != null) {
args.arg2 = port;
} else {
- args.arg2 = "uinput:" + id + ":" + vid + ":" + pid;
+ args.arg2 = "uinput:" + id + ":" + vendorId + ":" + productId;
}
mHandler.obtainMessage(MSG_OPEN_UINPUT_DEVICE, args).sendToTarget();
@@ -161,8 +163,10 @@
case MSG_OPEN_UINPUT_DEVICE:
SomeArgs args = (SomeArgs) msg.obj;
String name = (String) args.arg1;
- mPtr = nativeOpenUinputDevice(name, args.argi1, args.argi2,
- args.argi3, args.argi4, args.argi5, (String) args.arg2,
+ mPtr = nativeOpenUinputDevice(name, args.argi1 /* id */,
+ args.argi2 /* vendorId */, args.argi3 /* productId */,
+ args.argi4 /* versionId */, args.argi5 /* bus */,
+ args.argi6 /* ffEffectsMax */, (String) args.arg2 /* port */,
new DeviceCallback());
if (mPtr == 0) {
RuntimeException ex = new RuntimeException(
diff --git a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
index b89e2cd..7652f24 100644
--- a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
@@ -19,8 +19,8 @@
import android.annotation.Nullable;
import android.util.SparseArray;
-import java.io.BufferedReader;
import java.io.IOException;
+import java.io.LineNumberReader;
import java.io.Reader;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -47,10 +47,11 @@
private static final int REGISTRATION_DELAY_MILLIS = 500;
private static class CommentAwareReader {
- private final BufferedReader mReader;
+ private final LineNumberReader mReader;
+ private String mPreviousLine;
private String mNextLine;
- CommentAwareReader(BufferedReader in) throws IOException {
+ CommentAwareReader(LineNumberReader in) throws IOException {
mReader = in;
mNextLine = findNextLine();
}
@@ -90,12 +91,46 @@
/** Moves to the next line of the file. */
public void advance() throws IOException {
+ mPreviousLine = mNextLine;
mNextLine = findNextLine();
}
public boolean isAtEndOfFile() {
return mNextLine == null;
}
+
+ /** Returns the previous line, for error messages. */
+ public String getPreviousLine() {
+ return mPreviousLine;
+ }
+
+ /** Returns the number of the <b>previous</b> line. */
+ public int getPreviousLineNumber() {
+ return mReader.getLineNumber() - 1;
+ }
+ }
+
+ public static class ParsingException extends RuntimeException {
+ private final int mLineNumber;
+ private final String mLine;
+
+ ParsingException(String message, CommentAwareReader reader) {
+ this(message, reader.getPreviousLine(), reader.getPreviousLineNumber());
+ }
+
+ ParsingException(String message, String line, int lineNumber) {
+ super(message);
+ mLineNumber = lineNumber;
+ mLine = line;
+ }
+
+ /** Returns a nicely formatted error message, including the line number and line. */
+ public String makeErrorMessage() {
+ return String.format("""
+ Parsing error on line %d: %s
+ --> %s
+ """, mLineNumber, getMessage(), mLine);
+ }
}
private final CommentAwareReader mReader;
@@ -107,7 +142,7 @@
private final Queue<Event> mQueuedEvents = new ArrayDeque<>(2);
public EvemuParser(Reader in) throws IOException {
- mReader = new CommentAwareReader(new BufferedReader(in));
+ mReader = new CommentAwareReader(new LineNumberReader(in));
mQueuedEvents.add(parseRegistrationEvent());
// The kernel takes a little time to set up an evdev device after the initial
@@ -133,20 +168,22 @@
return null;
}
- final String[] parts = expectLineWithParts("E", 4);
+ final String line = expectLine("E");
+ final String[] parts = expectParts(line, 4);
final String[] timeParts = parts[0].split("\\.");
if (timeParts.length != 2) {
- throw new RuntimeException("Invalid timestamp (does not contain a '.')");
+ throw new ParsingException(
+ "Invalid timestamp '" + parts[0] + "' (should contain a single '.')", mReader);
}
// TODO(b/310958309): use timeMicros to set the timestamp on the event being sent.
final long timeMicros =
- Long.parseLong(timeParts[0]) * 1_000_000 + Integer.parseInt(timeParts[1]);
+ parseLong(timeParts[0], 10) * 1_000_000 + parseInt(timeParts[1], 10);
final Event.Builder eb = new Event.Builder();
eb.setId(DEVICE_ID);
eb.setCommand(Event.Command.INJECT);
- final int eventType = Integer.parseInt(parts[1], 16);
- final int eventCode = Integer.parseInt(parts[2], 16);
- final int value = Integer.parseInt(parts[3]);
+ final int eventType = parseInt(parts[1], 16);
+ final int eventCode = parseInt(parts[2], 16);
+ final int value = parseInt(parts[3], 10);
eb.setInjections(new int[] {eventType, eventCode, value});
if (mLastEventTimeMicros == -1) {
@@ -184,11 +221,12 @@
eb.setCommand(Event.Command.REGISTER);
eb.setName(expectLine("N"));
- final String[] idStrings = expectLineWithParts("I", 4);
- eb.setBusId(Integer.parseInt(idStrings[0], 16));
- eb.setVid(Integer.parseInt(idStrings[1], 16));
- eb.setPid(Integer.parseInt(idStrings[2], 16));
- // TODO(b/302297266): support setting the version ID, and set it to idStrings[3].
+ final String idsLine = expectLine("I");
+ final String[] idStrings = expectParts(idsLine, 4);
+ eb.setBusId(parseInt(idStrings[0], 16));
+ eb.setVendorId(parseInt(idStrings[1], 16));
+ eb.setProductId(parseInt(idStrings[2], 16));
+ eb.setVersionId(parseInt(idStrings[3], 16));
final SparseArray<int[]> config = new SparseArray<>();
config.append(Event.UinputControlCode.UI_SET_PROPBIT.getValue(), parseProperties());
@@ -215,33 +253,39 @@
}
private int[] parseProperties() throws IOException {
- final List<String> propBitmapParts = new ArrayList<>();
+ final ArrayList<Integer> propBitmapParts = new ArrayList<>();
String line = acceptLine("P");
while (line != null) {
- propBitmapParts.addAll(List.of(line.strip().split(" ")));
+ String[] parts = line.strip().split(" ");
+ propBitmapParts.ensureCapacity(propBitmapParts.size() + parts.length);
+ for (String part : parts) {
+ propBitmapParts.add(parseBitmapPart(part, line));
+ }
line = acceptLine("P");
}
- return hexStringBitmapToEventCodes(propBitmapParts);
+ return bitmapToEventCodes(propBitmapParts);
}
private void parseAxisBitmaps(SparseArray<int[]> config) throws IOException {
- final Map<Integer, List<String>> axisBitmapParts = new HashMap<>();
+ final Map<Integer, ArrayList<Integer>> axisBitmapParts = new HashMap<>();
String line = acceptLine("B");
while (line != null) {
final String[] parts = line.strip().split(" ");
if (parts.length < 2) {
- throw new RuntimeException(
+ throw new ParsingException(
"Expected event type and at least one bitmap byte on 'B:' line; only found "
- + parts.length + " elements");
+ + parts.length + " elements", mReader);
}
- final int eventType = Integer.parseInt(parts[0], 16);
+ final int eventType = parseInt(parts[0], 16);
// EV_SYN cannot be configured through uinput, so skip it.
if (eventType != Event.EV_SYN) {
if (!axisBitmapParts.containsKey(eventType)) {
axisBitmapParts.put(eventType, new ArrayList<>());
}
+ ArrayList<Integer> bitmapParts = axisBitmapParts.get(eventType);
+ bitmapParts.ensureCapacity(bitmapParts.size() + parts.length);
for (int i = 1; i < parts.length; i++) {
- axisBitmapParts.get(eventType).add(parts[i]);
+ axisBitmapParts.get(eventType).add(parseBitmapPart(parts[i], line));
}
}
line = acceptLine("B");
@@ -253,7 +297,7 @@
}
final Event.UinputControlCode controlCode =
Event.UinputControlCode.forEventType(entry.getKey());
- final int[] eventCodes = hexStringBitmapToEventCodes(entry.getValue());
+ final int[] eventCodes = bitmapToEventCodes(entry.getValue());
if (controlCode != null && eventCodes.length > 0) {
config.append(controlCode.getValue(), eventCodes);
eventTypesToSet.add(entry.getKey());
@@ -263,24 +307,33 @@
Event.UinputControlCode.UI_SET_EVBIT.getValue(), unboxIntList(eventTypesToSet));
}
+ private int parseBitmapPart(String part, String line) {
+ int b = parseInt(part, 16);
+ if (b < 0x0 || b > 0xff) {
+ throw new ParsingException("Bitmap part '" + part
+ + "' invalid; parts must be hexadecimal values between 00 and ff.", mReader);
+ }
+ return b;
+ }
+
private SparseArray<InputAbsInfo> parseAbsInfos() throws IOException {
final SparseArray<InputAbsInfo> absInfos = new SparseArray<>();
String line = acceptLine("A");
while (line != null) {
final String[] parts = line.strip().split(" ");
if (parts.length < 5 || parts.length > 6) {
- throw new RuntimeException(
- "'A:' lines should have the format 'A: <index (hex)> <min> <max> <fuzz> "
+ throw new ParsingException(
+ "AbsInfo lines should have the format 'A: <index (hex)> <min> <max> <fuzz> "
+ "<flat> [<resolution>]'; expected 5 or 6 numbers but found "
- + parts.length);
+ + parts.length, mReader);
}
- final int axisCode = Integer.parseInt(parts[0], 16);
+ final int axisCode = parseInt(parts[0], 16);
final InputAbsInfo info = new InputAbsInfo();
- info.minimum = Integer.parseInt(parts[1]);
- info.maximum = Integer.parseInt(parts[2]);
- info.fuzz = Integer.parseInt(parts[3]);
- info.flat = Integer.parseInt(parts[4]);
- info.resolution = parts.length > 5 ? Integer.parseInt(parts[5]) : 0;
+ info.minimum = parseInt(parts[1], 10);
+ info.maximum = parseInt(parts[2], 10);
+ info.fuzz = parseInt(parts[3], 10);
+ info.flat = parseInt(parts[4], 10);
+ info.resolution = parts.length > 5 ? parseInt(parts[5], 10) : 0;
absInfos.append(axisCode, info);
line = acceptLine("A");
}
@@ -305,7 +358,9 @@
private String expectLine(String type) throws IOException {
final String line = acceptLine(type);
if (line == null) {
- throw new RuntimeException("Expected line of type '" + type + "'");
+ throw new ParsingException("Expected line of type '" + type + "'. (Lines should be in "
+ + "the order N, I, P, B, A, L, S, E.)",
+ mReader.peekLine(), mReader.getPreviousLineNumber() + 1);
} else {
return line;
}
@@ -325,9 +380,8 @@
}
final String[] lineParts = line.split(": ", 2);
if (lineParts.length < 2) {
- // TODO(b/302297266): make a proper exception class for syntax errors, including line
- // numbers, etc.. (We can use LineNumberReader to track them.)
- throw new RuntimeException("Line without ': '");
+ throw new ParsingException("Missing type separator ': '",
+ line, mReader.getPreviousLineNumber() + 1);
}
if (lineParts[0].equals(type)) {
mReader.advance();
@@ -337,31 +391,37 @@
}
}
- /**
- * Like {@link #expectLine(String)}, but also checks that the contents of the line is formed of
- * {@code numParts} space-separated parts.
- *
- * @param type the type of the line to expect, represented by the letter before the ':'.
- * @param numParts the number of parts to expect.
- * @return the part of the line after the ": ", split into {@code numParts} sections.
- */
- private String[] expectLineWithParts(String type, int numParts) throws IOException {
- final String[] parts = expectLine(type).strip().split(" ");
+ private String[] expectParts(String line, int numParts) {
+ final String[] parts = line.strip().split(" ");
if (parts.length != numParts) {
- throw new RuntimeException("Expected a '" + type + "' line with " + numParts
- + " parts, found one with " + parts.length);
+ throw new ParsingException(
+ "Expected a line with " + numParts + " space-separated parts, but found one "
+ + "with " + parts.length, mReader);
}
return parts;
}
- private static int[] hexStringBitmapToEventCodes(List<String> strs) {
+ private int parseInt(String s, int radix) {
+ try {
+ return Integer.parseInt(s, radix);
+ } catch (NumberFormatException ex) {
+ throw new ParsingException(
+ "'" + s + "' is not a valid integer of base " + radix, mReader);
+ }
+ }
+
+ private long parseLong(String s, int radix) {
+ try {
+ return Long.parseLong(s, radix);
+ } catch (NumberFormatException ex) {
+ throw new ParsingException("'" + s + "' is not a valid long of base " + radix, mReader);
+ }
+ }
+
+ private static int[] bitmapToEventCodes(List<Integer> bytes) {
final List<Integer> codes = new ArrayList<>();
- for (int iByte = 0; iByte < strs.size(); iByte++) {
- int b = Integer.parseInt(strs.get(iByte), 16);
- if (b < 0x0 || b > 0xff) {
- throw new RuntimeException("Bitmap part '" + strs.get(iByte)
- + "' invalid; parts must be between 00 and ff.");
- }
+ for (int iByte = 0; iByte < bytes.size(); iByte++) {
+ int b = bytes.get(iByte);
for (int iBit = 0; iBit < 8; iBit++) {
if ((b & 1) != 0) {
codes.add(iByte * 8 + iBit);
diff --git a/cmds/uinput/src/com/android/commands/uinput/Event.java b/cmds/uinput/src/com/android/commands/uinput/Event.java
index 5ec40e5..0f16a27 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Event.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Event.java
@@ -94,8 +94,9 @@
private int mId;
private Command mCommand;
private String mName;
- private int mVid;
- private int mPid;
+ private int mVendorId;
+ private int mProductId;
+ private int mVersionId;
private int mBusId;
private int[] mInjections;
private SparseArray<int[]> mConfiguration;
@@ -118,11 +119,15 @@
}
public int getVendorId() {
- return mVid;
+ return mVendorId;
}
public int getProductId() {
- return mPid;
+ return mProductId;
+ }
+
+ public int getVersionId() {
+ return mVersionId;
}
public int getBus() {
@@ -172,8 +177,8 @@
return "Event{id=" + mId
+ ", command=" + mCommand
+ ", name=" + mName
- + ", vid=" + mVid
- + ", pid=" + mPid
+ + ", vid=" + mVendorId
+ + ", pid=" + mProductId
+ ", busId=" + mBusId
+ ", events=" + Arrays.toString(mInjections)
+ ", configuration=" + mConfiguration
@@ -216,12 +221,16 @@
mEvent.mConfiguration = configuration;
}
- public void setVid(int vid) {
- mEvent.mVid = vid;
+ public void setVendorId(int vendorId) {
+ mEvent.mVendorId = vendorId;
}
- public void setPid(int pid) {
- mEvent.mPid = pid;
+ public void setProductId(int productId) {
+ mEvent.mProductId = productId;
+ }
+
+ public void setVersionId(int versionId) {
+ mEvent.mVersionId = versionId;
}
public void setBusId(int busId) {
diff --git a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
index 888ec5a..ed3ff33 100644
--- a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
@@ -60,8 +60,8 @@
case "command" -> eb.setCommand(
Event.Command.valueOf(mReader.nextString().toUpperCase()));
case "name" -> eb.setName(mReader.nextString());
- case "vid" -> eb.setVid(readInt());
- case "pid" -> eb.setPid(readInt());
+ case "vid" -> eb.setVendorId(readInt());
+ case "pid" -> eb.setProductId(readInt());
case "bus" -> eb.setBusId(readBus());
case "events" -> {
int[] injections = readInjectedEvents().stream()
diff --git a/cmds/uinput/src/com/android/commands/uinput/Uinput.java b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
index 684a12f..04df279 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Uinput.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
@@ -60,6 +60,10 @@
stream = new FileInputStream(f);
}
(new Uinput(stream)).run();
+ } catch (EvemuParser.ParsingException e) {
+ System.err.println(e.makeErrorMessage());
+ error(e.makeErrorMessage(), e);
+ System.exit(1);
} catch (Exception e) {
error("Uinput injection failed.", e);
System.exit(1);
@@ -142,8 +146,9 @@
"Tried to send command \"" + e.getCommand() + "\" to an unregistered device!");
}
int id = e.getId();
- Device d = new Device(id, e.getName(), e.getVendorId(), e.getProductId(), e.getBus(),
- e.getConfiguration(), e.getFfEffectsMax(), e.getAbsInfo(), e.getPort());
+ Device d = new Device(id, e.getName(), e.getVendorId(), e.getProductId(),
+ e.getVersionId(), e.getBus(), e.getConfiguration(), e.getFfEffectsMax(),
+ e.getAbsInfo(), e.getPort());
mDevices.append(id, d);
}
diff --git a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
index 6ee987f..06b0aac2 100644
--- a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
+++ b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
@@ -19,6 +19,8 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.fail;
+
import android.platform.test.annotations.Postsubmit;
import android.util.SparseArray;
@@ -68,7 +70,7 @@
assertThat(event.getBus()).isEqualTo(0x0001);
assertThat(event.getVendorId()).isEqualTo(0x1234);
assertThat(event.getProductId()).isEqualTo(0x5678);
- // TODO(b/302297266): check version ID once it's supported
+ assertThat(event.getVersionId()).isEqualTo(0x9abc);
}
@Test
@@ -241,6 +243,25 @@
}
@Test
+ public void testErrorLineNumberReporting() throws IOException {
+ StringReader reader = new StringReader("""
+ # EVEMU 1.3
+ N: ACME Widget
+ # Comment to make sure they're taken into account when numbering lines
+ I: 0001 1234 5678 9abc
+ 00 00 00 00 00 00 00 00 # Missing a type
+ E: 0.000001 0001 0015 0001 # KEY_Y press
+ E: 0.000001 0000 0000 0000 # SYN_REPORT
+ """);
+ try {
+ new EvemuParser(reader);
+ fail("Parser should have thrown an error about the line with the missing type.");
+ } catch (EvemuParser.ParsingException ex) {
+ assertThat(ex.makeErrorMessage()).startsWith("Parsing error on line 5:");
+ }
+ }
+
+ @Test
public void testFreeDesktopEvemuRecording() throws IOException {
// This is a real recording from FreeDesktop's evemu-record tool, as a basic compatibility
// check with the FreeDesktop tools.
diff --git a/core/api/current.txt b/core/api/current.txt
index d7341e0..9d13d8a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -43742,7 +43742,9 @@
field public static final String KEY_SUPPORTED_IKE_SESSION_ENCRYPTION_ALGORITHMS_INT_ARRAY = "iwlan.supported_ike_session_encryption_algorithms_int_array";
field public static final String KEY_SUPPORTED_INTEGRITY_ALGORITHMS_INT_ARRAY = "iwlan.supported_integrity_algorithms_int_array";
field public static final String KEY_SUPPORTED_PRF_ALGORITHMS_INT_ARRAY = "iwlan.supported_prf_algorithms_int_array";
+ field @FlaggedApi("com.android.internal.telephony.flags.enable_multiple_sa_proposals") public static final String KEY_SUPPORTS_CHILD_SESSION_MULTIPLE_SA_PROPOSALS_BOOL = "iwlan.supports_child_session_multiple_sa_proposals_bool";
field public static final String KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL = "iwlan.supports_eap_aka_fast_reauth_bool";
+ field @FlaggedApi("com.android.internal.telephony.flags.enable_multiple_sa_proposals") public static final String KEY_SUPPORTS_IKE_SESSION_MULTIPLE_SA_PROPOSALS_BOOL = "iwlan.supports_ike_session_multiple_sa_proposals_bool";
}
public abstract class CellIdentity implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 01ca6d9..ce5752f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -4211,10 +4211,18 @@
public final class UserProperties implements android.os.Parcelable {
method public int describeContents();
+ method public int getShowInQuietMode();
+ method public int getShowInSharingSurfaces();
method public boolean isCredentialShareableWithParent();
method public boolean isMediaSharedWithParent();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.UserProperties> CREATOR;
+ field public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; // 0x2
+ field public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; // 0x1
+ field public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; // 0x0
+ field public static final int SHOW_IN_SHARING_SURFACES_NO = 2; // 0x2
+ field public static final int SHOW_IN_SHARING_SURFACES_SEPARATE = 1; // 0x1
+ field public static final int SHOW_IN_SHARING_SURFACES_WITH_PARENT = 0; // 0x0
}
}
@@ -10560,6 +10568,7 @@
method @NonNull public java.util.List<android.os.UserHandle> getEnabledProfiles();
method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public android.os.UserHandle getMainUser();
method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public android.os.UserHandle getPreviousForegroundUser();
+ method @NonNull public String getProfileLabel();
method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public android.os.UserHandle getProfileParent(@NonNull android.os.UserHandle);
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public int getRemainingCreatableProfileCount(@NonNull String);
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public int getRemainingCreatableUserCount(@NonNull String);
diff --git a/core/java/android/app/time/TEST_MAPPING b/core/java/android/app/time/TEST_MAPPING
index 47a152a..0f7a070 100644
--- a/core/java/android/app/time/TEST_MAPPING
+++ b/core/java/android/app/time/TEST_MAPPING
@@ -7,20 +7,6 @@
"include-filter": "android.app."
}
]
- }
- ],
- // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
- "postsubmit": [
- {
- "name": "FrameworksServicesTests",
- "options": [
- {
- "include-filter": "com.android.server.timezonedetector."
- },
- {
- "include-filter": "com.android.server.timedetector."
- }
- ]
},
{
"name": "CtsTimeTestCases",
@@ -30,5 +16,19 @@
}
]
}
+ ],
+ // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
+ "postsubmit": [
+ {
+ "name": "FrameworksTimeServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.timezonedetector."
+ },
+ {
+ "include-filter": "com.android.server.timedetector."
+ }
+ ]
+ }
]
}
diff --git a/core/java/android/app/timedetector/TEST_MAPPING b/core/java/android/app/timedetector/TEST_MAPPING
index 9517fb9..43dd82f 100644
--- a/core/java/android/app/timedetector/TEST_MAPPING
+++ b/core/java/android/app/timedetector/TEST_MAPPING
@@ -7,17 +7,6 @@
"include-filter": "android.app."
}
]
- }
- ],
- // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
- "postsubmit": [
- {
- "name": "FrameworksServicesTests",
- "options": [
- {
- "include-filter": "com.android.server.timedetector."
- }
- ]
},
{
"name": "CtsTimeTestCases",
@@ -27,5 +16,16 @@
}
]
}
+ ],
+ // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
+ "postsubmit": [
+ {
+ "name": "FrameworksTimeServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.timedetector."
+ }
+ ]
+ }
]
}
diff --git a/core/java/android/app/timezonedetector/TEST_MAPPING b/core/java/android/app/timezonedetector/TEST_MAPPING
index fd41b86..2be5614 100644
--- a/core/java/android/app/timezonedetector/TEST_MAPPING
+++ b/core/java/android/app/timezonedetector/TEST_MAPPING
@@ -7,17 +7,6 @@
"include-filter": "android.app."
}
]
- }
- ],
- // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
- "postsubmit": [
- {
- "name": "FrameworksServicesTests",
- "options": [
- {
- "include-filter": "com.android.server.timezonedetector."
- }
- ]
},
{
"name": "CtsTimeTestCases",
@@ -27,5 +16,16 @@
}
]
}
+ ],
+ // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
+ "postsubmit": [
+ {
+ "name": "FrameworksTimeServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.timezonedetector."
+ }
+ ]
+ }
]
}
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index f532c4c..445ca0c 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -19,6 +19,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.os.Parcel;
@@ -49,7 +50,8 @@
private static final String ATTR_SHOW_IN_LAUNCHER = "showInLauncher";
private static final String ATTR_START_WITH_PARENT = "startWithParent";
private static final String ATTR_SHOW_IN_SETTINGS = "showInSettings";
- private static final String ATTR_HIDE_IN_SETTINGS_IN_QUIET_MODE = "hideInSettingsInQuietMode";
+ private static final String ATTR_SHOW_IN_QUIET_MODE = "showInQuietMode";
+ private static final String ATTR_SHOW_IN_SHARING_SURFACES = "showInSharingSurfaces";
private static final String ATTR_INHERIT_DEVICE_POLICY = "inheritDevicePolicy";
private static final String ATTR_USE_PARENTS_CONTACTS = "useParentsContacts";
private static final String ATTR_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA =
@@ -81,7 +83,8 @@
INDEX_CREDENTIAL_SHAREABLE_WITH_PARENT,
INDEX_DELETE_APP_WITH_PARENT,
INDEX_ALWAYS_VISIBLE,
- INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE,
+ INDEX_SHOW_IN_QUIET_MODE,
+ INDEX_SHOW_IN_SHARING_SURFACES,
INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE,
})
@Retention(RetentionPolicy.SOURCE)
@@ -99,8 +102,9 @@
private static final int INDEX_CREDENTIAL_SHAREABLE_WITH_PARENT = 9;
private static final int INDEX_DELETE_APP_WITH_PARENT = 10;
private static final int INDEX_ALWAYS_VISIBLE = 11;
- private static final int INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE = 12;
+ private static final int INDEX_SHOW_IN_QUIET_MODE = 12;
private static final int INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE = 13;
+ private static final int INDEX_SHOW_IN_SHARING_SURFACES = 14;
/** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
private long mPropertiesPresent = 0;
@@ -286,6 +290,81 @@
*/
public static final int CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_NO_FILTERING = 1;
+ /**
+ * Possible values for the profile visibility when in quiet mode. This affects the profile data
+ * and apps surfacing in Settings, sharing surfaces, and file picker surfaces. It signifies
+ * whether the profile data and apps will be shown or not.
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "SHOW_IN_QUIET_MODE_",
+ value = {
+ SHOW_IN_QUIET_MODE_PAUSED,
+ SHOW_IN_QUIET_MODE_HIDDEN,
+ SHOW_IN_QUIET_MODE_DEFAULT,
+ }
+ )
+ public @interface ShowInQuietMode {
+ }
+
+ /**
+ * Indicates that the profile should still be visible in quiet mode but should be shown as
+ * paused (e.g. by greying out its icons).
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_QUIET_MODE_PAUSED = 0;
+ /**
+ * Indicates that the profile should not be visible when the profile is in quiet mode.
+ * For example, the profile should not be shown in tabbed views in Settings, files sharing
+ * surfaces etc when in quiet mode.
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1;
+ /**
+ * Indicates that quiet mode should not have any effect on the profile visibility. If the
+ * profile is meant to be visible, it will remain visible and vice versa.
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2;
+
+ /**
+ * Possible values for the profile apps visibility in sharing surfaces. This indicates the
+ * profile data and apps should be shown in separate tabs or mixed with its parent user's data
+ * and apps in sharing surfaces and file picker surfaces.
+ *
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "SHOW_IN_SHARING_SURFACES_",
+ value = {
+ SHOW_IN_SHARING_SURFACES_SEPARATE,
+ SHOW_IN_SHARING_SURFACES_WITH_PARENT,
+ SHOW_IN_SHARING_SURFACES_NO,
+ }
+ )
+ public @interface ShowInSharingSurfaces {
+ }
+
+ /**
+ * Indicates that the profile data and apps should be shown in sharing surfaces intermixed with
+ * parent user's data and apps.
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_SHARING_SURFACES_WITH_PARENT = SHOW_IN_LAUNCHER_WITH_PARENT;
+
+ /**
+ * Indicates that the profile data and apps should be shown in sharing surfaces separate from
+ * parent user's data and apps.
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_SHARING_SURFACES_SEPARATE = SHOW_IN_LAUNCHER_SEPARATE;
+
+ /**
+ * Indicates that the profile data and apps should not be shown in sharing surfaces at all.
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public static final int SHOW_IN_SHARING_SURFACES_NO = SHOW_IN_LAUNCHER_NO;
/**
* Creates a UserProperties (intended for the SystemServer) that stores a reference to the given
@@ -331,7 +410,6 @@
if (hasManagePermission) {
// Add items that require MANAGE_USERS or stronger.
setShowInSettings(orig.getShowInSettings());
- setHideInSettingsInQuietMode(orig.getHideInSettingsInQuietMode());
setUseParentsContacts(orig.getUseParentsContacts());
setAuthAlwaysRequiredToDisableQuietMode(
orig.isAuthAlwaysRequiredToDisableQuietMode());
@@ -343,6 +421,8 @@
setShowInLauncher(orig.getShowInLauncher());
setMediaSharedWithParent(orig.isMediaSharedWithParent());
setCredentialShareableWithParent(orig.isCredentialShareableWithParent());
+ setShowInQuietMode(orig.getShowInQuietMode());
+ setShowInSharingSurfaces(orig.getShowInSharingSurfaces());
}
/**
@@ -419,40 +499,59 @@
private @ShowInSettings int mShowInSettings;
/**
- * Returns whether a user should be shown in the Settings app depending on the quiet mode.
- * This is generally inapplicable for non-profile users.
+ * Returns whether a user should be shown in the Settings and sharing surfaces depending on the
+ * {@link android.os.UserManager#requestQuietModeEnabled(boolean, android.os.UserHandle)
+ * quiet mode}. This is only applicable to profile users since the quiet mode concept is only
+ * applicable to profile users.
*
- * <p> {@link #getShowInSettings()} returns whether / how a user should be shown in Settings.
- * However, if this behaviour should be changed based on the quiet mode of the user, then this
- * property can be used. If the property is not set then the user is shown in the Settings app
- * irrespective of whether the user is in quiet mode or not. If the property is set, then the
- * user is shown in the Settings app only if the user is not in the quiet mode. Please note that
- * this property takes effect only if {@link #getShowInSettings()} does not return
- * {@link #SHOW_IN_SETTINGS_NO}.
+ * <p> Please note that, in Settings, this property takes effect only if
+ * {@link #getShowInSettings()} does not return {@link #SHOW_IN_SETTINGS_NO}.
+ * Also note that in Sharing surfaces this property takes effect only if
+ * {@link #getShowInSharingSurfaces()} does not return {@link #SHOW_IN_SHARING_SURFACES_NO}.
*
- * <p> The caller must have {@link android.Manifest.permission#MANAGE_USERS} to query this
- * property.
- *
- * @return true if a profile should be shown in the Settings only when the user is not in the
- * quiet mode.
- *
- * See also {@link #getShowInSettings()}, {@link #setShowInSettings(int)},
- * {@link ShowInSettings}
- *
- * @hide
+ * @return One of {@link #SHOW_IN_QUIET_MODE_HIDDEN},
+ * {@link #SHOW_IN_QUIET_MODE_PAUSED}, or
+ * {@link #SHOW_IN_QUIET_MODE_DEFAULT} depending on whether the profile should be
+ * shown in quiet mode or not.
*/
- public boolean getHideInSettingsInQuietMode() {
- if (isPresent(INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE)) return mHideInSettingsInQuietMode;
- if (mDefaultProperties != null) return mDefaultProperties.mHideInSettingsInQuietMode;
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public @ShowInQuietMode int getShowInQuietMode() {
+ // NOTE: Launcher currently does not make use of this property.
+ if (isPresent(INDEX_SHOW_IN_QUIET_MODE)) return mShowInQuietMode;
+ if (mDefaultProperties != null) return mDefaultProperties.mShowInQuietMode;
throw new SecurityException(
- "You don't have permission to query HideInSettingsInQuietMode");
+ "You don't have permission to query ShowInQuietMode");
}
/** @hide */
- public void setHideInSettingsInQuietMode(boolean hideInSettingsInQuietMode) {
- this.mHideInSettingsInQuietMode = hideInSettingsInQuietMode;
- setPresent(INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE);
+ public void setShowInQuietMode(@ShowInQuietMode int showInQuietMode) {
+ this.mShowInQuietMode = showInQuietMode;
+ setPresent(INDEX_SHOW_IN_QUIET_MODE);
}
- private boolean mHideInSettingsInQuietMode;
+ private int mShowInQuietMode;
+
+ /**
+ * Returns whether a user's data and apps should be shown in sharing surfaces in a separate tab
+ * or mixed with the parent user's data/apps. This is only applicable to profile users.
+ *
+ * @return One of {@link #SHOW_IN_SHARING_SURFACES_NO},
+ * {@link #SHOW_IN_SHARING_SURFACES_SEPARATE}, or
+ * {@link #SHOW_IN_SHARING_SURFACES_WITH_PARENT} depending on whether the profile
+ * should be shown separate from its parent's data, mixed with the parent's data, or
+ * not shown at all.
+ */
+ @SuppressLint("UnflaggedApi") // b/306636213
+ public @ShowInSharingSurfaces int getShowInSharingSurfaces() {
+ if (isPresent(INDEX_SHOW_IN_SHARING_SURFACES)) return mShowInSharingSurfaces;
+ if (mDefaultProperties != null) return mDefaultProperties.mShowInSharingSurfaces;
+ throw new SecurityException(
+ "You don't have permission to query ShowInSharingSurfaces");
+ }
+ /** @hide */
+ public void setShowInSharingSurfaces(@ShowInSharingSurfaces int showInSharingSurfaces) {
+ this.mShowInSharingSurfaces = showInSharingSurfaces;
+ setPresent(INDEX_SHOW_IN_SHARING_SURFACES);
+ }
+ private int mShowInSharingSurfaces;
/**
* Returns whether a profile should be started when its parent starts (unless in quiet mode).
@@ -799,8 +898,11 @@
case ATTR_SHOW_IN_SETTINGS:
setShowInSettings(parser.getAttributeInt(i));
break;
- case ATTR_HIDE_IN_SETTINGS_IN_QUIET_MODE:
- setHideInSettingsInQuietMode(parser.getAttributeBoolean(i));
+ case ATTR_SHOW_IN_QUIET_MODE:
+ setShowInQuietMode(parser.getAttributeInt(i));
+ break;
+ case ATTR_SHOW_IN_SHARING_SURFACES:
+ setShowInSharingSurfaces(parser.getAttributeInt(i));
break;
case ATTR_INHERIT_DEVICE_POLICY:
setInheritDevicePolicy(parser.getAttributeInt(i));
@@ -858,9 +960,12 @@
if (isPresent(INDEX_SHOW_IN_SETTINGS)) {
serializer.attributeInt(null, ATTR_SHOW_IN_SETTINGS, mShowInSettings);
}
- if (isPresent(INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE)) {
- serializer.attributeBoolean(null, ATTR_HIDE_IN_SETTINGS_IN_QUIET_MODE,
- mHideInSettingsInQuietMode);
+ if (isPresent(INDEX_SHOW_IN_QUIET_MODE)) {
+ serializer.attributeInt(null, ATTR_SHOW_IN_QUIET_MODE,
+ mShowInQuietMode);
+ }
+ if (isPresent(INDEX_SHOW_IN_SHARING_SURFACES)) {
+ serializer.attributeInt(null, ATTR_SHOW_IN_SHARING_SURFACES, mShowInSharingSurfaces);
}
if (isPresent(INDEX_INHERIT_DEVICE_POLICY)) {
serializer.attributeInt(null, ATTR_INHERIT_DEVICE_POLICY,
@@ -912,7 +1017,8 @@
dest.writeInt(mShowInLauncher);
dest.writeBoolean(mStartWithParent);
dest.writeInt(mShowInSettings);
- dest.writeBoolean(mHideInSettingsInQuietMode);
+ dest.writeInt(mShowInQuietMode);
+ dest.writeInt(mShowInSharingSurfaces);
dest.writeInt(mInheritDevicePolicy);
dest.writeBoolean(mUseParentsContacts);
dest.writeBoolean(mUpdateCrossProfileIntentFiltersOnOTA);
@@ -936,7 +1042,8 @@
mShowInLauncher = source.readInt();
mStartWithParent = source.readBoolean();
mShowInSettings = source.readInt();
- mHideInSettingsInQuietMode = source.readBoolean();
+ mShowInQuietMode = source.readInt();
+ mShowInSharingSurfaces = source.readInt();
mInheritDevicePolicy = source.readInt();
mUseParentsContacts = source.readBoolean();
mUpdateCrossProfileIntentFiltersOnOTA = source.readBoolean();
@@ -974,7 +1081,10 @@
private @ShowInLauncher int mShowInLauncher = SHOW_IN_LAUNCHER_WITH_PARENT;
private boolean mStartWithParent = false;
private @ShowInSettings int mShowInSettings = SHOW_IN_SETTINGS_WITH_PARENT;
- private boolean mHideInSettingsInQuietMode = false;
+ private @ShowInQuietMode int mShowInQuietMode =
+ SHOW_IN_QUIET_MODE_PAUSED;
+ private @ShowInSharingSurfaces int mShowInSharingSurfaces =
+ SHOW_IN_SHARING_SURFACES_SEPARATE;
private @InheritDevicePolicy int mInheritDevicePolicy = INHERIT_DEVICE_POLICY_NO;
private boolean mUseParentsContacts = false;
private boolean mUpdateCrossProfileIntentFiltersOnOTA = false;
@@ -1005,9 +1115,15 @@
return this;
}
- /** Sets the value for {@link #mHideInSettingsInQuietMode} */
- public Builder setHideInSettingsInQuietMode(boolean hideInSettingsInQuietMode) {
- mHideInSettingsInQuietMode = hideInSettingsInQuietMode;
+ /** Sets the value for {@link #mShowInQuietMode} */
+ public Builder setShowInQuietMode(@ShowInQuietMode int showInQuietMode) {
+ mShowInQuietMode = showInQuietMode;
+ return this;
+ }
+
+ /** Sets the value for {@link #mShowInSharingSurfaces}. */
+ public Builder setShowInSharingSurfaces(@ShowInSharingSurfaces int showInSharingSurfaces) {
+ mShowInSharingSurfaces = showInSharingSurfaces;
return this;
}
@@ -1081,7 +1197,8 @@
mShowInLauncher,
mStartWithParent,
mShowInSettings,
- mHideInSettingsInQuietMode,
+ mShowInQuietMode,
+ mShowInSharingSurfaces,
mInheritDevicePolicy,
mUseParentsContacts,
mUpdateCrossProfileIntentFiltersOnOTA,
@@ -1100,7 +1217,8 @@
@ShowInLauncher int showInLauncher,
boolean startWithParent,
@ShowInSettings int showInSettings,
- boolean hideInSettingsInQuietMode,
+ @ShowInQuietMode int showInQuietMode,
+ @ShowInSharingSurfaces int showInSharingSurfaces,
@InheritDevicePolicy int inheritDevicePolicy,
boolean useParentsContacts, boolean updateCrossProfileIntentFiltersOnOTA,
@CrossProfileIntentFilterAccessControlLevel int crossProfileIntentFilterAccessControl,
@@ -1114,7 +1232,8 @@
setShowInLauncher(showInLauncher);
setStartWithParent(startWithParent);
setShowInSettings(showInSettings);
- setHideInSettingsInQuietMode(hideInSettingsInQuietMode);
+ setShowInQuietMode(showInQuietMode);
+ setShowInSharingSurfaces(showInSharingSurfaces);
setInheritDevicePolicy(inheritDevicePolicy);
setUseParentsContacts(useParentsContacts);
setUpdateCrossProfileIntentFiltersOnOTA(updateCrossProfileIntentFiltersOnOTA);
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 9ec082a..6c6b33b 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -42,3 +42,10 @@
description: "Allow using all cpu cores during a user switch."
bug: "308105403"
}
+
+flag {
+ name: "enable_biometrics_to_unlock_private_space"
+ namespace: "profile_experiences"
+ description: "Add support to unlock the private space using biometrics"
+ bug: "312184187"
+}
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index 330b992..c0d1fb9 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -131,6 +131,7 @@
int getUserBadgeDarkColorResId(int userId);
int getUserStatusBarIconResId(int userId);
boolean hasBadge(int userId);
+ int getProfileLabelResId(int userId);
boolean isUserUnlocked(int userId);
boolean isUserRunning(int userId);
boolean isUserForeground(int userId);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 08d6e02..ec6d20f 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5693,6 +5693,44 @@
}
/**
+ * Returns the string/label that should be used to represent the context user. For example,
+ * this string can represent a profile in tabbed views. This is only applicable to
+ * {@link #isProfile() profile users}. This string is translated to the device default language.
+ *
+ * @return String representing the label for the context user.
+ *
+ * @throws android.content.res.Resources.NotFoundException if the user does not have a label
+ * defined.
+ *
+ * @hide
+ */
+ @SystemApi
+ @SuppressLint("UnflaggedApi") // b/306636213
+ @UserHandleAware(
+ requiresAnyOfPermissionsIfNotCallerProfileGroup = {
+ Manifest.permission.MANAGE_USERS,
+ Manifest.permission.QUERY_USERS,
+ Manifest.permission.INTERACT_ACROSS_USERS})
+ public @NonNull String getProfileLabel() {
+ if (isManagedProfile(mUserId)) {
+ DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
+ return dpm.getResources().getString(
+ android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB,
+ () -> getDefaultProfileLabel(mUserId));
+ }
+ return getDefaultProfileLabel(mUserId);
+ }
+
+ private String getDefaultProfileLabel(int userId) {
+ try {
+ final int resourceId = mService.getProfileLabelResId(userId);
+ return Resources.getSystem().getString(resourceId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* If the user is a {@link UserManager#isProfile profile}, checks if the user
* shares media with its parent user (the user that created this profile).
* Returns false for any other type of user.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7b45600..7a6c292 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -130,7 +130,6 @@
import android.graphics.HardwareRenderer;
import android.graphics.HardwareRenderer.FrameDrawingCallback;
import android.graphics.HardwareRendererObserver;
-import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
@@ -206,7 +205,6 @@
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
-import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.ContentCaptureSession;
@@ -4029,56 +4027,20 @@
}
private void notifyContentCaptureEvents() {
- try {
- if (!isContentCaptureEnabled()) {
- if (DEBUG_CONTENT_CAPTURE) {
- Log.d(mTag, "notifyContentCaptureEvents while disabled");
- }
- mAttachInfo.mContentCaptureEvents = null;
- return;
- }
- if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
- Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
- }
- MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager
- .getMainContentCaptureSession();
- for (int i = 0; i < mAttachInfo.mContentCaptureEvents.size(); i++) {
- int sessionId = mAttachInfo.mContentCaptureEvents.keyAt(i);
- mainSession.notifyViewTreeEvent(sessionId, /* started= */ true);
- ArrayList<Object> events = mAttachInfo.mContentCaptureEvents
- .valueAt(i);
- for_each_event: for (int j = 0; j < events.size(); j++) {
- Object event = events.get(j);
- if (event instanceof AutofillId) {
- mainSession.notifyViewDisappeared(sessionId, (AutofillId) event);
- } else if (event instanceof View) {
- View view = (View) event;
- ContentCaptureSession session = view.getContentCaptureSession();
- if (session == null) {
- Log.w(mTag, "no content capture session on view: " + view);
- continue for_each_event;
- }
- int actualId = session.getId();
- if (actualId != sessionId) {
- Log.w(mTag, "content capture session mismatch for view (" + view
- + "): was " + sessionId + " before, it's " + actualId + " now");
- continue for_each_event;
- }
- ViewStructure structure = session.newViewStructure(view);
- view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
- session.notifyViewAppeared(structure);
- } else if (event instanceof Insets) {
- mainSession.notifyViewInsetsChanged(sessionId, (Insets) event);
- } else {
- Log.w(mTag, "invalid content capture event: " + event);
- }
- }
- mainSession.notifyViewTreeEvent(sessionId, /* started= */ false);
+ if (!isContentCaptureEnabled()) {
+ if (DEBUG_CONTENT_CAPTURE) {
+ Log.d(mTag, "notifyContentCaptureEvents while disabled");
}
mAttachInfo.mContentCaptureEvents = null;
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ return;
}
+
+ final ContentCaptureManager manager = mAttachInfo.mContentCaptureManager;
+ if (manager != null && mAttachInfo.mContentCaptureEvents != null) {
+ final MainContentCaptureSession session = manager.getMainContentCaptureSession();
+ session.notifyContentCaptureEvents(mAttachInfo.mContentCaptureEvents);
+ }
+ mAttachInfo.mContentCaptureEvents = null;
}
private void notifyHolderSurfaceDestroyed() {
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 5a058ff..a829747 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -18,6 +18,7 @@
import static android.view.contentcapture.ContentCaptureHelper.sDebug;
import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
import static android.view.contentcapture.ContentCaptureHelper.toSet;
+import static android.view.contentcapture.flags.Flags.runOnBackgroundThreadEnabled;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
@@ -52,6 +53,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
import com.android.internal.util.RingBuffer;
import com.android.internal.util.SyncResultReceiver;
@@ -495,10 +497,9 @@
@GuardedBy("mLock")
private int mFlags;
- // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler
- // held at the Application level
- @NonNull
- private final Handler mHandler;
+ @Nullable
+ @GuardedBy("mLock")
+ private Handler mHandler;
@GuardedBy("mLock")
private MainContentCaptureSession mMainSession;
@@ -562,11 +563,6 @@
if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName());
- // TODO(b/119220549): we might not even need a handler, as the IPCs are oneway. But if we
- // do, then we should optimize it to run the tests after the Choreographer finishes the most
- // important steps of the frame.
- mHandler = Handler.createAsync(Looper.getMainLooper());
-
mDataShareAdapterResourceManager = new LocalDataShareAdapterResourceManager();
if (mOptions.contentProtectionOptions.enableReceiver
@@ -594,13 +590,27 @@
public MainContentCaptureSession getMainContentCaptureSession() {
synchronized (mLock) {
if (mMainSession == null) {
- mMainSession = new MainContentCaptureSession(mContext, this, mHandler, mService);
+ mMainSession = new MainContentCaptureSession(
+ mContext, this, prepareContentCaptureHandler(), mService);
if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession);
}
return mMainSession;
}
}
+ @NonNull
+ @GuardedBy("mLock")
+ private Handler prepareContentCaptureHandler() {
+ if (mHandler == null) {
+ if (runOnBackgroundThreadEnabled()) {
+ mHandler = BackgroundThread.getHandler();
+ } else {
+ mHandler = Handler.createAsync(Looper.getMainLooper());
+ }
+ }
+ return mHandler;
+ }
+
/** @hide */
@UiThread
public void onActivityCreated(@NonNull IBinder applicationToken,
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index d9b0f80..542c783c 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -34,7 +34,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.UiThread;
import android.content.ComponentName;
import android.content.pm.ParceledListSlice;
import android.graphics.Insets;
@@ -50,7 +49,10 @@
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Log;
+import android.util.SparseArray;
import android.util.TimeUtils;
+import android.view.View;
+import android.view.ViewStructure;
import android.view.autofill.AutofillId;
import android.view.contentcapture.ViewNode.ViewStructureImpl;
import android.view.contentprotection.ContentProtectionEventProcessor;
@@ -207,7 +209,8 @@
} else {
binder = null;
}
- mainSession.mHandler.post(() -> mainSession.onSessionStarted(resultCode, binder));
+ mainSession.mHandler.post(() ->
+ mainSession.onSessionStarted(resultCode, binder));
}
}
@@ -244,9 +247,14 @@
/**
* Starts this session.
*/
- @UiThread
void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
@NonNull ComponentName component, int flags) {
+ runOnContentCaptureThread(() -> startImpl(token, shareableActivityToken, component, flags));
+ }
+
+ private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
+ @NonNull ComponentName component, int flags) {
+ checkOnContentCaptureThread();
if (!isContentCaptureEnabled()) return;
if (sVerbose) {
@@ -280,17 +288,15 @@
Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
}
}
-
@Override
void onDestroy() {
- mHandler.removeMessages(MSG_FLUSH);
- mHandler.post(() -> {
+ clearAndRunOnContentCaptureThread(() -> {
try {
flush(FLUSH_REASON_SESSION_FINISHED);
} finally {
destroySession();
}
- });
+ }, MSG_FLUSH);
}
/**
@@ -302,8 +308,8 @@
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
- @UiThread
public void onSessionStarted(int resultCode, @Nullable IBinder binder) {
+ checkOnContentCaptureThread();
if (binder != null) {
mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
mDirectServiceVulture = () -> {
@@ -347,13 +353,12 @@
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
- @UiThread
public void sendEvent(@NonNull ContentCaptureEvent event) {
sendEvent(event, /* forceFlush= */ false);
}
- @UiThread
private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+ checkOnContentCaptureThread();
final int eventType = event.getType();
if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
@@ -396,15 +401,15 @@
}
}
- @UiThread
private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) {
+ checkOnContentCaptureThread();
if (mContentProtectionEventProcessor != null) {
mContentProtectionEventProcessor.processEvent(event);
}
}
- @UiThread
private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+ checkOnContentCaptureThread();
final int eventType = event.getType();
final int maxBufferSize = mManager.mOptions.maxBufferSize;
if (mEvents == null) {
@@ -538,13 +543,13 @@
flush(flushReason);
}
- @UiThread
private boolean hasStarted() {
+ checkOnContentCaptureThread();
return mState != UNKNOWN_STATE;
}
- @UiThread
private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
+ checkOnContentCaptureThread();
if (sVerbose) {
Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
+ ", checkExisting=" + checkExisting);
@@ -588,8 +593,8 @@
mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs);
}
- @UiThread
private void flushIfNeeded(@FlushReason int reason) {
+ checkOnContentCaptureThread();
if (mEvents == null || mEvents.isEmpty()) {
if (sVerbose) Log.v(TAG, "Nothing to flush");
return;
@@ -600,8 +605,12 @@
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@Override
- @UiThread
public void flush(@FlushReason int reason) {
+ runOnContentCaptureThread(() -> flushImpl(reason));
+ }
+
+ private void flushImpl(@FlushReason int reason) {
+ checkOnContentCaptureThread();
if (mEvents == null || mEvents.size() == 0) {
if (sVerbose) {
Log.v(TAG, "Don't flush for empty event buffer.");
@@ -669,8 +678,8 @@
* Resets the buffer and return a {@link ParceledListSlice} with the previous events.
*/
@NonNull
- @UiThread
private ParceledListSlice<ContentCaptureEvent> clearEvents() {
+ checkOnContentCaptureThread();
// NOTE: we must save a reference to the current mEvents and then set it to to null,
// otherwise clearing it would clear it in the receiving side if the service is also local.
if (mEvents == null) {
@@ -684,8 +693,8 @@
/** hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
- @UiThread
public void destroySession() {
+ checkOnContentCaptureThread();
if (sDebug) {
Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
+ (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
@@ -710,8 +719,8 @@
// clearings out.
/** @hide */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
- @UiThread
public void resetSession(int newState) {
+ checkOnContentCaptureThread();
if (sVerbose) {
Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
+ getStateAsString(mState) + " to " + getStateAsString(newState));
@@ -794,24 +803,26 @@
// change should also get get rid of the "internalNotifyXXXX" methods above
void notifyChildSessionStarted(int parentSessionId, int childSessionId,
@NonNull ContentCaptureContext clientContext) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
+ runOnContentCaptureThread(
+ () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
.setParentSessionId(parentSessionId).setClientContext(clientContext),
FORCE_FLUSH));
}
void notifyChildSessionFinished(int parentSessionId, int childSessionId) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
+ runOnContentCaptureThread(
+ () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
.setParentSessionId(parentSessionId), FORCE_FLUSH));
}
void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
+ runOnContentCaptureThread(() ->
+ sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
.setViewNode(node.mNode)));
}
- /** Public because is also used by ViewRootImpl */
- public void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
- mHandler.post(() -> sendEvent(
+ void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id)));
}
@@ -836,52 +847,102 @@
final int startIndex = Selection.getSelectionStart(text);
final int endIndex = Selection.getSelectionEnd(text);
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED)
.setAutofillId(id).setText(eventText)
.setComposingIndex(composingStart, composingEnd)
.setSelectionIndex(startIndex, endIndex)));
}
- /** Public because is also used by ViewRootImpl */
- public void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
+ void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
+ runOnContentCaptureThread(() ->
+ sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
.setInsets(viewInsets)));
}
- /** Public because is also used by ViewRootImpl */
- public void notifyViewTreeEvent(int sessionId, boolean started) {
+ void notifyViewTreeEvent(int sessionId, boolean started) {
final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED;
final boolean disableFlush = mManager.getFlushViewTreeAppearingEventDisabled();
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, type),
disableFlush ? !started : FORCE_FLUSH));
}
void notifySessionResumed(int sessionId) {
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_SESSION_RESUMED), FORCE_FLUSH));
}
void notifySessionPaused(int sessionId) {
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_SESSION_PAUSED), FORCE_FLUSH));
}
void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) {
- mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
+ runOnContentCaptureThread(() ->
+ sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
.setClientContext(context), FORCE_FLUSH));
}
/** public because is also used by ViewRootImpl */
public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) {
- mHandler.post(() -> sendEvent(
+ runOnContentCaptureThread(() -> sendEvent(
new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
.setBounds(bounds)
));
}
+ /** public because is also used by ViewRootImpl */
+ public void notifyContentCaptureEvents(
+ @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
+ runOnContentCaptureThread(() -> notifyContentCaptureEventsImpl(contentCaptureEvents));
+ }
+
+ private void notifyContentCaptureEventsImpl(
+ @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
+ checkOnContentCaptureThread();
+ try {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+ Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
+ }
+ for (int i = 0; i < contentCaptureEvents.size(); i++) {
+ int sessionId = contentCaptureEvents.keyAt(i);
+ notifyViewTreeEvent(sessionId, /* started= */ true);
+ ArrayList<Object> events = contentCaptureEvents.valueAt(i);
+ for_each_event: for (int j = 0; j < events.size(); j++) {
+ Object event = events.get(j);
+ if (event instanceof AutofillId) {
+ notifyViewDisappeared(sessionId, (AutofillId) event);
+ } else if (event instanceof View) {
+ View view = (View) event;
+ ContentCaptureSession session = view.getContentCaptureSession();
+ if (session == null) {
+ Log.w(TAG, "no content capture session on view: " + view);
+ continue for_each_event;
+ }
+ int actualId = session.getId();
+ if (actualId != sessionId) {
+ Log.w(TAG, "content capture session mismatch for view (" + view
+ + "): was " + sessionId + " before, it's " + actualId + " now");
+ continue for_each_event;
+ }
+ ViewStructure structure = session.newViewStructure(view);
+ view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
+ session.notifyViewAppeared(structure);
+ } else if (event instanceof Insets) {
+ notifyViewInsetsChanged(sessionId, (Insets) event);
+ } else {
+ Log.w(TAG, "invalid content capture event: " + event);
+ }
+ }
+ notifyViewTreeEvent(sessionId, /* started= */ false);
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+ }
+ }
+
@Override
void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
super.dump(prefix, pw);
@@ -960,17 +1021,14 @@
return getDebugState() + ", reason=" + getFlushReasonAsString(reason);
}
- @UiThread
private boolean isContentProtectionReceiverEnabled() {
return mManager.mOptions.contentProtectionOptions.enableReceiver;
}
- @UiThread
private boolean isContentCaptureReceiverEnabled() {
return mManager.mOptions.enableReceiver;
}
- @UiThread
private boolean isContentProtectionEnabled() {
// Should not be possible for mComponentName to be null here but check anyway
// Should not be possible for groups to be empty if receiver is enabled but check anyway
@@ -980,4 +1038,42 @@
&& (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty()
|| !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty());
}
+
+ /**
+ * Checks that the current work is running on the assigned thread from {@code mHandler}.
+ *
+ * <p>It is not guaranteed that the callers always invoke function from a single thread.
+ * Therefore, accessing internal properties in {@link MainContentCaptureSession} should
+ * always delegate to the assigned thread from {@code mHandler} for synchronization.</p>
+ */
+ private void checkOnContentCaptureThread() {
+ // TODO(b/309411951): Add metrics to track the issue instead.
+ final boolean onContentCaptureThread = mHandler.getLooper().isCurrentThread();
+ if (!onContentCaptureThread) {
+ Log.e(TAG, "MainContentCaptureSession running on " + Thread.currentThread());
+ }
+ }
+
+ /**
+ * Ensures that {@code r} will be running on the assigned thread.
+ *
+ * <p>This is to prevent unnecessary delegation to Handler that results in fragmented runnable.
+ * </p>
+ */
+ private void runOnContentCaptureThread(@NonNull Runnable r) {
+ if (!mHandler.getLooper().isCurrentThread()) {
+ mHandler.post(r);
+ } else {
+ r.run();
+ }
+ }
+
+ private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) {
+ if (!mHandler.getLooper().isCurrentThread()) {
+ mHandler.removeMessages(what);
+ mHandler.post(r);
+ } else {
+ r.run();
+ }
+ }
}
diff --git a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
index aaf90bd..858401a9 100644
--- a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
+++ b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
@@ -18,7 +18,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.UiThread;
import android.content.ContentCaptureOptions;
import android.content.pm.ParceledListSlice;
import android.os.Handler;
@@ -102,7 +101,6 @@
}
/** Main entry point for {@link ContentCaptureEvent} processing. */
- @UiThread
public void processEvent(@NonNull ContentCaptureEvent event) {
if (EVENT_TYPES_TO_STORE.contains(event.getType())) {
storeEvent(event);
@@ -112,7 +110,6 @@
}
}
- @UiThread
private void storeEvent(@NonNull ContentCaptureEvent event) {
// Ensure receiver gets the package name which might not be set
ViewNode viewNode = (event.getViewNode() != null) ? event.getViewNode() : new ViewNode();
@@ -121,7 +118,6 @@
mEventBuffer.append(event);
}
- @UiThread
private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) {
ViewNode viewNode = event.getViewNode();
String eventText = ContentProtectionUtils.getEventTextLower(event);
@@ -154,7 +150,6 @@
}
}
- @UiThread
private void loginDetected() {
if (mLastFlushTime == null
|| Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) {
@@ -163,13 +158,11 @@
resetLoginFlags();
}
- @UiThread
private void resetLoginFlags() {
mGroupsAll.forEach(group -> group.mFound = false);
mAnyGroupFound = false;
}
- @UiThread
private void maybeResetLoginFlags() {
if (mAnyGroupFound) {
if (mResetLoginRemainingEventsToProcess <= 0) {
@@ -183,7 +176,6 @@
}
}
- @UiThread
private void flush() {
mLastFlushTime = Instant.now();
@@ -192,7 +184,6 @@
mHandler.post(() -> handlerOnLoginDetected(events));
}
- @UiThread
@NonNull
private ParceledListSlice<ContentCaptureEvent> clearEvents() {
List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray());
diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java
index 1b9ff44..8e89541 100644
--- a/core/java/android/webkit/WebViewDelegate.java
+++ b/core/java/android/webkit/WebViewDelegate.java
@@ -16,6 +16,8 @@
package android.webkit;
+import static android.webkit.Flags.updateServiceV2;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
@@ -205,6 +207,9 @@
* Returns whether WebView should run in multiprocess mode.
*/
public boolean isMultiProcessEnabled() {
+ if (updateServiceV2()) {
+ return true;
+ }
try {
return WebViewFactory.getUpdateService().isMultiProcessEnabled();
} catch (RemoteException e) {
diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java
index bc7a5fd..e14ae72 100644
--- a/core/java/android/webkit/WebViewZygote.java
+++ b/core/java/android/webkit/WebViewZygote.java
@@ -16,6 +16,8 @@
package android.webkit;
+import static android.webkit.Flags.updateServiceV2;
+
import android.content.pm.PackageInfo;
import android.os.Build;
import android.os.ChildZygoteProcess;
@@ -50,8 +52,8 @@
private static PackageInfo sPackage;
/**
- * Flag for whether multi-process WebView is enabled. If this is {@code false}, the zygote
- * will not be started.
+ * Flag for whether multi-process WebView is enabled. If this is {@code false}, the zygote will
+ * not be started. Should be removed entirely after we remove the updateServiceV2 flag.
*/
@GuardedBy("sLock")
private static boolean sMultiprocessEnabled = false;
@@ -73,11 +75,19 @@
public static boolean isMultiprocessEnabled() {
synchronized (sLock) {
- return sMultiprocessEnabled && sPackage != null;
+ if (updateServiceV2()) {
+ return sPackage != null;
+ } else {
+ return sMultiprocessEnabled && sPackage != null;
+ }
}
}
public static void setMultiprocessEnabled(boolean enabled) {
+ if (updateServiceV2()) {
+ throw new IllegalStateException(
+ "setMultiprocessEnabled shouldn't be called if update_service_v2 flag is set.");
+ }
synchronized (sLock) {
sMultiprocessEnabled = enabled;
diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig
index 0da03fb..7f93213 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -35,8 +35,8 @@
flag {
namespace: "window_surfaces"
- name: "remove_capture_display"
- description: "Remove uses of ScreenCapture#captureDisplay"
+ name: "delete_capture_display"
+ description: "Delete uses of ScreenCapture#captureDisplay"
is_fixed_read_only: true
bug: "293445881"
}
diff --git a/core/java/com/android/internal/os/MonotonicClock.java b/core/java/com/android/internal/os/MonotonicClock.java
index 6f114e3..d0d2354 100644
--- a/core/java/com/android/internal/os/MonotonicClock.java
+++ b/core/java/com/android/internal/os/MonotonicClock.java
@@ -30,6 +30,7 @@
import java.io.ByteArrayInputStream;
import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -98,14 +99,11 @@
return;
}
- mFile.write(out -> {
- try {
- writeXml(out, Xml.newBinarySerializer());
- out.close();
- } catch (IOException e) {
- Log.e(TAG, "Cannot write monotonic clock to " + mFile.getBaseFile(), e);
- }
- });
+ try (FileOutputStream out = mFile.startWrite()) {
+ writeXml(out, Xml.newBinarySerializer());
+ } catch (IOException e) {
+ Log.e(TAG, "Cannot write monotonic clock to " + mFile.getBaseFile(), e);
+ }
}
/**
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index eed186a..73a7e42 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6350,4 +6350,20 @@
<string name="keyboard_layout_notification_multiple_selected_title">Physical keyboards configured</string>
<!-- Notification message when multiple keyboards with selected layouts have been connected the first time simultaneously [CHAR LIMIT=NOTIF_BODY] -->
<string name="keyboard_layout_notification_multiple_selected_message">Tap to view keyboards</string>
+
+ <!-- Private profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+ <string name="profile_label_private">Private</string>
+ <!-- Clone profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+ <string name="profile_label_clone">Clone</string>
+ <!-- Work profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+ <string name="profile_label_work">Work</string>
+ <!-- 2nd Work profile label on a screen in case a device has more than one work profiles. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+ <string name="profile_label_work_2">Work 2</string>
+ <!-- 3rd Work profile label on a screen in case a device has more than two work profiles. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+ <string name="profile_label_work_3">Work 3</string>
+ <!-- Test profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+ <string name="profile_label_test">Test</string>
+ <!-- Communal profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+ <string name="profile_label_communal">Communal</string>
+
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 24b39bc..38f1f67 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1086,6 +1086,13 @@
<java-symbol type="string" name="managed_profile_label_badge_3" />
<java-symbol type="string" name="clone_profile_label_badge" />
<java-symbol type="string" name="private_profile_label_badge" />
+ <java-symbol type="string" name="profile_label_private" />
+ <java-symbol type="string" name="profile_label_clone" />
+ <java-symbol type="string" name="profile_label_work" />
+ <java-symbol type="string" name="profile_label_work_2" />
+ <java-symbol type="string" name="profile_label_work_3" />
+ <java-symbol type="string" name="profile_label_test" />
+ <java-symbol type="string" name="profile_label_communal" />
<java-symbol type="string" name="mediasize_unknown_portrait" />
<java-symbol type="string" name="mediasize_unknown_landscape" />
<java-symbol type="string" name="mediasize_iso_a0" />
diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
index d47d789..1cdcb37 100644
--- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
@@ -17,11 +17,15 @@
package android.view.contentcapture;
import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
+import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARED;
+import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARING;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -29,14 +33,20 @@
import android.content.ContentCaptureOptions;
import android.content.Context;
import android.content.pm.ParceledListSlice;
+import android.graphics.Insets;
import android.os.Handler;
-import android.os.Looper;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.autofill.AutofillId;
import android.view.contentprotection.ContentProtectionEventProcessor;
import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -56,8 +66,9 @@
* <p>Run with: {@code atest
* FrameworksCoreTests:android.view.contentcapture.MainContentCaptureSessionTest}
*/
-@RunWith(AndroidJUnit4.class)
+@RunWith(AndroidTestingRunner.class)
@SmallTest
+@TestableLooper.RunWithLooper
public class MainContentCaptureSessionTest {
private static final int BUFFER_SIZE = 100;
@@ -75,6 +86,8 @@
private static final ContentCaptureManager.StrippedContext sStrippedContext =
new ContentCaptureManager.StrippedContext(sContext);
+ private TestableLooper mTestableLooper;
+
@Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock private IContentCaptureManager mMockSystemServerInterface;
@@ -83,12 +96,18 @@
@Mock private IContentCaptureDirectManager mMockContentCaptureDirectManager;
+ @Before
+ public void setup() {
+ mTestableLooper = TestableLooper.get(this);
+ }
+
@Test
public void onSessionStarted_contentProtectionEnabled_processorCreated() {
MainContentCaptureSession session = createSession();
assertThat(session.mContentProtectionEventProcessor).isNull();
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNotNull();
}
@@ -102,6 +121,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNull();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -122,6 +142,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNull();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -142,6 +163,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNull();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -153,6 +175,7 @@
session.mComponentName = null;
session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+ mTestableLooper.processAllMessages();
assertThat(session.mContentProtectionEventProcessor).isNull();
}
@@ -166,6 +189,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isNull();
@@ -180,6 +204,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
assertThat(session.mEvents).isNull();
@@ -194,6 +219,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isNotNull();
@@ -206,6 +232,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
assertThat(session.mEvents).isNotNull();
@@ -220,6 +247,7 @@
/* enableContentProtectionReceiver= */ true);
session.sendEvent(EVENT);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isNull();
@@ -236,6 +264,7 @@
session.mDirectServiceInterface = mMockContentCaptureDirectManager;
session.flush(REASON);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
verifyZeroInteractions(mMockContentCaptureDirectManager);
@@ -252,6 +281,7 @@
session.mDirectServiceInterface = mMockContentCaptureDirectManager;
session.flush(REASON);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
verifyZeroInteractions(mMockContentCaptureDirectManager);
@@ -269,6 +299,7 @@
session.mDirectServiceInterface = mMockContentCaptureDirectManager;
session.flush(REASON);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isEmpty();
@@ -286,6 +317,7 @@
session.mDirectServiceInterface = mMockContentCaptureDirectManager;
session.flush(REASON);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockContentProtectionEventProcessor);
assertThat(session.mEvents).isEmpty();
@@ -298,6 +330,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.destroySession();
+ mTestableLooper.processAllMessages();
verify(mMockSystemServerInterface).finishSession(anyInt());
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -311,6 +344,7 @@
session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
session.resetSession(/* newState= */ 0);
+ mTestableLooper.processAllMessages();
verifyZeroInteractions(mMockSystemServerInterface);
verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -318,6 +352,111 @@
assertThat(session.mContentProtectionEventProcessor).isNull();
}
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void notifyContentCaptureEvents_notStarted_ContentCaptureDisabled_ProtectionDisabled() {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ false,
+ /* enableContentProtectionReceiver= */ false);
+ MainContentCaptureSession session = createSession(options);
+
+ notifyContentCaptureEvents(session);
+ mTestableLooper.processAllMessages();
+
+ verifyZeroInteractions(mMockContentCaptureDirectManager);
+ verifyZeroInteractions(mMockContentProtectionEventProcessor);
+ assertThat(session.mEvents).isNull();
+ }
+
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void notifyContentCaptureEvents_started_ContentCaptureDisabled_ProtectionDisabled() {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ false,
+ /* enableContentProtectionReceiver= */ false);
+ MainContentCaptureSession session = createSession(options);
+
+ session.onSessionStarted(0x2, null);
+ notifyContentCaptureEvents(session);
+ mTestableLooper.processAllMessages();
+
+ verifyZeroInteractions(mMockContentCaptureDirectManager);
+ verifyZeroInteractions(mMockContentProtectionEventProcessor);
+ assertThat(session.mEvents).isNull();
+ }
+
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void notifyContentCaptureEvents_notStarted_ContentCaptureEnabled_ProtectionEnabled() {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ true,
+ /* enableContentProtectionReceiver= */ true);
+ MainContentCaptureSession session = createSession(options);
+ session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+ session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+ notifyContentCaptureEvents(session);
+ mTestableLooper.processAllMessages();
+
+ verifyZeroInteractions(mMockContentCaptureDirectManager);
+ verifyZeroInteractions(mMockContentProtectionEventProcessor);
+ assertThat(session.mEvents).isNull();
+ }
+
+ @Test
+ @SuppressWarnings("GuardedBy")
+ public void notifyContentCaptureEvents_started_ContentCaptureEnabled_ProtectionEnabled()
+ throws RemoteException {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ true,
+ /* enableContentProtectionReceiver= */ true);
+ MainContentCaptureSession session = createSession(options);
+ session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+ session.onSessionStarted(0x2, null);
+ // Override the processor for interaction verification.
+ session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+ notifyContentCaptureEvents(session);
+ mTestableLooper.processAllMessages();
+
+ // Force flush will happen twice.
+ verify(mMockContentCaptureDirectManager, times(1))
+ .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARING), any());
+ verify(mMockContentCaptureDirectManager, times(1))
+ .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARED), any());
+ // Other than the five view events, there will be two additional tree appearing events.
+ verify(mMockContentProtectionEventProcessor, times(7)).processEvent(any());
+ assertThat(session.mEvents).isEmpty();
+ }
+
+ /** Simulates the regular content capture events sequence. */
+ private void notifyContentCaptureEvents(final MainContentCaptureSession session) {
+ final ArrayList<Object> events = new ArrayList<>(
+ List.of(
+ prepareView(session),
+ prepareView(session),
+ new AutofillId(0),
+ prepareView(session),
+ Insets.of(0, 0, 0, 0)
+ )
+ );
+
+ final SparseArray<ArrayList<Object>> contentCaptureEvents = new SparseArray<>();
+ contentCaptureEvents.set(session.getId(), events);
+
+ session.notifyContentCaptureEvents(contentCaptureEvents);
+ }
+
+ private View prepareView(final MainContentCaptureSession session) {
+ final View view = new View(sContext);
+ view.setContentCaptureSession(session);
+ return view;
+ }
+
private static ContentCaptureOptions createOptions(
boolean enableContentCaptureReceiver,
ContentCaptureOptions.ContentProtectionOptions contentProtectionOptions) {
@@ -354,7 +493,7 @@
new MainContentCaptureSession(
sStrippedContext,
manager,
- new Handler(Looper.getMainLooper()),
+ Handler.createAsync(mTestableLooper.getLooper()),
mMockSystemServerInterface);
session.mComponentName = COMPONENT_NAME;
return session;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 4a9ea6f..144555d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -21,7 +21,6 @@
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
-import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
@@ -321,24 +320,10 @@
}
/** Move a task with given `taskId` to fullscreen */
- fun moveToFullscreen(taskId: Int) {
- shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) }
- }
-
- /** Move a task to fullscreen */
- fun moveToFullscreen(task: RunningTaskInfo) {
- KtProtoLog.v(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: moveToFullscreen taskId=%d",
- task.taskId
- )
-
- val wct = WindowContainerTransaction()
- addMoveToFullscreenChanges(wct, task)
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
- } else {
- shellTaskOrganizer.applyTransaction(wct)
+ fun moveToFullscreen(taskId: Int, windowDecor: DesktopModeWindowDecoration) {
+ shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
+ windowDecor.incrementRelayoutBlock()
+ moveToFullscreenWithAnimation(task, task.positionInParent)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index dd6ca8d..03006f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -428,7 +428,8 @@
if (isTaskInSplitScreen(mTaskId)) {
mSplitScreenController.moveTaskToFullscreen(mTaskId);
} else {
- mDesktopTasksController.ifPresent(c -> c.moveToFullscreen(mTaskId));
+ mDesktopTasksController.ifPresent(c ->
+ c.moveToFullscreen(mTaskId, mWindowDecorByTaskId.get(mTaskId)));
}
} else if (id == R.id.split_screen_button) {
decoration.closeHandleMenu();
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml
index fdda597..05f937a 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml
@@ -95,6 +95,8 @@
<option name="pull-pattern-keys" value="perfetto_file_path"/>
<option name="directory-keys"
value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/>
+ <option name="directory-keys"
+ value="/data/user/0/com.android.wm.shell.flicker.service/files"/>
<option name="collect-on-run-ended-only" value="true"/>
<option name="clean-up" value="true"/>
</metrics_collector>
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto
index b55f4ec..67316d2 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto
@@ -63,6 +63,7 @@
atrace_categories: "sched_process_exit"
atrace_apps: "com.android.server.wm.flicker.testapp"
atrace_apps: "com.android.systemui"
+ atrace_apps: "com.android.wm.shell.flicker.service"
atrace_apps: "com.android.wm.shell.flicker.splitscreen"
atrace_apps: "com.google.android.apps.nexuslauncher"
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index fde6acb..94c862b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -63,6 +63,7 @@
import com.android.wm.shell.transition.OneShotRemoteHandler
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
+import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_DESKTOP_MODE
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
import com.google.common.truth.Truth.assertThat
@@ -392,8 +393,8 @@
fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() {
val task = setUpFreeformTask()
task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
- controller.moveToFullscreen(task)
- val wct = getLatestWct(type = TRANSIT_CHANGE)
+ controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration)
+ val wct = getLatestExitDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_UNDEFINED)
}
@@ -402,15 +403,15 @@
fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() {
val task = setUpFreeformTask()
task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
- controller.moveToFullscreen(task)
- val wct = getLatestWct(type = TRANSIT_CHANGE)
+ controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration)
+ val wct = getLatestExitDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FULLSCREEN)
}
@Test
fun moveToFullscreen_nonExistentTask_doesNothing() {
- controller.moveToFullscreen(999)
+ controller.moveToFullscreen(999, desktopModeWindowDecoration)
verifyWCTNotExecuted()
}
@@ -419,9 +420,9 @@
val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY)
- controller.moveToFullscreen(taskDefaultDisplay)
+ controller.moveToFullscreen(taskDefaultDisplay.taskId, desktopModeWindowDecoration)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
+ with(getLatestExitDesktopWct()) {
assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder())
assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder())
}
@@ -808,6 +809,17 @@
return arg.value
}
+ private fun getLatestExitDesktopWct(): WindowContainerTransaction {
+ val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(exitDesktopTransitionHandler)
+ .startTransition(eq(TRANSIT_EXIT_DESKTOP_MODE), arg.capture(), any(), any())
+ } else {
+ verify(shellTaskOrganizer).applyTransaction(arg.capture())
+ }
+ return arg.value
+ }
+
private fun verifyWCTNotExecuted() {
if (ENABLE_SHELL_TRANSITIONS) {
verify(transitions, never()).startTransition(anyInt(), any(), isNull())
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index a9da832..8f5f1f6 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -49,6 +49,18 @@
{"exclude-annotation": "org.junit.Ignore"}
]
}
+ ],
+ "postsubmit": [
+ {
+ "file_patterns": [
+ "[^/]*(LoudnessCodec)[^/]*\\.java"
+ ],
+ "name": "LoudnessCodecApiTest",
+ "options": [
+ {
+ "include-annotation": "android.platform.test.annotations.Presubmit"
+ }
+ ]
+ }
]
}
-
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 9f63dfd..9ae6f8d 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -19,8 +19,6 @@
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
import static android.content.Context.DEVICE_ID_DEFAULT;
-import static android.media.audio.Flags.autoPublicVolumeApiHardening;
-import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
import android.Manifest;
@@ -2924,33 +2922,6 @@
}
//====================================================================
- // Loudness management
- private final Object mLoudnessCodecLock = new Object();
-
- @GuardedBy("mLoudnessCodecLock")
- private LoudnessCodecDispatcher mLoudnessCodecDispatcher = null;
-
- /**
- * Creates a new instance of {@link LoudnessCodecConfigurator}.
- * @return the {@link LoudnessCodecConfigurator} instance
- *
- * TODO: remove hide once API is final
- * @hide
- */
- @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public @NonNull LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
- LoudnessCodecConfigurator configurator;
- synchronized (mLoudnessCodecLock) {
- // initialize lazily
- if (mLoudnessCodecDispatcher == null) {
- mLoudnessCodecDispatcher = new LoudnessCodecDispatcher(this);
- }
- configurator = mLoudnessCodecDispatcher.createLoudnessCodecConfigurator();
- }
- return configurator;
- }
-
- //====================================================================
// Bluetooth SCO control
/**
* Sticky broadcast intent action indicating that the Bluetooth SCO audio
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 61b5fd5..367b38a 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2462,6 +2462,8 @@
public static final int PLATFORM_VOICE = 1;
/** @hide The platform is a television or a set-top box */
public static final int PLATFORM_TELEVISION = 2;
+ /** @hide The platform is automotive */
+ public static final int PLATFORM_AUTOMOTIVE = 3;
/**
* @hide
@@ -2478,6 +2480,9 @@
return PLATFORM_VOICE;
} else if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
return PLATFORM_TELEVISION;
+ } else if (context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_AUTOMOTIVE)) {
+ return PLATFORM_AUTOMOTIVE;
} else {
return PLATFORM_DEFAULT;
}
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index b4ca485..42400d1 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -52,7 +52,7 @@
import android.media.ISpatializerOutputCallback;
import android.media.IStreamAliasingDispatcher;
import android.media.IVolumeController;
-import android.media.LoudnessCodecFormat;
+import android.media.LoudnessCodecInfo;
import android.media.PlayerBase;
import android.media.VolumeInfo;
import android.media.VolumePolicy;
@@ -731,15 +731,13 @@
void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);
- oneway void startLoudnessCodecUpdates(in int piid);
+ oneway void startLoudnessCodecUpdates(int piid, in List<LoudnessCodecInfo> codecInfoSet);
- oneway void stopLoudnessCodecUpdates(in int piid);
+ oneway void stopLoudnessCodecUpdates(int piid);
- oneway void addLoudnesssCodecFormat(in int piid, in LoudnessCodecFormat format);
+ oneway void addLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo);
- oneway void addLoudnesssCodecFormatList(in int piid, in List<LoudnessCodecFormat> format);
+ oneway void removeLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo);
- oneway void removeLoudnessCodecFormat(in int piid, in LoudnessCodecFormat format);
-
- PersistableBundle getLoudnessParams(in int piid, in LoudnessCodecFormat format);
+ PersistableBundle getLoudnessParams(int piid, in LoudnessCodecInfo codecInfo);
}
diff --git a/media/java/android/media/LoudnessCodecConfigurator.java b/media/java/android/media/LoudnessCodecConfigurator.java
index 409abc2..92f3372 100644
--- a/media/java/android/media/LoudnessCodecConfigurator.java
+++ b/media/java/android/media/LoudnessCodecConfigurator.java
@@ -16,6 +16,9 @@
package android.media;
+import static android.media.AudioPlaybackConfiguration.PLAYER_PIID_INVALID;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D;
import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
import android.annotation.CallbackExecutor;
@@ -23,21 +26,27 @@
import android.os.Bundle;
import android.util.Log;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
-import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class for getting recommended loudness parameter updates for audio decoders, according to the
* encoded format and current audio routing. Those updates can be automatically applied to the
* {@link MediaCodec} instance(s), or be provided to the user. The codec loudness management
- * updates are defined by the CTA-2075 standard.
+ * parameter updates are defined by the CTA-2075 standard.
* <p>A new object should be instantiated for each {@link AudioTrack} with the help
- * of {@link AudioManager#createLoudnessCodecConfigurator()}.
+ * of {@link #create()} or {@link #create(Executor, OnLoudnessCodecUpdateListener)}.
*
* TODO: remove hide once API is final
* @hide
@@ -81,120 +90,255 @@
@NonNull private final LoudnessCodecDispatcher mLcDispatcher;
+ private final Object mConfiguratorLock = new Object();
+
+ @GuardedBy("mConfiguratorLock")
private AudioTrack mAudioTrack;
- private final List<MediaCodec> mMediaCodecs = new ArrayList<>();
+ @GuardedBy("mConfiguratorLock")
+ private final Executor mExecutor;
- /** @hide */
- protected LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher) {
- mLcDispatcher = Objects.requireNonNull(lcDispatcher);
- }
+ @GuardedBy("mConfiguratorLock")
+ private final OnLoudnessCodecUpdateListener mListener;
+ @GuardedBy("mConfiguratorLock")
+ private final HashMap<LoudnessCodecInfo, Set<MediaCodec>> mMediaCodecs = new HashMap<>();
/**
- * Starts receiving asynchronous loudness updates and registers the listener for
- * receiving {@link MediaCodec} loudness parameter updates.
- * <p>This method should be called before {@link #startLoudnessCodecUpdates()} or
- * after {@link #stopLoudnessCodecUpdates()}.
+ * Creates a new instance of {@link LoudnessCodecConfigurator}
+ *
+ * <p>This method should be used when the client does not need to alter the
+ * codec loudness parameters before they are applied to the audio decoders.
+ * Otherwise, use {@link #create(Executor, OnLoudnessCodecUpdateListener)}.
+ *
+ * @return the {@link LoudnessCodecConfigurator} instance
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public static @NonNull LoudnessCodecConfigurator create() {
+ return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
+ Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
+ }
+
+ /**
+ * Creates a new instance of {@link LoudnessCodecConfigurator}
+ *
+ * <p>This method should be used when the client wants to alter the codec
+ * loudness parameters before they are applied to the audio decoders.
+ * Otherwise, use {@link #create()}.
*
* @param executor {@link Executor} to handle the callbacks
- * @param listener used to receive updates
+ * @param listener used for receiving updates
*
- * @return {@code true} if there is at least one {@link MediaCodec} and
- * {@link AudioTrack} set and the user can expect receiving updates.
+ * @return the {@link LoudnessCodecConfigurator} instance
*
* TODO: remove hide once API is final
* @hide
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public boolean startLoudnessCodecUpdates(@NonNull @CallbackExecutor Executor executor,
- @NonNull OnLoudnessCodecUpdateListener listener) {
- Objects.requireNonNull(executor,
- "Executor must not be null");
- Objects.requireNonNull(listener,
- "OnLoudnessCodecUpdateListener must not be null");
- mLcDispatcher.addLoudnessCodecListener(this, executor, listener);
+ public static @NonNull LoudnessCodecConfigurator create(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ Objects.requireNonNull(executor, "Executor cannot be null");
+ Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
- return checkStartLoudnessConfigurator();
+ return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
+ executor, listener);
}
/**
- * Starts receiving asynchronous loudness updates.
- * <p>The registered MediaCodecs will be updated automatically without any client
- * callbacks.
+ * Creates a new instance of {@link LoudnessCodecConfigurator}
*
- * @return {@code true} if there is at least one MediaCodec and AudioTrack set
- * (see {@link #setAudioTrack(AudioTrack)}, {@link #addMediaCodec(MediaCodec)})
- * and the user can expect receiving updates.
+ * <p>This method should be used only in testing
*
- * TODO: remove hide once API is final
+ * @param service interface for communicating with AudioService
+ * @param executor {@link Executor} to handle the callbacks
+ * @param listener used for receiving updates
+ *
+ * @return the {@link LoudnessCodecConfigurator} instance
+ *
* @hide
*/
- @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public boolean startLoudnessCodecUpdates() {
- mLcDispatcher.addLoudnessCodecListener(this,
- Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
- return checkStartLoudnessConfigurator();
+ public static @NonNull LoudnessCodecConfigurator createForTesting(
+ @NonNull IAudioService service,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ Objects.requireNonNull(service, "IAudioService cannot be null");
+ Objects.requireNonNull(executor, "Executor cannot be null");
+ Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
+
+ return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(service),
+ executor, listener);
+ }
+
+ /** @hide */
+ private LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ mLcDispatcher = Objects.requireNonNull(lcDispatcher, "Dispatcher cannot be null");
+ mExecutor = Objects.requireNonNull(executor, "Executor cannot be null");
+ mListener = Objects.requireNonNull(listener,
+ "OnLoudnessCodecUpdateListener cannot be null");
}
/**
- * Stops receiving asynchronous loudness updates.
+ * Sets the {@link AudioTrack} and starts receiving asynchronous updates for
+ * the registered {@link MediaCodec}s (see {@link #addMediaCodec(MediaCodec)})
+ *
+ * <p>The AudioTrack should be the one that receives audio data from the
+ * added audio decoders and is used to determine the device routing on which
+ * the audio streaming will take place. This will directly influence the
+ * loudness parameters.
+ * <p>After calling this method the framework will compute the initial set of
+ * parameters which will be applied to the registered codecs/returned to the
+ * listener for modification.
+ *
+ * @param audioTrack the track that will receive audio data from the provided
+ * audio decoders. In case this is {@code null} this
+ * method will have the effect of clearing the existing set
+ * {@link AudioTrack} and will stop receiving asynchronous
+ * loudness updates
*
* TODO: remove hide once API is final
* @hide
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public void stopLoudnessCodecUpdates() {
- mLcDispatcher.removeLoudnessCodecListener(this);
+ public void setAudioTrack(AudioTrack audioTrack) {
+ List<LoudnessCodecInfo> codecInfos;
+ int piid = PLAYER_PIID_INVALID;
+ int oldPiid = PLAYER_PIID_INVALID;
+ synchronized (mConfiguratorLock) {
+ if (mAudioTrack != null && mAudioTrack == audioTrack) {
+ Log.v(TAG, "Loudness configurator already started for piid: "
+ + mAudioTrack.getPlayerIId());
+ return;
+ }
+
+ codecInfos = getLoudnessCodecInfoList_l();
+ if (mAudioTrack != null) {
+ oldPiid = mAudioTrack.getPlayerIId();
+ mLcDispatcher.removeLoudnessCodecListener(this);
+ }
+ if (audioTrack != null) {
+ piid = audioTrack.getPlayerIId();
+ mLcDispatcher.addLoudnessCodecListener(this, mExecutor, mListener);
+ }
+
+ mAudioTrack = audioTrack;
+ }
+
+ if (oldPiid != PLAYER_PIID_INVALID) {
+ Log.v(TAG, "Loudness configurator stopping updates for piid: " + oldPiid);
+ mLcDispatcher.stopLoudnessCodecUpdates(oldPiid);
+ }
+ if (piid != PLAYER_PIID_INVALID) {
+ Log.v(TAG, "Loudness configurator starting updates for piid: " + piid);
+ mLcDispatcher.startLoudnessCodecUpdates(piid, codecInfos);
+ }
}
/**
* Adds a new {@link MediaCodec} that will stream data to an {@link AudioTrack}
- * which is registered through {@link #setAudioTrack(AudioTrack)}.
+ * which the client sets
+ * (see {@link LoudnessCodecConfigurator#setAudioTrack(AudioTrack)}).
+ *
+ * <p>This method can be called while asynchronous updates are live.
+ *
+ * <p>No new element will be added if the passed {@code mediaCodec} was
+ * previously added.
+ *
+ * @param mediaCodec the codec to start receiving asynchronous loudness
+ * updates
*
* TODO: remove hide once API is final
* @hide
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
public void addMediaCodec(@NonNull MediaCodec mediaCodec) {
- mMediaCodecs.add(Objects.requireNonNull(mediaCodec,
- "MediaCodec for addMediaCodec must not be null"));
+ final MediaCodec mc = Objects.requireNonNull(mediaCodec,
+ "MediaCodec for addMediaCodec cannot be null");
+ int piid = PLAYER_PIID_INVALID;
+ final LoudnessCodecInfo mcInfo = getCodecInfo(mc);
+
+ if (mcInfo != null) {
+ synchronized (mConfiguratorLock) {
+ final AtomicBoolean containsCodec = new AtomicBoolean(false);
+ Set<MediaCodec> newSet = mMediaCodecs.computeIfPresent(mcInfo, (info, codecSet) -> {
+ containsCodec.set(!codecSet.add(mc));
+ return codecSet;
+ });
+ if (newSet == null) {
+ newSet = new HashSet<>();
+ newSet.add(mc);
+ mMediaCodecs.put(mcInfo, newSet);
+ }
+ if (containsCodec.get()) {
+ Log.v(TAG, "Loudness configurator already added media codec " + mediaCodec);
+ return;
+ }
+ if (mAudioTrack != null) {
+ piid = mAudioTrack.getPlayerIId();
+ }
+ }
+
+ if (piid != PLAYER_PIID_INVALID) {
+ mLcDispatcher.addLoudnessCodecInfo(piid, mcInfo);
+ }
+ }
}
/**
* Removes the {@link MediaCodec} from receiving loudness updates.
*
+ * <p>This method can be called while asynchronous updates are live.
+ *
+ * <p>No elements will be removed if the passed mediaCodec was not added before.
+ *
+ * @param mediaCodec the element to remove for receiving asynchronous updates
+ *
* TODO: remove hide once API is final
* @hide
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
public void removeMediaCodec(@NonNull MediaCodec mediaCodec) {
- mMediaCodecs.remove(Objects.requireNonNull(mediaCodec,
- "MediaCodec for removeMediaCodec must not be null"));
+ int piid = PLAYER_PIID_INVALID;
+ LoudnessCodecInfo mcInfo;
+ AtomicBoolean removed = new AtomicBoolean(false);
+
+ mcInfo = getCodecInfo(Objects.requireNonNull(mediaCodec,
+ "MediaCodec for removeMediaCodec cannot be null"));
+
+ if (mcInfo != null) {
+ synchronized (mConfiguratorLock) {
+ if (mAudioTrack != null) {
+ piid = mAudioTrack.getPlayerIId();
+ }
+ mMediaCodecs.computeIfPresent(mcInfo, (format, mcs) -> {
+ removed.set(mcs.remove(mediaCodec));
+ if (mcs.isEmpty()) {
+ // remove the entry
+ return null;
+ }
+ return mcs;
+ });
+ }
+
+ if (piid != PLAYER_PIID_INVALID && removed.get()) {
+ mLcDispatcher.removeLoudnessCodecInfo(piid, mcInfo);
+ }
+ }
}
/**
- * Sets the {@link AudioTrack} that can receive audio data from the added
- * {@link MediaCodec}'s. The {@link AudioTrack} is used to determine the devices
- * on which the streaming will take place and hence will directly influence the
- * loudness params.
- * <p>Should be called before starting the loudness updates
- * (see {@link #startLoudnessCodecUpdates()},
- * {@link #startLoudnessCodecUpdates(Executor, OnLoudnessCodecUpdateListener)})
+ * Gets synchronous loudness updates when no listener is required. The provided
+ * {@link MediaCodec} streams audio data to the passed {@link AudioTrack}.
*
- * TODO: remove hide once API is final
- * @hide
- */
- @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public void setAudioTrack(@NonNull AudioTrack audioTrack) {
- mAudioTrack = Objects.requireNonNull(audioTrack,
- "AudioTrack for setAudioTrack must not be null");
- }
-
- /**
- * Gets synchronous loudness updates when no listener is required and at least one
- * {@link MediaCodec} which streams to a registered {@link AudioTrack} is set.
- * Otherwise, an empty {@link Bundle} will be returned.
+ * @param audioTrack track that receives audio data from the passed
+ * {@link MediaCodec}
+ * @param mediaCodec codec that decodes loudness annotated data for the passed
+ * {@link AudioTrack}
*
* @return the {@link Bundle} containing the current loudness parameters. Caller is
* responsible to update the {@link MediaCodec}
@@ -204,22 +348,89 @@
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
@NonNull
- public Bundle getLoudnessCodecParams(@NonNull MediaCodec mediaCodec) {
- // TODO: implement synchronous loudness params updates
- return new Bundle();
+ public Bundle getLoudnessCodecParams(@NonNull AudioTrack audioTrack,
+ @NonNull MediaCodec mediaCodec) {
+ Objects.requireNonNull(audioTrack, "Passed audio track cannot be null");
+
+ LoudnessCodecInfo codecInfo = getCodecInfo(mediaCodec);
+ if (codecInfo == null) {
+ return new Bundle();
+ }
+
+ return mLcDispatcher.getLoudnessCodecParams(audioTrack.getPlayerIId(), codecInfo);
}
- private boolean checkStartLoudnessConfigurator() {
- if (mAudioTrack == null) {
- Log.w(TAG, "Cannot start loudness configurator without an AudioTrack");
- return false;
+ /** @hide */
+ /*package*/ int getAssignedTrackPiid() {
+ int piid = PLAYER_PIID_INVALID;
+
+ synchronized (mConfiguratorLock) {
+ if (mAudioTrack == null) {
+ return piid;
+ }
+ piid = mAudioTrack.getPlayerIId();
}
- if (mMediaCodecs.isEmpty()) {
- Log.w(TAG, "Cannot start loudness configurator without at least one MediaCodec");
- return false;
+ return piid;
+ }
+
+ /** @hide */
+ /*package*/ List<MediaCodec> getRegisteredMediaCodecList() {
+ synchronized (mConfiguratorLock) {
+ return mMediaCodecs.values().stream().flatMap(Collection::stream).toList();
+ }
+ }
+
+ @GuardedBy("mConfiguratorLock")
+ private List<LoudnessCodecInfo> getLoudnessCodecInfoList_l() {
+ return mMediaCodecs.values().stream().flatMap(listMc -> listMc.stream().map(
+ LoudnessCodecConfigurator::getCodecInfo)).toList();
+ }
+
+ @Nullable
+ private static LoudnessCodecInfo getCodecInfo(@NonNull MediaCodec mediaCodec) {
+ LoudnessCodecInfo lci = new LoudnessCodecInfo();
+ final MediaCodecInfo codecInfo = mediaCodec.getCodecInfo();
+ if (codecInfo.isEncoder()) {
+ // loudness info only for decoders
+ Log.w(TAG, "MediaCodec used for encoding does not support loudness annotation");
+ return null;
}
- return true;
+ final MediaFormat inputFormat = mediaCodec.getInputFormat();
+ final String mimeType = inputFormat.getString(MediaFormat.KEY_MIME);
+ if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mimeType)) {
+ // check both KEY_AAC_PROFILE and KEY_PROFILE as some codecs may only recognize one of
+ // these two keys
+ int aacProfile = -1;
+ int profile = -1;
+ try {
+ aacProfile = inputFormat.getInteger(MediaFormat.KEY_AAC_PROFILE);
+ } catch (NullPointerException e) {
+ // does not contain KEY_AAC_PROFILE. do nothing
+ }
+ try {
+ profile = inputFormat.getInteger(MediaFormat.KEY_PROFILE);
+ } catch (NullPointerException e) {
+ // does not contain KEY_PROFILE. do nothing
+ }
+ if (aacProfile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE
+ || profile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE) {
+ lci.metadataType = CODEC_METADATA_TYPE_MPEG_D;
+ } else {
+ lci.metadataType = CODEC_METADATA_TYPE_MPEG_4;
+ }
+ } else {
+ Log.w(TAG, "MediaCodec mime type not supported for loudness annotation");
+ return null;
+ }
+
+ final MediaFormat outputFormat = mediaCodec.getOutputFormat();
+ lci.isDownmixing = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
+ < inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+
+ lci.mediaCodecHashCode = mediaCodec.hashCode();
+
+ return lci;
}
}
diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java
index fc5c354..be881b1 100644
--- a/media/java/android/media/LoudnessCodecDispatcher.java
+++ b/media/java/android/media/LoudnessCodecDispatcher.java
@@ -16,94 +16,217 @@
package android.media;
+import static android.media.MediaFormat.KEY_AAC_DRC_EFFECT_TYPE;
+import static android.media.MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION;
+import static android.media.MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL;
+
import android.annotation.CallbackExecutor;
import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
+import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.RemoteException;
+import android.util.Log;
import androidx.annotation.NonNull;
import java.util.HashMap;
+import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* Class used to handle the loudness related communication with the audio service.
+ *
* @hide
*/
-public class LoudnessCodecDispatcher {
- private final class LoudnessCodecUpdatesDispatcherStub
- extends ILoudnessCodecUpdatesDispatcher.Stub
- implements CallbackUtil.DispatcherStub {
+public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub {
+ private static final String TAG = "LoudnessCodecDispatcher";
+
+ private static final boolean DEBUG = false;
+
+ private static final class LoudnessCodecUpdatesDispatcherStub
+ extends ILoudnessCodecUpdatesDispatcher.Stub {
+ private static LoudnessCodecUpdatesDispatcherStub sLoudnessCodecStub;
+
+ private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener>
+ mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>();
+
+ private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
+ mConfiguratorListener = new HashMap<>();
+
+ public static synchronized LoudnessCodecUpdatesDispatcherStub getInstance() {
+ if (sLoudnessCodecStub == null) {
+ sLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub();
+ }
+ return sLoudnessCodecStub;
+ }
+
+ private LoudnessCodecUpdatesDispatcherStub() {}
+
@Override
public void dispatchLoudnessCodecParameterChange(int piid, PersistableBundle params) {
mLoudnessListenerMgr.callListeners(listener ->
- mConfiguratorListener.computeIfPresent(listener, (l, c) -> {
- // TODO: send the bundle for the user to update
- return c;
+ mConfiguratorListener.computeIfPresent(listener, (l, lcConfig) -> {
+ // send the appropriate bundle for the user to update
+ if (lcConfig.getAssignedTrackPiid() == piid) {
+ final List<MediaCodec> mediaCodecs =
+ lcConfig.getRegisteredMediaCodecList();
+ for (MediaCodec mediaCodec : mediaCodecs) {
+ final String infoKey = Integer.toString(mediaCodec.hashCode());
+ if (params.containsKey(infoKey)) {
+ Bundle bundle = new Bundle(
+ params.getPersistableBundle(infoKey));
+ if (DEBUG) {
+ Log.d(TAG,
+ "Received for piid " + piid + " bundle: " + bundle);
+ }
+ bundle =
+ LoudnessCodecUpdatesDispatcherStub.filterLoudnessParams(
+ l.onLoudnessCodecUpdate(mediaCodec, bundle));
+ if (DEBUG) {
+ Log.d(TAG, "User changed for piid " + piid
+ + " to filtered bundle: " + bundle);
+ }
+
+ if (!bundle.isDefinitelyEmpty()) {
+ mediaCodec.setParameters(bundle);
+ }
+ }
+ }
+ }
+
+ return lcConfig;
}));
}
- @Override
- public void register(boolean register) {
- try {
- if (register) {
- mAm.getService().registerLoudnessCodecUpdatesDispatcher(this);
- } else {
- mAm.getService().unregisterLoudnessCodecUpdatesDispatcher(this);
+ private static Bundle filterLoudnessParams(Bundle bundle) {
+ Bundle filteredBundle = new Bundle();
+
+ if (bundle.containsKey(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL)) {
+ filteredBundle.putInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL,
+ bundle.getInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL));
+ }
+ if (bundle.containsKey(KEY_AAC_DRC_HEAVY_COMPRESSION)) {
+ filteredBundle.putInt(KEY_AAC_DRC_HEAVY_COMPRESSION,
+ bundle.getInt(KEY_AAC_DRC_HEAVY_COMPRESSION));
+ }
+ if (bundle.containsKey(KEY_AAC_DRC_EFFECT_TYPE)) {
+ filteredBundle.putInt(KEY_AAC_DRC_EFFECT_TYPE,
+ bundle.getInt(KEY_AAC_DRC_EFFECT_TYPE));
+ }
+
+ return filteredBundle;
+ }
+
+ void addLoudnessCodecListener(@NonNull CallbackUtil.DispatcherStub dispatcher,
+ @NonNull LoudnessCodecConfigurator configurator,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ Objects.requireNonNull(configurator);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+
+ mLoudnessListenerMgr.addListener(
+ executor, listener, "addLoudnessCodecListener",
+ () -> dispatcher);
+ mConfiguratorListener.put(listener, configurator);
+ }
+
+ void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
+ Objects.requireNonNull(configurator);
+
+ for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e :
+ mConfiguratorListener.entrySet()) {
+ if (e.getValue() == configurator) {
+ final OnLoudnessCodecUpdateListener listener = e.getKey();
+ mConfiguratorListener.remove(listener);
+ mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener");
+ break;
}
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
}
}
}
- private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener>
- mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>();
-
- @NonNull private final LoudnessCodecUpdatesDispatcherStub mLoudnessCodecStub;
-
- private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
- mConfiguratorListener = new HashMap<>();
-
- @NonNull private final AudioManager mAm;
-
- protected LoudnessCodecDispatcher(@NonNull AudioManager am) {
- mAm = Objects.requireNonNull(am);
- mLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub();
- }
+ @NonNull private final IAudioService mAudioService;
/** @hide */
- public LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
- return new LoudnessCodecConfigurator(this);
+ public LoudnessCodecDispatcher(@NonNull IAudioService audioService) {
+ mAudioService = Objects.requireNonNull(audioService);
+ }
+
+ @Override
+ public void register(boolean register) {
+ try {
+ if (register) {
+ mAudioService.registerLoudnessCodecUpdatesDispatcher(
+ LoudnessCodecUpdatesDispatcherStub.getInstance());
+ } else {
+ mAudioService.unregisterLoudnessCodecUpdatesDispatcher(
+ LoudnessCodecUpdatesDispatcherStub.getInstance());
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
}
/** @hide */
public void addLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator,
@NonNull @CallbackExecutor Executor executor,
@NonNull OnLoudnessCodecUpdateListener listener) {
- Objects.requireNonNull(configurator);
- Objects.requireNonNull(executor);
- Objects.requireNonNull(listener);
-
- mConfiguratorListener.put(listener, configurator);
- mLoudnessListenerMgr.addListener(
- executor, listener, "addLoudnessCodecListener", () -> mLoudnessCodecStub);
+ LoudnessCodecUpdatesDispatcherStub.getInstance().addLoudnessCodecListener(this,
+ configurator, executor, listener);
}
/** @hide */
public void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
- Objects.requireNonNull(configurator);
+ LoudnessCodecUpdatesDispatcherStub.getInstance().removeLoudnessCodecListener(configurator);
+ }
- for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e :
- mConfiguratorListener.entrySet()) {
- if (e.getValue() == configurator) {
- final OnLoudnessCodecUpdateListener listener = e.getKey();
- mConfiguratorListener.remove(listener);
- mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener");
- break;
- }
+ /** @hide */
+ public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+ try {
+ mAudioService.startLoudnessCodecUpdates(piid, codecInfoList);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
}
}
+
+ /** @hide */
+ public void stopLoudnessCodecUpdates(int piid) {
+ try {
+ mAudioService.stopLoudnessCodecUpdates(piid);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void addLoudnessCodecInfo(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+ try {
+ mAudioService.addLoudnessCodecInfo(piid, mcInfo);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void removeLoudnessCodecInfo(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+ try {
+ mAudioService.removeLoudnessCodecInfo(piid, mcInfo);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public Bundle getLoudnessCodecParams(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+ Bundle loudnessParams = null;
+ try {
+ loudnessParams = new Bundle(mAudioService.getLoudnessParams(piid, mcInfo));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return loudnessParams;
+ }
}
diff --git a/media/java/android/media/LoudnessCodecFormat.aidl b/media/java/android/media/LoudnessCodecFormat.aidl
deleted file mode 100644
index 75c9060..0000000
--- a/media/java/android/media/LoudnessCodecFormat.aidl
+++ /dev/null
@@ -1,30 +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 android.media;
-
-
-/**
- * Loudness format which specifies the input attributes used for measuring
- * the parameters required to perform loudness alignment as specified by the
- * CTA2075 standard.
- *
- * {@hide}
- */
-parcelable LoudnessCodecFormat {
- String metadataType;
- boolean isDownmixing;
-}
\ No newline at end of file
diff --git a/media/java/android/media/LoudnessCodecInfo.aidl b/media/java/android/media/LoudnessCodecInfo.aidl
new file mode 100644
index 0000000..fd69517
--- /dev/null
+++ b/media/java/android/media/LoudnessCodecInfo.aidl
@@ -0,0 +1,43 @@
+/*
+ * 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.media;
+
+/**
+ * Loudness information for a {@link MediaCodec} object which specifies the
+ * input attributes used for measuring the parameters required to perform
+ * loudness alignment as specified by the CTA2075 standard.
+ *
+ * {@hide}
+ */
+@JavaDerive(equals = true)
+parcelable LoudnessCodecInfo {
+ /** Supported codec metadata types for loudness updates. */
+ @Backing(type="int")
+ enum CodecMetadataType {
+ CODEC_METADATA_TYPE_INVALID = 0,
+ CODEC_METADATA_TYPE_MPEG_4 = 1,
+ CODEC_METADATA_TYPE_MPEG_D = 2,
+ CODEC_METADATA_TYPE_AC_3 = 3,
+ CODEC_METADATA_TYPE_AC_4 = 4,
+ CODEC_METADATA_TYPE_DTS_HD = 5,
+ CODEC_METADATA_TYPE_DTS_UHD = 6
+ }
+
+ int mediaCodecHashCode;
+ CodecMetadataType metadataType;
+ boolean isDownmixing;
+}
\ No newline at end of file
diff --git a/media/tests/LoudnessCodecApiTest/Android.bp b/media/tests/LoudnessCodecApiTest/Android.bp
new file mode 100644
index 0000000..5ca0fc9
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/Android.bp
@@ -0,0 +1,27 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "LoudnessCodecApiTest",
+ srcs: ["**/*.java"],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "junit",
+ "junit-params",
+ "mockito-target-minus-junit4",
+ "flag-junit",
+ "hamcrest-library",
+ "platform-test-annotations",
+ ],
+ platform_apis: true,
+ certificate: "platform",
+ resource_dirs: ["res"],
+ test_suites: ["device-tests"],
+}
diff --git a/media/tests/LoudnessCodecApiTest/AndroidManifest.xml b/media/tests/LoudnessCodecApiTest/AndroidManifest.xml
new file mode 100644
index 0000000..91a671f
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.loudnesscodecapitest">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.loudnesscodecapitest"
+ android:label="AudioManager loudness codec integration tests InstrumentationRunner">
+ </instrumentation>
+</manifest>
diff --git a/media/tests/LoudnessCodecApiTest/AndroidTest.xml b/media/tests/LoudnessCodecApiTest/AndroidTest.xml
new file mode 100644
index 0000000..0099d98
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Runs Media Framework Tests">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="LoudnessCodecApiTest.apk" />
+ </target_preparer>
+
+ <option name="test-tag" value="LoudnessCodecApiTest" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.loudnesscodecapitest" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml b/media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml
new file mode 100644
index 0000000..17fdba6
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical">
+</LinearLayout>
diff --git a/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a b/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a
new file mode 100644
index 0000000..acba4b3
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a
Binary files differ
diff --git a/media/tests/LoudnessCodecApiTest/res/values/strings.xml b/media/tests/LoudnessCodecApiTest/res/values/strings.xml
new file mode 100644
index 0000000..0c4227c
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- name of the app [CHAR LIMIT=25]-->
+ <string name="app_name">Loudness Codec API Tests</string>
+</resources>
diff --git a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java
new file mode 100644
index 0000000..65a9799
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java
@@ -0,0 +1,273 @@
+/*
+ * 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.loudnesscodecapitest;
+
+import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.AssetFileDescriptor;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+import android.media.IAudioService;
+import android.media.LoudnessCodecConfigurator;
+import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit tests for {@link LoudnessCodecConfigurator} checking the internal interactions with a mocked
+ * {@link IAudioService} without any real IPC interactions.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LoudnessCodecConfiguratorTest {
+ private static final String TAG = "LoudnessCodecConfiguratorTest";
+
+ private static final String TEST_MEDIA_AUDIO_CODEC_PREFIX = "audio/";
+ private static final int TEST_AUDIO_TRACK_BUFFER_SIZE = 2048;
+ private static final int TEST_AUDIO_TRACK_SAMPLERATE = 48000;
+ private static final int TEST_AUDIO_TRACK_CHANNELS = 2;
+
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Mock
+ private IAudioService mAudioService;
+
+ private LoudnessCodecConfigurator mLcc;
+
+ @Before
+ public void setUp() {
+ mLcc = LoudnessCodecConfigurator.createForTesting(mAudioService,
+ Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setAudioTrack_callsAudioServiceStart() throws Exception {
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
+ anyList());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void getLoudnessCodecParams_callsAudioServiceGetLoudness() throws Exception {
+ when(mAudioService.getLoudnessParams(anyInt(), any())).thenReturn(new PersistableBundle());
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.getLoudnessCodecParams(track, createAndConfigureMediaCodec());
+
+ verify(mAudioService).getLoudnessParams(eq(track.getPlayerIId()), any());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setAudioTrack_addsAudioServicePiidCodecs() throws Exception {
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setAudioTrackTwice_ignoresSecondCall() throws Exception {
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+ mLcc.setAudioTrack(track);
+
+ verify(mAudioService, times(1)).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
+ anyList());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setTrackNull_stopCodecUpdates() throws Exception {
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+
+ mLcc.setAudioTrack(null); // stops updates
+ verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId()));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void addMediaCodecTwice_ignoresSecondCall() throws Exception {
+ final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+
+ verify(mAudioService, times(1)).startLoudnessCodecUpdates(
+ eq(track.getPlayerIId()), argument.capture());
+ assertEquals(argument.getValue().size(), 1);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setClearTrack_removeAllAudioServicePiidCodecs() throws Exception {
+ final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
+
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
+ argument.capture());
+ assertEquals(argument.getValue().size(), 1);
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(null);
+ verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId()));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void removeAddedMediaCodecAfterSetTrack_callsAudioServiceRemoveCodec() throws Exception {
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+ mLcc.removeMediaCodec(mediaCodec);
+
+ verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void addMediaCodecAfterSetTrack_callsAudioServiceAdd() throws Exception {
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ verify(mAudioService).addLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void removeMediaCodecAfterSetTrack_callsAudioServiceRemove() throws Exception {
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+
+ mLcc.removeMediaCodec(mediaCodec);
+ verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void removeWrongMediaCodecAfterSetTrack_noAudioServiceRemoveCall() throws Exception {
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+
+ mLcc.removeMediaCodec(createAndConfigureMediaCodec());
+ verify(mAudioService, times(0)).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+ }
+
+ private static AudioTrack createAudioTrack() {
+ return new AudioTrack.Builder()
+ .setAudioAttributes(new AudioAttributes.Builder().build())
+ .setBufferSizeInBytes(TEST_AUDIO_TRACK_BUFFER_SIZE)
+ .setAudioFormat(new AudioFormat.Builder()
+ .setChannelMask(TEST_AUDIO_TRACK_CHANNELS)
+ .setSampleRate(TEST_AUDIO_TRACK_SAMPLERATE).build())
+ .build();
+ }
+
+ private MediaCodec createAndConfigureMediaCodec() throws Exception {
+ AssetFileDescriptor testFd = InstrumentationRegistry.getInstrumentation().getContext()
+ .getResources()
+ .openRawResourceFd(R.raw.noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4);
+
+ MediaExtractor extractor;
+ extractor = new MediaExtractor();
+ extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
+ testFd.getLength());
+ testFd.close();
+
+ assertEquals("wrong number of tracks", 1, extractor.getTrackCount());
+ MediaFormat format = extractor.getTrackFormat(0);
+ String mime = format.getString(MediaFormat.KEY_MIME);
+ assertTrue("not an audio file", mime.startsWith(TEST_MEDIA_AUDIO_CODEC_PREFIX));
+ final MediaCodec mediaCodec = MediaCodec.createDecoderByType(mime);
+
+ Log.v(TAG, "configuring with " + format);
+ mediaCodec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
+
+ return mediaCodec;
+ }
+}
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 3a2b1ce..ef218fd 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -156,6 +156,15 @@
</intent-filter>
</receiver>
+ <receiver android:name=".v2.model.UninstallEventReceiver"
+ android:permission="android.permission.INSTALL_PACKAGES"
+ android:exported="false"
+ android:enabled="false">
+ <intent-filter android:priority="1">
+ <action android:name="com.android.packageinstaller.ACTION_UNINSTALL_COMMIT" />
+ </intent-filter>
+ </receiver>
+
<receiver android:name=".PackageInstalledReceiver"
android:exported="false">
<intent-filter android:priority="1">
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
index e188838..fe05237 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
@@ -30,6 +30,8 @@
import android.net.Uri;
import android.os.Build;
import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.File;
@@ -404,6 +406,24 @@
}
/**
+ * Is a profile part of a user?
+ *
+ * @param userManager The user manager
+ * @param userHandle The handle of the user
+ * @param profileHandle The handle of the profile
+ *
+ * @return If the profile is part of the user or the profile parent of the user
+ */
+ public static boolean isProfileOfOrSame(UserManager userManager, UserHandle userHandle,
+ UserHandle profileHandle) {
+ if (userHandle.equals(profileHandle)) {
+ return true;
+ }
+ return userManager.getProfileParent(profileHandle) != null
+ && userManager.getProfileParent(profileHandle).equals(userHandle);
+ }
+
+ /**
* The class to hold an incoming package's icon and label.
* See {@link #getAppSnippet(Context, SessionInfo)},
* {@link #getAppSnippet(Context, PackageInfo)},
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallEventReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallEventReceiver.java
new file mode 100644
index 0000000..79e00df
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallEventReceiver.java
@@ -0,0 +1,85 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.model;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import androidx.annotation.NonNull;
+
+/**
+ * Receives uninstall events and persists them using a {@link EventResultPersister}.
+ */
+public class UninstallEventReceiver extends BroadcastReceiver {
+ private static final Object sLock = new Object();
+ private static EventResultPersister sReceiver;
+
+ /**
+ * Get the event receiver persisting the results
+ *
+ * @return The event receiver.
+ */
+ @NonNull private static EventResultPersister getReceiver(@NonNull Context context) {
+ synchronized (sLock) {
+ if (sReceiver == null) {
+ sReceiver = new EventResultPersister(
+ TemporaryFileManager.getUninstallStateFile(context));
+ }
+ }
+
+ return sReceiver;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ getReceiver(context).onEventReceived(context, intent);
+ }
+
+ /**
+ * Add an observer. If there is already an event for this id, call back inside of this call.
+ *
+ * @param context A context of the current app
+ * @param id The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one.
+ * @param observer The observer to call back.
+ *
+ * @return The id for this event
+ */
+ public static int addObserver(@NonNull Context context, int id,
+ @NonNull EventResultPersister.EventResultObserver observer)
+ throws EventResultPersister.OutOfIdsException {
+ return getReceiver(context).addObserver(id, observer);
+ }
+
+ /**
+ * Remove a observer.
+ *
+ * @param context A context of the current app
+ * @param id The id the observer was added for
+ */
+ static void removeObserver(@NonNull Context context, int id) {
+ getReceiver(context).removeObserver(id);
+ }
+
+ /**
+ * @param context A context of the current app
+ *
+ * @return A new uninstall id
+ */
+ static int getNewId(@NonNull Context context) throws EventResultPersister.OutOfIdsException {
+ return getReceiver(context).getNewId();
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
index 6533c50..2e43b75 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
@@ -16,14 +16,699 @@
package com.android.packageinstaller.v2.model;
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
+import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+import static com.android.packageinstaller.v2.model.PackageUtil.getMaxTargetSdkVersionForUid;
+import static com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid;
+import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted;
+import static com.android.packageinstaller.v2.model.PackageUtil.isProfileOfOrSame;
+import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_APP_UNAVAILABLE;
+import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_GENERIC_ERROR;
+import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
+import android.app.usage.StorageStats;
+import android.app.usage.StorageStatsManager;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.UninstallCompleteCallback;
+import android.content.pm.VersionedPackage;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.MutableLiveData;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallFailed;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallReady;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallSuccess;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
+import java.io.IOException;
+import java.util.List;
public class UninstallRepository {
private static final String TAG = UninstallRepository.class.getSimpleName();
+ private static final String UNINSTALL_FAILURE_CHANNEL = "uninstall_failure";
+ private static final String BROADCAST_ACTION =
+ "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT";
+
+ private static final String EXTRA_UNINSTALL_ID =
+ "com.android.packageinstaller.extra.UNINSTALL_ID";
+ private static final String EXTRA_APP_LABEL =
+ "com.android.packageinstaller.extra.APP_LABEL";
+ private static final String EXTRA_IS_CLONE_APP =
+ "com.android.packageinstaller.extra.IS_CLONE_APP";
+ private static final String EXTRA_PACKAGE_NAME =
+ "com.android.packageinstaller.extra.EXTRA_PACKAGE_NAME";
+
private final Context mContext;
+ private final AppOpsManager mAppOpsManager;
+ private final PackageManager mPackageManager;
+ private final UserManager mUserManager;
+ private final NotificationManager mNotificationManager;
+ private final MutableLiveData<UninstallStage> mUninstallResult = new MutableLiveData<>();
+ public UserHandle mUninstalledUser;
+ public UninstallCompleteCallback mCallback;
+ private ApplicationInfo mTargetAppInfo;
+ private ActivityInfo mTargetActivityInfo;
+ private Intent mIntent;
+ private CharSequence mTargetAppLabel;
+ private String mTargetPackageName;
+ private String mCallingActivity;
+ private boolean mUninstallFromAllUsers;
+ private boolean mIsClonedApp;
+ private int mUninstallId;
public UninstallRepository(Context context) {
mContext = context;
+ mAppOpsManager = context.getSystemService(AppOpsManager.class);
+ mPackageManager = context.getPackageManager();
+ mUserManager = context.getSystemService(UserManager.class);
+ mNotificationManager = context.getSystemService(NotificationManager.class);
+ }
+
+ public UninstallStage performPreUninstallChecks(Intent intent, CallerInfo callerInfo) {
+ mIntent = intent;
+
+ int callingUid = callerInfo.getUid();
+ mCallingActivity = callerInfo.getActivityName();
+
+ if (callingUid == Process.INVALID_UID) {
+ Log.e(TAG, "Could not determine the launching uid.");
+ return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
+ // TODO: should we give any indication to the user?
+ }
+
+ String callingPackage = getPackageNameForUid(mContext, callingUid, null);
+ if (callingPackage == null) {
+ Log.e(TAG, "Package not found for originating uid " + callingUid);
+ return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
+ } else {
+ if (mAppOpsManager.noteOpNoThrow(
+ AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage)
+ != MODE_ALLOWED) {
+ Log.e(TAG, "Install from uid " + callingUid + " disallowed by AppOps");
+ return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
+ }
+ }
+
+ if (getMaxTargetSdkVersionForUid(mContext, callingUid) >= Build.VERSION_CODES.P
+ && !isPermissionGranted(mContext, Manifest.permission.REQUEST_DELETE_PACKAGES,
+ callingUid)
+ && !isPermissionGranted(mContext, Manifest.permission.DELETE_PACKAGES, callingUid)) {
+ Log.e(TAG, "Uid " + callingUid + " does not have "
+ + Manifest.permission.REQUEST_DELETE_PACKAGES + " or "
+ + Manifest.permission.DELETE_PACKAGES);
+
+ return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
+ }
+
+ // Get intent information.
+ // We expect an intent with URI of the form package:<packageName>#<className>
+ // className is optional; if specified, it is the activity the user chose to uninstall
+ final Uri packageUri = intent.getData();
+ if (packageUri == null) {
+ Log.e(TAG, "No package URI in intent");
+ return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE);
+ }
+ mTargetPackageName = packageUri.getEncodedSchemeSpecificPart();
+ if (mTargetPackageName == null) {
+ Log.e(TAG, "Invalid package name in URI: " + packageUri);
+ return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE);
+ }
+
+ mUninstallFromAllUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS,
+ false);
+ if (mUninstallFromAllUsers && !mUserManager.isAdminUser()) {
+ Log.e(TAG, "Only admin user can request uninstall for all users");
+ return new UninstallAborted(ABORT_REASON_USER_NOT_ALLOWED);
+ }
+
+ mUninstalledUser = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class);
+ if (mUninstalledUser == null) {
+ mUninstalledUser = Process.myUserHandle();
+ } else {
+ List<UserHandle> profiles = mUserManager.getUserProfiles();
+ if (!profiles.contains(mUninstalledUser)) {
+ Log.e(TAG, "User " + Process.myUserHandle() + " can't request uninstall "
+ + "for user " + mUninstalledUser);
+ return new UninstallAborted(ABORT_REASON_USER_NOT_ALLOWED);
+ }
+ }
+
+ mCallback = intent.getParcelableExtra(PackageInstaller.EXTRA_CALLBACK,
+ PackageManager.UninstallCompleteCallback.class);
+
+ try {
+ mTargetAppInfo = mPackageManager.getApplicationInfo(mTargetPackageName,
+ PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_ANY_USER));
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Unable to get packageName");
+ }
+
+ if (mTargetAppInfo == null) {
+ Log.e(TAG, "Invalid packageName: " + mTargetPackageName);
+ return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE);
+ }
+
+ // The class name may have been specified (e.g. when deleting an app from all apps)
+ final String className = packageUri.getFragment();
+ if (className != null) {
+ try {
+ mTargetActivityInfo = mPackageManager.getActivityInfo(
+ new ComponentName(mTargetPackageName, className),
+ PackageManager.ComponentInfoFlags.of(0));
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Unable to get className");
+ // Continue as the ActivityInfo isn't critical.
+ }
+ }
+
+ return new UninstallReady();
+ }
+
+ public UninstallStage generateUninstallDetails() {
+ UninstallUserActionRequired.Builder uarBuilder = new UninstallUserActionRequired.Builder();
+ StringBuilder messageBuilder = new StringBuilder();
+
+ mTargetAppLabel = mTargetAppInfo.loadSafeLabel(mPackageManager);
+
+ // If the Activity label differs from the App label, then make sure the user
+ // knows the Activity belongs to the App being uninstalled.
+ if (mTargetActivityInfo != null) {
+ final CharSequence activityLabel = mTargetActivityInfo.loadSafeLabel(mPackageManager);
+ if (CharSequence.compare(activityLabel, mTargetAppLabel) != 0) {
+ messageBuilder.append(
+ mContext.getString(R.string.uninstall_activity_text, activityLabel));
+ messageBuilder.append(" ").append(mTargetAppLabel).append(".\n\n");
+ }
+ }
+
+ final boolean isUpdate =
+ (mTargetAppInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
+ final UserHandle myUserHandle = Process.myUserHandle();
+ boolean isSingleUser = isSingleUser();
+
+ if (isUpdate) {
+ messageBuilder.append(mContext.getString(
+ isSingleUser ? R.string.uninstall_update_text :
+ R.string.uninstall_update_text_multiuser));
+ } else if (mUninstallFromAllUsers && !isSingleUser) {
+ messageBuilder.append(mContext.getString(
+ R.string.uninstall_application_text_all_users));
+ } else if (!mUninstalledUser.equals(myUserHandle)) {
+ // Uninstalling user is issuing uninstall for another user
+ UserManager customUserManager = mContext.createContextAsUser(mUninstalledUser, 0)
+ .getSystemService(UserManager.class);
+ String userName = customUserManager.getUserName();
+
+ String uninstalledUserType = getUninstalledUserType(myUserHandle, mUninstalledUser);
+ String messageString;
+ if (USER_TYPE_PROFILE_MANAGED.equals(uninstalledUserType)) {
+ messageString = mContext.getString(
+ R.string.uninstall_application_text_current_user_work_profile, userName);
+ } else if (USER_TYPE_PROFILE_CLONE.equals(uninstalledUserType)) {
+ mIsClonedApp = true;
+ messageString = mContext.getString(
+ R.string.uninstall_application_text_current_user_clone_profile);
+ } else {
+ messageString = mContext.getString(
+ R.string.uninstall_application_text_user, userName);
+ }
+ messageBuilder.append(messageString);
+ } else if (isCloneProfile(mUninstalledUser)) {
+ mIsClonedApp = true;
+ messageBuilder.append(mContext.getString(
+ R.string.uninstall_application_text_current_user_clone_profile));
+ } else if (myUserHandle.equals(UserHandle.SYSTEM)
+ && hasClonedInstance(mTargetAppInfo.packageName)) {
+ messageBuilder.append(mContext.getString(
+ R.string.uninstall_application_text_with_clone_instance, mTargetAppLabel));
+ } else {
+ messageBuilder.append(mContext.getString(R.string.uninstall_application_text));
+ }
+
+ uarBuilder.setMessage(messageBuilder.toString());
+
+ if (mIsClonedApp) {
+ uarBuilder.setTitle(mContext.getString(R.string.cloned_app_label, mTargetAppLabel));
+ } else {
+ uarBuilder.setTitle(mTargetAppLabel.toString());
+ }
+
+ boolean suggestToKeepAppData = false;
+ try {
+ PackageInfo pkgInfo = mPackageManager.getPackageInfo(mTargetPackageName, 0);
+ suggestToKeepAppData =
+ pkgInfo.applicationInfo != null && pkgInfo.applicationInfo.hasFragileUserData();
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Cannot check hasFragileUserData for " + mTargetPackageName, e);
+ }
+
+ long appDataSize = 0;
+ if (suggestToKeepAppData) {
+ appDataSize = getAppDataSize(mTargetPackageName,
+ mUninstallFromAllUsers ? null : mUninstalledUser);
+ }
+ uarBuilder.setAppDataSize(appDataSize);
+
+ return uarBuilder.build();
+ }
+
+ /**
+ * Returns whether there is only one "full" user on this device.
+ *
+ * <p><b>Note:</b> on devices that use {@link android.os.UserManager#isHeadlessSystemUserMode()
+ * headless system user mode}, the system user is not "full", so it's not be considered in the
+ * calculation.</p>
+ */
+ private boolean isSingleUser() {
+ final int userCount = mUserManager.getUserCount();
+ return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2);
+ }
+
+ /**
+ * Returns the type of the user from where an app is being uninstalled. We are concerned with
+ * only USER_TYPE_PROFILE_MANAGED and USER_TYPE_PROFILE_CLONE and whether the user and profile
+ * belong to the same profile group.
+ */
+ @Nullable
+ private String getUninstalledUserType(UserHandle myUserHandle,
+ UserHandle uninstalledUserHandle) {
+ if (!mUserManager.isSameProfileGroup(myUserHandle, uninstalledUserHandle)) {
+ return null;
+ }
+
+ UserManager customUserManager = mContext.createContextAsUser(uninstalledUserHandle, 0)
+ .getSystemService(UserManager.class);
+ String[] userTypes = {USER_TYPE_PROFILE_MANAGED, USER_TYPE_PROFILE_CLONE};
+ for (String userType : userTypes) {
+ if (customUserManager.isUserOfType(userType)) {
+ return userType;
+ }
+ }
+ return null;
+ }
+
+ private boolean hasClonedInstance(String packageName) {
+ // Check if clone user is present on the device.
+ UserHandle cloneUser = null;
+ List<UserHandle> profiles = mUserManager.getUserProfiles();
+ for (UserHandle userHandle : profiles) {
+ if (!userHandle.equals(UserHandle.SYSTEM) && isCloneProfile(userHandle)) {
+ cloneUser = userHandle;
+ break;
+ }
+ }
+ // Check if another instance of given package exists in clone user profile.
+ try {
+ return cloneUser != null
+ && mPackageManager.getPackageUidAsUser(packageName,
+ PackageManager.PackageInfoFlags.of(0), cloneUser.getIdentifier()) > 0;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private boolean isCloneProfile(UserHandle userHandle) {
+ UserManager customUserManager = mContext.createContextAsUser(userHandle, 0)
+ .getSystemService(UserManager.class);
+ return customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE);
+ }
+
+ /**
+ * Get number of bytes of the app data of the package.
+ *
+ * @param pkg The package that might have app data.
+ * @param user The user the package belongs to or {@code null} if files of all users should
+ * be counted.
+ * @return The number of bytes.
+ */
+ private long getAppDataSize(@NonNull String pkg, @Nullable UserHandle user) {
+ if (user != null) {
+ return getAppDataSizeForUser(pkg, user);
+ }
+ // We are uninstalling from all users. Get cumulative app data size for all users.
+ List<UserHandle> userHandles = mUserManager.getUserHandles(true);
+ long totalAppDataSize = 0;
+ int numUsers = userHandles.size();
+ for (int i = 0; i < numUsers; i++) {
+ totalAppDataSize += getAppDataSizeForUser(pkg, userHandles.get(i));
+ }
+ return totalAppDataSize;
+ }
+
+ /**
+ * Get number of bytes of the app data of the package.
+ *
+ * @param pkg The package that might have app data.
+ * @param user The user the package belongs to
+ * @return The number of bytes.
+ */
+ private long getAppDataSizeForUser(@NonNull String pkg, @NonNull UserHandle user) {
+ StorageStatsManager storageStatsManager =
+ mContext.getSystemService(StorageStatsManager.class);
+ try {
+ StorageStats stats = storageStatsManager.queryStatsForPackage(
+ mPackageManager.getApplicationInfo(pkg, 0).storageUuid, pkg, user);
+ return stats.getDataBytes();
+ } catch (PackageManager.NameNotFoundException | IOException | SecurityException e) {
+ Log.e(TAG, "Cannot determine amount of app data for " + pkg, e);
+ }
+ return 0;
+ }
+
+ public void initiateUninstall(boolean keepData) {
+ // Get an uninstallId to track results and show a notification on non-TV devices.
+ try {
+ mUninstallId = UninstallEventReceiver.addObserver(mContext,
+ EventResultPersister.GENERATE_NEW_ID, this::handleUninstallResult);
+ } catch (EventResultPersister.OutOfIdsException e) {
+ Log.e(TAG, "Failed to start uninstall", e);
+ handleUninstallResult(PackageInstaller.STATUS_FAILURE,
+ PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0);
+ return;
+ }
+
+ // TODO: Check with UX whether to show UninstallUninstalling dialog / notification?
+ mUninstallResult.setValue(new UninstallUninstalling(mTargetAppLabel, mIsClonedApp));
+
+ Bundle uninstallData = new Bundle();
+ uninstallData.putInt(EXTRA_UNINSTALL_ID, mUninstallId);
+ uninstallData.putString(EXTRA_PACKAGE_NAME, mTargetPackageName);
+ uninstallData.putBoolean(Intent.EXTRA_UNINSTALL_ALL_USERS, mUninstallFromAllUsers);
+ uninstallData.putCharSequence(EXTRA_APP_LABEL, mTargetAppLabel);
+ uninstallData.putBoolean(EXTRA_IS_CLONE_APP, mIsClonedApp);
+ Log.i(TAG, "Uninstalling extras = " + uninstallData);
+
+ // Get a PendingIntent for result broadcast and issue an uninstall request
+ Intent broadcastIntent = new Intent(BROADCAST_ACTION);
+ broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mUninstallId);
+ broadcastIntent.setPackage(mContext.getPackageName());
+
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(mContext, mUninstallId, broadcastIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
+
+ if (!startUninstall(mTargetPackageName, mUninstalledUser, pendingIntent,
+ mUninstallFromAllUsers, keepData)) {
+ handleUninstallResult(PackageInstaller.STATUS_FAILURE,
+ PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0);
+ }
+ }
+
+ private void handleUninstallResult(int status, int legacyStatus, @Nullable String message,
+ int serviceId) {
+ if (mCallback != null) {
+ // The caller will be informed about the result via a callback
+ mCallback.onUninstallComplete(mTargetPackageName, legacyStatus, message);
+
+ // Since the caller already received the results, just finish the app at this point
+ mUninstallResult.setValue(null);
+ return;
+ }
+
+ boolean returnResult = mIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
+ if (returnResult || mCallingActivity != null) {
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus);
+
+ if (status == PackageInstaller.STATUS_SUCCESS) {
+ UninstallSuccess.Builder successBuilder = new UninstallSuccess.Builder()
+ .setResultIntent(intent)
+ .setActivityResultCode(Activity.RESULT_OK);
+ mUninstallResult.setValue(successBuilder.build());
+ } else {
+ UninstallFailed.Builder failedBuilder = new UninstallFailed.Builder(true)
+ .setResultIntent(intent)
+ .setActivityResultCode(Activity.RESULT_FIRST_USER);
+ mUninstallResult.setValue(failedBuilder.build());
+ }
+ return;
+ }
+
+ // Caller did not want the result back. So, we either show a Toast, or a Notification.
+ if (status == PackageInstaller.STATUS_SUCCESS) {
+ UninstallSuccess.Builder successBuilder = new UninstallSuccess.Builder()
+ .setActivityResultCode(legacyStatus)
+ .setMessage(mIsClonedApp
+ ? mContext.getString(R.string.uninstall_done_clone_app, mTargetAppLabel)
+ : mContext.getString(R.string.uninstall_done_app, mTargetAppLabel));
+ mUninstallResult.setValue(successBuilder.build());
+ } else {
+ UninstallFailed.Builder failedBuilder = new UninstallFailed.Builder(false);
+ Notification.Builder uninstallFailedNotification = null;
+
+ NotificationChannel uninstallFailureChannel = new NotificationChannel(
+ UNINSTALL_FAILURE_CHANNEL,
+ mContext.getString(R.string.uninstall_failure_notification_channel),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ mNotificationManager.createNotificationChannel(uninstallFailureChannel);
+
+ uninstallFailedNotification = new Notification.Builder(mContext,
+ UNINSTALL_FAILURE_CHANNEL);
+
+ UserHandle myUserHandle = Process.myUserHandle();
+ switch (legacyStatus) {
+ case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER -> {
+ // Find out if the package is an active admin for some non-current user.
+ UserHandle otherBlockingUserHandle =
+ findUserOfDeviceAdmin(myUserHandle, mTargetPackageName);
+
+ if (otherBlockingUserHandle == null) {
+ Log.d(TAG, "Uninstall failed because " + mTargetPackageName
+ + " is a device admin");
+
+ addDeviceManagerButton(mContext, uninstallFailedNotification);
+ setBigText(uninstallFailedNotification, mContext.getString(
+ R.string.uninstall_failed_device_policy_manager));
+ } else {
+ Log.d(TAG, "Uninstall failed because " + mTargetPackageName
+ + " is a device admin of user " + otherBlockingUserHandle);
+
+ String userName =
+ mContext.createContextAsUser(otherBlockingUserHandle, 0)
+ .getSystemService(UserManager.class).getUserName();
+ setBigText(uninstallFailedNotification, String.format(
+ mContext.getString(
+ R.string.uninstall_failed_device_policy_manager_of_user),
+ userName));
+ }
+ }
+ case PackageManager.DELETE_FAILED_OWNER_BLOCKED -> {
+ UserHandle otherBlockingUserHandle = findBlockingUser(mTargetPackageName);
+ boolean isProfileOfOrSame = isProfileOfOrSame(mUserManager, myUserHandle,
+ otherBlockingUserHandle);
+
+ if (isProfileOfOrSame) {
+ addDeviceManagerButton(mContext, uninstallFailedNotification);
+ } else {
+ addManageUsersButton(mContext, uninstallFailedNotification);
+ }
+
+ String bigText = null;
+ if (otherBlockingUserHandle == null) {
+ Log.d(TAG, "Uninstall failed for " + mTargetPackageName +
+ " with code " + status + " no blocking user");
+ } else if (otherBlockingUserHandle == UserHandle.SYSTEM) {
+ bigText = mContext.getString(
+ R.string.uninstall_blocked_device_owner);
+ } else {
+ bigText = mContext.getString(mUninstallFromAllUsers ?
+ R.string.uninstall_all_blocked_profile_owner
+ : R.string.uninstall_blocked_profile_owner);
+ }
+ if (bigText != null) {
+ setBigText(uninstallFailedNotification, bigText);
+ }
+ }
+ default -> {
+ Log.d(TAG, "Uninstall blocked for " + mTargetPackageName
+ + " with legacy code " + legacyStatus);
+ }
+ }
+
+ uninstallFailedNotification.setContentTitle(
+ mContext.getString(R.string.uninstall_failed_app, mTargetAppLabel));
+ uninstallFailedNotification.setOngoing(false);
+ uninstallFailedNotification.setSmallIcon(R.drawable.ic_error);
+ failedBuilder.setUninstallNotification(mUninstallId,
+ uninstallFailedNotification.build());
+
+ mUninstallResult.setValue(failedBuilder.build());
+ }
+ }
+
+ /**
+ * @param myUserHandle {@link UserHandle} of the current user.
+ * @param packageName Name of the package being uninstalled.
+ * @return the {@link UserHandle} of the user in which a package is a device admin.
+ */
+ @Nullable
+ private UserHandle findUserOfDeviceAdmin(UserHandle myUserHandle, String packageName) {
+ for (UserHandle otherUserHandle : mUserManager.getUserHandles(true)) {
+ // We only catch the case when the user in question is neither the
+ // current user nor its profile.
+ if (isProfileOfOrSame(mUserManager, myUserHandle, otherUserHandle)) {
+ continue;
+ }
+ DevicePolicyManager dpm = mContext.createContextAsUser(otherUserHandle, 0)
+ .getSystemService(DevicePolicyManager.class);
+ if (dpm.packageHasActiveAdmins(packageName)) {
+ return otherUserHandle;
+ }
+ }
+ return null;
+ }
+
+ /**
+ *
+ * @param packageName Name of the package being uninstalled.
+ * @return {@link UserHandle} of the user in which a package is blocked from being uninstalled.
+ */
+ @Nullable
+ private UserHandle findBlockingUser(String packageName) {
+ for (UserHandle otherUserHandle : mUserManager.getUserHandles(true)) {
+ // TODO (b/307399586): Add a negation when the logic of the method
+ // is fixed
+ if (mPackageManager.canUserUninstall(packageName, otherUserHandle)) {
+ return otherUserHandle;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Set big text for the notification.
+ *
+ * @param builder The builder of the notification
+ * @param text The text to set.
+ */
+ private void setBigText(@NonNull Notification.Builder builder,
+ @NonNull CharSequence text) {
+ builder.setStyle(new Notification.BigTextStyle().bigText(text));
+ }
+
+ /**
+ * Add a button to the notification that links to the user management.
+ *
+ * @param context The context the notification is created in
+ * @param builder The builder of the notification
+ */
+ private void addManageUsersButton(@NonNull Context context,
+ @NonNull Notification.Builder builder) {
+ builder.addAction((new Notification.Action.Builder(
+ Icon.createWithResource(context, R.drawable.ic_settings_multiuser),
+ context.getString(R.string.manage_users),
+ PendingIntent.getActivity(context, 0, getUserSettingsIntent(),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build());
+ }
+
+ private Intent getUserSettingsIntent() {
+ Intent intent = new Intent(Settings.ACTION_USER_SETTINGS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK);
+ return intent;
+ }
+
+ /**
+ * Add a button to the notification that links to the device policy management.
+ *
+ * @param context The context the notification is created in
+ * @param builder The builder of the notification
+ */
+ private void addDeviceManagerButton(@NonNull Context context,
+ @NonNull Notification.Builder builder) {
+ builder.addAction((new Notification.Action.Builder(
+ Icon.createWithResource(context, R.drawable.ic_lock),
+ context.getString(R.string.manage_device_administrators),
+ PendingIntent.getActivity(context, 0, getDeviceManagerIntent(),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build());
+ }
+
+ private Intent getDeviceManagerIntent() {
+ Intent intent = new Intent();
+ intent.setClassName("com.android.settings",
+ "com.android.settings.Settings$DeviceAdminSettingsActivity");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK);
+ return intent;
+ }
+
+ /**
+ * Starts an uninstall for the given package.
+ *
+ * @return {@code true} if there was no exception while uninstalling. This does not represent
+ * the result of the uninstall. Result will be made available in
+ * {@link #handleUninstallResult(int, int, String, int)}
+ */
+ private boolean startUninstall(String packageName, UserHandle targetUser,
+ PendingIntent pendingIntent, boolean uninstallFromAllUsers, boolean keepData) {
+ int flags = uninstallFromAllUsers ? PackageManager.DELETE_ALL_USERS : 0;
+ flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
+ try {
+ mContext.createContextAsUser(targetUser, 0)
+ .getPackageManager().getPackageInstaller().uninstall(
+ new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
+ flags, pendingIntent.getIntentSender());
+ return true;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to uninstall", e);
+ return false;
+ }
+ }
+
+ public void cancelInstall() {
+ if (mCallback != null) {
+ mCallback.onUninstallComplete(mTargetPackageName,
+ PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user");
+ }
+ }
+
+ public MutableLiveData<UninstallStage> getUninstallResult() {
+ return mUninstallResult;
+ }
+
+ public static class CallerInfo {
+
+ private final String mActivityName;
+ private final int mUid;
+
+ public CallerInfo(String activityName, int uid) {
+ mActivityName = activityName;
+ mUid = uid;
+ }
+
+ public String getActivityName() {
+ return mActivityName;
+ }
+
+ public int getUid() {
+ return mUid;
+ }
}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java
new file mode 100644
index 0000000..9aea6b1
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java
@@ -0,0 +1,71 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.model.uninstallstagedata;
+
+import android.app.Activity;
+import com.android.packageinstaller.R;
+
+public class UninstallAborted extends UninstallStage {
+
+ public static final int ABORT_REASON_GENERIC_ERROR = 0;
+ public static final int ABORT_REASON_APP_UNAVAILABLE = 1;
+ public static final int ABORT_REASON_USER_NOT_ALLOWED = 2;
+ private final int mStage = UninstallStage.STAGE_ABORTED;
+ private final int mAbortReason;
+ private final int mDialogTitleResource;
+ private final int mDialogTextResource;
+ private final int mActivityResultCode = Activity.RESULT_FIRST_USER;
+
+ public UninstallAborted(int abortReason) {
+ mAbortReason = abortReason;
+ switch (abortReason) {
+ case ABORT_REASON_APP_UNAVAILABLE -> {
+ mDialogTitleResource = R.string.app_not_found_dlg_title;
+ mDialogTextResource = R.string.app_not_found_dlg_text;
+ }
+ case ABORT_REASON_USER_NOT_ALLOWED -> {
+ mDialogTitleResource = 0;
+ mDialogTextResource = R.string.user_is_not_allowed_dlg_text;
+ }
+ default -> {
+ mDialogTitleResource = 0;
+ mDialogTextResource = R.string.generic_error_dlg_text;
+ }
+ }
+ }
+
+ public int getAbortReason() {
+ return mAbortReason;
+ }
+
+ public int getActivityResultCode() {
+ return mActivityResultCode;
+ }
+
+ public int getDialogTitleResource() {
+ return mDialogTitleResource;
+ }
+
+ public int getDialogTextResource() {
+ return mDialogTextResource;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java
new file mode 100644
index 0000000..6ed8883
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java
@@ -0,0 +1,119 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.model.uninstallstagedata;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.content.Intent;
+
+public class UninstallFailed extends UninstallStage {
+
+ private final int mStage = UninstallStage.STAGE_FAILED;
+ private final boolean mReturnResult;
+ /**
+ * If the caller wants the result back, the intent will hold the uninstall failure status code
+ * and legacy code.
+ */
+ private final Intent mResultIntent;
+ /**
+ * When the user does not request a result back, this notification will be shown indicating the
+ * reason for uninstall failure.
+ */
+ private final Notification mUninstallNotification;
+ /**
+ * ID used to show {@link #mUninstallNotification}
+ */
+ private final int mUninstallId;
+ private final int mActivityResultCode;
+
+ public UninstallFailed(boolean returnResult, Intent resultIntent, int activityResultCode,
+ int uninstallId, Notification uninstallNotification) {
+ mReturnResult = returnResult;
+ mResultIntent = resultIntent;
+ mActivityResultCode = activityResultCode;
+ mUninstallId = uninstallId;
+ mUninstallNotification = uninstallNotification;
+ }
+
+ public boolean returnResult() {
+ return mReturnResult;
+ }
+
+ public Intent getResultIntent() {
+ return mResultIntent;
+ }
+
+ public int getActivityResultCode() {
+ return mActivityResultCode;
+ }
+
+ public Notification getUninstallNotification() {
+ return mUninstallNotification;
+ }
+
+ public int getUninstallId() {
+ return mUninstallId;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+
+ public static class Builder {
+
+ private final boolean mReturnResult;
+ private int mActivityResultCode = Activity.RESULT_CANCELED;
+ /**
+ * See {@link UninstallFailed#mResultIntent}
+ */
+ private Intent mResultIntent = null;
+ /**
+ * See {@link UninstallFailed#mUninstallNotification}
+ */
+ private Notification mUninstallNotification;
+ /**
+ * See {@link UninstallFailed#mUninstallId}
+ */
+ private int mUninstallId;
+
+ public Builder(boolean returnResult) {
+ mReturnResult = returnResult;
+ }
+
+ public Builder setUninstallNotification(int uninstallId, Notification notification) {
+ mUninstallId = uninstallId;
+ mUninstallNotification = notification;
+ return this;
+ }
+
+ public Builder setResultIntent(Intent intent) {
+ mResultIntent = intent;
+ return this;
+ }
+
+ public Builder setActivityResultCode(int resultCode) {
+ mActivityResultCode = resultCode;
+ return this;
+ }
+
+ public UninstallFailed build() {
+ return new UninstallFailed(mReturnResult, mResultIntent, mActivityResultCode,
+ mUninstallId, mUninstallNotification);
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java
similarity index 64%
copy from packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
copy to packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java
index fd6a5dd..0108cb4 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java
@@ -5,7 +5,7 @@
* 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
+ * https://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,
@@ -14,7 +14,14 @@
* limitations under the License.
*/
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.preference;
+package com.android.packageinstaller.v2.model.uninstallstagedata;
-import org.robolectric.annotation.GraphicsMode;
+public class UninstallReady extends UninstallStage {
+
+ private final int mStage = UninstallStage.STAGE_READY;
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java
new file mode 100644
index 0000000..5df6b02
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java
@@ -0,0 +1,79 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.model.uninstallstagedata;
+
+import android.content.Intent;
+
+public class UninstallSuccess extends UninstallStage {
+
+ private final int mStage = UninstallStage.STAGE_SUCCESS;
+ private final String mMessage;
+ private final Intent mResultIntent;
+ private final int mActivityResultCode;
+
+ public UninstallSuccess(Intent resultIntent, int activityResultCode, String message) {
+ mResultIntent = resultIntent;
+ mActivityResultCode = activityResultCode;
+ mMessage = message;
+ }
+
+ public String getMessage() {
+ return mMessage;
+ }
+
+ public Intent getResultIntent() {
+ return mResultIntent;
+ }
+
+ public int getActivityResultCode() {
+ return mActivityResultCode;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+
+ public static class Builder {
+
+ private Intent mResultIntent;
+ private int mActivityResultCode;
+ private String mMessage;
+
+ public Builder() {
+ }
+
+ public Builder setResultIntent(Intent intent) {
+ mResultIntent = intent;
+ return this;
+ }
+
+ public Builder setActivityResultCode(int resultCode) {
+ mActivityResultCode = resultCode;
+ return this;
+ }
+
+ public Builder setMessage(String message) {
+ mMessage = message;
+ return this;
+ }
+
+ public UninstallSuccess build() {
+ return new UninstallSuccess(mResultIntent, mActivityResultCode, mMessage);
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java
new file mode 100644
index 0000000..f5156cb
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java
@@ -0,0 +1,43 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.model.uninstallstagedata;
+
+public class UninstallUninstalling extends UninstallStage {
+
+ private final int mStage = UninstallStage.STAGE_UNINSTALLING;
+
+ private final CharSequence mAppLabel;
+ private final boolean mIsCloneUser;
+
+ public UninstallUninstalling(CharSequence appLabel, boolean isCloneUser) {
+ mAppLabel = appLabel;
+ mIsCloneUser = isCloneUser;
+ }
+
+ public CharSequence getAppLabel() {
+ return mAppLabel;
+ }
+
+ public boolean isCloneUser() {
+ return mIsCloneUser;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java
new file mode 100644
index 0000000..b600149
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java
@@ -0,0 +1,74 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.model.uninstallstagedata;
+
+public class UninstallUserActionRequired extends UninstallStage {
+
+ private final int mStage = UninstallStage.STAGE_USER_ACTION_REQUIRED;
+ private final String mTitle;
+ private final String mMessage;
+ private final long mAppDataSize;
+
+ public UninstallUserActionRequired(String title, String message, long appDataSize) {
+ mTitle = title;
+ mMessage = message;
+ mAppDataSize = appDataSize;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public String getMessage() {
+ return mMessage;
+ }
+
+ public long getAppDataSize() {
+ return mAppDataSize;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+
+ public static class Builder {
+
+ private String mTitle;
+ private String mMessage;
+ private long mAppDataSize = 0;
+
+ public Builder setTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ public Builder setMessage(String message) {
+ mMessage = message;
+ return this;
+ }
+
+ public Builder setAppDataSize(long appDataSize) {
+ mAppDataSize = appDataSize;
+ return this;
+ }
+
+ public UninstallUserActionRequired build() {
+ return new UninstallUserActionRequired(mTitle, mMessage, mAppDataSize);
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java
similarity index 71%
rename from packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
rename to packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java
index fd6a5dd..b8a9355 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java
@@ -5,7 +5,7 @@
* 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
+ * https://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,
@@ -14,7 +14,11 @@
* limitations under the License.
*/
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.preference;
+package com.android.packageinstaller.v2.ui;
-import org.robolectric.annotation.GraphicsMode;
+public interface UninstallActionListener {
+
+ void onPositiveResponse(boolean keepData);
+
+ void onNegativeResponse();
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
index 5d47da1..7638e91 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
@@ -16,26 +16,47 @@
package com.android.packageinstaller.v2.ui;
+import static android.os.Process.INVALID_UID;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.content.Intent;
import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import com.android.packageinstaller.v2.model.UninstallRepository;
+import com.android.packageinstaller.v2.model.UninstallRepository.CallerInfo;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallFailed;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallSuccess;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
+import com.android.packageinstaller.v2.ui.fragments.UninstallConfirmationFragment;
+import com.android.packageinstaller.v2.ui.fragments.UninstallErrorFragment;
+import com.android.packageinstaller.v2.ui.fragments.UninstallUninstallingFragment;
import com.android.packageinstaller.v2.viewmodel.UninstallViewModel;
import com.android.packageinstaller.v2.viewmodel.UninstallViewModelFactory;
-public class UninstallLaunch extends FragmentActivity{
+public class UninstallLaunch extends FragmentActivity implements UninstallActionListener {
public static final String EXTRA_CALLING_PKG_UID =
UninstallLaunch.class.getPackageName() + ".callingPkgUid";
public static final String EXTRA_CALLING_ACTIVITY_NAME =
UninstallLaunch.class.getPackageName() + ".callingActivityName";
public static final String TAG = UninstallLaunch.class.getSimpleName();
+ private static final String TAG_DIALOG = "dialog";
private UninstallViewModel mUninstallViewModel;
private UninstallRepository mUninstallRepository;
+ private FragmentManager mFragmentManager;
+ private NotificationManager mNotificationManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -45,9 +66,102 @@
// be stale, if e.g. the app was uninstalled while the activity was destroyed.
super.onCreate(null);
+ mFragmentManager = getSupportFragmentManager();
+ mNotificationManager = getSystemService(NotificationManager.class);
+
mUninstallRepository = new UninstallRepository(getApplicationContext());
mUninstallViewModel = new ViewModelProvider(this,
new UninstallViewModelFactory(this.getApplication(), mUninstallRepository)).get(
UninstallViewModel.class);
+
+ Intent intent = getIntent();
+ CallerInfo callerInfo = new CallerInfo(
+ intent.getStringExtra(EXTRA_CALLING_ACTIVITY_NAME),
+ intent.getIntExtra(EXTRA_CALLING_PKG_UID, INVALID_UID));
+ mUninstallViewModel.preprocessIntent(intent, callerInfo);
+
+ mUninstallViewModel.getCurrentUninstallStage().observe(this,
+ this::onUninstallStageChange);
+ }
+
+ /**
+ * Main controller of the UI. This method shows relevant dialogs / fragments based on the
+ * uninstall stage
+ */
+ private void onUninstallStageChange(UninstallStage uninstallStage) {
+ if (uninstallStage.getStageCode() == UninstallStage.STAGE_ABORTED) {
+ UninstallAborted aborted = (UninstallAborted) uninstallStage;
+ if (aborted.getAbortReason() == UninstallAborted.ABORT_REASON_APP_UNAVAILABLE ||
+ aborted.getAbortReason() == UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED) {
+ UninstallErrorFragment errorDialog = new UninstallErrorFragment(aborted);
+ showDialogInner(errorDialog);
+ } else {
+ setResult(aborted.getActivityResultCode(), null, true);
+ }
+ } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_USER_ACTION_REQUIRED) {
+ UninstallUserActionRequired uar = (UninstallUserActionRequired) uninstallStage;
+ UninstallConfirmationFragment confirmationDialog = new UninstallConfirmationFragment(
+ uar);
+ showDialogInner(confirmationDialog);
+ } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_UNINSTALLING) {
+ // TODO: This shows a fragment whether or not user requests a result or not.
+ // Originally, if the user does not request a result, we used to show a notification.
+ // And a fragment if the user requests a result back. Should we consolidate and
+ // show a fragment always?
+ UninstallUninstalling uninstalling = (UninstallUninstalling) uninstallStage;
+ UninstallUninstallingFragment uninstallingDialog = new UninstallUninstallingFragment(
+ uninstalling);
+ showDialogInner(uninstallingDialog);
+ } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_FAILED) {
+ UninstallFailed failed = (UninstallFailed) uninstallStage;
+ if (!failed.returnResult()) {
+ mNotificationManager.notify(failed.getUninstallId(),
+ failed.getUninstallNotification());
+ }
+ setResult(failed.getActivityResultCode(), failed.getResultIntent(), true);
+ } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_SUCCESS) {
+ UninstallSuccess success = (UninstallSuccess) uninstallStage;
+ if (success.getMessage() != null) {
+ Toast.makeText(this, success.getMessage(), Toast.LENGTH_LONG).show();
+ }
+ setResult(success.getActivityResultCode(), success.getResultIntent(), true);
+ } else {
+ Log.e(TAG, "Invalid stage: " + uninstallStage.getStageCode());
+ showDialogInner(null);
+ }
+ }
+
+ /**
+ * Replace any visible dialog by the dialog returned by InstallRepository
+ *
+ * @param newDialog The new dialog to display
+ */
+ private void showDialogInner(DialogFragment newDialog) {
+ DialogFragment currentDialog = (DialogFragment) mFragmentManager.findFragmentByTag(
+ TAG_DIALOG);
+ if (currentDialog != null) {
+ currentDialog.dismissAllowingStateLoss();
+ }
+ if (newDialog != null) {
+ newDialog.show(mFragmentManager, TAG_DIALOG);
+ }
+ }
+
+ public void setResult(int resultCode, Intent data, boolean shouldFinish) {
+ super.setResult(resultCode, data);
+ if (shouldFinish) {
+ finish();
+ }
+ }
+
+ @Override
+ public void onPositiveResponse(boolean keepData) {
+ mUninstallViewModel.initiateUninstall(keepData);
+ }
+
+ @Override
+ public void onNegativeResponse() {
+ mUninstallViewModel.cancelInstall();
+ setResult(Activity.RESULT_FIRST_USER, null, true);
}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java
new file mode 100644
index 0000000..1b0885e
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java
@@ -0,0 +1,89 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.ui.fragments;
+
+import static android.text.format.Formatter.formatFileSize;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
+import com.android.packageinstaller.v2.ui.UninstallActionListener;
+
+/**
+ * Dialog to show while requesting user confirmation for uninstalling an app.
+ */
+public class UninstallConfirmationFragment extends DialogFragment {
+
+ private final UninstallUserActionRequired mDialogData;
+ private UninstallActionListener mUninstallActionListener;
+
+ private CheckBox mKeepData;
+
+ public UninstallConfirmationFragment(UninstallUserActionRequired dialogData) {
+ mDialogData = dialogData;
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mUninstallActionListener = (UninstallActionListener) context;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
+ .setTitle(mDialogData.getTitle())
+ .setPositiveButton(R.string.ok,
+ (dialogInt, which) -> mUninstallActionListener.onPositiveResponse(
+ mKeepData != null && mKeepData.isChecked()))
+ .setNegativeButton(R.string.cancel,
+ (dialogInt, which) -> mUninstallActionListener.onNegativeResponse());
+
+ long appDataSize = mDialogData.getAppDataSize();
+ if (appDataSize == 0) {
+ builder.setMessage(mDialogData.getMessage());
+ } else {
+ View dialogView = getLayoutInflater().inflate(R.layout.uninstall_content_view, null);
+
+ ((TextView) dialogView.requireViewById(R.id.message)).setText(mDialogData.getMessage());
+ mKeepData = dialogView.requireViewById(R.id.keepData);
+ mKeepData.setVisibility(View.VISIBLE);
+ mKeepData.setText(getString(R.string.uninstall_keep_data,
+ formatFileSize(getContext(), appDataSize)));
+
+ builder.setView(dialogView);
+ }
+ return builder.create();
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ super.onCancel(dialog);
+ mUninstallActionListener.onNegativeResponse();
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java
new file mode 100644
index 0000000..305daba
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java
@@ -0,0 +1,68 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted;
+import com.android.packageinstaller.v2.ui.UninstallActionListener;
+
+/**
+ * Dialog to show when an app cannot be uninstalled
+ */
+public class UninstallErrorFragment extends DialogFragment {
+
+ private final UninstallAborted mDialogData;
+ private UninstallActionListener mUninstallActionListener;
+
+ public UninstallErrorFragment(UninstallAborted dialogData) {
+ mDialogData = dialogData;
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mUninstallActionListener = (UninstallActionListener) context;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
+ .setMessage(mDialogData.getDialogTextResource())
+ .setNegativeButton(R.string.ok,
+ (dialogInt, which) -> mUninstallActionListener.onNegativeResponse());
+
+ if (mDialogData.getDialogTitleResource() != 0) {
+ builder.setTitle(mDialogData.getDialogTitleResource());
+ }
+ return builder.create();
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ super.onCancel(dialog);
+ mUninstallActionListener.onNegativeResponse();
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java
new file mode 100644
index 0000000..23cc421
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java
@@ -0,0 +1,55 @@
+/*
+ * 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
+ *
+ * https://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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
+
+/**
+ * Dialog to show that the app is uninstalling.
+ */
+public class UninstallUninstallingFragment extends DialogFragment {
+
+ UninstallUninstalling mDialogData;
+
+ public UninstallUninstallingFragment(UninstallUninstalling dialogData) {
+ mDialogData = dialogData;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
+ .setCancelable(false);
+ if (mDialogData.isCloneUser()) {
+ builder.setTitle(requireContext().getString(R.string.uninstalling_cloned_app,
+ mDialogData.getAppLabel()));
+ } else {
+ builder.setTitle(requireContext().getString(R.string.uninstalling_app,
+ mDialogData.getAppLabel()));
+ }
+ Dialog dialog = builder.create();
+ dialog.setCanceledOnTouchOutside(false);
+
+ return dialog;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
index 8187ea5..3f7bce8 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
@@ -17,17 +17,53 @@
package com.android.packageinstaller.v2.viewmodel;
import android.app.Application;
+import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.MutableLiveData;
import com.android.packageinstaller.v2.model.UninstallRepository;
+import com.android.packageinstaller.v2.model.UninstallRepository.CallerInfo;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
public class UninstallViewModel extends AndroidViewModel {
private static final String TAG = UninstallViewModel.class.getSimpleName();
private final UninstallRepository mRepository;
+ private final MediatorLiveData<UninstallStage> mCurrentUninstallStage =
+ new MediatorLiveData<>();
public UninstallViewModel(@NonNull Application application, UninstallRepository repository) {
super(application);
mRepository = repository;
}
+
+ public MutableLiveData<UninstallStage> getCurrentUninstallStage() {
+ return mCurrentUninstallStage;
+ }
+
+ public void preprocessIntent(Intent intent, CallerInfo callerInfo) {
+ UninstallStage stage = mRepository.performPreUninstallChecks(intent, callerInfo);
+ if (stage.getStageCode() != UninstallStage.STAGE_ABORTED) {
+ stage = mRepository.generateUninstallDetails();
+ }
+ mCurrentUninstallStage.setValue(stage);
+ }
+
+ public void initiateUninstall(boolean keepData) {
+ mRepository.initiateUninstall(keepData);
+ // Since uninstall is an async operation, we will get the uninstall result later in time.
+ // Result of the uninstall will be set in UninstallRepository#mUninstallResult.
+ // As such, mCurrentUninstallStage will need to add another MutableLiveData
+ // as a data source
+ mCurrentUninstallStage.addSource(mRepository.getUninstallResult(), uninstallStage -> {
+ if (uninstallStage != null) {
+ mCurrentUninstallStage.setValue(uninstallStage);
+ }
+ });
+ }
+
+ public void cancelInstall() {
+ mRepository.cancelInstall();
+ }
}
diff --git a/packages/SettingsLib/ProfileSelector/res/values/styles.xml b/packages/SettingsLib/ProfileSelector/res/values/styles.xml
index 0b703c9..365dcb2 100644
--- a/packages/SettingsLib/ProfileSelector/res/values/styles.xml
+++ b/packages/SettingsLib/ProfileSelector/res/values/styles.xml
@@ -37,5 +37,6 @@
<item name="tabIndicatorAnimationDuration">0</item>
<item name="tabTextAppearance">@style/SettingsLibTabsTextAppearance</item>
<item name="tabTextColor">@color/settingslib_tabs_text_color</item>
+ <item name="tabRippleColor">@android:color/transparent</item>
</style>
</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties b/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties
index 83d7549..23fdc01 100644
--- a/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties
+++ b/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties
@@ -12,4 +12,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-sdk=NEWEST_SDK
\ No newline at end of file
+sdk=NEWEST_SDK
+graphicsMode=NATIVE
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/package-info.java b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/package-info.java
deleted file mode 100644
index 8e55695..0000000
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/package-info.java
+++ /dev/null
@@ -1,20 +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.
- */
-
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.button;
-
-import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/package-info.java b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/package-info.java
deleted file mode 100644
index afe3f07..0000000
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/package-info.java
+++ /dev/null
@@ -1,20 +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.
- */
-
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.chart;
-
-import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/package-info.java b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/package-info.java
deleted file mode 100644
index 0089c2e..0000000
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/package-info.java
+++ /dev/null
@@ -1,20 +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.
- */
-
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.illustration;
-
-import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/package-info.java b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/package-info.java
deleted file mode 100644
index 45210ab..0000000
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/package-info.java
+++ /dev/null
@@ -1,20 +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.
- */
-
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.ui;
-
-import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
index 223e99e..5679694 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
@@ -72,7 +72,8 @@
private fun UserManager.showInSettings(userInfo: UserInfo): Int {
val userProperties = getUserProperties(userInfo.userHandle)
- return if (userInfo.isQuietModeEnabled && userProperties.hideInSettingsInQuietMode) {
+ return if (userInfo.isQuietModeEnabled && userProperties.showInQuietMode
+ == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) {
UserProperties.SHOW_IN_SETTINGS_NO
} else {
userProperties.showInSettings
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 5dacba5..52b51d7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -413,12 +413,8 @@
*/
@NonNull
List<MediaDevice> getSelectedMediaDevices() {
- if (TextUtils.isEmpty(mPackageName)) {
- Log.w(TAG, "getSelectedMediaDevices() package name is null or empty!");
- return Collections.emptyList();
- }
+ RoutingSessionInfo info = getRoutingSessionInfo();
- final RoutingSessionInfo info = getRoutingSessionInfo();
if (info == null) {
Log.w(TAG, "getSelectedMediaDevices() cannot find selectable MediaDevice from : "
+ mPackageName);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputConstants.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputConstants.java
index 3514932..02ec90d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputConstants.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputConstants.java
@@ -48,6 +48,17 @@
"com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG";
/**
+ * An intent action to launch a media output dialog without any app or playback metadata, which
+ * only controls system routing.
+ *
+ * <p>System routes are those provided by the system, such as built-in speakers, wired headsets,
+ * bluetooth devices, and other outputs that require the app to feed media samples to the
+ * framework.
+ */
+ public static final String ACTION_LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG =
+ "com.android.systemui.action.LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG";
+
+ /**
* An intent action to launch media output broadcast dialog.
*/
public static final String ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG =
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java
index faccf2f..90140d4 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java
@@ -259,13 +259,6 @@
}
@Test
- public void isSearchable_noMetadata_isTrue() {
- final Tile tile = new ProviderTile(mProviderInfo, "category", null);
-
- assertThat(tile.isSearchable()).isTrue();
- }
-
- @Test
public void isSearchable_notSet_isTrue() {
final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData);
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
index 0cabab2..542f101 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
@@ -168,10 +168,10 @@
private static final String EXPECTED_NEW_HTML_STRING = HTML_HEAD_STRING + HTML_NEW_BODY_STRING;
private static final String EXPECTED_OLD_HTML_STRING_WITH_CUSTOM_HEADING =
- HTML_HEAD_STRING + HTML_CUSTOM_HEADING + "\n" + HTML_OLD_BODY_STRING;
+ HTML_HEAD_STRING + HTML_CUSTOM_HEADING + "\n<br/>\n" + HTML_OLD_BODY_STRING;
private static final String EXPECTED_NEW_HTML_STRING_WITH_CUSTOM_HEADING =
- HTML_HEAD_STRING + HTML_CUSTOM_HEADING + "\n" + HTML_NEW_BODY_STRING;
+ HTML_HEAD_STRING + HTML_CUSTOM_HEADING + "\n<br/>\n" + HTML_NEW_BODY_STRING;
@Test
public void testParseValidXmlStream() throws XmlPullParserException, IOException {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
index 721e69d..f0f53d6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
@@ -39,9 +39,9 @@
import androidx.annotation.ColorRes;
import androidx.preference.PreferenceViewHolder;
-import com.android.settingslib.widget.preference.banner.R;
import com.android.settingslib.testutils.OverpoweredReflectionHelper;
+import com.android.settingslib.widget.preference.banner.R;
import org.junit.Before;
import org.junit.Test;
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index e218308..0a71cda 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1044,6 +1044,7 @@
android:exported="true">
<intent-filter android:priority="1">
<action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG" />
+ <action android:name="com.android.systemui.action.LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG" />
<action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG" />
<action android:name="com.android.systemui.action.DISMISS_MEDIA_OUTPUT_DIALOG" />
</intent-filter>
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a745ab5..a9dc145 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -143,8 +143,17 @@
}
flag {
+ name: "theme_overlay_controller_wakefulness_deprecation"
+ namespace: "systemui"
+ description: "Replacing WakefulnessLifecycle by KeyguardTransitionInteractor in "
+ "ThemOverlayController to mitigate flickering when locking the device"
+ bug: "308676488"
+}
+
+flag {
name: "media_in_scene_container"
namespace: "systemui"
description: "Enable media in the scene container framework"
bug: "296122467"
}
+
diff --git a/packages/SystemUI/res/anim/instant_fade_out.xml b/packages/SystemUI/res/anim/instant_fade_out.xml
new file mode 100644
index 0000000..800420b
--- /dev/null
+++ b/packages/SystemUI/res/anim/instant_fade_out.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 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.
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"
+ android:interpolator="@android:interpolator/linear_out_slow_in"
+ android:duration="0"/>
+
diff --git a/packages/SystemUI/res/layout/screen_record_options.xml b/packages/SystemUI/res/layout/screen_record_options.xml
index d9f4b79..8916e42 100644
--- a/packages/SystemUI/res/layout/screen_record_options.xml
+++ b/packages/SystemUI/res/layout/screen_record_options.xml
@@ -47,6 +47,7 @@
android:layout_weight="0"
android:layout_gravity="end"
android:id="@+id/screenrecord_audio_switch"
+ android:contentDescription="@string/screenrecord_audio_label"
style="@style/ScreenRecord.Switch"
android:importantForAccessibility="yes"/>
</LinearLayout>
@@ -79,6 +80,7 @@
android:minWidth="48dp"
android:layout_height="48dp"
android:id="@+id/screenrecord_taps_switch"
+ android:contentDescription="@string/screenrecord_taps_label"
style="@style/ScreenRecord.Switch"
android:importantForAccessibility="yes"/>
</LinearLayout>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index daf6cb3..b20fa89 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1853,51 +1853,51 @@
<string name="keyboard_shortcut_search_category_current_app">Current app</string>
<!-- User visible title for the keyboard shortcut that triggers the notification shade. [CHAR LIMIT=70] -->
- <string name="group_system_access_notification_shade">Access notification shade</string>
+ <string name="group_system_access_notification_shade">View notifications</string>
<!-- User visible title for the keyboard shortcut that takes a full screenshot. [CHAR LIMIT=70] -->
- <string name="group_system_full_screenshot">Take a full screenshot</string>
+ <string name="group_system_full_screenshot">Take screenshot</string>
<!-- User visible title for the keyboard shortcut that access list of system / apps shortcuts. [CHAR LIMIT=70] -->
- <string name="group_system_access_system_app_shortcuts">Access list of system / apps shortcuts</string>
+ <string name="group_system_access_system_app_shortcuts">Show shortcuts</string>
<!-- User visible title for the keyboard shortcut that goes back to previous state. [CHAR LIMIT=70] -->
- <string name="group_system_go_back">Back: go back to previous state (back button)</string>
+ <string name="group_system_go_back">Go back</string>
<!-- User visible title for the keyboard shortcut that takes the user to the home screen. [CHAR LIMIT=70] -->
- <string name="group_system_access_home_screen">Access home screen</string>
+ <string name="group_system_access_home_screen">Go to home screen</string>
<!-- User visible title for the keyboard shortcut that triggers overview of open apps. [CHAR LIMIT=70] -->
- <string name="group_system_overview_open_apps">Overview of open apps</string>
+ <string name="group_system_overview_open_apps">View recent apps</string>
<!-- User visible title for the keyboard shortcut that cycles through recent apps (forward). [CHAR LIMIT=70] -->
- <string name="group_system_cycle_forward">Cycle through recent apps (forward)</string>
+ <string name="group_system_cycle_forward">Cycle forward through recent apps</string>
<!-- User visible title for the keyboard shortcut that cycles through recent apps (back). [CHAR LIMIT=70] -->
- <string name="group_system_cycle_back">Cycle through recent apps (back)</string>
+ <string name="group_system_cycle_back">Cycle backward through recent apps</string>
<!-- User visible title for the keyboard shortcut that accesses list of all apps and search. [CHAR LIMIT=70] -->
- <string name="group_system_access_all_apps_search">Access list of all apps and search (i.e. Search/Launcher)</string>
+ <string name="group_system_access_all_apps_search">Open apps list</string>
<!-- User visible title for the keyboard shortcut that hides and (re)showes taskbar. [CHAR LIMIT=70] -->
- <string name="group_system_hide_reshow_taskbar">Hide and (re)show taskbar</string>
- <!-- User visible title for the keyboard shortcut that accesses system settings. [CHAR LIMIT=70] -->
- <string name="group_system_access_system_settings">Access system settings</string>
- <!-- User visible title for the keyboard shortcut that accesses Google Assistant. [CHAR LIMIT=70] -->
- <string name="group_system_access_google_assistant">Access Google Assistant</string>
+ <string name="group_system_hide_reshow_taskbar">Show taskbar</string>
+ <!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] -->
+ <string name="group_system_access_system_settings">Open settings</string>
+ <!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] -->
+ <string name="group_system_access_google_assistant">Open assistant</string>
<!-- User visible title for the keyboard shortcut that locks screen. [CHAR LIMIT=70] -->
<string name="group_system_lock_screen">Lock screen</string>
<!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] -->
- <string name="group_system_quick_memo">Pull up Notes app for quick memo</string>
+ <string name="group_system_quick_memo">Open notes</string>
<!-- User visible title for the system multitasking keyboard shortcuts list. [CHAR LIMIT=70] -->
<string name="keyboard_shortcut_group_system_multitasking">System multitasking</string>
<!-- User visible title for the keyboard shortcut that enters split screen with current app to RHS [CHAR LIMIT=70] -->
- <string name="system_multitasking_rhs">Enter Split screen with current app to RHS</string>
+ <string name="system_multitasking_rhs">Enter split screen with current app to RHS</string>
<!-- User visible title for the keyboard shortcut that enters split screen with current app to LHS [CHAR LIMIT=70] -->
- <string name="system_multitasking_lhs">Enter Split screen with current app to LHS</string>
+ <string name="system_multitasking_lhs">Enter split screen with current app to LHS</string>
<!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
- <string name="system_multitasking_full_screen">Switch from Split screen to full screen</string>
+ <string name="system_multitasking_full_screen">Switch from split screen to full screen</string>
<!-- User visible title for the keyboard shortcut that replaces an app from one to another during split screen [CHAR LIMIT=70] -->
- <string name="system_multitasking_replace">During Split screen: replace an app from one to another</string>
+ <string name="system_multitasking_replace">During split screen: replace an app from one to another</string>
<!-- User visible title for the input keyboard shortcuts list. [CHAR LIMIT=70] -->
<string name="keyboard_shortcut_group_input">Input</string>
<!-- User visible title for the keyboard shortcut that switches input language (next language). [CHAR LIMIT=70] -->
- <string name="input_switch_input_language_next">Switch input language (next language)</string>
+ <string name="input_switch_input_language_next">Switch to next language</string>
<!-- User visible title for the keyboard shortcut that switches input language (previous language). [CHAR LIMIT=70] -->
- <string name="input_switch_input_language_previous">Switch input language (previous language)</string>
+ <string name="input_switch_input_language_previous">Switch to previous language</string>
<!-- User visible title for the keyboard shortcut that accesses emoji. [CHAR LIMIT=70] -->
<string name="input_access_emoji">Access emoji</string>
<!-- User visible title for the keyboard shortcut that accesses voice typing. [CHAR LIMIT=70] -->
@@ -1905,8 +1905,8 @@
<!-- User visible title for the system-wide applications keyboard shortcuts list. [CHAR LIMIT=70] -->
<string name="keyboard_shortcut_group_applications">Applications</string>
- <!-- User visible title for the keyboard shortcut that takes the user to the assist app. [CHAR LIMIT=70] -->
- <string name="keyboard_shortcut_group_applications_assist">Assist</string>
+ <!-- User visible title for the keyboard shortcut that takes the user to the assistant app. [CHAR LIMIT=70] -->
+ <string name="keyboard_shortcut_group_applications_assist">Assistant</string>
<!-- User visible title for the keyboard shortcut that takes the user to the browser app. [CHAR LIMIT=70] -->
<string name="keyboard_shortcut_group_applications_browser">Browser (Chrome as default)</string>
<!-- User visible title for the keyboard shortcut that takes the user to the contacts app. [CHAR LIMIT=70] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index befee2b..3db56c5 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -420,6 +420,11 @@
<!-- Cannot double inherit. Use Theme.SystemUI.QuickSettings in code to match -->
<style name="BrightnessDialog" parent="@android:style/Theme.DeviceDefault.Dialog">
<item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowAnimationStyle">@style/Animation.BrightnessDialog</item>
+ </style>
+
+ <style name="Animation.BrightnessDialog">
+ <item name="android:windowExitAnimation">@anim/instant_fade_out</item>
</style>
<style name="Theme.SystemUI.ContrastDialog" parent="@android:style/Theme.DeviceDefault.Dialog">
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 4da48f6..706aba3c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -302,4 +302,11 @@
fun isFinishedInState(state: KeyguardState): Flow<Boolean> {
return finishedKeyguardState.map { it == state }.distinctUntilChanged()
}
+
+ /**
+ * Whether we've FINISHED a transition to a state that matches the given predicate. Consider
+ * using [isFinishedInStateWhere] whenever possible instead
+ */
+ fun isFinishedInStateWhereValue(stateMatcher: (KeyguardState) -> Boolean) =
+ stateMatcher(finishedKeyguardState.replayCache.last())
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 0c5a14f..48f432e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -58,8 +58,8 @@
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import com.android.systemui.res.R;
import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import java.util.concurrent.Executor;
@@ -85,6 +85,13 @@
final MediaOutputController mMediaOutputController;
final BroadcastSender mBroadcastSender;
+ /**
+ * Signals whether the dialog should NOT show app-related metadata.
+ *
+ * <p>A metadata-less dialog hides the title, subtitle, and app icon in the header.
+ */
+ private final boolean mIncludePlaybackAndAppMetadata;
+
@VisibleForTesting
View mDialogView;
private TextView mHeaderTitle;
@@ -210,8 +217,11 @@
}
}
- public MediaOutputBaseDialog(Context context, BroadcastSender broadcastSender,
- MediaOutputController mediaOutputController) {
+ public MediaOutputBaseDialog(
+ Context context,
+ BroadcastSender broadcastSender,
+ MediaOutputController mediaOutputController,
+ boolean includePlaybackAndAppMetadata) {
super(context, R.style.Theme_SystemUI_Dialog_Media);
// Save the context that is wrapped with our theme.
@@ -226,6 +236,7 @@
mListPaddingTop = mContext.getResources().getDimensionPixelSize(
R.dimen.media_output_dialog_list_padding_top);
mExecutor = Executors.newSingleThreadExecutor();
+ mIncludePlaybackAndAppMetadata = includePlaybackAndAppMetadata;
}
@Override
@@ -354,7 +365,10 @@
updateDialogBackgroundColor();
mHeaderIcon.setVisibility(View.GONE);
}
- if (appSourceIcon != null) {
+
+ if (!mIncludePlaybackAndAppMetadata) {
+ mAppResourceIcon.setVisibility(View.GONE);
+ } else if (appSourceIcon != null) {
Icon appIcon = appSourceIcon.toIcon(mContext);
mAppResourceIcon.setColorFilter(mMediaOutputController.getColorItemContent());
mAppResourceIcon.setImageIcon(appIcon);
@@ -373,17 +387,24 @@
mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size));
}
mAppButton.setText(mMediaOutputController.getAppSourceName());
- // Update title and subtitle
- mHeaderTitle.setText(getHeaderText());
- final CharSequence subTitle = getHeaderSubtitle();
- if (TextUtils.isEmpty(subTitle)) {
+
+ if (!mIncludePlaybackAndAppMetadata) {
+ mHeaderTitle.setVisibility(View.GONE);
mHeaderSubtitle.setVisibility(View.GONE);
- mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
} else {
- mHeaderSubtitle.setVisibility(View.VISIBLE);
- mHeaderSubtitle.setText(subTitle);
- mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
+ // Update title and subtitle
+ mHeaderTitle.setText(getHeaderText());
+ final CharSequence subTitle = getHeaderSubtitle();
+ if (TextUtils.isEmpty(subTitle)) {
+ mHeaderSubtitle.setVisibility(View.GONE);
+ mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+ } else {
+ mHeaderSubtitle.setVisibility(View.VISIBLE);
+ mHeaderSubtitle.setText(subTitle);
+ mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
+ }
}
+
// Show when remote media session is available or
// when the device supports BT LE audio + media is playing
mStopButton.setVisibility(getStopButtonVisibility());
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index ac64300..8e0191e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -42,12 +42,10 @@
import androidx.core.graphics.drawable.IconCompat;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.settingslib.media.BluetoothMediaDevice;
-import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.qrcode.QrCodeGenerator;
-import com.android.systemui.res.R;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.google.zxing.WriterException;
@@ -237,7 +235,11 @@
MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar,
BroadcastSender broadcastSender, MediaOutputController mediaOutputController) {
- super(context, broadcastSender, mediaOutputController);
+ super(
+ context,
+ broadcastSender,
+ mediaOutputController, /* includePlaybackAndAppMetadata */
+ true);
mAdapter = new MediaOutputAdapter(mMediaOutputController);
// TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
// that extends MediaOutputBaseDialog
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 426a497..375a0ce 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -78,7 +78,6 @@
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
import com.android.settingslib.utils.ThreadUtils;
-import com.android.systemui.res.R;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastSender;
@@ -86,6 +85,7 @@
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.monet.ColorScheme;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
@@ -358,7 +358,7 @@
}
Drawable getAppSourceIconFromPackage() {
- if (mPackageName.isEmpty()) {
+ if (TextUtils.isEmpty(mPackageName)) {
return null;
}
try {
@@ -372,7 +372,7 @@
}
String getAppSourceName() {
- if (mPackageName.isEmpty()) {
+ if (TextUtils.isEmpty(mPackageName)) {
return null;
}
final PackageManager packageManager = mContext.getPackageManager();
@@ -391,7 +391,7 @@
}
Intent getAppLaunchIntent() {
- if (mPackageName.isEmpty()) {
+ if (TextUtils.isEmpty(mPackageName)) {
return null;
}
return mContext.getPackageManager().getLaunchIntentForPackage(mPackageName);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 4640a5d..d40699c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -27,10 +27,10 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.res.R;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.res.R;
/**
* Dialog for media output transferring.
@@ -40,10 +40,15 @@
private final DialogLaunchAnimator mDialogLaunchAnimator;
private final UiEventLogger mUiEventLogger;
- MediaOutputDialog(Context context, boolean aboveStatusbar, BroadcastSender broadcastSender,
- MediaOutputController mediaOutputController, DialogLaunchAnimator dialogLaunchAnimator,
- UiEventLogger uiEventLogger) {
- super(context, broadcastSender, mediaOutputController);
+ MediaOutputDialog(
+ Context context,
+ boolean aboveStatusbar,
+ BroadcastSender broadcastSender,
+ MediaOutputController mediaOutputController,
+ DialogLaunchAnimator dialogLaunchAnimator,
+ UiEventLogger uiEventLogger,
+ boolean includePlaybackAndAppMetadata) {
+ super(context, broadcastSender, mediaOutputController, includePlaybackAndAppMetadata);
mDialogLaunchAnimator = dialogLaunchAnimator;
mUiEventLogger = uiEventLogger;
mAdapter = new MediaOutputAdapter(mMediaOutputController);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index 2b38edb..b04a7a4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -61,6 +61,19 @@
/** Creates a [MediaOutputDialog] for the given package. */
open fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
+ create(packageName, aboveStatusBar, view, includePlaybackAndAppMetadata = true)
+ }
+
+ open fun createDialogForSystemRouting() {
+ create(packageName = null, aboveStatusBar = false, includePlaybackAndAppMetadata = false)
+ }
+
+ private fun create(
+ packageName: String?,
+ aboveStatusBar: Boolean,
+ view: View? = null,
+ includePlaybackAndAppMetadata: Boolean = true
+ ) {
// Dismiss the previous dialog, if any.
mediaOutputDialog?.dismiss()
@@ -71,7 +84,7 @@
powerExemptionManager, keyGuardManager, featureFlags, userTracker)
val dialog =
MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller,
- dialogLaunchAnimator, uiEventLogger)
+ dialogLaunchAnimator, uiEventLogger, includePlaybackAndAppMetadata)
mediaOutputDialog = dialog
// Show the dialog.
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
index 132bf99..1002cc3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
@@ -19,7 +19,6 @@
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import android.text.TextUtils
import android.util.Log
import com.android.settingslib.media.MediaOutputConstants
import javax.inject.Inject
@@ -35,16 +34,16 @@
private val mediaOutputBroadcastDialogFactory: MediaOutputBroadcastDialogFactory
) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
- when {
- TextUtils.equals(
- MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG, intent.action) -> {
+ when (intent.action) {
+ MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG -> {
val packageName: String? =
intent.getStringExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME)
launchMediaOutputDialogIfPossible(packageName)
}
- TextUtils.equals(
- MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG,
- intent.action) -> {
+ MediaOutputConstants.ACTION_LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG -> {
+ mediaOutputDialogFactory.createDialogForSystemRouting()
+ }
+ MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG -> {
val packageName: String? =
intent.getStringExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME)
launchMediaOutputBroadcastDialogIfPossible(packageName)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt
index 654fffe8..1983a67 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt
@@ -24,6 +24,7 @@
import android.view.ViewGroup
import android.view.ViewStub
import android.view.WindowManager
+import android.view.accessibility.AccessibilityNodeInfo
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
@@ -106,6 +107,19 @@
screenShareModeSpinner = dialog.requireViewById(R.id.screen_share_mode_spinner)
screenShareModeSpinner.adapter = adapter
screenShareModeSpinner.onItemSelectedListener = this
+
+ // disable redundant Touch & Hold accessibility action for Switch Access
+ screenShareModeSpinner.accessibilityDelegate =
+ object : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfo
+ ) {
+ info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK)
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ }
+ }
+ screenShareModeSpinner.isLongClickable = false
}
override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 0299114..e0eee96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -34,34 +34,38 @@
viewModel: FooterViewModel,
clearAllNotifications: View.OnClickListener,
): DisposableHandle {
- // Listen for changes when the view is attached.
+ // Bind the resource IDs
+ footer.setMessageString(viewModel.message.messageId)
+ footer.setMessageIcon(viewModel.message.iconId)
+ footer.setClearAllButtonText(viewModel.clearAllButton.labelId)
+ footer.setClearAllButtonDescription(viewModel.clearAllButton.accessibilityDescriptionId)
+
+ // Bind the click listeners
+ footer.setClearAllButtonClickListener(clearAllNotifications)
+
+ // Listen for visibility changes when the view is attached.
return footer.repeatWhenAttached {
lifecycleScope.launch {
- viewModel.clearAllButton.collect { button ->
- if (button.isVisible.isAnimating) {
+ viewModel.clearAllButton.isVisible.collect { isVisible ->
+ if (isVisible.isAnimating) {
footer.setClearAllButtonVisible(
- button.isVisible.value,
+ isVisible.value,
/* animate = */ true,
) { _ ->
- button.isVisible.stopAnimating()
+ isVisible.stopAnimating()
}
} else {
footer.setClearAllButtonVisible(
- button.isVisible.value,
+ isVisible.value,
/* animate = */ false,
)
}
- footer.setClearAllButtonText(button.labelId)
- footer.setClearAllButtonDescription(button.accessibilityDescriptionId)
- footer.setClearAllButtonClickListener(clearAllNotifications)
}
}
lifecycleScope.launch {
- viewModel.message.collect { message ->
- footer.setFooterLabelVisible(message.visible)
- footer.setMessageString(message.messageId)
- footer.setMessageIcon(message.iconId)
+ viewModel.message.isVisible.collect { visible ->
+ footer.setFooterLabelVisible(visible)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
index ea5abef..244555a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
@@ -18,9 +18,10 @@
import android.annotation.StringRes
import com.android.systemui.util.ui.AnimatedValue
+import kotlinx.coroutines.flow.Flow
data class FooterButtonViewModel(
@StringRes val labelId: Int,
@StringRes val accessibilityDescriptionId: Int,
- val isVisible: AnimatedValue<Boolean>,
+ val isVisible: Flow<AnimatedValue<Boolean>>,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterMessageViewModel.kt
index bc912fb..85cd397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterMessageViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterMessageViewModel.kt
@@ -18,10 +18,11 @@
import android.annotation.DrawableRes
import android.annotation.StringRes
+import kotlinx.coroutines.flow.StateFlow
/** A ViewModel for the string message that can be shown in the footer. */
data class FooterMessageViewModel(
@StringRes val messageId: Int,
@DrawableRes val iconId: Int,
- val visible: Boolean,
+ val isVisible: StateFlow<Boolean>,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index 721bea1..e6b0abc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -30,9 +30,7 @@
import dagger.Provides
import java.util.Optional
import javax.inject.Provider
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
/** ViewModel for [FooterView]. */
@@ -41,36 +39,32 @@
seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
) {
- val clearAllButton: Flow<FooterButtonViewModel> =
- activeNotificationsInteractor.hasClearableNotifications
- .sample(
- combine(
- shadeInteractor.isShadeFullyExpanded,
- shadeInteractor.isShadeTouchable,
- ::Pair
- )
- .onStart { emit(Pair(false, false)) }
- ) { hasClearableNotifications, (isShadeFullyExpanded, animationsEnabled) ->
- val shouldAnimate = isShadeFullyExpanded && animationsEnabled
- AnimatableEvent(hasClearableNotifications, shouldAnimate)
- }
- .toAnimatedValueFlow()
- .map { visible ->
- FooterButtonViewModel(
- labelId = R.string.clear_all_notifications_text,
- accessibilityDescriptionId = R.string.accessibility_clear_all,
- isVisible = visible,
- )
- }
+ val clearAllButton: FooterButtonViewModel =
+ FooterButtonViewModel(
+ labelId = R.string.clear_all_notifications_text,
+ accessibilityDescriptionId = R.string.accessibility_clear_all,
+ isVisible =
+ activeNotificationsInteractor.hasClearableNotifications
+ .sample(
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { hasClearableNotifications, (isShadeFullyExpanded, animationsEnabled) ->
+ val shouldAnimate = isShadeFullyExpanded && animationsEnabled
+ AnimatableEvent(hasClearableNotifications, shouldAnimate)
+ }
+ .toAnimatedValueFlow(),
+ )
- val message: Flow<FooterMessageViewModel> =
- seenNotificationsInteractor.hasFilteredOutSeenNotifications.map { hasFilteredOutNotifs ->
- FooterMessageViewModel(
- messageId = R.string.unlock_to_see_notif_text,
- iconId = R.drawable.ic_friction_lock_closed,
- visible = hasFilteredOutNotifs,
- )
- }
+ val message: FooterMessageViewModel =
+ FooterMessageViewModel(
+ messageId = R.string.unlock_to_see_notif_text,
+ iconId = R.drawable.ic_friction_lock_closed,
+ isVisible = seenNotificationsInteractor.hasFilteredOutSeenNotifications,
+ )
}
@Module
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 4554085..aa0d3ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -28,6 +28,7 @@
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.NotificationShelf
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
@@ -62,14 +63,17 @@
viewController: NotificationStackScrollLayoutController
) {
bindShelf(view)
- bindFooter(view)
- bindEmptyShade(view)
bindHideList(viewController, viewModel)
- view.repeatWhenAttached {
- lifecycleScope.launch {
- viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
- view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+ if (FooterViewRefactor.isEnabled) {
+ bindFooter(view)
+ bindEmptyShade(view)
+
+ view.repeatWhenAttached {
+ lifecycleScope.launch {
+ viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
+ view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 5a9f5d5..886fa70 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -19,6 +19,7 @@
import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8;
import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
+import static com.android.systemui.Flags.themeOverlayControllerWakefulnessDeprecation;
import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME;
import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK;
import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET;
@@ -71,12 +72,15 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.monet.ColorScheme;
import com.android.systemui.monet.Style;
import com.android.systemui.monet.TonalPalette;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.settings.SecureSettings;
import com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors;
@@ -127,7 +131,6 @@
private final SecureSettings mSecureSettings;
private final Executor mMainExecutor;
private final Handler mBgHandler;
- private final boolean mIsMonochromaticEnabled;
private final Context mContext;
private final boolean mIsMonetEnabled;
private final boolean mIsFidelityEnabled;
@@ -161,6 +164,8 @@
private final SparseArray<WallpaperColors> mDeferredWallpaperColors = new SparseArray<>();
private final SparseIntArray mDeferredWallpaperColorsFlags = new SparseIntArray();
private final WakefulnessLifecycle mWakefulnessLifecycle;
+ private final JavaAdapter mJavaAdapter;
+ private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final UiModeManager mUiModeManager;
private DynamicScheme mDynamicSchemeDark;
private DynamicScheme mDynamicSchemeLight;
@@ -200,8 +205,12 @@
return;
}
boolean currentUser = userId == mUserTracker.getUserId();
- if (currentUser && !mAcceptColorEvents
- && mWakefulnessLifecycle.getWakefulness() != WAKEFULNESS_ASLEEP) {
+ boolean isAsleep = themeOverlayControllerWakefulnessDeprecation()
+ ? mKeyguardTransitionInteractor.isFinishedInStateWhereValue(
+ state -> KeyguardState.Companion.deviceIsAsleepInState(state))
+ : mWakefulnessLifecycle.getWakefulness() != WAKEFULNESS_ASLEEP;
+
+ if (currentUser && !mAcceptColorEvents && isAsleep) {
mDeferredWallpaperColors.put(userId, wallpaperColors);
mDeferredWallpaperColorsFlags.put(userId, which);
Log.i(TAG, "colors received; processing deferred until screen off: "
@@ -395,9 +404,10 @@
FeatureFlags featureFlags,
@Main Resources resources,
WakefulnessLifecycle wakefulnessLifecycle,
+ JavaAdapter javaAdapter,
+ KeyguardTransitionInteractor keyguardTransitionInteractor,
UiModeManager uiModeManager) {
mContext = context;
- mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME);
mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY);
mDeviceProvisionedController = deviceProvisionedController;
@@ -412,6 +422,8 @@
mUserTracker = userTracker;
mResources = resources;
mWakefulnessLifecycle = wakefulnessLifecycle;
+ mJavaAdapter = javaAdapter;
+ mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mUiModeManager = uiModeManager;
dumpManager.registerDumpable(TAG, this);
}
@@ -494,21 +506,34 @@
}
mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null,
UserHandle.USER_ALL);
- mWakefulnessLifecycle.addObserver(new WakefulnessLifecycle.Observer() {
- @Override
- public void onFinishedGoingToSleep() {
- final int userId = mUserTracker.getUserId();
- final WallpaperColors colors = mDeferredWallpaperColors.get(userId);
- if (colors != null) {
- int flags = mDeferredWallpaperColorsFlags.get(userId);
- mDeferredWallpaperColors.put(userId, null);
- mDeferredWallpaperColorsFlags.put(userId, 0);
+ Runnable whenAsleepHandler = () -> {
+ final int userId = mUserTracker.getUserId();
+ final WallpaperColors colors = mDeferredWallpaperColors.get(userId);
+ if (colors != null) {
+ int flags = mDeferredWallpaperColorsFlags.get(userId);
- handleWallpaperColors(colors, flags, userId);
- }
+ mDeferredWallpaperColors.put(userId, null);
+ mDeferredWallpaperColorsFlags.put(userId, 0);
+
+ handleWallpaperColors(colors, flags, userId);
}
- });
+ };
+
+ if (themeOverlayControllerWakefulnessDeprecation()) {
+ mJavaAdapter.alwaysCollectFlow(
+ mKeyguardTransitionInteractor.isFinishedInState(KeyguardState.DOZING),
+ isFinishedInDozing -> {
+ if (isFinishedInDozing) whenAsleepHandler.run();
+ });
+ } else {
+ mWakefulnessLifecycle.addObserver(new WakefulnessLifecycle.Observer() {
+ @Override
+ public void onFinishedGoingToSleep() {
+ whenAsleepHandler.run();
+ }
+ });
+ }
}
private void reevaluateSystemTheme(boolean forceReload) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 9dfb5a5..e082ca8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -47,13 +47,13 @@
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
-import com.android.systemui.res.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
@@ -305,7 +305,11 @@
MediaOutputBaseDialogImpl(Context context, BroadcastSender broadcastSender,
MediaOutputController mediaOutputController) {
- super(context, broadcastSender, mediaOutputController);
+ super(
+ context,
+ broadcastSender,
+ mediaOutputController, /* includePlaybackAndAppMetadata */
+ true);
mAdapter = mMediaOutputBaseAdapter;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index 379136b..d5dc502 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -49,13 +49,13 @@
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
-import com.android.systemui.res.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogLaunchAnimator;
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
@@ -394,8 +394,14 @@
@NonNull
private MediaOutputDialog makeTestDialog(MediaOutputController controller) {
- return new MediaOutputDialog(mContext, false, mBroadcastSender,
- controller, mDialogLaunchAnimator, mUiEventLogger);
+ return new MediaOutputDialog(
+ mContext,
+ false,
+ mBroadcastSender,
+ controller,
+ mDialogLaunchAnimator,
+ mUiEventLogger,
+ true);
}
private void withTestDialog(MediaOutputController controller, Consumer<MediaOutputDialog> c) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 94dcf7a..0ba820f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -116,27 +116,27 @@
@Test
fun testMessageVisible_whenFilteredNotifications() =
testComponent.runTest {
- val message by collectLastValue(footerViewModel.message)
+ val visible by collectLastValue(footerViewModel.message.isVisible)
activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
- assertThat(message?.visible).isTrue()
+ assertThat(visible).isTrue()
}
@Test
fun testMessageVisible_whenNoFilteredNotifications() =
testComponent.runTest {
- val message by collectLastValue(footerViewModel.message)
+ val visible by collectLastValue(footerViewModel.message.isVisible)
activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
- assertThat(message?.visible).isFalse()
+ assertThat(visible).isFalse()
}
@Test
fun testClearAllButtonVisible_whenHasClearableNotifs() =
testComponent.runTest {
- val button by collectLastValue(footerViewModel.clearAllButton)
+ val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
activeNotificationListRepository.notifStats.value =
NotifStats(
@@ -148,13 +148,13 @@
)
runCurrent()
- assertThat(button?.isVisible?.value).isTrue()
+ assertThat(visible?.value).isTrue()
}
@Test
fun testClearAllButtonVisible_whenHasNoClearableNotifs() =
testComponent.runTest {
- val button by collectLastValue(footerViewModel.clearAllButton)
+ val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
activeNotificationListRepository.notifStats.value =
NotifStats(
@@ -166,13 +166,13 @@
)
runCurrent()
- assertThat(button?.isVisible?.value).isFalse()
+ assertThat(visible?.value).isFalse()
}
@Test
fun testClearAllButtonAnimating_whenShadeExpandedAndTouchable() =
testComponent.runTest {
- val button by collectLastValue(footerViewModel.clearAllButton)
+ val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
runCurrent()
// WHEN shade is expanded
@@ -200,13 +200,13 @@
runCurrent()
// THEN button visibility should animate
- assertThat(button?.isVisible?.isAnimating).isTrue()
+ assertThat(visible?.isAnimating).isTrue()
}
@Test
fun testClearAllButtonAnimating_whenShadeNotExpanded() =
testComponent.runTest {
- val button by collectLastValue(footerViewModel.clearAllButton)
+ val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
runCurrent()
// WHEN shade is collapsed
@@ -234,6 +234,6 @@
runCurrent()
// THEN button visibility should not animate
- assertThat(button?.isVisible?.isAnimating).isFalse()
+ assertThat(visible?.isAnimating).isFalse()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index c454b45..1123688 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -61,10 +61,12 @@
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.monet.Style;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+import com.android.systemui.util.kotlin.JavaAdapter;
import com.android.systemui.util.settings.SecureSettings;
import com.google.common.util.concurrent.MoreExecutors;
@@ -88,7 +90,10 @@
private static final int USER_SYSTEM = UserHandle.USER_SYSTEM;
private static final int USER_SECONDARY = 10;
-
+ @Mock
+ private JavaAdapter mJavaAdapter;
+ @Mock
+ private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private ThemeOverlayController mThemeOverlayController;
@Mock
private Executor mBgExecutor;
@@ -150,11 +155,12 @@
.thenReturn(Color.YELLOW);
when(mResources.getColor(eq(android.R.color.system_neutral2_500), any()))
.thenReturn(Color.BLACK);
+
mThemeOverlayController = new ThemeOverlayController(mContext,
mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier,
mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
- mUiModeManager) {
+ mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager) {
@VisibleForTesting
protected boolean isNightMode() {
return false;
@@ -736,7 +742,7 @@
mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
- mUiModeManager) {
+ mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager) {
@VisibleForTesting
protected boolean isNightMode() {
return false;
@@ -776,7 +782,7 @@
mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
- mUiModeManager) {
+ mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager) {
@VisibleForTesting
protected boolean isNightMode() {
return false;
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 041cd75..7187895 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -34,7 +34,7 @@
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
-import static com.android.window.flags.Flags.removeCaptureDisplay;
+import static com.android.window.flags.Flags.deleteCaptureDisplay;
import android.accessibilityservice.AccessibilityGestureEvent;
import android.accessibilityservice.AccessibilityService;
@@ -1443,7 +1443,7 @@
return;
}
final long identity = Binder.clearCallingIdentity();
- if (removeCaptureDisplay()) {
+ if (deleteCaptureDisplay()) {
try {
ScreenCapture.ScreenCaptureListener screenCaptureListener = new
ScreenCapture.ScreenCaptureListener(
@@ -1485,7 +1485,7 @@
private void sendScreenshotSuccess(ScreenshotHardwareBuffer screenshotBuffer,
RemoteCallback callback) {
- if (removeCaptureDisplay()) {
+ if (deleteCaptureDisplay()) {
mMainHandler.post(PooledLambda.obtainRunnable((nonArg) -> {
final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
final ParcelableColorSpace colorSpace =
diff --git a/services/companion/java/com/android/server/companion/virtual/OWNERS b/services/companion/java/com/android/server/companion/virtual/OWNERS
index 5295ec8..4fe0592 100644
--- a/services/companion/java/com/android/server/companion/virtual/OWNERS
+++ b/services/companion/java/com/android/server/companion/virtual/OWNERS
@@ -2,7 +2,7 @@
set noparent
-ogunwale@google.com
-michaelwr@google.com
+marvinramin@google.com
vladokom@google.com
-marvinramin@google.com
\ No newline at end of file
+ogunwale@google.com
+michaelwr@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c64fb23..54c8ed3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4852,8 +4852,10 @@
} else {
Slog.wtf(TAG, "Mismatched or missing ProcessRecord: " + app + ". Pid: " + pid
+ ". Uid: " + uid);
- killProcess(pid);
- killProcessGroup(uid, pid);
+ if (pid > 0) {
+ killProcess(pid);
+ killProcessGroup(uid, pid);
+ }
mProcessList.noteAppKill(pid, uid,
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE,
ApplicationExitInfo.SUBREASON_UNKNOWN,
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index e7ea0be..9701fc8 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -107,6 +107,7 @@
import android.media.AudioRecordingConfiguration;
import android.media.AudioRoutesInfo;
import android.media.AudioSystem;
+import android.media.AudioTrack;
import android.media.BluetoothProfileConnectionInfo;
import android.media.IAudioDeviceVolumeDispatcher;
import android.media.IAudioFocusDispatcher;
@@ -133,7 +134,9 @@
import android.media.IStrategyPreferredDevicesDispatcher;
import android.media.IStreamAliasingDispatcher;
import android.media.IVolumeController;
-import android.media.LoudnessCodecFormat;
+import android.media.LoudnessCodecConfigurator;
+import android.media.LoudnessCodecInfo;
+import android.media.MediaCodec;
import android.media.MediaMetrics;
import android.media.MediaRecorder.AudioSource;
import android.media.PlayerBase;
@@ -347,7 +350,7 @@
}
/*package*/ boolean isPlatformAutomotive() {
- return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ return mPlatformType == AudioSystem.PLATFORM_AUTOMOTIVE;
}
/** The controller for the volume UI. */
@@ -941,6 +944,8 @@
private final SoundDoseHelper mSoundDoseHelper;
+ private final LoudnessCodecHelper mLoudnessCodecHelper;
+
private final Object mSupportedSystemUsagesLock = new Object();
@GuardedBy("mSupportedSystemUsagesLock")
private @AttributeSystemUsage int[] mSupportedSystemUsages =
@@ -1275,6 +1280,8 @@
readPersistedSettings();
readUserRestrictions();
+ mLoudnessCodecHelper = new LoudnessCodecHelper(this);
+
mPlaybackMonitor =
new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM],
device -> onMuteAwaitConnectionTimeout(device));
@@ -4366,6 +4373,8 @@
mSoundDoseHelper.scheduleMusicActiveCheck();
}
+ mLoudnessCodecHelper.updateCodecParameters(configs);
+
// Update playback active state for all apps in audio mode stack.
// When the audio mode owner becomes active, replace any delayed MSG_UPDATE_AUDIO_MODE
// and request an audio mode update immediately. Upon any other change, queue the message
@@ -10562,44 +10571,43 @@
@Override
public void registerLoudnessCodecUpdatesDispatcher(ILoudnessCodecUpdatesDispatcher dispatcher) {
- // TODO: implement
+ mLoudnessCodecHelper.registerLoudnessCodecUpdatesDispatcher(dispatcher);
}
@Override
public void unregisterLoudnessCodecUpdatesDispatcher(
ILoudnessCodecUpdatesDispatcher dispatcher) {
- // TODO: implement
+ mLoudnessCodecHelper.unregisterLoudnessCodecUpdatesDispatcher(dispatcher);
}
+ /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */
@Override
- public void startLoudnessCodecUpdates(int piid) {
- // TODO: implement
+ public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+ mLoudnessCodecHelper.startLoudnessCodecUpdates(piid, codecInfoList);
}
+ /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */
@Override
public void stopLoudnessCodecUpdates(int piid) {
- // TODO: implement
+ mLoudnessCodecHelper.stopLoudnessCodecUpdates(piid);
}
+ /** @see LoudnessCodecConfigurator#addMediaCodec(MediaCodec) */
@Override
- public void addLoudnesssCodecFormat(int piid, LoudnessCodecFormat format) {
- // TODO: implement
+ public void addLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
+ mLoudnessCodecHelper.addLoudnessCodecInfo(piid, codecInfo);
}
+ /** @see LoudnessCodecConfigurator#removeMediaCodec(MediaCodec) */
@Override
- public void addLoudnesssCodecFormatList(int piid, List<LoudnessCodecFormat> format) {
- // TODO: implement
+ public void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
+ mLoudnessCodecHelper.removeLoudnessCodecInfo(piid, codecInfo);
}
+ /** @see LoudnessCodecConfigurator#getLoudnessCodecParams(AudioTrack, MediaCodec) */
@Override
- public void removeLoudnessCodecFormat(int piid, LoudnessCodecFormat format) {
- // TODO: implement
- }
-
- @Override
- public PersistableBundle getLoudnessParams(int piid, LoudnessCodecFormat format) {
- // TODO: implement
- return null;
+ public PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) {
+ return mLoudnessCodecHelper.getLoudnessParams(piid, codecInfo);
}
//==========================================================================================
diff --git a/services/core/java/com/android/server/audio/LoudnessCodecHelper.java b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
new file mode 100644
index 0000000..3c67e9d
--- /dev/null
+++ b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.audio;
+
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_CARKIT;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEARING_AID;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_WATCH;
+import static android.media.AudioPlaybackConfiguration.PLAYER_DEVICEID_INVALID;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.media.AudioDeviceInfo;
+import android.media.AudioPlaybackConfiguration;
+import android.media.AudioSystem;
+import android.media.ILoudnessCodecUpdatesDispatcher;
+import android.media.LoudnessCodecInfo;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
+import android.os.Binder;
+import android.os.PersistableBundle;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Class to handle the updates in loudness parameters and responsible to generate parameters that
+ * can be set directly on a MediaCodec.
+ */
+public class LoudnessCodecHelper {
+ private static final String TAG = "AS.LoudnessCodecHelper";
+
+ private static final boolean DEBUG = false;
+
+ /**
+ * Property containing a string to set for a custom built in speaker SPL range as defined by
+ * CTA2075. The options that can be set are:
+ * - "small": for max SPL with test signal < 75 dB,
+ * - "medium": for max SPL with test signal between 70 and 90 dB,
+ * - "large": for max SPL with test signal > 85 dB.
+ */
+ private static final String SYSTEM_PROPERTY_SPEAKER_SPL_RANGE_SIZE =
+ "audio.loudness.builtin-speaker-spl-range-size";
+
+ private static final int SPL_RANGE_UNKNOWN = 0;
+ private static final int SPL_RANGE_SMALL = 1;
+ private static final int SPL_RANGE_MEDIUM = 2;
+ private static final int SPL_RANGE_LARGE = 3;
+
+ /** The possible transducer SPL ranges as defined in CTA2075 */
+ @IntDef({
+ SPL_RANGE_UNKNOWN,
+ SPL_RANGE_SMALL,
+ SPL_RANGE_MEDIUM,
+ SPL_RANGE_LARGE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeviceSplRange {}
+
+ private static final class LoudnessRemoteCallbackList extends
+ RemoteCallbackList<ILoudnessCodecUpdatesDispatcher> {
+ private final LoudnessCodecHelper mLoudnessCodecHelper;
+ LoudnessRemoteCallbackList(LoudnessCodecHelper loudnessCodecHelper) {
+ mLoudnessCodecHelper = loudnessCodecHelper;
+ }
+
+ @Override
+ public void onCallbackDied(ILoudnessCodecUpdatesDispatcher callback, Object cookie) {
+ Integer pid = null;
+ if (cookie instanceof Integer) {
+ pid = (Integer) cookie;
+ }
+ if (pid != null) {
+ mLoudnessCodecHelper.removePid(pid);
+ }
+ super.onCallbackDied(callback, cookie);
+ }
+ }
+
+ private final LoudnessRemoteCallbackList mLoudnessUpdateDispatchers =
+ new LoudnessRemoteCallbackList(this);
+
+ private final Object mLock = new Object();
+
+ /** Contains for each started piid the set corresponding to unique registered audio codecs. */
+ @GuardedBy("mLock")
+ private final SparseArray<Set<LoudnessCodecInfo>> mStartedPiids = new SparseArray<>();
+
+ /** Contains the current device id assignment for each piid. */
+ @GuardedBy("mLock")
+ private final SparseIntArray mPiidToDeviceIdCache = new SparseIntArray();
+
+ /** Maps each piid to the owner process of the player. */
+ @GuardedBy("mLock")
+ private final SparseIntArray mPiidToPidCache = new SparseIntArray();
+
+ private final AudioService mAudioService;
+
+ /** Contains the properties necessary to compute the codec loudness related parameters. */
+ private static final class LoudnessCodecInputProperties {
+ private final int mMetadataType;
+
+ private final boolean mIsDownmixing;
+
+ @DeviceSplRange
+ private final int mDeviceSplRange;
+
+ static final class Builder {
+ private int mMetadataType;
+
+ private boolean mIsDownmixing;
+
+ @DeviceSplRange
+ private int mDeviceSplRange;
+
+ Builder setMetadataType(int metadataType) {
+ mMetadataType = metadataType;
+ return this;
+ }
+ Builder setIsDownmixing(boolean isDownmixing) {
+ mIsDownmixing = isDownmixing;
+ return this;
+ }
+ Builder setDeviceSplRange(@DeviceSplRange int deviceSplRange) {
+ mDeviceSplRange = deviceSplRange;
+ return this;
+ }
+
+ LoudnessCodecInputProperties build() {
+ return new LoudnessCodecInputProperties(mMetadataType,
+ mIsDownmixing, mDeviceSplRange);
+ }
+ }
+
+ private LoudnessCodecInputProperties(int metadataType,
+ boolean isDownmixing,
+ @DeviceSplRange int deviceSplRange) {
+ mMetadataType = metadataType;
+ mIsDownmixing = isDownmixing;
+ mDeviceSplRange = deviceSplRange;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ // type check and cast
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final LoudnessCodecInputProperties lcip = (LoudnessCodecInputProperties) obj;
+ return mMetadataType == lcip.mMetadataType
+ && mIsDownmixing == lcip.mIsDownmixing
+ && mDeviceSplRange == lcip.mDeviceSplRange;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mMetadataType, mIsDownmixing, mDeviceSplRange);
+ }
+
+ @Override
+ public String toString() {
+ return "Loudness properties:"
+ + " device SPL range: " + splRangeToString(mDeviceSplRange)
+ + " down-mixing: " + mIsDownmixing
+ + " metadata type: " + mMetadataType;
+ }
+
+ PersistableBundle createLoudnessParameters() {
+ // TODO: create bundle with new parameters
+ return new PersistableBundle();
+ }
+
+ }
+
+ @GuardedBy("mLock")
+ private final HashMap<LoudnessCodecInputProperties, PersistableBundle> mCachedProperties =
+ new HashMap<>();
+
+ LoudnessCodecHelper(@NonNull AudioService audioService) {
+ mAudioService = Objects.requireNonNull(audioService);
+ }
+
+ void registerLoudnessCodecUpdatesDispatcher(ILoudnessCodecUpdatesDispatcher dispatcher) {
+ mLoudnessUpdateDispatchers.register(dispatcher, Binder.getCallingPid());
+ }
+
+ void unregisterLoudnessCodecUpdatesDispatcher(
+ ILoudnessCodecUpdatesDispatcher dispatcher) {
+ mLoudnessUpdateDispatchers.unregister(dispatcher);
+ }
+
+ void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+ if (DEBUG) {
+ Log.d(TAG, "startLoudnessCodecUpdates: piid " + piid + " codecInfos " + codecInfoList);
+ }
+ Set<LoudnessCodecInfo> infoSet;
+ synchronized (mLock) {
+ if (mStartedPiids.contains(piid)) {
+ Log.w(TAG, "Already started loudness updates for piid " + piid);
+ return;
+ }
+ infoSet = new HashSet<>(codecInfoList);
+ mStartedPiids.put(piid, infoSet);
+
+ mPiidToPidCache.put(piid, Binder.getCallingPid());
+ }
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ mAudioService.getActivePlaybackConfigurations().stream().filter(
+ conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent(
+ apc -> updateCodecParametersForConfiguration(apc, infoSet));
+ }
+ }
+
+ void stopLoudnessCodecUpdates(int piid) {
+ if (DEBUG) {
+ Log.d(TAG, "stopLoudnessCodecUpdates: piid " + piid);
+ }
+ synchronized (mLock) {
+ if (!mStartedPiids.contains(piid)) {
+ Log.w(TAG, "Loudness updates are already stopped for piid " + piid);
+ return;
+ }
+ mStartedPiids.remove(piid);
+ mPiidToDeviceIdCache.delete(piid);
+ mPiidToPidCache.delete(piid);
+ }
+ }
+
+ void addLoudnessCodecInfo(int piid, LoudnessCodecInfo info) {
+ if (DEBUG) {
+ Log.d(TAG, "addLoudnessCodecInfo: piid " + piid + " info " + info);
+ }
+
+ Set<LoudnessCodecInfo> infoSet;
+ synchronized (mLock) {
+ if (!mStartedPiids.contains(piid)) {
+ Log.w(TAG, "Cannot add new loudness info for stopped piid " + piid);
+ return;
+ }
+
+ infoSet = mStartedPiids.get(piid);
+ infoSet.add(info);
+ }
+
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ mAudioService.getActivePlaybackConfigurations().stream().filter(
+ conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent(
+ apc -> updateCodecParametersForConfiguration(apc, Set.of(info)));
+ }
+ }
+
+ void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
+ if (DEBUG) {
+ Log.d(TAG, "removeLoudnessCodecInfo: piid " + piid + " info " + codecInfo);
+ }
+ synchronized (mLock) {
+ if (!mStartedPiids.contains(piid)) {
+ Log.w(TAG, "Cannot remove loudness info for stopped piid " + piid);
+ return;
+ }
+ final Set<LoudnessCodecInfo> infoSet = mStartedPiids.get(piid);
+ infoSet.remove(codecInfo);
+ }
+ }
+
+ void removePid(int pid) {
+ if (DEBUG) {
+ Log.d(TAG, "Removing pid " + pid + " from receiving updates");
+ }
+ synchronized (mLock) {
+ for (int i = 0; i < mPiidToPidCache.size(); ++i) {
+ int piid = mPiidToPidCache.keyAt(i);
+ if (mPiidToPidCache.get(piid) == pid) {
+ if (DEBUG) {
+ Log.d(TAG, "Removing piid " + piid);
+ }
+ mStartedPiids.delete(piid);
+ mPiidToDeviceIdCache.delete(piid);
+ }
+ }
+ }
+ }
+
+ PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) {
+ if (DEBUG) {
+ Log.d(TAG, "getLoudnessParams: piid " + piid + " codecInfo " + codecInfo);
+ }
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ final List<AudioPlaybackConfiguration> configs =
+ mAudioService.getActivePlaybackConfigurations();
+
+ for (final AudioPlaybackConfiguration apc : configs) {
+ if (apc.getPlayerInterfaceId() == piid) {
+ final AudioDeviceInfo info = apc.getAudioDeviceInfo();
+ if (info == null) {
+ Log.i(TAG, "Player with piid " + piid + " is not assigned any device");
+ break;
+ }
+ synchronized (mLock) {
+ return getCodecBundle_l(info, codecInfo);
+ }
+ }
+ }
+ }
+
+ // return empty Bundle
+ return new PersistableBundle();
+ }
+
+ /** Method to be called whenever there is a changed in the active playback configurations. */
+ void updateCodecParameters(List<AudioPlaybackConfiguration> configs) {
+ if (DEBUG) {
+ Log.d(TAG, "updateCodecParameters: configs " + configs);
+ }
+
+ List<AudioPlaybackConfiguration> updateApcList = new ArrayList<>();
+ synchronized (mLock) {
+ for (final AudioPlaybackConfiguration apc : configs) {
+ int piid = apc.getPlayerInterfaceId();
+ int cachedDeviceId = mPiidToDeviceIdCache.get(piid, PLAYER_DEVICEID_INVALID);
+ AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo();
+ if (deviceInfo == null) {
+ if (DEBUG) {
+ Log.d(TAG, "No device info for piid: " + piid);
+ }
+ if (cachedDeviceId != PLAYER_DEVICEID_INVALID) {
+ mPiidToDeviceIdCache.delete(piid);
+ if (DEBUG) {
+ Log.d(TAG, "Remove cached device id for piid: " + piid);
+ }
+ }
+ continue;
+ }
+ if (cachedDeviceId == deviceInfo.getId()) {
+ // deviceId did not change
+ if (DEBUG) {
+ Log.d(TAG, "DeviceId " + cachedDeviceId + " for piid: " + piid
+ + " did not change");
+ }
+ continue;
+ }
+ mPiidToDeviceIdCache.put(piid, deviceInfo.getId());
+ if (mStartedPiids.contains(piid)) {
+ updateApcList.add(apc);
+ }
+ }
+ }
+
+ updateApcList.forEach(apc -> updateCodecParametersForConfiguration(apc, null));
+ }
+
+ /** Updates and dispatches the new loudness parameters for the {@code codecInfos} set.
+ *
+ * @param apc the player configuration for which the loudness parameters are updated.
+ * @param codecInfos the codec info for which the parameters are updated. If {@code null},
+ * send updates for all the started codecs assigned to {@code apc}
+ */
+ private void updateCodecParametersForConfiguration(AudioPlaybackConfiguration apc,
+ Set<LoudnessCodecInfo> codecInfos) {
+ if (DEBUG) {
+ Log.d(TAG, "updateCodecParametersForConfiguration apc:" + apc + " codecInfos: "
+ + codecInfos);
+ }
+ final PersistableBundle allBundles = new PersistableBundle();
+ final int piid = apc.getPlayerInterfaceId();
+ synchronized (mLock) {
+ if (codecInfos == null) {
+ codecInfos = mStartedPiids.get(piid);
+ }
+
+ final AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo();
+ if (codecInfos != null && deviceInfo != null) {
+ for (LoudnessCodecInfo info : codecInfos) {
+ allBundles.putPersistableBundle(Integer.toString(info.mediaCodecHashCode),
+ getCodecBundle_l(deviceInfo, info));
+ }
+ }
+ }
+
+ if (!allBundles.isDefinitelyEmpty()) {
+ if (DEBUG) {
+ Log.d(TAG, "Dispatching for piid: " + piid + " bundle: " + allBundles);
+ }
+ dispatchNewLoudnessParameters(piid, allBundles);
+ }
+ }
+
+ private void dispatchNewLoudnessParameters(int piid, PersistableBundle bundle) {
+ if (DEBUG) {
+ Log.d(TAG, "dispatchNewLoudnessParameters: piid " + piid);
+ }
+ final int nbDispatchers = mLoudnessUpdateDispatchers.beginBroadcast();
+ for (int i = 0; i < nbDispatchers; ++i) {
+ try {
+ mLoudnessUpdateDispatchers.getBroadcastItem(i)
+ .dispatchLoudnessCodecParameterChange(piid, bundle);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error dispatching for piid: " + piid + " bundle: " + bundle , e);
+ }
+ }
+ mLoudnessUpdateDispatchers.finishBroadcast();
+ }
+
+ @GuardedBy("mLock")
+ private PersistableBundle getCodecBundle_l(AudioDeviceInfo deviceInfo,
+ LoudnessCodecInfo codecInfo) {
+ LoudnessCodecInputProperties.Builder builder = new LoudnessCodecInputProperties.Builder();
+ LoudnessCodecInputProperties prop = builder.setDeviceSplRange(getDeviceSplRange(deviceInfo))
+ .setIsDownmixing(codecInfo.isDownmixing)
+ .setMetadataType(codecInfo.metadataType)
+ .build();
+
+ if (mCachedProperties.containsKey(prop)) {
+ return mCachedProperties.get(prop);
+ }
+ final PersistableBundle codecBundle = prop.createLoudnessParameters();
+ mCachedProperties.put(prop, codecBundle);
+ return codecBundle;
+ }
+
+ @DeviceSplRange
+ private int getDeviceSplRange(AudioDeviceInfo deviceInfo) {
+ final int internalDeviceType = deviceInfo.getInternalType();
+ if (internalDeviceType == AudioSystem.DEVICE_OUT_SPEAKER) {
+ final String splRange = SystemProperties.get(
+ SYSTEM_PROPERTY_SPEAKER_SPL_RANGE_SIZE, "unknown");
+ if (!splRange.equals("unknown")) {
+ return stringToSplRange(splRange);
+ }
+
+ @DeviceSplRange int result = SPL_RANGE_SMALL; // default for phone/tablet/watch
+ if (mAudioService.isPlatformAutomotive() || mAudioService.isPlatformTelevision()) {
+ result = SPL_RANGE_MEDIUM;
+ }
+
+ return result;
+ } else if (internalDeviceType == AudioSystem.DEVICE_OUT_USB_HEADSET
+ || internalDeviceType == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE
+ || internalDeviceType == AudioSystem.DEVICE_OUT_WIRED_HEADSET
+ || (AudioSystem.isBluetoothDevice(internalDeviceType)
+ && mAudioService.getBluetoothAudioDeviceCategory(deviceInfo.getAddress(),
+ AudioSystem.isBluetoothLeDevice(internalDeviceType))
+ == AUDIO_DEVICE_CATEGORY_HEADPHONES)) {
+ return SPL_RANGE_LARGE;
+ } else if (AudioSystem.isBluetoothDevice(internalDeviceType)) {
+ final int audioDeviceType = mAudioService.getBluetoothAudioDeviceCategory(
+ deviceInfo.getAddress(), AudioSystem.isBluetoothLeDevice(internalDeviceType));
+ if (audioDeviceType == AUDIO_DEVICE_CATEGORY_CARKIT) {
+ return SPL_RANGE_MEDIUM;
+ } else if (audioDeviceType == AUDIO_DEVICE_CATEGORY_WATCH) {
+ return SPL_RANGE_SMALL;
+ } else if (audioDeviceType == AUDIO_DEVICE_CATEGORY_HEARING_AID) {
+ return SPL_RANGE_SMALL;
+ }
+ }
+
+ return SPL_RANGE_UNKNOWN;
+ }
+
+ private static String splRangeToString(@DeviceSplRange int splRange) {
+ switch (splRange) {
+ case SPL_RANGE_LARGE: return "large";
+ case SPL_RANGE_MEDIUM: return "medium";
+ case SPL_RANGE_SMALL: return "small";
+ default: return "unknown";
+ }
+ }
+
+ @DeviceSplRange
+ private static int stringToSplRange(String splRange) {
+ switch (splRange) {
+ case "large": return SPL_RANGE_LARGE;
+ case "medium": return SPL_RANGE_MEDIUM;
+ case "small": return SPL_RANGE_SMALL;
+ default: return SPL_RANGE_UNKNOWN;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index af33de0..50ab3f8 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -63,13 +63,27 @@
*/
int SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED = 5;
+ /**
+ * Indicating that the supported device states have changed because an external display was
+ * added.
+ */
+ int SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED = 6;
+
+ /**
+ * Indicating that the supported device states have changed because an external display was
+ * removed.
+ */
+ int SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED = 7;
+
@IntDef(prefix = { "SUPPORTED_DEVICE_STATES_CHANGED_" }, value = {
SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT,
SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED,
SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL,
SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL,
SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED,
- SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED
+ SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED,
+ SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED,
+ SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED
})
@Retention(RetentionPolicy.SOURCE)
@interface SupportedStatesUpdatedReason {}
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index ba321ae..db636d6 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -17,6 +17,8 @@
package com.android.server.display;
import static com.android.server.display.DisplayDeviceInfo.TOUCH_NONE;
+import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS;
+import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -33,6 +35,7 @@
import com.android.server.display.layout.Layout;
import com.android.server.display.mode.DisplayModeDirector;
+import com.android.server.wm.utils.DisplayInfoOverrides;
import com.android.server.wm.utils.InsetUtils;
import java.io.PrintWriter;
@@ -252,24 +255,8 @@
public DisplayInfo getDisplayInfoLocked() {
if (mInfo.get() == null) {
DisplayInfo info = new DisplayInfo();
- info.copyFrom(mBaseDisplayInfo);
- if (mOverrideDisplayInfo != null) {
- info.appWidth = mOverrideDisplayInfo.appWidth;
- info.appHeight = mOverrideDisplayInfo.appHeight;
- info.smallestNominalAppWidth = mOverrideDisplayInfo.smallestNominalAppWidth;
- info.smallestNominalAppHeight = mOverrideDisplayInfo.smallestNominalAppHeight;
- info.largestNominalAppWidth = mOverrideDisplayInfo.largestNominalAppWidth;
- info.largestNominalAppHeight = mOverrideDisplayInfo.largestNominalAppHeight;
- info.logicalWidth = mOverrideDisplayInfo.logicalWidth;
- info.logicalHeight = mOverrideDisplayInfo.logicalHeight;
- info.physicalXDpi = mOverrideDisplayInfo.physicalXDpi;
- info.physicalYDpi = mOverrideDisplayInfo.physicalYDpi;
- info.rotation = mOverrideDisplayInfo.rotation;
- info.displayCutout = mOverrideDisplayInfo.displayCutout;
- info.logicalDensityDpi = mOverrideDisplayInfo.logicalDensityDpi;
- info.roundedCorners = mOverrideDisplayInfo.roundedCorners;
- info.displayShape = mOverrideDisplayInfo.displayShape;
- }
+ copyDisplayInfoFields(info, mBaseDisplayInfo, mOverrideDisplayInfo,
+ WM_OVERRIDE_FIELDS);
mInfo.set(info);
}
return mInfo.get();
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index aa80612..5cfbf26 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -86,6 +86,10 @@
Flags.FLAG_BRIGHTNESS_INT_RANGE_USER_PERCEPTION,
Flags::brightnessIntRangeUserPerception);
+ private final FlagState mVsyncProximityVote = new FlagState(
+ Flags.FLAG_ENABLE_EXTERNAL_VSYNC_PROXIMITY_VOTE,
+ Flags::enableExternalVsyncProximityVote);
+
/** Returns whether connected display management is enabled or not. */
public boolean isConnectedDisplayManagementEnabled() {
return mConnectedDisplayManagementFlagState.isEnabled();
@@ -170,6 +174,10 @@
return mBrightnessIntRangeUserPerceptionFlagState.isEnabled();
}
+ public boolean isExternalVsyncProximityVoteEnabled() {
+ return mVsyncProximityVote.isEnabled();
+ }
+
/**
* dumps all flagstates
* @param pw printWriter
@@ -188,6 +196,7 @@
pw.println(" " + mPowerThrottlingClamperFlagState);
pw.println(" " + mSmallAreaDetectionFlagState);
pw.println(" " + mBrightnessIntRangeUserPerceptionFlagState);
+ pw.println(" " + mVsyncProximityVote);
}
private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index e28b415..d95bdae 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -112,3 +112,11 @@
bug: "183655602"
is_fixed_read_only: true
}
+
+flag {
+ name: "enable_external_vsync_proximity_vote"
+ namespace: "display_manager"
+ description: "Feature flag for external vsync proximity vote"
+ bug: "284866750"
+ is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/display/mode/BaseModeRefreshRateVote.java b/services/core/java/com/android/server/display/mode/BaseModeRefreshRateVote.java
new file mode 100644
index 0000000..c04df64
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/BaseModeRefreshRateVote.java
@@ -0,0 +1,59 @@
+/*
+ * 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.server.display.mode;
+
+import java.util.Objects;
+
+class BaseModeRefreshRateVote implements Vote {
+
+ /**
+ * The preferred refresh rate selected by the app. It is used to validate that the summary
+ * refresh rate ranges include this value, and are not restricted by a lower priority vote.
+ */
+ final float mAppRequestBaseModeRefreshRate;
+
+ BaseModeRefreshRateVote(float baseModeRefreshRate) {
+ mAppRequestBaseModeRefreshRate = baseModeRefreshRate;
+ }
+
+ @Override
+ public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+ if (summary.appRequestBaseModeRefreshRate == 0f
+ && mAppRequestBaseModeRefreshRate > 0f) {
+ summary.appRequestBaseModeRefreshRate = mAppRequestBaseModeRefreshRate;
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof BaseModeRefreshRateVote that)) return false;
+ return Float.compare(that.mAppRequestBaseModeRefreshRate,
+ mAppRequestBaseModeRefreshRate) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAppRequestBaseModeRefreshRate);
+ }
+
+ @Override
+ public String toString() {
+ return "BaseModeRefreshRateVote{ mAppRequestBaseModeRefreshRate="
+ + mAppRequestBaseModeRefreshRate + " }";
+ }
+}
diff --git a/services/core/java/com/android/server/display/mode/CombinedVote.java b/services/core/java/com/android/server/display/mode/CombinedVote.java
new file mode 100644
index 0000000..f24fe3a
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/CombinedVote.java
@@ -0,0 +1,51 @@
+/*
+ * 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.server.display.mode;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+class CombinedVote implements Vote {
+ final List<Vote> mVotes;
+
+ CombinedVote(List<Vote> votes) {
+ mVotes = Collections.unmodifiableList(votes);
+ }
+
+ @Override
+ public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+ mVotes.forEach(vote -> vote.updateSummary(summary));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CombinedVote that)) return false;
+ return Objects.equals(mVotes, that.mVotes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mVotes);
+ }
+
+ @Override
+ public String toString() {
+ return "CombinedVote{ mVotes=" + mVotes + " }";
+ }
+}
diff --git a/services/core/java/com/android/server/display/mode/DisableRefreshRateSwitchingVote.java b/services/core/java/com/android/server/display/mode/DisableRefreshRateSwitchingVote.java
new file mode 100644
index 0000000..2fc5590
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/DisableRefreshRateSwitchingVote.java
@@ -0,0 +1,56 @@
+/*
+ * 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.server.display.mode;
+
+import java.util.Objects;
+
+class DisableRefreshRateSwitchingVote implements Vote {
+
+ /**
+ * Whether refresh rate switching should be disabled (i.e. the refresh rate range is
+ * a single value).
+ */
+ final boolean mDisableRefreshRateSwitching;
+
+ DisableRefreshRateSwitchingVote(boolean disableRefreshRateSwitching) {
+ mDisableRefreshRateSwitching = disableRefreshRateSwitching;
+ }
+
+ @Override
+ public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+ summary.disableRefreshRateSwitching =
+ summary.disableRefreshRateSwitching || mDisableRefreshRateSwitching;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DisableRefreshRateSwitchingVote that)) return false;
+ return mDisableRefreshRateSwitching == that.mDisableRefreshRateSwitching;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDisableRefreshRateSwitching);
+ }
+
+ @Override
+ public String toString() {
+ return "DisableRefreshRateSwitchingVote{ mDisableRefreshRateSwitching="
+ + mDisableRefreshRateSwitching + " }";
+ }
+}
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 9587f55..8eb03ec 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -262,7 +262,7 @@
mVotesStorage.setLoggingEnabled(loggingEnabled);
}
- private static final class VoteSummary {
+ static final class VoteSummary {
public float minPhysicalRefreshRate;
public float maxPhysicalRefreshRate;
public float minRenderFrameRate;
@@ -274,7 +274,12 @@
public boolean disableRefreshRateSwitching;
public float appRequestBaseModeRefreshRate;
- VoteSummary() {
+ public List<SupportedModesVote.SupportedMode> supportedModes;
+
+ final boolean mIsDisplayResolutionRangeVotingEnabled;
+
+ VoteSummary(boolean isDisplayResolutionRangeVotingEnabled) {
+ mIsDisplayResolutionRangeVotingEnabled = isDisplayResolutionRangeVotingEnabled;
reset();
}
@@ -322,46 +327,7 @@
continue;
}
- // For physical refresh rates, just use the tightest bounds of all the votes.
- // The refresh rate cannot be lower than the minimal render frame rate.
- final float minPhysicalRefreshRate = Math.max(vote.refreshRateRanges.physical.min,
- vote.refreshRateRanges.render.min);
- summary.minPhysicalRefreshRate = Math.max(summary.minPhysicalRefreshRate,
- minPhysicalRefreshRate);
- summary.maxPhysicalRefreshRate = Math.min(summary.maxPhysicalRefreshRate,
- vote.refreshRateRanges.physical.max);
-
- // Same goes to render frame rate, but frame rate cannot exceed the max physical
- // refresh rate
- final float maxRenderFrameRate = Math.min(vote.refreshRateRanges.render.max,
- vote.refreshRateRanges.physical.max);
- summary.minRenderFrameRate = Math.max(summary.minRenderFrameRate,
- vote.refreshRateRanges.render.min);
- summary.maxRenderFrameRate = Math.min(summary.maxRenderFrameRate, maxRenderFrameRate);
-
- // For display size, disable refresh rate switching and base mode refresh rate use only
- // the first vote we come across (i.e. the highest priority vote that includes the
- // attribute).
- if (vote.height > 0 && vote.width > 0) {
- if (summary.width == Vote.INVALID_SIZE && summary.height == Vote.INVALID_SIZE) {
- summary.width = vote.width;
- summary.height = vote.height;
- summary.minWidth = vote.minWidth;
- summary.minHeight = vote.minHeight;
- } else if (mIsDisplayResolutionRangeVotingEnabled) {
- summary.width = Math.min(summary.width, vote.width);
- summary.height = Math.min(summary.height, vote.height);
- summary.minWidth = Math.max(summary.minWidth, vote.minWidth);
- summary.minHeight = Math.max(summary.minHeight, vote.minHeight);
- }
- }
- if (!summary.disableRefreshRateSwitching && vote.disableRefreshRateSwitching) {
- summary.disableRefreshRateSwitching = true;
- }
- if (summary.appRequestBaseModeRefreshRate == 0f
- && vote.appRequestBaseModeRefreshRate > 0f) {
- summary.appRequestBaseModeRefreshRate = vote.appRequestBaseModeRefreshRate;
- }
+ vote.updateSummary(summary);
if (mLoggingEnabled) {
Slog.w(TAG, "Vote summary for priority " + Vote.priorityToString(priority)
@@ -443,7 +409,7 @@
ArrayList<Display.Mode> availableModes = new ArrayList<>();
availableModes.add(defaultMode);
- VoteSummary primarySummary = new VoteSummary();
+ VoteSummary primarySummary = new VoteSummary(mIsDisplayResolutionRangeVotingEnabled);
int lowestConsideredPriority = Vote.MIN_PRIORITY;
int highestConsideredPriority = Vote.MAX_PRIORITY;
@@ -526,7 +492,7 @@
+ "]");
}
- VoteSummary appRequestSummary = new VoteSummary();
+ VoteSummary appRequestSummary = new VoteSummary(mIsDisplayResolutionRangeVotingEnabled);
summarizeVotes(
votes,
Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF,
diff --git a/services/core/java/com/android/server/display/mode/RefreshRateVote.java b/services/core/java/com/android/server/display/mode/RefreshRateVote.java
new file mode 100644
index 0000000..173b3c5
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/RefreshRateVote.java
@@ -0,0 +1,120 @@
+/*
+ * 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.server.display.mode;
+
+import java.util.Objects;
+
+
+/**
+ * Information about the refresh rate frame rate ranges DM would like to set the display to.
+ */
+abstract class RefreshRateVote implements Vote {
+ final float mMinRefreshRate;
+
+ final float mMaxRefreshRate;
+
+ RefreshRateVote(float minRefreshRate, float maxRefreshRate) {
+ mMinRefreshRate = minRefreshRate;
+ mMaxRefreshRate = maxRefreshRate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof RefreshRateVote that)) return false;
+ return Float.compare(that.mMinRefreshRate, mMinRefreshRate) == 0
+ && Float.compare(that.mMaxRefreshRate, mMaxRefreshRate) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mMinRefreshRate, mMaxRefreshRate);
+ }
+
+ @Override
+ public String toString() {
+ return "RefreshRateVote{ mMinRefreshRate=" + mMinRefreshRate
+ + ", mMaxRefreshRate=" + mMaxRefreshRate + " }";
+ }
+
+ static class RenderVote extends RefreshRateVote {
+ RenderVote(float minRefreshRate, float maxRefreshRate) {
+ super(minRefreshRate, maxRefreshRate);
+ }
+
+ /**
+ * Summary: minRender minPhysical maxRender
+ * v v v
+ * -------------------|---------------------"-----------------------------|---------
+ * ^ ^ ^* ^ ^
+ * Vote: min(ignored) min(applied) min(applied+physical) max(applied) max(ignored)
+ */
+ @Override
+ public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+ summary.minRenderFrameRate = Math.max(summary.minRenderFrameRate, mMinRefreshRate);
+ summary.maxRenderFrameRate = Math.min(summary.maxRenderFrameRate, mMaxRefreshRate);
+ // Physical refresh rate cannot be lower than the minimal render frame rate.
+ summary.minPhysicalRefreshRate = Math.max(summary.minPhysicalRefreshRate,
+ mMinRefreshRate);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RefreshRateVote.RenderVote)) return false;
+ return super.equals(o);
+ }
+
+ @Override
+ public String toString() {
+ return "RenderVote{ " + super.toString() + " }";
+ }
+ }
+
+ static class PhysicalVote extends RefreshRateVote {
+ PhysicalVote(float minRefreshRate, float maxRefreshRate) {
+ super(minRefreshRate, maxRefreshRate);
+ }
+
+ /**
+ * Summary: minPhysical maxRender maxPhysical
+ * v v v
+ * -------------------"-----------------------------|----------------------"----------
+ * ^ ^ ^* ^ ^
+ * Vote: min(ignored) min(applied) max(applied+render) max(applied) max(ignored)
+ */
+ @Override
+ public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+ summary.minPhysicalRefreshRate = Math.max(summary.minPhysicalRefreshRate,
+ mMinRefreshRate);
+ summary.maxPhysicalRefreshRate = Math.min(summary.maxPhysicalRefreshRate,
+ mMaxRefreshRate);
+ // Render frame rate cannot exceed the max physical refresh rate
+ summary.maxRenderFrameRate = Math.min(summary.maxRenderFrameRate, mMaxRefreshRate);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RefreshRateVote.PhysicalVote)) return false;
+ return super.equals(o);
+ }
+
+ @Override
+ public String toString() {
+ return "PhysicalVote{ " + super.toString() + " }";
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/display/mode/SizeVote.java b/services/core/java/com/android/server/display/mode/SizeVote.java
new file mode 100644
index 0000000..a9b18a5
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/SizeVote.java
@@ -0,0 +1,88 @@
+/*
+ * 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.server.display.mode;
+
+import java.util.Objects;
+
+class SizeVote implements Vote {
+
+ /**
+ * The requested width of the display in pixels;
+ */
+ final int mWidth;
+
+ /**
+ * The requested height of the display in pixels;
+ */
+ final int mHeight;
+
+ /**
+ * Min requested width of the display in pixels;
+ */
+ final int mMinWidth;
+
+ /**
+ * Min requested height of the display in pixels;
+ */
+ final int mMinHeight;
+
+ SizeVote(int width, int height, int minWidth, int minHeight) {
+ mWidth = width;
+ mHeight = height;
+ mMinWidth = minWidth;
+ mMinHeight = minHeight;
+ }
+
+ @Override
+ public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+ if (mHeight > 0 && mWidth > 0) {
+ // For display size, disable refresh rate switching and base mode refresh rate use
+ // only the first vote we come across (i.e. the highest priority vote that includes
+ // the attribute).
+ if (summary.width == Vote.INVALID_SIZE && summary.height == Vote.INVALID_SIZE) {
+ summary.width = mWidth;
+ summary.height = mHeight;
+ summary.minWidth = mMinWidth;
+ summary.minHeight = mMinHeight;
+ } else if (summary.mIsDisplayResolutionRangeVotingEnabled) {
+ summary.width = Math.min(summary.width, mWidth);
+ summary.height = Math.min(summary.height, mHeight);
+ summary.minWidth = Math.max(summary.minWidth, mMinWidth);
+ summary.minHeight = Math.max(summary.minHeight, mMinHeight);
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SizeVote sizeVote)) return false;
+ return mWidth == sizeVote.mWidth && mHeight == sizeVote.mHeight
+ && mMinWidth == sizeVote.mMinWidth && mMinHeight == sizeVote.mMinHeight;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mWidth, mHeight, mMinWidth, mMinHeight);
+ }
+
+ @Override
+ public String toString() {
+ return "SizeVote{ mWidth=" + mWidth + ", mHeight=" + mHeight
+ + ", mMinWidth=" + mMinWidth + ", mMinHeight=" + mMinHeight + " }";
+ }
+}
diff --git a/services/core/java/com/android/server/display/mode/SupportedModesVote.java b/services/core/java/com/android/server/display/mode/SupportedModesVote.java
new file mode 100644
index 0000000..b31461f
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/SupportedModesVote.java
@@ -0,0 +1,92 @@
+/*
+ * 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.server.display.mode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+class SupportedModesVote implements Vote {
+
+ final List<SupportedMode> mSupportedModes;
+
+ SupportedModesVote(List<SupportedMode> supportedModes) {
+ mSupportedModes = Collections.unmodifiableList(supportedModes);
+ }
+
+ /**
+ * Summary should have subset of supported modes.
+ * If Vote1.supportedModes=(A,B), Vote2.supportedModes=(B,C) then summary.supportedModes=(B)
+ * If summary.supportedModes==null then there is no restriction on supportedModes
+ */
+ @Override
+ public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+ if (summary.supportedModes == null) {
+ summary.supportedModes = new ArrayList<>(mSupportedModes);
+ } else {
+ summary.supportedModes.retainAll(mSupportedModes);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SupportedModesVote that)) return false;
+ return mSupportedModes.equals(that.mSupportedModes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSupportedModes);
+ }
+
+ @Override
+ public String toString() {
+ return "SupportedModesVote{ mSupportedModes=" + mSupportedModes + " }";
+ }
+
+ static class SupportedMode {
+ final float mPeakRefreshRate;
+ final float mVsyncRate;
+
+
+ SupportedMode(float peakRefreshRate, float vsyncRate) {
+ mPeakRefreshRate = peakRefreshRate;
+ mVsyncRate = vsyncRate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SupportedMode that)) return false;
+ return Float.compare(that.mPeakRefreshRate, mPeakRefreshRate) == 0
+ && Float.compare(that.mVsyncRate, mVsyncRate) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mPeakRefreshRate, mVsyncRate);
+ }
+
+ @Override
+ public String toString() {
+ return "SupportedMode{ mPeakRefreshRate=" + mPeakRefreshRate
+ + ", mVsyncRate=" + mVsyncRate + " }";
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java
index b6a6069..c1cdd69 100644
--- a/services/core/java/com/android/server/display/mode/Vote.java
+++ b/services/core/java/com/android/server/display/mode/Vote.java
@@ -16,15 +16,13 @@
package com.android.server.display.mode;
-import android.view.SurfaceControl;
+import java.util.List;
-import java.util.Objects;
-
-final class Vote {
+interface Vote {
// DEFAULT_RENDER_FRAME_RATE votes for render frame rate [0, DEFAULT]. As the lowest
// priority vote, it's overridden by all other considerations. It acts to set a default
// frame rate for a device.
- static final int PRIORITY_DEFAULT_RENDER_FRAME_RATE = 0;
+ int PRIORITY_DEFAULT_RENDER_FRAME_RATE = 0;
// PRIORITY_FLICKER_REFRESH_RATE votes for a single refresh rate like [60,60], [90,90] or
// null. It is used to set a preferred refresh rate value in case the higher priority votes
@@ -32,21 +30,21 @@
static final int PRIORITY_FLICKER_REFRESH_RATE = 1;
// High-brightness-mode may need a specific range of refresh-rates to function properly.
- static final int PRIORITY_HIGH_BRIGHTNESS_MODE = 2;
+ int PRIORITY_HIGH_BRIGHTNESS_MODE = 2;
// SETTING_MIN_RENDER_FRAME_RATE is used to propose a lower bound of the render frame rate.
// It votes [minRefreshRate, Float.POSITIVE_INFINITY]
- static final int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
+ int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
// User setting preferred display resolution.
- static final int PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE = 4;
+ int PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE = 4;
// APP_REQUEST_RENDER_FRAME_RATE_RANGE is used to for internal apps to limit the render
// frame rate in certain cases, mostly to preserve power.
// @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate
// @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate
// It votes to [preferredMinRefreshRate, preferredMaxRefreshRate].
- static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 5;
+ int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 5;
// We split the app request into different priorities in case we can satisfy one desire
// without the other.
@@ -72,181 +70,100 @@
// The preferred refresh rate is set on the main surface of the app outside of
// DisplayModeDirector.
// @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded
- static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 6;
+ int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 6;
- static final int PRIORITY_APP_REQUEST_SIZE = 7;
+ int PRIORITY_APP_REQUEST_SIZE = 7;
// SETTING_PEAK_RENDER_FRAME_RATE has a high priority and will restrict the bounds of the
// rest of low priority voters. It votes [0, max(PEAK, MIN)]
- static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 8;
+ int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 8;
// Restrict all displays to 60Hz when external display is connected. It votes [59Hz, 61Hz].
- static final int PRIORITY_SYNCHRONIZED_REFRESH_RATE = 9;
+ int PRIORITY_SYNCHRONIZED_REFRESH_RATE = 9;
// Restrict displays max available resolution and refresh rates. It votes [0, LIMIT]
- static final int PRIORITY_LIMIT_MODE = 10;
+ int PRIORITY_LIMIT_MODE = 10;
// To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh
// rate to max value (same as for PRIORITY_UDFPS) on lock screen
- static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 11;
+ int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 11;
// For concurrent displays we want to limit refresh rate on all displays
- static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 12;
+ int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 12;
// LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
// Settings.Global.LOW_POWER_MODE is on.
- static final int PRIORITY_LOW_POWER_MODE = 13;
+ int PRIORITY_LOW_POWER_MODE = 13;
// PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
// higher priority voters' result is a range, it will fix the rate to a single choice.
// It's used to avoid refresh rate switches in certain conditions which may result in the
// user seeing the display flickering when the switches occur.
- static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 14;
+ int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 14;
// Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
- static final int PRIORITY_SKIN_TEMPERATURE = 15;
+ int PRIORITY_SKIN_TEMPERATURE = 15;
// The proximity sensor needs the refresh rate to be locked in order to function, so this is
// set to a high priority.
- static final int PRIORITY_PROXIMITY = 16;
+ int PRIORITY_PROXIMITY = 16;
// The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
// to function, so this needs to be the highest priority of all votes.
- static final int PRIORITY_UDFPS = 17;
+ int PRIORITY_UDFPS = 17;
// Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
// APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
- static final int MIN_PRIORITY = PRIORITY_DEFAULT_RENDER_FRAME_RATE;
- static final int MAX_PRIORITY = PRIORITY_UDFPS;
+ int MIN_PRIORITY = PRIORITY_DEFAULT_RENDER_FRAME_RATE;
+ int MAX_PRIORITY = PRIORITY_UDFPS;
// The cutoff for the app request refresh rate range. Votes with priorities lower than this
// value will not be considered when constructing the app request refresh rate range.
- static final int APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF =
+ int APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF =
PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE;
/**
* A value signifying an invalid width or height in a vote.
*/
- static final int INVALID_SIZE = -1;
+ int INVALID_SIZE = -1;
- /**
- * The requested width of the display in pixels, or INVALID_SIZE;
- */
- public final int width;
- /**
- * The requested height of the display in pixels, or INVALID_SIZE;
- */
- public final int height;
- /**
- * Min requested width of the display in pixels, or 0;
- */
- public final int minWidth;
- /**
- * Min requested height of the display in pixels, or 0;
- */
- public final int minHeight;
- /**
- * Information about the refresh rate frame rate ranges DM would like to set the display to.
- */
- public final SurfaceControl.RefreshRateRanges refreshRateRanges;
-
- /**
- * Whether refresh rate switching should be disabled (i.e. the refresh rate range is
- * a single value).
- */
- public final boolean disableRefreshRateSwitching;
-
- /**
- * The preferred refresh rate selected by the app. It is used to validate that the summary
- * refresh rate ranges include this value, and are not restricted by a lower priority vote.
- */
- public final float appRequestBaseModeRefreshRate;
+ void updateSummary(DisplayModeDirector.VoteSummary summary);
static Vote forPhysicalRefreshRates(float minRefreshRate, float maxRefreshRate) {
- return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
- /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
- /* minPhysicalRefreshRate= */ minRefreshRate,
- /* maxPhysicalRefreshRate= */ maxRefreshRate,
- /* minRenderFrameRate= */ 0,
- /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
- /* disableRefreshRateSwitching= */ minRefreshRate == maxRefreshRate,
- /* baseModeRefreshRate= */ 0f);
+ return new CombinedVote(
+ List.of(
+ new RefreshRateVote.PhysicalVote(minRefreshRate, maxRefreshRate),
+ new DisableRefreshRateSwitchingVote(minRefreshRate == maxRefreshRate)
+ )
+ );
}
static Vote forRenderFrameRates(float minFrameRate, float maxFrameRate) {
- return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
- /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
- /* minPhysicalRefreshRate= */ 0,
- /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
- minFrameRate,
- maxFrameRate,
- /* disableRefreshRateSwitching= */ false,
- /* baseModeRefreshRate= */ 0f);
+ return new RefreshRateVote.RenderVote(minFrameRate, maxFrameRate);
}
static Vote forSize(int width, int height) {
- return new Vote(/* minWidth= */ width, /* minHeight= */ height,
- width, height,
- /* minPhysicalRefreshRate= */ 0,
- /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
- /* minRenderFrameRate= */ 0,
- /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
- /* disableRefreshRateSwitching= */ false,
- /* baseModeRefreshRate= */ 0f);
+ return new SizeVote(width, height, width, height);
}
static Vote forSizeAndPhysicalRefreshRatesRange(int minWidth, int minHeight,
int width, int height, float minRefreshRate, float maxRefreshRate) {
- return new Vote(minWidth, minHeight,
- width, height,
- minRefreshRate,
- maxRefreshRate,
- /* minRenderFrameRate= */ 0,
- /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
- /* disableRefreshRateSwitching= */ minRefreshRate == maxRefreshRate,
- /* baseModeRefreshRate= */ 0f);
+ return new CombinedVote(
+ List.of(
+ new SizeVote(width, height, minWidth, minHeight),
+ new RefreshRateVote.PhysicalVote(minRefreshRate, maxRefreshRate),
+ new DisableRefreshRateSwitchingVote(minRefreshRate == maxRefreshRate)
+ )
+ );
}
static Vote forDisableRefreshRateSwitching() {
- return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
- /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
- /* minPhysicalRefreshRate= */ 0,
- /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
- /* minRenderFrameRate= */ 0,
- /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
- /* disableRefreshRateSwitching= */ true,
- /* baseModeRefreshRate= */ 0f);
+ return new DisableRefreshRateSwitchingVote(true);
}
static Vote forBaseModeRefreshRate(float baseModeRefreshRate) {
- return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
- /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
- /* minPhysicalRefreshRate= */ 0,
- /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
- /* minRenderFrameRate= */ 0,
- /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
- /* disableRefreshRateSwitching= */ false,
- /* baseModeRefreshRate= */ baseModeRefreshRate);
- }
-
- private Vote(int minWidth, int minHeight,
- int width, int height,
- float minPhysicalRefreshRate,
- float maxPhysicalRefreshRate,
- float minRenderFrameRate,
- float maxRenderFrameRate,
- boolean disableRefreshRateSwitching,
- float baseModeRefreshRate) {
- this.minWidth = minWidth;
- this.minHeight = minHeight;
- this.width = width;
- this.height = height;
- this.refreshRateRanges = new SurfaceControl.RefreshRateRanges(
- new SurfaceControl.RefreshRateRange(minPhysicalRefreshRate, maxPhysicalRefreshRate),
- new SurfaceControl.RefreshRateRange(minRenderFrameRate, maxRenderFrameRate));
- this.disableRefreshRateSwitching = disableRefreshRateSwitching;
- this.appRequestBaseModeRefreshRate = baseModeRefreshRate;
+ return new BaseModeRefreshRateVote(baseModeRefreshRate);
}
static String priorityToString(int priority) {
@@ -291,33 +208,4 @@
return Integer.toString(priority);
}
}
-
- @Override
- public String toString() {
- return "Vote: {"
- + "minWidth: " + minWidth + ", minHeight: " + minHeight
- + ", width: " + width + ", height: " + height
- + ", refreshRateRanges: " + refreshRateRanges
- + ", disableRefreshRateSwitching: " + disableRefreshRateSwitching
- + ", appRequestBaseModeRefreshRate: " + appRequestBaseModeRefreshRate + "}";
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(minWidth, minHeight, width, height, refreshRateRanges,
- disableRefreshRateSwitching, appRequestBaseModeRefreshRate);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof Vote)) return false;
- final var vote = (Vote) o;
- return minWidth == vote.minWidth && minHeight == vote.minHeight
- && width == vote.width && height == vote.height
- && disableRefreshRateSwitching == vote.disableRefreshRateSwitching
- && Float.compare(vote.appRequestBaseModeRefreshRate,
- appRequestBaseModeRefreshRate) == 0
- && refreshRateRanges.equals(vote.refreshRateRanges);
- }
}
diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java
index 49c587a..95fb8fc 100644
--- a/services/core/java/com/android/server/display/mode/VotesStorage.java
+++ b/services/core/java/com/android/server/display/mode/VotesStorage.java
@@ -157,13 +157,19 @@
}
}
- private int getMaxPhysicalRefreshRate(@Nullable Vote vote) {
+ private static int getMaxPhysicalRefreshRate(@Nullable Vote vote) {
if (vote == null) {
return -1;
- } else if (vote.refreshRateRanges.physical.max == Float.POSITIVE_INFINITY) {
- return 1000; // for visualisation, otherwise e.g. -1 -> 60 will be unnoticeable
+ } else if (vote instanceof RefreshRateVote.PhysicalVote physicalVote) {
+ return (int) physicalVote.mMaxRefreshRate;
+ } else if (vote instanceof CombinedVote combinedVote) {
+ return combinedVote.mVotes.stream()
+ .filter(v -> v instanceof RefreshRateVote.PhysicalVote)
+ .map(pv -> (int) (((RefreshRateVote.PhysicalVote) pv).mMaxRefreshRate))
+ .min(Integer::compare)
+ .orElse(1000); // for visualisation
}
- return (int) vote.refreshRateRanges.physical.max;
+ return 1000; // for visualisation, otherwise e.g. -1 -> 60 will be unnoticeable
}
interface Listener {
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 6a43697..4821fbe 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -2486,6 +2486,13 @@
private void onRequestFailedOnHandler(@NonNull MediaRoute2Provider provider,
long uniqueRequestId, int reason) {
if (handleSessionCreationRequestFailed(provider, uniqueRequestId, reason)) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "onRequestFailedOnHandler | Finished handling session creation"
+ + " request failed for provider: %s, uniqueRequestId: %d,"
+ + " reason: %d",
+ provider.getUniqueId(), uniqueRequestId, reason));
return;
}
@@ -2515,6 +2522,12 @@
if (matchingRequest == null) {
// The failure is not about creating a session.
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "handleSessionCreationRequestFailed | No matching request found for"
+ + " provider: %s, uniqueRequestId: %d, reason: %d",
+ provider.getUniqueId(), uniqueRequestId, reason));
return false;
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 1b640fc..2707b45 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1765,8 +1765,7 @@
if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
// update system notification channels
SystemNotificationChannels.createAll(context);
- mZenModeHelper.updateDefaultZenRules(Binder.getCallingUid(),
- isCallerIsSystemOrSystemUi());
+ mZenModeHelper.updateDefaultZenRules(Binder.getCallingUid());
mPreferencesHelper.onLocaleChanged(context, ActivityManager.getCurrentUser());
}
}
@@ -5316,7 +5315,9 @@
return mZenModeHelper.addAutomaticZenRule(rulePkg, automaticZenRule,
"addAutomaticZenRule", Binder.getCallingUid(),
- isCallerIsSystemOrSystemUi());
+ // TODO: b/308670715: Distinguish FROM_APP from FROM_USER
+ isCallerIsSystemOrSystemUi() ? ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI
+ : ZenModeHelper.FROM_APP);
}
@Override
@@ -5334,7 +5335,9 @@
return mZenModeHelper.updateAutomaticZenRule(id, automaticZenRule,
"updateAutomaticZenRule", Binder.getCallingUid(),
- isCallerIsSystemOrSystemUi());
+ // TODO: b/308670715: Distinguish FROM_APP from FROM_USER
+ isCallerIsSystemOrSystemUi() ? ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI
+ : ZenModeHelper.FROM_APP);
}
@Override
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 2ef0ca6..89d8200 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -27,6 +27,7 @@
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
+import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
@@ -73,6 +74,7 @@
import android.provider.Settings.Global;
import android.service.notification.Condition;
import android.service.notification.ConditionProviderService;
+import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeConfig.ZenRule;
import android.service.notification.ZenModeProto;
@@ -105,6 +107,8 @@
import java.io.IOException;
import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -129,6 +133,21 @@
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
static final long SEND_ACTIVATION_AZR_STATUSES = 308673617L;
+ /** A rule addition or update that is initiated by the System or SystemUI. */
+ static final int FROM_SYSTEM_OR_SYSTEMUI = 1;
+ /** A rule addition or update that is initiated by the user (through system settings). */
+ static final int FROM_USER = 2;
+ /** A rule addition or update that is initiated by an app (via NotificationManager APIs). */
+ static final int FROM_APP = 3;
+
+ @IntDef(prefix = { "FROM_" }, value = {
+ FROM_SYSTEM_OR_SYSTEMUI,
+ FROM_USER,
+ FROM_APP
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ChangeOrigin {}
+
// pkg|userId => uid
@VisibleForTesting protected final ArrayMap<String, Integer> mRulesUidCache = new ArrayMap<>();
@@ -378,7 +397,7 @@
}
public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule,
- String reason, int callingUid, boolean fromSystemOrSystemUi) {
+ String reason, int callingUid, @ChangeOrigin int origin) {
if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) {
PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner());
if (component == null) {
@@ -412,10 +431,10 @@
}
newConfig = mConfig.copy();
ZenRule rule = new ZenRule();
- populateZenRule(pkg, automaticZenRule, rule, true);
+ populateZenRule(pkg, automaticZenRule, rule, true, origin);
newConfig.automaticRules.put(rule.id, rule);
if (setConfigLocked(newConfig, reason, rule.component, true, callingUid,
- fromSystemOrSystemUi)) {
+ origin == FROM_SYSTEM_OR_SYSTEMUI)) {
return rule.id;
} else {
throw new AndroidRuntimeException("Could not create rule");
@@ -424,7 +443,7 @@
}
public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule,
- String reason, int callingUid, boolean fromSystemOrSystemUi) {
+ String reason, int callingUid, @ChangeOrigin int origin) {
ZenModeConfig newConfig;
synchronized (mConfigLock) {
if (mConfig == null) return false;
@@ -452,9 +471,9 @@
}
}
- populateZenRule(rule.pkg, automaticZenRule, rule, false);
+ populateZenRule(rule.pkg, automaticZenRule, rule, false, origin);
return setConfigLocked(newConfig, reason, rule.component, true, callingUid,
- fromSystemOrSystemUi);
+ origin == FROM_SYSTEM_OR_SYSTEMUI);
}
}
@@ -790,7 +809,7 @@
}
}
- protected void updateDefaultZenRules(int callingUid, boolean fromSystemOrSystemUi) {
+ protected void updateDefaultZenRules(int callingUid) {
updateDefaultAutomaticRuleNames();
synchronized (mConfigLock) {
for (ZenRule defaultRule : mDefaultConfig.automaticRules.values()) {
@@ -807,7 +826,7 @@
// update default rule (if locale changed, name of rule will change)
currRule.name = defaultRule.name;
updateAutomaticZenRule(defaultRule.id, zenRuleToAutomaticZenRule(currRule),
- "locale changed", callingUid, fromSystemOrSystemUi);
+ "locale changed", callingUid, FROM_SYSTEM_OR_SYSTEMUI);
}
}
}
@@ -850,7 +869,11 @@
}
private static void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
- boolean isNew) {
+ boolean isNew, @ChangeOrigin int origin) {
+ // TODO: b/308671593,b/311406021 - Handle origins more precisely:
+ // - FROM_USER can override anything and updates bitmask of user-modified fields;
+ // - FROM_SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask;
+ // - FROM_APP can only update if not user-modified.
if (rule.enabled != automaticZenRule.isEnabled()) {
rule.snoozing = false;
}
@@ -861,7 +884,10 @@
rule.modified = automaticZenRule.isModified();
rule.zenPolicy = automaticZenRule.getZenPolicy();
if (Flags.modesApi()) {
- rule.zenDeviceEffects = automaticZenRule.getDeviceEffects();
+ rule.zenDeviceEffects = fixZenDeviceEffects(
+ rule.zenDeviceEffects,
+ automaticZenRule.getDeviceEffects(),
+ origin);
}
rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
@@ -882,6 +908,50 @@
}
}
+ /** "
+ * Fix" {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule.
+ *
+ * <ul>
+ * <li> Apps cannot turn on hidden effects (those tagged as {@code @hide}) since they are
+ * intended for platform-specific rules (e.g. wearables). If it's a new rule, we blank them
+ * out; if it's an update, we preserve the previous values.
+ * </ul>
+ */
+ @Nullable
+ private static ZenDeviceEffects fixZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects,
+ @Nullable ZenDeviceEffects newEffects, @ChangeOrigin int origin) {
+ // TODO: b/308671593,b/311406021 - Handle origins more precisely:
+ // - FROM_USER can override anything and updates bitmask of user-modified fields;
+ // - FROM_SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask;
+ // - FROM_APP can only update if not user-modified.
+ if (origin == FROM_SYSTEM_OR_SYSTEMUI || origin == FROM_USER) {
+ return newEffects;
+ }
+
+ if (newEffects == null) {
+ return null;
+ }
+ if (oldEffects != null) {
+ return new ZenDeviceEffects.Builder(newEffects)
+ .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness())
+ .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake())
+ .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake())
+ .setShouldDisableTouch(oldEffects.shouldDisableTouch())
+ .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage())
+ .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze())
+ .build();
+ } else {
+ return new ZenDeviceEffects.Builder(newEffects)
+ .setShouldDisableAutoBrightness(false)
+ .setShouldDisableTapToWake(false)
+ .setShouldDisableTiltToWake(false)
+ .setShouldDisableTouch(false)
+ .setShouldMinimizeRadioUsage(false)
+ .setShouldMaximizeDoze(false)
+ .build();
+ }
+ }
+
private static AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
AutomaticZenRule azr;
if (Flags.modesApi()) {
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 8556317..f90bf4b 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1957,6 +1957,19 @@
return userTypeDetails.getStatusBarIcon();
}
+ @Override
+ public @StringRes int getProfileLabelResId(@UserIdInt int userId) {
+ checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId,
+ "getProfileLabelResId");
+ final UserInfo userInfo = getUserInfoNoChecks(userId);
+ final UserTypeDetails userTypeDetails = getUserTypeDetails(userInfo);
+ if (userInfo == null || userTypeDetails == null) {
+ return Resources.ID_NULL;
+ }
+ final int userIndex = userInfo.profileBadge;
+ return userTypeDetails.getLabel(userIndex);
+ }
+
public boolean isProfile(@UserIdInt int userId) {
checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isProfile");
return isProfileUnchecked(userId);
diff --git a/services/core/java/com/android/server/pm/UserTypeDetails.java b/services/core/java/com/android/server/pm/UserTypeDetails.java
index 7bdcd68..56c400a0 100644
--- a/services/core/java/com/android/server/pm/UserTypeDetails.java
+++ b/services/core/java/com/android/server/pm/UserTypeDetails.java
@@ -54,8 +54,15 @@
/** Whether users of this type can be created. */
private final boolean mEnabled;
- // TODO(b/142482943): Currently unused and not set. Hook this up.
- private final int mLabel;
+ /**
+ * Resource IDs ({@link StringRes}) of the user's labels. This might be used to label a
+ * user/profile in tabbed views, etc.
+ * The values are resource IDs referring to the strings not the strings themselves.
+ *
+ * <p>This is an array because, in general, there may be multiple users of the same user type.
+ * In this case, the user is indexed according to its {@link UserInfo#profileBadge}.
+ */
+ private final @Nullable int[] mLabels;
/**
* Maximum number of this user type allowed on the device.
@@ -160,8 +167,8 @@
private final @NonNull UserProperties mDefaultUserProperties;
private UserTypeDetails(@NonNull String name, boolean enabled, int maxAllowed,
- @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags, int label,
- int maxAllowedPerParent,
+ @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags,
+ @Nullable int[] labels, int maxAllowedPerParent,
int iconBadge, int badgePlain, int badgeNoBackground,
int statusBarIcon,
@Nullable int[] badgeLabels, @Nullable int[] badgeColors,
@@ -181,12 +188,11 @@
this.mDefaultSystemSettings = defaultSystemSettings;
this.mDefaultSecureSettings = defaultSecureSettings;
this.mDefaultCrossProfileIntentFilters = defaultCrossProfileIntentFilters;
-
this.mIconBadge = iconBadge;
this.mBadgePlain = badgePlain;
this.mBadgeNoBackground = badgeNoBackground;
this.mStatusBarIcon = statusBarIcon;
- this.mLabel = label;
+ this.mLabels = labels;
this.mBadgeLabels = badgeLabels;
this.mBadgeColors = badgeColors;
this.mDarkThemeBadgeColors = darkThemeBadgeColors;
@@ -234,9 +240,16 @@
return mDefaultUserInfoPropertyFlags | mBaseType;
}
- // TODO(b/142482943) Hook this up; it is currently unused.
- public int getLabel() {
- return mLabel;
+ /**
+ * Returns the resource ID corresponding to the badgeIndexth label name where the badgeIndex is
+ * expected to be the {@link UserInfo#profileBadge} of the user. If badgeIndex exceeds the
+ * number of labels, returns the label for the highest index.
+ */
+ public @StringRes int getLabel(int badgeIndex) {
+ if (mLabels == null || mLabels.length == 0 || badgeIndex < 0) {
+ return Resources.ID_NULL;
+ }
+ return mLabels[Math.min(badgeIndex, mLabels.length - 1)];
}
/** Returns whether users of this user type should be badged. */
@@ -358,7 +371,6 @@
pw.print(prefix); pw.print("mMaxAllowedPerParent: "); pw.println(mMaxAllowedPerParent);
pw.print(prefix); pw.print("mDefaultUserInfoFlags: ");
pw.println(UserInfo.flagsToString(mDefaultUserInfoPropertyFlags));
- pw.print(prefix); pw.print("mLabel: "); pw.println(mLabel);
mDefaultUserProperties.println(pw, prefix);
final String restrictionsPrefix = prefix + " ";
@@ -392,6 +404,8 @@
pw.println(mBadgeColors != null ? mBadgeColors.length : "0(null)");
pw.print(prefix); pw.print("mDarkThemeBadgeColors.length: ");
pw.println(mDarkThemeBadgeColors != null ? mDarkThemeBadgeColors.length : "0(null)");
+ pw.print(prefix); pw.print("mLabels.length: ");
+ pw.println(mLabels != null ? mLabels.length : "0(null)");
}
/** Builder for a {@link UserTypeDetails}; see that class for documentation. */
@@ -408,7 +422,7 @@
private @Nullable List<DefaultCrossProfileIntentFilter> mDefaultCrossProfileIntentFilters =
null;
private int mEnabled = 1;
- private int mLabel = Resources.ID_NULL;
+ private @Nullable int[] mLabels = null;
private @Nullable int[] mBadgeLabels = null;
private @Nullable int[] mBadgeColors = null;
private @Nullable int[] mDarkThemeBadgeColors = null;
@@ -488,8 +502,8 @@
return this;
}
- public Builder setLabel(int label) {
- mLabel = label;
+ public Builder setLabels(@StringRes int ... labels) {
+ mLabels = labels;
return this;
}
@@ -562,7 +576,7 @@
mMaxAllowed,
mBaseType,
mDefaultUserInfoPropertyFlags,
- mLabel,
+ mLabels,
mMaxAllowedPerParent,
mIconBadge,
mBadgePlain,
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 7da76c1..4ef8cb7 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -128,7 +128,7 @@
.setName(USER_TYPE_PROFILE_CLONE)
.setBaseType(FLAG_PROFILE)
.setMaxAllowedPerParent(1)
- .setLabel(0)
+ .setLabels(R.string.profile_label_clone)
.setIconBadge(com.android.internal.R.drawable.ic_clone_icon_badge)
.setBadgePlain(com.android.internal.R.drawable.ic_clone_badge)
// Clone doesn't use BadgeNoBackground, so just set to BadgePlain as a placeholder.
@@ -154,6 +154,10 @@
UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM)
.setCrossProfileIntentResolutionStrategy(UserProperties
.CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_NO_FILTERING)
+ .setShowInQuietMode(
+ UserProperties.SHOW_IN_QUIET_MODE_DEFAULT)
+ .setShowInSharingSurfaces(
+ UserProperties.SHOW_IN_SHARING_SURFACES_WITH_PARENT)
.setMediaSharedWithParent(true)
.setCredentialShareableWithParent(true)
.setDeleteAppWithParent(true));
@@ -169,7 +173,10 @@
.setBaseType(FLAG_PROFILE)
.setDefaultUserInfoPropertyFlags(FLAG_MANAGED_PROFILE)
.setMaxAllowedPerParent(1)
- .setLabel(0)
+ .setLabels(
+ R.string.profile_label_work,
+ R.string.profile_label_work_2,
+ R.string.profile_label_work_3)
.setIconBadge(com.android.internal.R.drawable.ic_corp_icon_badge_case)
.setBadgePlain(com.android.internal.R.drawable.ic_corp_badge_case)
.setBadgeNoBackground(com.android.internal.R.drawable.ic_corp_badge_no_background)
@@ -193,6 +200,10 @@
.setStartWithParent(true)
.setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
.setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
+ .setShowInQuietMode(
+ UserProperties.SHOW_IN_QUIET_MODE_PAUSED)
+ .setShowInSharingSurfaces(
+ UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
.setAuthAlwaysRequiredToDisableQuietMode(false)
.setCredentialShareableWithParent(true));
}
@@ -209,7 +220,10 @@
.setName(USER_TYPE_PROFILE_TEST)
.setBaseType(FLAG_PROFILE)
.setMaxAllowedPerParent(2)
- .setLabel(0)
+ .setLabels(
+ R.string.profile_label_test,
+ R.string.profile_label_test,
+ R.string.profile_label_test)
.setIconBadge(com.android.internal.R.drawable.ic_test_icon_badge_experiment)
.setBadgePlain(com.android.internal.R.drawable.ic_test_badge_experiment)
.setBadgeNoBackground(com.android.internal.R.drawable.ic_test_badge_no_background)
@@ -240,7 +254,7 @@
.setBaseType(FLAG_PROFILE)
.setMaxAllowed(1)
.setEnabled(UserManager.isCommunalProfileEnabled() ? 1 : 0)
- .setLabel(0)
+ .setLabels(R.string.profile_label_communal)
.setIconBadge(com.android.internal.R.drawable.ic_test_icon_badge_experiment)
.setBadgePlain(com.android.internal.R.drawable.ic_test_badge_experiment)
.setBadgeNoBackground(com.android.internal.R.drawable.ic_test_badge_no_background)
@@ -276,7 +290,7 @@
.setName(USER_TYPE_PROFILE_PRIVATE)
.setBaseType(FLAG_PROFILE)
.setMaxAllowedPerParent(1)
- .setLabel(0)
+ .setLabels(R.string.profile_label_private)
.setIconBadge(com.android.internal.R.drawable.ic_private_profile_icon_badge)
.setBadgePlain(com.android.internal.R.drawable.ic_private_profile_badge)
// Private Profile doesn't use BadgeNoBackground, so just set to BadgePlain
@@ -298,7 +312,10 @@
.setMediaSharedWithParent(false)
.setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
.setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
- .setHideInSettingsInQuietMode(true)
+ .setShowInQuietMode(
+ UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
+ .setShowInSharingSurfaces(
+ UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
.setCrossProfileIntentFilterAccessControl(
UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM)
.setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT));
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 986735f..73c4224 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3537,7 +3537,8 @@
mDisplayManager.setBrightness(screenDisplayId, adjustedLinearBrightness);
Intent intent = new Intent(Intent.ACTION_SHOW_BRIGHTNESS_DIALOG);
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION
+ | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
intent.putExtra(EXTRA_FROM_BRIGHTNESS_KEY, true);
startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
logKeyboardSystemsEvent(event, KeyboardLogEvent.getBrightnessEvent(keyCode));
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index eea13f1..eb40104 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -8239,20 +8239,20 @@
@GuardedBy("mBsi")
private void ensureMultiStateCounters(long timestampMs) {
- if (mProcStateTimeMs != null) {
- return;
+ if (mProcStateTimeMs == null) {
+ mProcStateTimeMs =
+ new TimeInFreqMultiStateCounter(mBsi.mOnBatteryTimeBase,
+ PROC_STATE_TIME_COUNTER_STATE_COUNT,
+ mBsi.mCpuScalingPolicies.getScalingStepCount(),
+ timestampMs);
}
-
- mProcStateTimeMs =
- new TimeInFreqMultiStateCounter(mBsi.mOnBatteryTimeBase,
- PROC_STATE_TIME_COUNTER_STATE_COUNT,
- mBsi.mCpuScalingPolicies.getScalingStepCount(),
- timestampMs);
- mProcStateScreenOffTimeMs =
- new TimeInFreqMultiStateCounter(mBsi.mOnBatteryScreenOffTimeBase,
- PROC_STATE_TIME_COUNTER_STATE_COUNT,
- mBsi.mCpuScalingPolicies.getScalingStepCount(),
- timestampMs);
+ if (mProcStateScreenOffTimeMs == null) {
+ mProcStateScreenOffTimeMs =
+ new TimeInFreqMultiStateCounter(mBsi.mOnBatteryScreenOffTimeBase,
+ PROC_STATE_TIME_COUNTER_STATE_COUNT,
+ mBsi.mCpuScalingPolicies.getScalingStepCount(),
+ timestampMs);
+ }
}
@GuardedBy("mBsi")
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index 68f554c..ea8a801 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -16,6 +16,8 @@
package com.android.server.webkit;
+import static android.webkit.Flags.updateServiceV2;
+
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.content.Context;
@@ -237,18 +239,30 @@
@Override
public int getMultiProcessSetting(Context context) {
- return Settings.Global.getInt(context.getContentResolver(),
- Settings.Global.WEBVIEW_MULTIPROCESS, 0);
+ if (updateServiceV2()) {
+ throw new IllegalStateException(
+ "getMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
+ }
+ return Settings.Global.getInt(
+ context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0);
}
@Override
public void setMultiProcessSetting(Context context, int value) {
- Settings.Global.putInt(context.getContentResolver(),
- Settings.Global.WEBVIEW_MULTIPROCESS, value);
+ if (updateServiceV2()) {
+ throw new IllegalStateException(
+ "setMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
+ }
+ Settings.Global.putInt(
+ context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value);
}
@Override
public void notifyZygote(boolean enableMultiProcess) {
+ if (updateServiceV2()) {
+ throw new IllegalStateException(
+ "notifyZygote shouldn't be called if update_service_v2 flag is set.");
+ }
WebViewZygote.setMultiprocessEnabled(enableMultiProcess);
}
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
index b3672ec..b12da61 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
@@ -157,8 +157,13 @@
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
ResultReceiver resultReceiver) {
- (new WebViewUpdateServiceShellCommand(this)).exec(
- this, in, out, err, args, callback, resultReceiver);
+ if (updateServiceV2()) {
+ (new WebViewUpdateServiceShellCommand2(this))
+ .exec(this, in, out, err, args, callback, resultReceiver);
+ } else {
+ (new WebViewUpdateServiceShellCommand(this))
+ .exec(this, in, out, err, args, callback, resultReceiver);
+ }
}
@@ -275,18 +280,31 @@
@Override // Binder call
public boolean isMultiProcessEnabled() {
+ if (updateServiceV2()) {
+ throw new IllegalStateException(
+ "isMultiProcessEnabled shouldn't be called if update_service_v2 flag is"
+ + " set.");
+ }
return WebViewUpdateService.this.mImpl.isMultiProcessEnabled();
}
@Override // Binder call
public void enableMultiProcess(boolean enable) {
- if (getContext().checkCallingPermission(
- android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ if (updateServiceV2()) {
+ throw new IllegalStateException(
+ "enableMultiProcess shouldn't be called if update_service_v2 flag is set.");
+ }
+ if (getContext()
+ .checkCallingPermission(
+ android.Manifest.permission.WRITE_SECURE_SETTINGS)
!= PackageManager.PERMISSION_GRANTED) {
- String msg = "Permission Denial: enableMultiProcess() from pid="
- + Binder.getCallingPid()
- + ", uid=" + Binder.getCallingUid()
- + " requires " + android.Manifest.permission.WRITE_SECURE_SETTINGS;
+ String msg =
+ "Permission Denial: enableMultiProcess() from pid="
+ + Binder.getCallingPid()
+ + ", uid="
+ + Binder.getCallingUid()
+ + " requires "
+ + android.Manifest.permission.WRITE_SECURE_SETTINGS;
Slog.w(TAG, msg);
throw new SecurityException(msg);
}
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index e618c7e..89cb4c8 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -23,6 +23,7 @@
import android.os.AsyncTask;
import android.os.Trace;
import android.os.UserHandle;
+import android.text.TextUtils;
import android.util.Slog;
import android.webkit.UserPackage;
import android.webkit.WebViewFactory;
@@ -70,10 +71,6 @@
WebViewPackageMissingException(String message) {
super(message);
}
-
- WebViewPackageMissingException(Exception e) {
- super(e);
- }
}
private static final int WAIT_TIMEOUT_MS = 1000; // KEY_DISPATCHING_TIMEOUT is 5000.
@@ -85,9 +82,6 @@
private static final int VALIDITY_INCORRECT_SIGNATURE = 3;
private static final int VALIDITY_NO_LIBRARY_FLAG = 4;
- private static final int MULTIPROCESS_SETTING_ON_VALUE = Integer.MAX_VALUE;
- private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE;
-
private final SystemInterface mSystemInterface;
private final Context mContext;
@@ -166,7 +160,6 @@
@Override
public void prepareWebViewInSystemServer() {
- mSystemInterface.notifyZygote(isMultiProcessEnabled());
try {
synchronized (mLock) {
mCurrentWebViewPackage = findPreferredWebViewPackage();
@@ -366,14 +359,10 @@
// Once we've notified the system that the provider has changed and started RELRO creation,
// try to restart the zygote so that it will be ready when apps use it.
- if (isMultiProcessEnabled()) {
- AsyncTask.THREAD_POOL_EXECUTOR.execute(this::startZygoteWhenReady);
- }
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(this::startZygoteWhenReady);
}
- /**
- * Fetch only the currently valid WebView packages.
- **/
+ /** Fetch only the currently valid WebView packages. */
@Override
public WebViewProviderInfo[] getValidWebViewPackages() {
ProviderAndPackageInfo[] providersAndPackageInfos = getValidWebViewPackagesAndInfos();
@@ -632,62 +621,56 @@
@Override
public boolean isMultiProcessEnabled() {
- int settingValue = mSystemInterface.getMultiProcessSetting(mContext);
- if (mSystemInterface.isMultiProcessDefaultEnabled()) {
- // Multiprocess should be enabled unless the user has turned it off manually.
- return settingValue > MULTIPROCESS_SETTING_OFF_VALUE;
- } else {
- // Multiprocess should not be enabled, unless the user has turned it on manually.
- return settingValue >= MULTIPROCESS_SETTING_ON_VALUE;
- }
+ throw new IllegalStateException(
+ "isMultiProcessEnabled shouldn't be called if update_service_v2 flag is set.");
}
@Override
public void enableMultiProcess(boolean enable) {
- PackageInfo current = getCurrentWebViewPackage();
- mSystemInterface.setMultiProcessSetting(mContext,
- enable ? MULTIPROCESS_SETTING_ON_VALUE : MULTIPROCESS_SETTING_OFF_VALUE);
- mSystemInterface.notifyZygote(enable);
- if (current != null) {
- mSystemInterface.killPackageDependents(current.packageName);
- }
+ throw new IllegalStateException(
+ "enableMultiProcess shouldn't be called if update_service_v2 flag is set.");
}
- /**
- * Dump the state of this Service.
- */
+ /** Dump the state of this Service. */
@Override
public void dumpState(PrintWriter pw) {
pw.println("Current WebView Update Service state");
- pw.println(String.format(" Multiprocess enabled: %b", isMultiProcessEnabled()));
synchronized (mLock) {
if (mCurrentWebViewPackage == null) {
pw.println(" Current WebView package is null");
} else {
- pw.println(String.format(" Current WebView package (name, version): (%s, %s)",
- mCurrentWebViewPackage.packageName,
- mCurrentWebViewPackage.versionName));
+ pw.println(
+ TextUtils.formatSimple(
+ " Current WebView package (name, version): (%s, %s)",
+ mCurrentWebViewPackage.packageName,
+ mCurrentWebViewPackage.versionName));
}
- pw.println(String.format(" Minimum targetSdkVersion: %d",
- UserPackage.MINIMUM_SUPPORTED_SDK));
- pw.println(String.format(" Minimum WebView version code: %d",
- mMinimumVersionCode));
- pw.println(String.format(" Number of relros started: %d",
- mNumRelroCreationsStarted));
- pw.println(String.format(" Number of relros finished: %d",
- mNumRelroCreationsFinished));
- pw.println(String.format(" WebView package dirty: %b", mWebViewPackageDirty));
- pw.println(String.format(" Any WebView package installed: %b",
- mAnyWebViewInstalled));
+ pw.println(
+ TextUtils.formatSimple(
+ " Minimum targetSdkVersion: %d", UserPackage.MINIMUM_SUPPORTED_SDK));
+ pw.println(
+ TextUtils.formatSimple(
+ " Minimum WebView version code: %d", mMinimumVersionCode));
+ pw.println(
+ TextUtils.formatSimple(
+ " Number of relros started: %d", mNumRelroCreationsStarted));
+ pw.println(
+ TextUtils.formatSimple(
+ " Number of relros finished: %d", mNumRelroCreationsFinished));
+ pw.println(TextUtils.formatSimple(" WebView package dirty: %b", mWebViewPackageDirty));
+ pw.println(
+ TextUtils.formatSimple(
+ " Any WebView package installed: %b", mAnyWebViewInstalled));
try {
PackageInfo preferredWebViewPackage = findPreferredWebViewPackage();
- pw.println(String.format(
- " Preferred WebView package (name, version): (%s, %s)",
- preferredWebViewPackage.packageName,
- preferredWebViewPackage.versionName));
+ pw.println(
+ TextUtils.formatSimple(
+ " Preferred WebView package (name, version): (%s, %s)",
+ preferredWebViewPackage.packageName,
+ preferredWebViewPackage.versionName));
} catch (WebViewPackageMissingException e) {
- pw.println(String.format(" Preferred WebView package: none"));
+ pw.println(" Preferred WebView package: none");
}
dumpAllPackageInformationLocked(pw);
@@ -703,29 +686,36 @@
PackageInfo systemUserPackageInfo =
userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo();
if (systemUserPackageInfo == null) {
- pw.println(String.format(" %s is NOT installed.", provider.packageName));
+ pw.println(
+ TextUtils.formatSimple(" %s is NOT installed.", provider.packageName));
continue;
}
int validity = validityResult(provider, systemUserPackageInfo);
- String packageDetails = String.format(
- "versionName: %s, versionCode: %d, targetSdkVersion: %d",
- systemUserPackageInfo.versionName,
- systemUserPackageInfo.getLongVersionCode(),
- systemUserPackageInfo.applicationInfo.targetSdkVersion);
+ String packageDetails =
+ TextUtils.formatSimple(
+ "versionName: %s, versionCode: %d, targetSdkVersion: %d",
+ systemUserPackageInfo.versionName,
+ systemUserPackageInfo.getLongVersionCode(),
+ systemUserPackageInfo.applicationInfo.targetSdkVersion);
if (validity == VALIDITY_OK) {
- boolean installedForAllUsers = isInstalledAndEnabledForAllUsers(
- mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider));
- pw.println(String.format(
- " Valid package %s (%s) is %s installed/enabled for all users",
- systemUserPackageInfo.packageName,
- packageDetails,
- installedForAllUsers ? "" : "NOT"));
+ boolean installedForAllUsers =
+ isInstalledAndEnabledForAllUsers(
+ mSystemInterface.getPackageInfoForProviderAllUsers(
+ mContext, provider));
+ pw.println(
+ TextUtils.formatSimple(
+ " Valid package %s (%s) is %s installed/enabled for all users",
+ systemUserPackageInfo.packageName,
+ packageDetails,
+ installedForAllUsers ? "" : "NOT"));
} else {
- pw.println(String.format(" Invalid package %s (%s), reason: %s",
- systemUserPackageInfo.packageName,
- packageDetails,
- getInvalidityReason(validity)));
+ pw.println(
+ TextUtils.formatSimple(
+ " Invalid package %s (%s), reason: %s",
+ systemUserPackageInfo.packageName,
+ packageDetails,
+ getInvalidityReason(validity)));
}
}
}
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand2.java
new file mode 100644
index 0000000..ce95b18
--- /dev/null
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand2.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.webkit;
+
+import android.os.RemoteException;
+import android.os.ShellCommand;
+import android.text.TextUtils;
+import android.webkit.IWebViewUpdateService;
+
+import java.io.PrintWriter;
+
+class WebViewUpdateServiceShellCommand2 extends ShellCommand {
+ final IWebViewUpdateService mInterface;
+
+ WebViewUpdateServiceShellCommand2(IWebViewUpdateService service) {
+ mInterface = service;
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ if (cmd == null) {
+ return handleDefaultCommands(cmd);
+ }
+
+ final PrintWriter pw = getOutPrintWriter();
+ try {
+ switch (cmd) {
+ case "set-webview-implementation":
+ return setWebViewImplementation();
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ } catch (RemoteException e) {
+ pw.println("Remote exception: " + e);
+ }
+ return -1;
+ }
+
+ private int setWebViewImplementation() throws RemoteException {
+ final PrintWriter pw = getOutPrintWriter();
+ String shellChosenPackage = getNextArg();
+ if (shellChosenPackage == null) {
+ pw.println("Failed to switch, no PACKAGE provided.");
+ pw.println("");
+ helpSetWebViewImplementation();
+ return 1;
+ }
+ String newPackage = mInterface.changeProviderAndSetting(shellChosenPackage);
+ if (shellChosenPackage.equals(newPackage)) {
+ pw.println("Success");
+ return 0;
+ } else {
+ pw.println(
+ TextUtils.formatSimple(
+ "Failed to switch to %s, the WebView implementation is now provided by"
+ + " %s.",
+ shellChosenPackage, newPackage));
+ return 1;
+ }
+ }
+
+ public void helpSetWebViewImplementation() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println(" set-webview-implementation PACKAGE");
+ pw.println(" Set the WebView implementation to the specified package.");
+ }
+
+ @Override
+ public void onHelp() {
+ PrintWriter pw = getOutPrintWriter();
+ pw.println("WebView updater commands:");
+ pw.println(" help");
+ pw.println(" Print this help text.");
+ pw.println("");
+ helpSetWebViewImplementation();
+ pw.println();
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d90d4ff..8498368 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -694,7 +694,7 @@
private boolean mCurrentLaunchCanTurnScreenOn = true;
/** Whether our surface was set to be showing in the last call to {@link #prepareSurfaces} */
- private boolean mLastSurfaceShowing;
+ boolean mLastSurfaceShowing;
/**
* The activity is opaque and fills the entire space of this task.
@@ -2565,7 +2565,7 @@
}
}
if (abort) {
- surface.remove(false /* prepareAnimation */);
+ surface.remove(false /* prepareAnimation */, false /* hasImeSurface */);
}
} else {
ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Surface returned was null: %s",
@@ -2898,6 +2898,7 @@
final StartingSurfaceController.StartingSurface surface;
final boolean animate;
+ final boolean hasImeSurface;
if (mStartingData != null) {
if (mStartingData.mWaitForSyncTransactionCommit
|| mTransitionController.isCollecting(this)) {
@@ -2907,6 +2908,7 @@
}
animate = prepareAnimation && mStartingData.needRevealAnimation()
&& mStartingWindow.isVisibleByPolicy();
+ hasImeSurface = mStartingData.hasImeSurface();
ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Schedule remove starting %s startingWindow=%s"
+ " animate=%b Callers=%s", this, mStartingWindow, animate,
Debug.getCallers(5));
@@ -2926,7 +2928,7 @@
this);
return;
}
- surface.remove(animate);
+ surface.remove(animate, hasImeSurface);
}
/**
@@ -5380,11 +5382,13 @@
// Finish should only ever commit visibility=false, so we can check full containment
// rather than just direct membership.
inFinishingTransition = mTransitionController.inFinishingTransition(this);
- if (!inFinishingTransition && (visible || !mDisplayContent.isSleeping())) {
+ if (!inFinishingTransition) {
if (visible) {
- mTransitionController.onVisibleWithoutCollectingTransition(this,
- Debug.getCallers(1, 1));
- } else {
+ if (!mDisplayContent.isSleeping() || canShowWhenLocked()) {
+ mTransitionController.onVisibleWithoutCollectingTransition(this,
+ Debug.getCallers(1, 1));
+ }
+ } else if (!mDisplayContent.isSleeping()) {
Slog.w(TAG, "Set invisible without transition " + this);
}
}
@@ -6434,20 +6438,22 @@
void stopIfPossible() {
if (DEBUG_SWITCH) Slog.d(TAG_SWITCH, "Stopping: " + this);
- final Task rootTask = getRootTask();
+ if (finishing) {
+ Slog.e(TAG, "Request to stop a finishing activity: " + this);
+ destroyIfPossible("stopIfPossible-finishing");
+ return;
+ }
if (isNoHistory()) {
- if (!finishing) {
- if (!rootTask.shouldSleepActivities()) {
- ProtoLog.d(WM_DEBUG_STATES, "no-history finish of %s", this);
- if (finishIfPossible("stop-no-history", false /* oomAdj */)
- != FINISH_RESULT_CANCELLED) {
- resumeKeyDispatchingLocked();
- return;
- }
- } else {
- ProtoLog.d(WM_DEBUG_STATES, "Not finishing noHistory %s on stop "
- + "because we're just sleeping", this);
+ if (!task.shouldSleepActivities()) {
+ ProtoLog.d(WM_DEBUG_STATES, "no-history finish of %s", this);
+ if (finishIfPossible("stop-no-history", false /* oomAdj */)
+ != FINISH_RESULT_CANCELLED) {
+ resumeKeyDispatchingLocked();
+ return;
}
+ } else {
+ ProtoLog.d(WM_DEBUG_STATES, "Not finishing noHistory %s on stop "
+ + "because we're just sleeping", this);
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
index c61d863..1a19787 100644
--- a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
+++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
@@ -88,9 +88,11 @@
|| activityBelowInTask.isUid(mActivityRecord.getUid()));
if (allowPassthrough || !mIsCompatEnabled || mActivityRecord.isInTransition()
|| !mActivityRecord.mActivityRecordInputSinkEnabled) {
+ // Set to non-touchable, so the touch events can pass through.
mInputWindowHandleWrapper.setInputConfigMasked(InputConfig.NOT_TOUCHABLE,
InputConfig.NOT_TOUCHABLE);
} else {
+ // Set to touchable, so it can block by intercepting the touch events.
mInputWindowHandleWrapper.setInputConfigMasked(0, InputConfig.NOT_TOUCHABLE);
}
mInputWindowHandleWrapper.setDisplayId(mActivityRecord.getDisplayId());
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 90eeed2..a21b9b4 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -55,6 +55,7 @@
import static com.android.server.wm.ActivityRecord.State.PAUSED;
import static com.android.server.wm.ActivityRecord.State.PAUSING;
import static com.android.server.wm.ActivityRecord.State.RESTARTING_PROCESS;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ALL;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_CLEANUP;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_IDLE;
@@ -1681,7 +1682,7 @@
ArrayList<ActivityRecord> activities = null;
for (int i = mStoppingActivities.size() - 1; i >= 0; i--) {
final ActivityRecord r = mStoppingActivities.get(i);
- if (r.getTask() == task) {
+ if (!r.finishing && r.isState(RESUMED) && r.getTask() == task) {
if (activities == null) {
activities = new ArrayList<>();
}
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 8cc197c..39e900a 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -319,8 +319,6 @@
return BackgroundStartPrivileges.NONE;
case ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED:
// no explicit choice by the app - let us decide what to do
- Slog.i(TAG, "balRequireOptInByPendingIntentCreator = "
- + balRequireOptInByPendingIntentCreator());
if (!balRequireOptInByPendingIntentCreator()) {
// if feature is disabled allow
return BackgroundStartPrivileges.ALLOW_BAL;
@@ -331,7 +329,6 @@
DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_CREATOR,
callingPackage,
UserHandle.getUserHandleForUid(callingUid));
- Slog.i(TAG, "changeEnabled = " + changeEnabled);
return changeEnabled ? BackgroundStartPrivileges.NONE
: BackgroundStartPrivileges.ALLOW_BAL;
}
@@ -340,7 +337,6 @@
boolean changeEnabled = CompatChanges.isChangeEnabled(
DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_CREATOR,
callingUid);
- Slog.i(TAG, "changeEnabled = " + changeEnabled);
return changeEnabled ? BackgroundStartPrivileges.NONE
: BackgroundStartPrivileges.ALLOW_BAL;
default:
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 2c224e4..07cbd58 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -69,8 +69,6 @@
import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_BOOT_PROGRESS;
@@ -158,6 +156,8 @@
import static com.android.server.wm.WindowState.EXCLUSION_RIGHT;
import static com.android.server.wm.WindowState.RESIZE_HANDLE_WIDTH_IN_DP;
import static com.android.server.wm.WindowStateAnimator.READY_TO_SHOW;
+import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS;
+import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
import static com.android.server.wm.utils.RegionUtils.forEachRectReverse;
import static com.android.server.wm.utils.RegionUtils.rectListToRegion;
import static com.android.window.flags.Flags.explicitRefreshRateHints;
@@ -465,11 +465,20 @@
boolean mDisplayScalingDisabled;
final Display mDisplay;
private final DisplayInfo mDisplayInfo = new DisplayInfo();
+
+ /**
+ * Contains the last DisplayInfo override that was sent to DisplayManager or null if we haven't
+ * set an override yet
+ */
+ @Nullable
+ private DisplayInfo mLastDisplayInfoOverride;
+
private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
private final DisplayPolicy mDisplayPolicy;
private final DisplayRotation mDisplayRotation;
@Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
DisplayFrames mDisplayFrames;
+ private final DisplayUpdater mDisplayUpdater;
private boolean mInTouchMode;
@@ -623,7 +632,7 @@
@VisibleForTesting
final DeviceStateController mDeviceStateController;
final Consumer<DeviceStateController.DeviceState> mDeviceStateConsumer;
- private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
+ final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
final RemoteDisplayChangeController mRemoteDisplayChangeController;
/** Windows added since {@link #mCurrentFocus} was set to null. Used for ANR blaming. */
@@ -1144,6 +1153,7 @@
mDisplay = display;
mDisplayId = display.getDisplayId();
mCurrentUniqueDisplayId = display.getUniqueId();
+ mDisplayUpdater = new ImmediateDisplayUpdater(this);
mOffTokenAcquirer = mRootWindowContainer.mDisplayOffTokenAcquirer;
mWallpaperController = new WallpaperController(mWmService, this);
mWallpaperController.resetLargestDisplay(display);
@@ -1917,28 +1927,6 @@
return true;
}
- /** Returns {@code true} if the IME is possible to show on the launching activity. */
- boolean mayImeShowOnLaunchingActivity(@NonNull ActivityRecord r) {
- final WindowState win = r.findMainWindow(false /* exclude starting window */);
- if (win == null) {
- return false;
- }
- // See InputMethodManagerService#shouldRestoreImeVisibility that we expecting the IME
- // should be hidden when the window set the hidden softInputMode.
- final int softInputMode = win.mAttrs.softInputMode;
- switch (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) {
- case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
- case SOFT_INPUT_STATE_HIDDEN:
- return false;
- }
- final boolean useIme = r.getWindow(
- w -> WindowManager.LayoutParams.mayUseInputMethod(w.mAttrs.flags)) != null;
- if (!useIme) {
- return false;
- }
- return r.mLastImeShown || (r.mStartingData != null && r.mStartingData.hasImeSurface());
- }
-
/** Returns {@code true} if the top activity is transformed with the new rotation of display. */
boolean hasTopFixedRotationLaunchingApp() {
return mFixedRotationLaunchingApp != null
@@ -2301,8 +2289,7 @@
computeSizeRanges(mDisplayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig);
- mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId,
- mDisplayInfo);
+ setDisplayInfoOverride();
if (isDefaultDisplay) {
mCompatibleScreenScale = CompatibilityInfo.computeCompatibleScaling(mDisplayMetrics,
@@ -2314,6 +2301,20 @@
return mDisplayInfo;
}
+ /**
+ * Sets the current DisplayInfo in DisplayContent as an override to DisplayManager
+ */
+ private void setDisplayInfoOverride() {
+ mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId,
+ mDisplayInfo);
+
+ if (mLastDisplayInfoOverride == null) {
+ mLastDisplayInfoOverride = new DisplayInfo();
+ }
+
+ mLastDisplayInfoOverride.copyFrom(mDisplayInfo);
+ }
+
DisplayCutout calculateDisplayCutoutForRotation(int rotation) {
return mDisplayCutoutCache.getOrCompute(
mIsSizeForced ? mBaseDisplayCutout : mInitialDisplayCutout, rotation)
@@ -2885,12 +2886,15 @@
return orientation;
}
- void updateDisplayInfo() {
+ void updateDisplayInfo(@NonNull DisplayInfo newDisplayInfo) {
// Check if display metrics changed and update base values if needed.
- updateBaseDisplayMetricsIfNeeded();
+ updateBaseDisplayMetricsIfNeeded(newDisplayInfo);
- mDisplay.getDisplayInfo(mDisplayInfo);
- mDisplay.getMetrics(mDisplayMetrics);
+ // Update mDisplayInfo with (newDisplayInfo + mLastDisplayInfoOverride) as
+ // updateBaseDisplayMetricsIfNeeded could have updated mLastDisplayInfoOverride
+ copyDisplayInfoFields(/* out= */ mDisplayInfo, /* base= */ newDisplayInfo,
+ /* override= */ mLastDisplayInfoOverride, /* fields= */ WM_OVERRIDE_FIELDS);
+ mDisplayInfo.getAppMetrics(mDisplayMetrics, mDisplay.getDisplayAdjustments());
onDisplayInfoChanged();
onDisplayChanged(this);
@@ -2976,9 +2980,9 @@
* If display metrics changed, overrides are not set and it's not just a rotation - update base
* values.
*/
- private void updateBaseDisplayMetricsIfNeeded() {
+ private void updateBaseDisplayMetricsIfNeeded(DisplayInfo newDisplayInfo) {
// Get real display metrics without overrides from WM.
- mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo(mDisplayId, mDisplayInfo);
+ mDisplayInfo.copyFrom(newDisplayInfo);
final int currentRotation = getRotation();
final int orientation = mDisplayInfo.rotation;
final boolean rotated = (orientation == ROTATION_90 || orientation == ROTATION_270);
@@ -3010,7 +3014,7 @@
// metrics are updated as rotation settings might depend on them
mWmService.mDisplayWindowSettings.applySettingsToDisplayLocked(this,
/* includeRotationSettings */ false);
- mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded(mDisplayId,
+ mDisplayUpdater.onDisplayContentDisplayPropertiesPreChanged(mDisplayId,
mInitialDisplayWidth, mInitialDisplayHeight, newWidth, newHeight);
mDisplayRotation.physicalDisplayChanged();
mDisplayPolicy.physicalDisplayChanged();
@@ -3046,8 +3050,8 @@
if (physicalDisplayChanged) {
mDisplayPolicy.physicalDisplayUpdated();
- mDisplaySwitchTransitionLauncher.onDisplayUpdated(currentRotation, getRotation(),
- getDisplayAreaInfo());
+ mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged(currentRotation,
+ getRotation(), getDisplayAreaInfo());
}
}
}
@@ -5494,8 +5498,7 @@
mDisplayReady = true;
if (mWmService.mDisplayManagerInternal != null) {
- mWmService.mDisplayManagerInternal
- .setDisplayInfoOverrideFromWindowManager(mDisplayId, getDisplayInfo());
+ setDisplayInfoOverride();
configureDisplayPolicy();
}
@@ -6138,9 +6141,17 @@
return mMetricsLogger;
}
- void onDisplayChanged() {
+ /**
+ * Triggers an update of DisplayInfo from DisplayManager
+ * @param onDisplayChangeApplied callback that is called when the changes are applied
+ */
+ void requestDisplayUpdate(@NonNull Runnable onDisplayChangeApplied) {
+ mDisplayUpdater.updateDisplayInfo(onDisplayChangeApplied);
+ }
+
+ void onDisplayInfoUpdated(@NonNull DisplayInfo newDisplayInfo) {
final int lastDisplayState = mDisplayInfo.state;
- updateDisplayInfo();
+ updateDisplayInfo(newDisplayInfo);
// The window policy is responsible for stopping activities on the default display.
final int displayId = mDisplay.getDisplayId();
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 708ee7f..b862d7c 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1737,11 +1737,12 @@
void onOverlayChanged() {
updateCurrentUserResources();
// Update the latest display size, cutout.
- mDisplayContent.updateDisplayInfo();
- onConfigurationChanged();
- if (!CLIENT_TRANSIENT) {
- mSystemGestures.onConfigurationChanged();
- }
+ mDisplayContent.requestDisplayUpdate(() -> {
+ onConfigurationChanged();
+ if (!CLIENT_TRANSIENT) {
+ mSystemGestures.onConfigurationChanged();
+ }
+ });
}
/**
diff --git a/services/core/java/com/android/server/wm/DisplayUpdater.java b/services/core/java/com/android/server/wm/DisplayUpdater.java
new file mode 100644
index 0000000..e611177
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DisplayUpdater.java
@@ -0,0 +1,52 @@
+/*
+ * 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.server.wm;
+
+import android.annotation.NonNull;
+import android.view.Surface;
+import android.window.DisplayAreaInfo;
+
+/**
+ * Interface for a helper class that manages updates of DisplayInfo coming from DisplayManager
+ */
+interface DisplayUpdater {
+ /**
+ * Reads the latest display parameters from the display manager and returns them in a callback.
+ * If there are pending display updates, it will wait for them to finish first and only then it
+ * will call the callback with the latest display parameters.
+ *
+ * @param callback is called when all pending display updates are finished
+ */
+ void updateDisplayInfo(@NonNull Runnable callback);
+
+ /**
+ * Called when physical display has changed and before DisplayContent has applied new display
+ * properties
+ */
+ default void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
+ int initialDisplayHeight, int newWidth, int newHeight) {
+ }
+
+ /**
+ * Called after physical display has changed and after DisplayContent applied new display
+ * properties
+ */
+ default void onDisplayContentDisplayPropertiesPostChanged(
+ @Surface.Rotation int previousRotation, @Surface.Rotation int newRotation,
+ @NonNull DisplayAreaInfo newDisplayAreaInfo) {
+ }
+}
diff --git a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java b/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
new file mode 100644
index 0000000..72e8fcb
--- /dev/null
+++ b/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
@@ -0,0 +1,57 @@
+/*
+ * 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.server.wm;
+
+import android.annotation.NonNull;
+import android.view.DisplayInfo;
+import android.window.DisplayAreaInfo;
+
+/**
+ * DisplayUpdater that immediately applies new DisplayInfo properties
+ */
+public class ImmediateDisplayUpdater implements DisplayUpdater {
+
+ private final DisplayContent mDisplayContent;
+ private final DisplayInfo mDisplayInfo = new DisplayInfo();
+
+ public ImmediateDisplayUpdater(@NonNull DisplayContent displayContent) {
+ mDisplayContent = displayContent;
+ }
+
+ @Override
+ public void updateDisplayInfo(Runnable callback) {
+ mDisplayContent.mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo(
+ mDisplayContent.mDisplayId, mDisplayInfo);
+ mDisplayContent.onDisplayInfoUpdated(mDisplayInfo);
+ callback.run();
+ }
+
+ @Override
+ public void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
+ int initialDisplayHeight, int newWidth, int newHeight) {
+ mDisplayContent.mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded(
+ displayId, initialDisplayWidth, initialDisplayHeight, newWidth, newHeight);
+ }
+
+ @Override
+ public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation,
+ DisplayAreaInfo newDisplayAreaInfo) {
+ mDisplayContent.mDisplaySwitchTransitionLauncher.onDisplayUpdated(previousRotation,
+ newRotation,
+ newDisplayAreaInfo);
+ }
+}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 0c235ba..d65d778 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2716,15 +2716,20 @@
synchronized (mService.mGlobalLock) {
final DisplayContent displayContent = getDisplayContent(displayId);
if (displayContent != null) {
- displayContent.onDisplayChanged();
+ displayContent.requestDisplayUpdate(() -> clearDisplayInfoCaches(displayId));
+ } else {
+ clearDisplayInfoCaches(displayId);
}
- // Drop any cached DisplayInfos associated with this display id - the values are now
- // out of date given this display changed event.
- mWmService.mPossibleDisplayInfoMapper.removePossibleDisplayInfos(displayId);
- updateDisplayImePolicyCache();
}
}
+ private void clearDisplayInfoCaches(int displayId) {
+ // Drop any cached DisplayInfos associated with this display id - the values are now
+ // out of date given this display changed event.
+ mWmService.mPossibleDisplayInfoMapper.removePossibleDisplayInfos(displayId);
+ updateDisplayImePolicyCache();
+ }
+
void updateDisplayImePolicyCache() {
ArrayMap<Integer, Integer> displayImePolicyMap = new ArrayMap<>();
forAllDisplays(dc -> displayImePolicyMap.put(dc.getDisplayId(), dc.getImePolicy()));
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 5c84cb0..e7bffdf 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -32,7 +32,7 @@
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.utils.CoordinateTransforms.computeRotationMatrix;
-import static com.android.window.flags.Flags.removeCaptureDisplay;
+import static com.android.window.flags.Flags.deleteCaptureDisplay;
import android.animation.ArgbEvaluator;
import android.content.Context;
@@ -171,7 +171,7 @@
try {
final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer;
- if (isSizeChanged && !removeCaptureDisplay()) {
+ if (isSizeChanged && !deleteCaptureDisplay()) {
final DisplayAddress address = displayInfo.address;
if (!(address instanceof DisplayAddress.Physical)) {
Slog.e(TAG, "Display does not have a physical address: " + displayId);
diff --git a/services/core/java/com/android/server/wm/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java
index 28a35b9..3032110 100644
--- a/services/core/java/com/android/server/wm/StartingSurfaceController.java
+++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java
@@ -276,12 +276,14 @@
/**
* Removes the starting window surface. Do not hold the window manager lock when calling
* this method!
+ *
* @param animate Whether need to play the default exit animation for starting window.
+ * @param hasImeSurface Whether the starting window has IME surface.
*/
- public void remove(boolean animate) {
+ public void remove(boolean animate, boolean hasImeSurface) {
synchronized (mService.mGlobalLock) {
mService.mAtmService.mTaskOrganizerController.removeStartingWindow(mTask,
- mTaskOrganizer, animate);
+ mTaskOrganizer, animate, hasImeSurface);
}
}
}
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index 17ab00d6..3a711b2 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -673,7 +673,8 @@
return true;
}
- void removeStartingWindow(Task task, ITaskOrganizer taskOrganizer, boolean prepareAnimation) {
+ void removeStartingWindow(Task task, ITaskOrganizer taskOrganizer, boolean prepareAnimation,
+ boolean hasImeSurface) {
final Task rootTask = task.getRootTask();
if (rootTask == null) {
return;
@@ -693,13 +694,13 @@
if (topActivity != null) {
// Set defer remove mode for IME
final DisplayContent dc = topActivity.getDisplayContent();
- final WindowState imeWindow = dc.mInputMethodWindow;
- if (topActivity.isVisibleRequested() && imeWindow != null
- && dc.mayImeShowOnLaunchingActivity(topActivity)
- && dc.isFixedRotationLaunchingApp(topActivity)) {
- removalInfo.deferRemoveForImeMode = DEFER_MODE_ROTATION;
- } else if (dc.mayImeShowOnLaunchingActivity(topActivity)) {
- removalInfo.deferRemoveForImeMode = DEFER_MODE_NORMAL;
+ if (hasImeSurface) {
+ if (topActivity.isVisibleRequested() && dc.mInputMethodWindow != null
+ && dc.isFixedRotationLaunchingApp(topActivity)) {
+ removalInfo.deferRemoveForImeMode = DEFER_MODE_ROTATION;
+ } else {
+ removalInfo.deferRemoveForImeMode = DEFER_MODE_NORMAL;
+ }
} else {
removalInfo.deferRemoveForImeMode = DEFER_MODE_NONE;
}
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index a736874..bacfda5 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -992,11 +992,19 @@
private void enforceSurfaceVisible(WindowContainer<?> wc) {
if (wc.mSurfaceControl == null) return;
wc.getSyncTransaction().show(wc.mSurfaceControl);
+ final ActivityRecord ar = wc.asActivityRecord();
+ if (ar != null) {
+ ar.mLastSurfaceShowing = true;
+ }
// Force showing the parents because they may be hidden by previous transition.
for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent;
p = p.getParent()) {
if (p.mSurfaceControl != null) {
p.getSyncTransaction().show(p.mSurfaceControl);
+ final Task task = p.asTask();
+ if (task != null) {
+ task.mLastSurfaceShowing = true;
+ }
}
}
wc.scheduleAnimation();
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index fd22f15..750fd50 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -196,7 +196,9 @@
updateRunningExpensiveAnimationsLegacy();
}
+ Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "applyTransaction");
mTransaction.apply();
+ Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
mService.mWindowTracing.logState("WindowAnimator");
ProtoLog.i(WM_SHOW_TRANSACTIONS, "<<< CLOSE TRANSACTION animate");
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index a69a07f..575ae69b 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -6238,9 +6238,11 @@
return;
}
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.doStartFreezingDisplay");
- doStartFreezingDisplay(exitAnim, enterAnim, displayContent, overrideOriginalRotation);
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ displayContent.requestDisplayUpdate(() -> {
+ Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.doStartFreezingDisplay");
+ doStartFreezingDisplay(exitAnim, enterAnim, displayContent, overrideOriginalRotation);
+ Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+ });
}
private void doStartFreezingDisplay(int exitAnim, int enterAnim, DisplayContent displayContent,
@@ -6276,7 +6278,6 @@
mExitAnimId = exitAnim;
mEnterAnimId = enterAnim;
- displayContent.updateDisplayInfo();
final int originalRotation = overrideOriginalRotation != ROTATION_UNDEFINED
? overrideOriginalRotation
: displayContent.getDisplayInfo().rotation;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 3e43908..6d6bcc8 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3276,7 +3276,9 @@
// just kill it. And if it is a window of foreground activity, the activity can be
// restarted automatically if needed.
Slog.w(TAG, "Exception thrown during dispatchAppVisibility " + this, e);
- android.os.Process.killProcess(mSession.mPid);
+ if (android.os.Process.getUidForPid(mSession.mPid) == mSession.mUid) {
+ android.os.Process.killProcess(mSession.mPid);
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java b/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java
new file mode 100644
index 0000000..8c8f6a6
--- /dev/null
+++ b/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java
@@ -0,0 +1,78 @@
+/*
+ * 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.server.wm.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.DisplayInfo;
+
+/**
+ * Helper class to copy only subset of fields of DisplayInfo object or to perform
+ * comparison operation between DisplayInfo objects only with a subset of fields.
+ */
+public class DisplayInfoOverrides {
+
+ /**
+ * Set of DisplayInfo fields that are overridden in DisplayManager using values from
+ * WindowManager
+ */
+ public static final DisplayInfoFields WM_OVERRIDE_FIELDS = (out, source) -> {
+ out.appWidth = source.appWidth;
+ out.appHeight = source.appHeight;
+ out.smallestNominalAppWidth = source.smallestNominalAppWidth;
+ out.smallestNominalAppHeight = source.smallestNominalAppHeight;
+ out.largestNominalAppWidth = source.largestNominalAppWidth;
+ out.largestNominalAppHeight = source.largestNominalAppHeight;
+ out.logicalWidth = source.logicalWidth;
+ out.logicalHeight = source.logicalHeight;
+ out.physicalXDpi = source.physicalXDpi;
+ out.physicalYDpi = source.physicalYDpi;
+ out.rotation = source.rotation;
+ out.displayCutout = source.displayCutout;
+ out.logicalDensityDpi = source.logicalDensityDpi;
+ out.roundedCorners = source.roundedCorners;
+ out.displayShape = source.displayShape;
+ };
+
+ /**
+ * Gets {@param base} DisplayInfo, overrides WindowManager-specific overrides using
+ * {@param override} and writes the result to {@param out}
+ */
+ public static void copyDisplayInfoFields(@NonNull DisplayInfo out,
+ @NonNull DisplayInfo base,
+ @Nullable DisplayInfo override,
+ @NonNull DisplayInfoFields fields) {
+ out.copyFrom(base);
+
+ if (override != null) {
+ fields.setFields(out, override);
+ }
+ }
+
+ /**
+ * Callback interface that allows to specify a subset of fields of DisplayInfo object
+ */
+ public interface DisplayInfoFields {
+ /**
+ * Copies a subset of fields from {@param source} to {@param out}
+ *
+ * @param out resulting DisplayInfo object
+ * @param source source DisplayInfo to copy fields from
+ */
+ void setFields(@NonNull DisplayInfo out, @NonNull DisplayInfo source);
+ }
+}
diff --git a/services/foldables/devicestateprovider/Android.bp b/services/foldables/devicestateprovider/Android.bp
index 34737ef..56daea7 100644
--- a/services/foldables/devicestateprovider/Android.bp
+++ b/services/foldables/devicestateprovider/Android.bp
@@ -5,9 +5,12 @@
java_library {
name: "foldable-device-state-provider",
srcs: [
- "src/**/*.java"
+ "src/**/*.java",
],
libs: [
"services",
],
+ static_libs: [
+ "device_state_flags_lib",
+ ],
}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index aea46d1..4c487a7 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -21,6 +21,7 @@
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.TYPE_EXTERNAL;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -33,11 +34,14 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
-import android.os.PowerManager;
import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
import android.os.Trace;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.view.Display;
import com.android.internal.annotations.GuardedBy;
@@ -45,24 +49,26 @@
import com.android.internal.util.Preconditions;
import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider;
+import com.android.server.policy.feature.flags.FeatureFlags;
+import com.android.server.policy.feature.flags.FeatureFlagsImpl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.function.BooleanSupplier;
-import java.util.function.Function;
+import java.util.function.Predicate;
/**
* Device state provider for foldable devices.
- *
+ * <p>
* It is an implementation of {@link DeviceStateProvider} tailored specifically for
* foldable devices and allows simple callback-based configuration with hall sensor
* and hinge angle sensor values.
*/
public final class FoldableDeviceStateProvider implements DeviceStateProvider,
SensorEventListener, PowerManager.OnThermalStatusChangedListener,
- DisplayManager.DisplayListener {
+ DisplayManager.DisplayListener {
private static final String TAG = "FoldableDeviceStateProvider";
private static final boolean DEBUG = false;
@@ -77,9 +83,17 @@
// are met for the device to be in the state.
private final SparseArray<BooleanSupplier> mStateConditions = new SparseArray<>();
+ // Map of state identifier to a boolean supplier that returns true when the device state has all
+ // the conditions needed for availability.
+ private final SparseArray<BooleanSupplier> mStateAvailabilityConditions = new SparseArray<>();
+
+ @GuardedBy("mLock")
+ private final SparseBooleanArray mExternalDisplaysConnected = new SparseBooleanArray();
+
private final Sensor mHingeAngleSensor;
private final DisplayManager mDisplayManager;
private final Sensor mHallSensor;
+ private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
@Nullable
@GuardedBy("mLock")
@@ -99,7 +113,23 @@
@GuardedBy("mLock")
private boolean mPowerSaveModeEnabled;
- public FoldableDeviceStateProvider(@NonNull Context context,
+ private final boolean mIsDualDisplayBlockingEnabled;
+
+ public FoldableDeviceStateProvider(
+ @NonNull Context context,
+ @NonNull SensorManager sensorManager,
+ @NonNull Sensor hingeAngleSensor,
+ @NonNull Sensor hallSensor,
+ @NonNull DisplayManager displayManager,
+ @NonNull DeviceStateConfiguration[] deviceStateConfigurations) {
+ this(new FeatureFlagsImpl(), context, sensorManager, hingeAngleSensor, hallSensor,
+ displayManager, deviceStateConfigurations);
+ }
+
+ @VisibleForTesting
+ public FoldableDeviceStateProvider(
+ @NonNull FeatureFlags featureFlags,
+ @NonNull Context context,
@NonNull SensorManager sensorManager,
@NonNull Sensor hingeAngleSensor,
@NonNull Sensor hallSensor,
@@ -112,6 +142,7 @@
mHingeAngleSensor = hingeAngleSensor;
mHallSensor = hallSensor;
mDisplayManager = displayManager;
+ mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
sensorManager.registerListener(this, mHingeAngleSensor, SENSOR_DELAY_FASTEST);
sensorManager.registerListener(this, mHallSensor, SENSOR_DELAY_FASTEST);
@@ -121,20 +152,15 @@
final DeviceStateConfiguration configuration = deviceStateConfigurations[i];
mOrderedStates[i] = configuration.mDeviceState;
- if (mStateConditions.get(configuration.mDeviceState.getIdentifier()) != null) {
- throw new IllegalArgumentException("Device state configurations must have unique"
- + " device state identifiers, found duplicated identifier: " +
- configuration.mDeviceState.getIdentifier());
- }
-
- mStateConditions.put(configuration.mDeviceState.getIdentifier(), () ->
- configuration.mPredicate.apply(this));
+ assertUniqueDeviceStateIdentifier(configuration);
+ initialiseStateConditions(configuration);
+ initialiseStateAvailabilityConditions(configuration);
}
+ Handler handler = new Handler(Looper.getMainLooper());
mDisplayManager.registerDisplayListener(
/* listener = */ this,
- /* handler= */ null,
- /* eventsMask= */ DisplayManager.EVENT_FLAG_DISPLAY_CHANGED);
+ /* handler= */ handler);
Arrays.sort(mOrderedStates, Comparator.comparingInt(DeviceState::getIdentifier));
@@ -167,6 +193,24 @@
}
}
+ private void assertUniqueDeviceStateIdentifier(DeviceStateConfiguration configuration) {
+ if (mStateConditions.get(configuration.mDeviceState.getIdentifier()) != null) {
+ throw new IllegalArgumentException("Device state configurations must have unique"
+ + " device state identifiers, found duplicated identifier: "
+ + configuration.mDeviceState.getIdentifier());
+ }
+ }
+
+ private void initialiseStateConditions(DeviceStateConfiguration configuration) {
+ mStateConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+ configuration.mActiveStatePredicate.test(this));
+ }
+
+ private void initialiseStateAvailabilityConditions(DeviceStateConfiguration configuration) {
+ mStateAvailabilityConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+ configuration.mAvailabilityPredicate.test(this));
+ }
+
@Override
public void setListener(Listener listener) {
synchronized (mLock) {
@@ -189,16 +233,9 @@
}
listener = mListener;
for (DeviceState deviceState : mOrderedStates) {
- if (isThermalStatusCriticalOrAbove(mThermalStatus)
- && deviceState.hasFlag(
- DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
- continue;
+ if (isStateSupported(deviceState)) {
+ supportedStates.add(deviceState);
}
- if (mPowerSaveModeEnabled && deviceState.hasFlag(
- DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
- continue;
- }
- supportedStates.add(deviceState);
}
}
@@ -206,6 +243,26 @@
supportedStates.toArray(new DeviceState[supportedStates.size()]), reason);
}
+ @GuardedBy("mLock")
+ private boolean isStateSupported(DeviceState deviceState) {
+ if (isThermalStatusCriticalOrAbove(mThermalStatus)
+ && deviceState.hasFlag(
+ DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
+ return false;
+ }
+ if (mPowerSaveModeEnabled && deviceState.hasFlag(
+ DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
+ return false;
+ }
+ if (mIsDualDisplayBlockingEnabled
+ && mStateAvailabilityConditions.contains(deviceState.getIdentifier())) {
+ return mStateAvailabilityConditions
+ .get(deviceState.getIdentifier())
+ .getAsBoolean();
+ }
+ return true;
+ }
+
/** Computes the current device state and notifies the listener of a change, if needed. */
void notifyDeviceStateChangedIfNeeded() {
int stateToReport = INVALID_DEVICE_STATE;
@@ -294,7 +351,7 @@
private void dumpSensorValues() {
Slog.i(TAG, "Sensor values:");
dumpSensorValues("Hall Sensor", mHallSensor, mLastHallSensorEvent);
- dumpSensorValues("Hinge Angle Sensor",mHingeAngleSensor, mLastHingeAngleSensorEvent);
+ dumpSensorValues("Hinge Angle Sensor", mHingeAngleSensor, mLastHingeAngleSensorEvent);
Slog.i(TAG, "isScreenOn: " + isScreenOn());
}
@@ -307,12 +364,35 @@
@Override
public void onDisplayAdded(int displayId) {
+ // TODO(b/312397262): consider virtual displays cases
+ synchronized (mLock) {
+ if (mIsDualDisplayBlockingEnabled
+ && !mExternalDisplaysConnected.get(displayId, false)
+ && mDisplayManager.getDisplay(displayId).getType() == TYPE_EXTERNAL) {
+ mExternalDisplaysConnected.put(displayId, true);
+ // Only update the supported state when going from 0 external display to 1
+ if (mExternalDisplaysConnected.size() == 1) {
+ notifySupportedStatesChanged(
+ SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED);
+ }
+ }
+ }
}
@Override
public void onDisplayRemoved(int displayId) {
+ synchronized (mLock) {
+ if (mIsDualDisplayBlockingEnabled && mExternalDisplaysConnected.get(displayId, false)) {
+ mExternalDisplaysConnected.delete(displayId);
+ // Only update the supported states when going from 1 external display to 0
+ if (mExternalDisplaysConnected.size() == 0) {
+ notifySupportedStatesChanged(
+ SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED);
+ }
+ }
+ }
}
@Override
@@ -338,48 +418,71 @@
*/
public static class DeviceStateConfiguration {
private final DeviceState mDeviceState;
- private final Function<FoldableDeviceStateProvider, Boolean> mPredicate;
+ private final Predicate<FoldableDeviceStateProvider> mActiveStatePredicate;
+ private final Predicate<FoldableDeviceStateProvider> mAvailabilityPredicate;
- private DeviceStateConfiguration(DeviceState deviceState,
- Function<FoldableDeviceStateProvider, Boolean> predicate) {
+ private DeviceStateConfiguration(
+ @NonNull DeviceState deviceState,
+ @NonNull Predicate<FoldableDeviceStateProvider> predicate) {
+ this(deviceState, predicate, ALLOWED);
+ }
+
+ private DeviceStateConfiguration(
+ @NonNull DeviceState deviceState,
+ @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate,
+ @NonNull Predicate<FoldableDeviceStateProvider> availabilityPredicate) {
+
mDeviceState = deviceState;
- mPredicate = predicate;
+ mActiveStatePredicate = activeStatePredicate;
+ mAvailabilityPredicate = availabilityPredicate;
}
public static DeviceStateConfiguration createConfig(
@IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
@NonNull String name,
@DeviceState.DeviceStateFlags int flags,
- Function<FoldableDeviceStateProvider, Boolean> predicate
+ @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate
) {
return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
- predicate);
+ activeStatePredicate);
}
public static DeviceStateConfiguration createConfig(
@IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
@NonNull String name,
- Function<FoldableDeviceStateProvider, Boolean> predicate
+ @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate
) {
return new DeviceStateConfiguration(new DeviceState(identifier, name, /* flags= */ 0),
- predicate);
+ activeStatePredicate);
+ }
+
+ /** Create a configuration with availability predicate **/
+ public static DeviceStateConfiguration createConfig(
+ @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
+ @NonNull String name,
+ @DeviceState.DeviceStateFlags int flags,
+ @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate,
+ @NonNull Predicate<FoldableDeviceStateProvider> availabilityPredicate
+ ) {
+ return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
+ activeStatePredicate, availabilityPredicate);
}
/**
* Creates a device state configuration for a closed tent-mode aware state.
- *
+ * <p>
* During tent mode:
* - The inner display is OFF
* - The outer display is ON
* - The device is partially unfolded (left and right edges could be on the table)
* In this mode the device the device so it could be used in a posture where both left
* and right edges of the unfolded device are on the table.
- *
+ * <p>
* The predicate returns false after the hinge angle reaches
* {@code tentModeSwitchAngleDegrees}. Then it switches back only when the hinge angle
* becomes less than {@code maxClosedAngleDegrees}. Hinge angle is 0 degrees when the device
* is fully closed and 180 degrees when it is fully unfolded.
- *
+ * <p>
* For example, when tentModeSwitchAngleDegrees = 90 and maxClosedAngleDegrees = 5 degrees:
* - when unfolding the device from fully closed posture (last state == closed or it is
* undefined yet) this state will become not matching after reaching the angle
@@ -435,6 +538,15 @@
}
/**
+ * @return Whether there is an external connected display.
+ */
+ public boolean hasNoConnectedExternalDisplay() {
+ synchronized (mLock) {
+ return mExternalDisplaysConnected.size() == 0;
+ }
+ }
+
+ /**
* @return Whether the screen is on.
*/
public boolean isScreenOn() {
@@ -442,6 +554,7 @@
return mIsScreenOn;
}
}
+
/**
* @return current hinge angle value of a foldable device
*/
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
index 5f2cf3c..5968b63 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
@@ -33,6 +33,10 @@
import com.android.server.devicestate.DeviceStatePolicy;
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+import com.android.server.policy.feature.flags.FeatureFlags;
+import com.android.server.policy.feature.flags.FeatureFlagsImpl;
+
+import java.util.function.Predicate;
/**
* Device state policy for a foldable device that supports tent mode: a mode when the device
@@ -55,6 +59,10 @@
private final DeviceStateProvider mProvider;
+ private final boolean mIsDualDisplayBlockingEnabled;
+ private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
+ private static final Predicate<FoldableDeviceStateProvider> NOT_ALLOWED = p -> false;
+
/**
* Creates TentModeDeviceStatePolicy
*
@@ -67,6 +75,12 @@
*/
public TentModeDeviceStatePolicy(@NonNull Context context,
@NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, int closeAngleDegrees) {
+ this(new FeatureFlagsImpl(), context, hingeAngleSensor, hallSensor, closeAngleDegrees);
+ }
+
+ public TentModeDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context,
+ @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor,
+ int closeAngleDegrees) {
super(context);
final SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
@@ -74,8 +88,10 @@
final DeviceStateConfiguration[] configuration = createConfiguration(closeAngleDegrees);
- mProvider = new FoldableDeviceStateProvider(mContext, sensorManager, hingeAngleSensor,
- hallSensor, displayManager, configuration);
+ mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
+
+ mProvider = new FoldableDeviceStateProvider(mContext, sensorManager,
+ hingeAngleSensor, hallSensor, displayManager, configuration);
}
private DeviceStateConfiguration[] createConfiguration(int closeAngleDegrees) {
@@ -83,24 +99,27 @@
createClosedConfiguration(closeAngleDegrees),
createConfig(DEVICE_STATE_HALF_OPENED,
/* name= */ "HALF_OPENED",
- (provider) -> {
+ /* activeStatePredicate= */ (provider) -> {
final float hingeAngle = provider.getHingeAngle();
return hingeAngle >= MAX_CLOSED_ANGLE_DEGREES
&& hingeAngle <= TABLE_TOP_MODE_SWITCH_ANGLE_DEGREES;
}),
createConfig(DEVICE_STATE_OPENED,
/* name= */ "OPENED",
- (provider) -> true),
+ /* activeStatePredicate= */ ALLOWED),
createConfig(DEVICE_STATE_REAR_DISPLAY_STATE,
/* name= */ "REAR_DISPLAY_STATE",
/* flags= */ FLAG_EMULATED_ONLY,
- (provider) -> false),
+ /* activeStatePredicate= */ NOT_ALLOWED),
createConfig(DEVICE_STATE_CONCURRENT_INNER_DEFAULT,
/* name= */ "CONCURRENT_INNER_DEFAULT",
/* flags= */ FLAG_EMULATED_ONLY | FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP
| FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
| FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE,
- (provider) -> false)
+ /* activeStatePredicate= */ NOT_ALLOWED,
+ /* availabilityPredicate= */
+ provider -> !mIsDualDisplayBlockingEnabled
+ || provider.hasNoConnectedExternalDisplay())
};
}
@@ -111,7 +130,7 @@
DEVICE_STATE_CLOSED,
/* name= */ "CLOSED",
/* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS,
- (provider) -> {
+ /* activeStatePredicate= */ (provider) -> {
final float hingeAngle = provider.getHingeAngle();
return hingeAngle <= closeAngleDegrees;
}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
new file mode 100644
index 0000000..6ad8d79
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
@@ -0,0 +1,12 @@
+aconfig_declarations {
+ name: "device_state_flags",
+ package: "com.android.server.policy.feature.flags",
+ srcs: [
+ "device_state_flags.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "device_state_flags_lib",
+ aconfig_declarations: "device_state_flags",
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
new file mode 100644
index 0000000..47c2a1b
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.policy.feature.flags"
+
+flag {
+ name: "enable_dual_display_blocking"
+ namespace: "display_manager"
+ description: "Feature flag for dual display blocking"
+ bug: "278667199"
+}
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
index 8fa4ce5..ddf4a08 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -17,18 +17,21 @@
package com.android.server.policy;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.STATE_OFF;
+import static android.view.Display.STATE_ON;
+import static android.view.Display.TYPE_EXTERNAL;
+import static android.view.Display.TYPE_INTERNAL;
+
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL;
import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL;
-import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Display.STATE_OFF;
-import static android.view.Display.STATE_ON;
-
import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertArrayEquals;
@@ -36,12 +39,11 @@
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.nullable;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -51,20 +53,21 @@
import android.hardware.SensorManager;
import android.hardware.display.DisplayManager;
import android.hardware.input.InputSensorInfo;
-import android.os.PowerManager;
import android.os.Handler;
+import android.os.PowerManager;
import android.testing.AndroidTestingRunner;
import android.view.Display;
import com.android.server.devicestate.DeviceState;
-import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.devicestate.DeviceStateProvider.Listener;
+import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl;
+import com.android.server.policy.feature.flags.Flags;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -95,10 +98,16 @@
@Mock
private DisplayManager mDisplayManager;
private FoldableDeviceStateProvider mProvider;
+ @Mock
+ private Display mDefaultDisplay;
+ @Mock
+ private Display mExternalDisplay;
+ private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
+ mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_DUAL_DISPLAY_BLOCKING, true);
mHallSensor = new Sensor(mInputSensorInfo);
mHingeAngleSensor = new Sensor(mInputSensorInfo);
@@ -473,6 +482,133 @@
assertThat(mProvider.isScreenOn()).isFalse();
}
+ @Test
+ public void test_dualScreenDisabledWhenExternalScreenIsConnected() throws Exception {
+ when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+ when(mDefaultDisplay.getType()).thenReturn(TYPE_INTERNAL);
+
+ createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+ (c) -> c.getHingeAngle() < 5f),
+ createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+ (c) -> c.getHingeAngle() < 90f),
+ createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+ (c) -> c.getHingeAngle() < 180f),
+ createConfig(/* identifier= */ 4, /* name= */ "DUAL_DISPLAY", /* flags */ 0,
+ (c) -> false, FoldableDeviceStateProvider::hasNoConnectedExternalDisplay));
+
+ Listener listener = mock(Listener.class);
+ mProvider.setListener(listener);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+ assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+ new DeviceState[]{
+ new DeviceState(1, "CLOSED", 0 /* flags */),
+ new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+ new DeviceState(3, "OPENED", 0 /* flags */),
+ new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+
+ clearInvocations(listener);
+
+ when(mDisplayManager.getDisplays())
+ .thenReturn(new Display[]{mDefaultDisplay, mExternalDisplay});
+ when(mDisplayManager.getDisplay(1)).thenReturn(mExternalDisplay);
+ when(mExternalDisplay.getType()).thenReturn(TYPE_EXTERNAL);
+
+ // The DUAL_DISPLAY state should be disabled.
+ mProvider.onDisplayAdded(1);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+ assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+ new DeviceState[]{
+ new DeviceState(1, "CLOSED", 0 /* flags */),
+ new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+ new DeviceState(3, "OPENED", 0 /* flags */)}).inOrder();
+ clearInvocations(listener);
+
+ // The DUAL_DISPLAY state should be re-enabled.
+ when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+ mProvider.onDisplayRemoved(1);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED));
+ assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+ new DeviceState[]{
+ new DeviceState(1, "CLOSED", 0 /* flags */),
+ new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+ new DeviceState(3, "OPENED", 0 /* flags */),
+ new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+ }
+
+ @Test
+ public void test_notifySupportedStatesChangedCalledOnlyOnInitialExternalScreenAddition() {
+ when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+ when(mDefaultDisplay.getType()).thenReturn(TYPE_INTERNAL);
+
+ createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+ (c) -> c.getHingeAngle() < 5f),
+ createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+ (c) -> c.getHingeAngle() < 90f),
+ createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+ (c) -> c.getHingeAngle() < 180f),
+ createConfig(/* identifier= */ 4, /* name= */ "DUAL_DISPLAY", /* flags */ 0,
+ (c) -> false, FoldableDeviceStateProvider::hasNoConnectedExternalDisplay));
+
+ Listener listener = mock(Listener.class);
+ mProvider.setListener(listener);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+ assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+ new DeviceState[]{
+ new DeviceState(1, "CLOSED", 0 /* flags */),
+ new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+ new DeviceState(3, "OPENED", 0 /* flags */),
+ new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+
+ clearInvocations(listener);
+
+ addExternalDisplay(1);
+ verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+ addExternalDisplay(2);
+ addExternalDisplay(3);
+ addExternalDisplay(4);
+ verify(listener, times(1))
+ .onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+ eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+ }
+
+ @Test
+ public void hasNoConnectedDisplay_afterExternalDisplayAdded_returnsFalse() {
+ createProvider(
+ createConfig(
+ /* identifier= */ 1, /* name= */ "ONE",
+ /* flags= */0, (c) -> true,
+ FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+ );
+
+ addExternalDisplay(/* displayId */ 1);
+
+ assertThat(mProvider.hasNoConnectedExternalDisplay()).isFalse();
+ }
+
+ @Test
+ public void hasNoConnectedDisplay_afterExternalDisplayAddedAndRemoved_returnsTrue() {
+ createProvider(
+ createConfig(
+ /* identifier= */ 1, /* name= */ "ONE",
+ /* flags= */0, (c) -> true,
+ FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+ );
+
+ addExternalDisplay(/* displayId */ 1);
+ mProvider.onDisplayRemoved(1);
+
+ assertThat(mProvider.hasNoConnectedExternalDisplay()).isTrue();
+ }
+ private void addExternalDisplay(int displayId) {
+ when(mDisplayManager.getDisplay(displayId)).thenReturn(mExternalDisplay);
+ when(mExternalDisplay.getType()).thenReturn(TYPE_EXTERNAL);
+ mProvider.onDisplayAdded(displayId);
+ }
private void setScreenOn(boolean isOn) {
Display mockDisplay = mock(Display.class);
int state = isOn ? STATE_ON : STATE_OFF;
@@ -508,12 +644,11 @@
}
private void createProvider(DeviceStateConfiguration... configurations) {
- mProvider = new FoldableDeviceStateProvider(mContext, mSensorManager, mHingeAngleSensor,
- mHallSensor, mDisplayManager, configurations);
+ mProvider = new FoldableDeviceStateProvider(mFakeFeatureFlags, mContext, mSensorManager,
+ mHingeAngleSensor, mHallSensor, mDisplayManager, configurations);
verify(mDisplayManager)
.registerDisplayListener(
mDisplayListenerCaptor.capture(),
- nullable(Handler.class),
- anyLong());
+ nullable(Handler.class));
}
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.kt
new file mode 100644
index 0000000..3f72364
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.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.server.display.mode
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+private const val BASE_REFRESH_RATE = 60f
+private const val OTHER_BASE_REFRESH_RATE = 90f
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BaseModeRefreshRateVoteTest {
+
+ private lateinit var baseModeVote: BaseModeRefreshRateVote
+
+ @Before
+ fun setUp() {
+ baseModeVote = BaseModeRefreshRateVote(BASE_REFRESH_RATE)
+ }
+
+ @Test
+ fun `updates summary with base mode refresh rate if not set`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+
+ baseModeVote.updateSummary(summary)
+
+ assertThat(summary.appRequestBaseModeRefreshRate).isEqualTo(BASE_REFRESH_RATE)
+ }
+
+ @Test
+ fun `keeps summary base mode refresh rate if set`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.appRequestBaseModeRefreshRate = OTHER_BASE_REFRESH_RATE
+
+ baseModeVote.updateSummary(summary)
+
+ assertThat(summary.appRequestBaseModeRefreshRate).isEqualTo(OTHER_BASE_REFRESH_RATE)
+ }
+
+ @Test
+ fun `keeps summary with base mode refresh rate if vote refresh rate is negative`() {
+ val invalidBaseModeVote = BaseModeRefreshRateVote(-10f)
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+
+ invalidBaseModeVote.updateSummary(summary)
+
+ assertThat(summary.appRequestBaseModeRefreshRate).isZero()
+ }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt
new file mode 100644
index 0000000..7f8da88
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.server.display.mode
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+
+
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CombinedVoteTest {
+ private lateinit var combinedVote: CombinedVote
+
+ @get:Rule
+ val mockitoRule = MockitoJUnit.rule()
+
+ private val mockVote1 = mock<Vote>()
+ private val mockVote2 = mock<Vote>()
+
+ @Before
+ fun setUp() {
+ combinedVote = CombinedVote(listOf(mockVote1, mockVote2))
+ }
+
+ @Test
+ fun `delegates update to children`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+
+ combinedVote.updateSummary(summary)
+
+ verify(mockVote1).updateSummary(summary)
+ verify(mockVote2).updateSummary(summary)
+ }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt
new file mode 100644
index 0000000..c624325
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.server.display.mode
+
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class DisableRefreshRateSwitchingVoteTest {
+
+ @Test
+ fun `disabled refresh rate switching is not changed`(
+ @TestParameter voteDisableSwitching: Boolean
+ ) {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.disableRefreshRateSwitching = true
+ val vote = DisableRefreshRateSwitchingVote(voteDisableSwitching)
+
+ vote.updateSummary(summary)
+
+ assertThat(summary.disableRefreshRateSwitching).isTrue()
+ }
+
+ @Test
+ fun `disables refresh rate switching if requested`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ val vote = DisableRefreshRateSwitchingVote(true)
+
+ vote.updateSummary(summary)
+
+ assertThat(summary.disableRefreshRateSwitching).isTrue()
+ }
+
+ @Test
+ fun `does not disable refresh rate switching if not requested`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ val vote = DisableRefreshRateSwitchingVote(false)
+
+ vote.updateSummary(summary)
+
+ assertThat(summary.disableRefreshRateSwitching).isFalse()
+ }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index 6798a2d..d085923 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -28,7 +28,6 @@
import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.server.display.mode.Vote.INVALID_SIZE;
import static com.google.common.truth.Truth.assertThat;
@@ -1193,7 +1192,9 @@
assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
assertThat(vote).isNotNull();
- assertThat(vote.disableRefreshRateSwitching).isTrue();
+ assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+ DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+ assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
}
@Test
@@ -1272,7 +1273,9 @@
assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
assertThat(vote).isNotNull();
- assertThat(vote.disableRefreshRateSwitching).isTrue();
+ assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+ DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+ assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
// We expect DisplayModeDirector to act on BrightnessInfo.adjustedBrightness; set only this
// parameter to the necessary threshold
@@ -1341,7 +1344,9 @@
assertVoteForPhysicalRefreshRate(vote, 60 /*fps*/);
vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
assertThat(vote).isNotNull();
- assertThat(vote.disableRefreshRateSwitching).isTrue();
+ assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+ DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+ assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
}
@Test
@@ -1424,7 +1429,9 @@
assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
assertThat(vote).isNotNull();
- assertThat(vote.disableRefreshRateSwitching).isTrue();
+ assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+ DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+ assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
// Set critical and check new refresh rate
Temperature temp = getSkinTemp(Temperature.THROTTLING_CRITICAL);
@@ -1436,7 +1443,9 @@
assertVoteForPhysicalRefreshRate(vote, 60 /*fps*/);
vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
assertThat(vote).isNotNull();
- assertThat(vote.disableRefreshRateSwitching).isTrue();
+ assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+ disableVote = (DisableRefreshRateSwitchingVote) vote;
+ assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
}
@Test
@@ -1519,7 +1528,9 @@
assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
assertThat(vote).isNotNull();
- assertThat(vote.disableRefreshRateSwitching).isTrue();
+ assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+ DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+ assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
// Set critical and check new refresh rate
Temperature temp = getSkinTemp(Temperature.THROTTLING_CRITICAL);
@@ -1531,7 +1542,9 @@
assertVoteForPhysicalRefreshRate(vote, 60 /*fps*/);
vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
assertThat(vote).isNotNull();
- assertThat(vote.disableRefreshRateSwitching).isTrue();
+ assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+ disableVote = (DisableRefreshRateSwitchingVote) vote;
+ assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
}
@Test
@@ -1877,61 +1890,43 @@
DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 0, 0);
- Vote appRequestRefreshRate =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
- assertNotNull(appRequestRefreshRate);
- assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
- assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
- assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
- assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
- assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
- .isWithin(FLOAT_TOLERANCE).of(60);
- assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
- assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(BaseModeRefreshRateVote.class);
+ BaseModeRefreshRateVote baseModeVote = (BaseModeRefreshRateVote) vote;
+ assertThat(baseModeVote.mAppRequestBaseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
- Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
- assertNotNull(appRequestSize);
- assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
- assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
- assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
- assertThat(appRequestSize.disableRefreshRateSwitching).isFalse();
- assertThat(appRequestSize.appRequestBaseModeRefreshRate).isZero();
- assertThat(appRequestSize.height).isEqualTo(1000);
- assertThat(appRequestSize.width).isEqualTo(1000);
+ vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(SizeVote.class);
+ SizeVote sizeVote = (SizeVote) vote;
+ assertThat(sizeVote.mHeight).isEqualTo(1000);
+ assertThat(sizeVote.mWidth).isEqualTo(1000);
+ assertThat(sizeVote.mMinHeight).isEqualTo(1000);
+ assertThat(sizeVote.mMinWidth).isEqualTo(1000);
- Vote appRequestRefreshRateRange =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
- assertNull(appRequestRefreshRateRange);
+ vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+ assertNull(vote);
director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 90, 0, 0);
- appRequestRefreshRate =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
- assertNotNull(appRequestRefreshRate);
- assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
- assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
- assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
- assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
- assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
- .isWithin(FLOAT_TOLERANCE).of(90);
- assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
- assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
+ vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(BaseModeRefreshRateVote.class);
+ baseModeVote = (BaseModeRefreshRateVote) vote;
+ assertThat(baseModeVote.mAppRequestBaseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
- appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
- assertNotNull(appRequestSize);
- assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
- assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
- assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
- assertThat(appRequestSize.height).isEqualTo(1000);
- assertThat(appRequestSize.width).isEqualTo(1000);
+ vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(SizeVote.class);
+ sizeVote = (SizeVote) vote;
+ assertThat(sizeVote.mHeight).isEqualTo(1000);
+ assertThat(sizeVote.mWidth).isEqualTo(1000);
+ assertThat(sizeVote.mMinHeight).isEqualTo(1000);
+ assertThat(sizeVote.mMinWidth).isEqualTo(1000);
- appRequestRefreshRateRange =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
- assertNull(appRequestRefreshRateRange);
+ vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+ assertNull(vote);
}
@Test
@@ -1945,17 +1940,12 @@
Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
assertNull(appRequestSize);
- Vote appRequestRefreshRateRange =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
- assertNotNull(appRequestRefreshRateRange);
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
- .isPositiveInfinity();
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
- .isWithin(FLOAT_TOLERANCE).of(60);
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max).isAtLeast(90);
- assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
- assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+ assertThat(renderVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
+ assertThat(renderVote.mMaxRefreshRate).isAtLeast(90);
director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 90, 0);
appRequestRefreshRate =
@@ -1965,18 +1955,12 @@
appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
assertNull(appRequestSize);
- appRequestRefreshRateRange =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
- assertNotNull(appRequestRefreshRateRange);
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
- .isPositiveInfinity();
-
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
- .isWithin(FLOAT_TOLERANCE).of(90);
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max).isAtLeast(90);
- assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
- assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+ vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ renderVote = (RefreshRateVote.RenderVote) vote;
+ assertThat(renderVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
+ assertThat(renderVote.mMaxRefreshRate).isAtLeast(90);
}
@Test
@@ -1990,18 +1974,12 @@
Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
assertNull(appRequestSize);
- Vote appRequestRefreshRateRange =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
- assertNotNull(appRequestRefreshRateRange);
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
- .isPositiveInfinity();
-
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min).isZero();
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
- .isWithin(FLOAT_TOLERANCE).of(90);
- assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
- assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+ assertThat(renderVote.mMinRefreshRate).isZero();
+ assertThat(renderVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 0, 60);
appRequestRefreshRate =
@@ -2011,18 +1989,12 @@
appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
assertNull(appRequestSize);
- appRequestRefreshRateRange =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
- assertNotNull(appRequestRefreshRateRange);
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
- .isPositiveInfinity();
-
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min).isZero();
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
- .isWithin(FLOAT_TOLERANCE).of(60);
- assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
- assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+ vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ renderVote = (RefreshRateVote.RenderVote) vote;
+ assertThat(renderVote.mMinRefreshRate).isZero();
+ assertThat(renderVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
}
@Test
@@ -2046,41 +2018,27 @@
DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 90, 90);
- Vote appRequestRefreshRate =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
- assertNotNull(appRequestRefreshRate);
- assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
- assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
- assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
- assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
- assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
- .isWithin(FLOAT_TOLERANCE).of(60);
- assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
- assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
+ Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(BaseModeRefreshRateVote.class);
+ BaseModeRefreshRateVote baseModeVote = (BaseModeRefreshRateVote) vote;
+ assertThat(baseModeVote.mAppRequestBaseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
- Vote appRequestSize =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
- assertNotNull(appRequestSize);
- assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
- assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
- assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
- assertThat(appRequestSize.height).isEqualTo(1000);
- assertThat(appRequestSize.width).isEqualTo(1000);
+ vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(SizeVote.class);
+ SizeVote sizeVote = (SizeVote) vote;
+ assertThat(sizeVote.mHeight).isEqualTo(1000);
+ assertThat(sizeVote.mWidth).isEqualTo(1000);
+ assertThat(sizeVote.mMinHeight).isEqualTo(1000);
+ assertThat(sizeVote.mMinWidth).isEqualTo(1000);
- Vote appRequestRefreshRateRange =
- director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
- assertNotNull(appRequestRefreshRateRange);
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
- assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
- .isPositiveInfinity();
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
- .isWithin(FLOAT_TOLERANCE).of(90);
- assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
- .isWithin(FLOAT_TOLERANCE).of(90);
- assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
- assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+ vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+ assertNotNull(vote);
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+ assertThat(renderVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
+ assertThat(renderVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
}
@Test
@@ -3150,8 +3108,7 @@
captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
- assertThat(vote.refreshRateRanges.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
- assertThat(vote.refreshRateRanges.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+ assertVoteForPhysicalRefreshRate(vote, 90);
}
@Test
@@ -3184,8 +3141,7 @@
captor.getValue().onRequestEnabled(DISPLAY_ID);
Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
- assertThat(vote.refreshRateRanges.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
- assertThat(vote.refreshRateRanges.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+ assertVoteForPhysicalRefreshRate(vote, 90);
}
@Test
@@ -3257,16 +3213,21 @@
private void assertVoteForPhysicalRefreshRate(Vote vote, float refreshRate) {
assertThat(vote).isNotNull();
- final RefreshRateRange expectedRange = new RefreshRateRange(refreshRate, refreshRate);
- assertThat(vote.refreshRateRanges.physical).isEqualTo(expectedRange);
+ assertThat(vote).isInstanceOf(CombinedVote.class);
+ CombinedVote combinedVote = (CombinedVote) vote;
+ RefreshRateVote.PhysicalVote physicalVote =
+ (RefreshRateVote.PhysicalVote) combinedVote.mVotes.get(0);
+ assertThat(physicalVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(refreshRate);
+ assertThat(physicalVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(refreshRate);
}
private void assertVoteForRenderFrameRateRange(
Vote vote, float frameRateLow, float frameRateHigh) {
assertThat(vote).isNotNull();
- final RefreshRateRange expectedRange =
- new RefreshRateRange(frameRateLow, frameRateHigh);
- assertThat(vote.refreshRateRanges.render).isEqualTo(expectedRange);
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+ assertThat(renderVote.mMinRefreshRate).isEqualTo(frameRateLow);
+ assertThat(renderVote.mMaxRefreshRate).isEqualTo(frameRateHigh);
}
public static class FakeDeviceConfig extends FakeDeviceConfigInterface {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt
new file mode 100644
index 0000000..547008e
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.server.display.mode
+
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val MIN_REFRESH_RATE = 60f
+private const val MAX_REFRESH_RATE = 90f
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class PhysicalVoteTest {
+ private lateinit var physicalVote: RefreshRateVote.PhysicalVote
+
+ @Before
+ fun setUp() {
+ physicalVote = RefreshRateVote.PhysicalVote(MIN_REFRESH_RATE, MAX_REFRESH_RATE)
+ }
+
+ @Test
+ fun `updates minPhysicalRefreshRate if summary has less`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.minPhysicalRefreshRate = 45f
+
+ physicalVote.updateSummary(summary)
+
+ assertThat(summary.minPhysicalRefreshRate).isEqualTo(MIN_REFRESH_RATE)
+ }
+
+ @Test
+ fun `does not update minPhysicalRefreshRate if summary has more`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.minPhysicalRefreshRate = 75f
+
+ physicalVote.updateSummary(summary)
+
+ assertThat(summary.minPhysicalRefreshRate).isEqualTo(75f)
+ }
+
+ @Test
+ fun `updates maxPhysicalRefreshRate if summary has more`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.maxPhysicalRefreshRate = 120f
+
+ physicalVote.updateSummary(summary)
+
+ assertThat(summary.maxPhysicalRefreshRate).isEqualTo(MAX_REFRESH_RATE)
+ }
+
+ @Test
+ fun `does not update maxPhysicalRefreshRate if summary has less`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.maxPhysicalRefreshRate = 75f
+
+ physicalVote.updateSummary(summary)
+
+ assertThat(summary.maxPhysicalRefreshRate).isEqualTo(75f)
+ }
+
+ @Test
+ fun `updates maxRenderFrameRate if summary has more`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.maxRenderFrameRate = 120f
+
+ physicalVote.updateSummary(summary)
+
+ assertThat(summary.maxRenderFrameRate).isEqualTo(MAX_REFRESH_RATE)
+ }
+
+ @Test
+ fun `does not update maxRenderFrameRate if summary has less`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.maxRenderFrameRate = 75f
+
+ physicalVote.updateSummary(summary)
+
+ assertThat(summary.maxRenderFrameRate).isEqualTo(75f)
+ }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt
new file mode 100644
index 0000000..868a893
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.server.display.mode
+
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val MIN_REFRESH_RATE = 60f
+private const val MAX_REFRESH_RATE = 90f
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class RenderVoteTest {
+
+ private lateinit var renderVote: RefreshRateVote.RenderVote
+
+ @Before
+ fun setUp() {
+ renderVote = RefreshRateVote.RenderVote(MIN_REFRESH_RATE, MAX_REFRESH_RATE)
+ }
+
+ @Test
+ fun `updates minRenderFrameRate if summary has less`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.minRenderFrameRate = 45f
+
+ renderVote.updateSummary(summary)
+
+ assertThat(summary.minRenderFrameRate).isEqualTo(MIN_REFRESH_RATE)
+ }
+
+ @Test
+ fun `does not update minRenderFrameRate if summary has more`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.minRenderFrameRate = 75f
+
+ renderVote.updateSummary(summary)
+
+ assertThat(summary.minRenderFrameRate).isEqualTo(75f)
+ }
+
+ @Test
+ fun `updates maxRenderFrameRate if summary has more`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.maxRenderFrameRate = 120f
+
+ renderVote.updateSummary(summary)
+
+ assertThat(summary.maxRenderFrameRate).isEqualTo(MAX_REFRESH_RATE)
+ }
+
+ @Test
+ fun `does not update maxRenderFrameRate if summary has less`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.maxRenderFrameRate = 75f
+
+ renderVote.updateSummary(summary)
+
+ assertThat(summary.maxRenderFrameRate).isEqualTo(75f)
+ }
+
+ @Test
+ fun `updates minPhysicalRefreshRate if summary has less`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.minPhysicalRefreshRate = 45f
+
+ renderVote.updateSummary(summary)
+
+ assertThat(summary.minPhysicalRefreshRate).isEqualTo(MIN_REFRESH_RATE)
+ }
+
+ @Test
+ fun `does not update minPhysicalRefreshRate if summary has more`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.minPhysicalRefreshRate = 75f
+
+ renderVote.updateSummary(summary)
+
+ assertThat(summary.minPhysicalRefreshRate).isEqualTo(75f)
+ }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt
new file mode 100644
index 0000000..1c631b0
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.server.display.mode
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+private const val WIDTH = 800
+private const val HEIGHT = 1600
+private const val MIN_WIDTH = 400
+private const val MIN_HEIGHT = 1200
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SizeVoteTest {
+ private lateinit var sizeVote: SizeVote
+
+ @Before
+ fun setUp() {
+ sizeVote = SizeVote(WIDTH, HEIGHT, MIN_WIDTH, MIN_HEIGHT)
+ }
+
+ @Test
+ fun `updates size if width and height not set and display resolution voting disabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ false)
+ summary.width = Vote.INVALID_SIZE
+ summary.height = Vote.INVALID_SIZE
+ summary.minWidth = 100
+ summary.minHeight = 200
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.width).isEqualTo(WIDTH)
+ assertThat(summary.height).isEqualTo(HEIGHT)
+ assertThat(summary.minWidth).isEqualTo(MIN_WIDTH)
+ assertThat(summary.minHeight).isEqualTo(MIN_HEIGHT)
+ }
+
+ @Test
+ fun `does not update size if width set and display resolution voting disabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ false)
+ summary.width = 150
+ summary.height = Vote.INVALID_SIZE
+ summary.minWidth = 100
+ summary.minHeight = 200
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.width).isEqualTo(150)
+ assertThat(summary.height).isEqualTo(Vote.INVALID_SIZE)
+ assertThat(summary.minWidth).isEqualTo(100)
+ assertThat(summary.minHeight).isEqualTo(200)
+ }
+
+ @Test
+ fun `does not update size if height set and display resolution voting disabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ false)
+ summary.width = Vote.INVALID_SIZE
+ summary.height = 250
+ summary.minWidth = 100
+ summary.minHeight = 200
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.width).isEqualTo(Vote.INVALID_SIZE)
+ assertThat(summary.height).isEqualTo(250)
+ assertThat(summary.minWidth).isEqualTo(100)
+ assertThat(summary.minHeight).isEqualTo(200)
+ }
+
+ @Test
+ fun `updates width if summary has more and display resolution voting enabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.width = 850
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.width).isEqualTo(WIDTH)
+ }
+
+ @Test
+ fun `does not update width if summary has less and display resolution voting enabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.width = 750
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.width).isEqualTo(750)
+ }
+
+ @Test
+ fun `updates height if summary has more and display resolution voting enabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.height = 1650
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.height).isEqualTo(HEIGHT)
+ }
+
+ @Test
+ fun `does not update height if summary has less and display resolution voting enabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.height = 1550
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.height).isEqualTo(1550)
+ }
+
+ @Test
+ fun `updates minWidth if summary has less and display resolution voting enabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.width = 150
+ summary.minWidth = 350
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.minWidth).isEqualTo(MIN_WIDTH)
+ }
+
+ @Test
+ fun `does not update minWidth if summary has more and display resolution voting enabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.width = 150
+ summary.minWidth = 450
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.minWidth).isEqualTo(450)
+ }
+
+ @Test
+ fun `updates minHeight if summary has less and display resolution voting enabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.width = 150
+ summary.minHeight = 1150
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.minHeight).isEqualTo(MIN_HEIGHT)
+ }
+
+ @Test
+ fun `does not update minHeight if summary has more and display resolution voting enabled`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.width = 150
+ summary.minHeight = 1250
+
+ sizeVote.updateSummary(summary)
+
+ assertThat(summary.minHeight).isEqualTo(1250)
+ }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
index 9ab6ee5..f677401 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
@@ -17,6 +17,8 @@
package com.android.server.display.mode;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -102,17 +104,21 @@
SparseArray<Vote> displayVotes = mStorage.getVotes(DISPLAY_ID);
assertEquals(1, displayVotes.size());
- Vote vote = displayVotes.get(
- Vote.PRIORITY_SKIN_TEMPERATURE);
- assertEquals(0, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
- assertEquals(60, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
+ Vote vote = displayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
+
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+ assertEquals(0, renderVote.mMinRefreshRate, FLOAT_TOLERANCE);
+ assertEquals(60, renderVote.mMaxRefreshRate, FLOAT_TOLERANCE);
SparseArray<Vote> otherDisplayVotes = mStorage.getVotes(DISPLAY_ID_OTHER);
assertEquals(1, otherDisplayVotes.size());
vote = otherDisplayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
- assertEquals(0, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
- assertEquals(60, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ renderVote = (RefreshRateVote.RenderVote) vote;
+ assertEquals(0, renderVote.mMinRefreshRate, FLOAT_TOLERANCE);
+ assertEquals(60, renderVote.mMaxRefreshRate, FLOAT_TOLERANCE);
}
@Test
@@ -167,8 +173,10 @@
SparseArray<Vote> displayVotes = mStorage.getVotes(DISPLAY_ID);
assertEquals(1, displayVotes.size());
Vote vote = displayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
- assertEquals(90, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
- assertEquals(120, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+ assertEquals(90, renderVote.mMinRefreshRate, FLOAT_TOLERANCE);
+ assertEquals(120, renderVote.mMaxRefreshRate, FLOAT_TOLERANCE);
assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
}
@@ -188,8 +196,10 @@
SparseArray<Vote> displayVotes = mStorage.getVotes(DISPLAY_ID_ADDED);
Vote vote = displayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
- assertEquals(0, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
- assertEquals(60, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
+ assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+ RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+ assertEquals(0, renderVote.mMinRefreshRate, FLOAT_TOLERANCE);
+ assertEquals(60, renderVote.mMaxRefreshRate, FLOAT_TOLERANCE);
}
@Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt
new file mode 100644
index 0000000..cc88003
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.server.display.mode
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SupportedModesVoteTest {
+ private val supportedModes = listOf(
+ SupportedModesVote.SupportedMode(60f, 90f ),
+ SupportedModesVote.SupportedMode(120f, 240f )
+ )
+
+ private val otherMode = SupportedModesVote.SupportedMode(120f, 120f )
+
+ private lateinit var supportedModesVote: SupportedModesVote
+
+ @Before
+ fun setUp() {
+ supportedModesVote = SupportedModesVote(supportedModes)
+ }
+
+ @Test
+ fun `adds supported modes if supportedModes in summary is null`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+
+ supportedModesVote.updateSummary(summary)
+
+ assertThat(summary.supportedModes).containsExactlyElementsIn(supportedModes)
+ }
+
+ @Test
+ fun `does not add supported modes if summary has empty list of modes`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.supportedModes = ArrayList()
+
+ supportedModesVote.updateSummary(summary)
+
+ assertThat(summary.supportedModes).isEmpty()
+ }
+
+ @Test
+ fun `filters out modes that does not match vote`() {
+ val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+ summary.supportedModes = ArrayList(listOf(otherMode, supportedModes[0]))
+
+ supportedModesVote.updateSummary(summary)
+
+ assertThat(summary.supportedModes).containsExactly(supportedModes[0])
+ }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java
new file mode 100644
index 0000000..749b07d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.audio;
+
+import static android.media.AudioManager.GET_DEVICES_OUTPUTS;
+import static android.media.AudioPlaybackConfiguration.PLAYER_UPDATE_DEVICE_ID;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D;
+
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
+import android.media.ILoudnessCodecUpdatesDispatcher;
+import android.media.LoudnessCodecInfo;
+import android.media.PlayerBase;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class LoudnessCodecHelperTest {
+ private static final String TAG = "LoudnessCodecHelperTest";
+
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ private LoudnessCodecHelper mLoudnessHelper;
+
+ @Mock
+ private AudioService mAudioService;
+ @Mock
+ private ILoudnessCodecUpdatesDispatcher.Default mDispatcher;
+
+ private final int mInitialApcPiid = 1;
+
+ @Before
+ public void setUp() throws Exception {
+ mLoudnessHelper = new LoudnessCodecHelper(mAudioService);
+
+ when(mAudioService.getActivePlaybackConfigurations()).thenReturn(
+ getApcListForPiids(mInitialApcPiid));
+
+ when(mDispatcher.asBinder()).thenReturn(Mockito.mock(IBinder.class));
+ }
+
+ @Test
+ public void registerDispatcher_sendsInitialUpdateOnStart() throws Exception {
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_4)));
+
+ verify(mDispatcher).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), any());
+ }
+
+ @Test
+ public void unregisterDispatcher_noInitialUpdateOnStart() throws Exception {
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+ mLoudnessHelper.unregisterLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/false,
+ CODEC_METADATA_TYPE_MPEG_D)));
+
+ verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void addCodecInfo_sendsInitialUpdateAfterStart() throws Exception {
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_4)));
+ mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid,
+ getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_D));
+
+ verify(mDispatcher, times(2)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void addCodecInfoForUnstartedPiid_noUpdateSent() throws Exception {
+ final int newPiid = 2;
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_4)));
+ mLoudnessHelper.addLoudnessCodecInfo(newPiid,
+ getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_D));
+
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void updateCodecParameters_updatesOnlyStartedPiids() throws Exception {
+ final int newPiid = 2;
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_4)));
+ //does not trigger dispatch since active apc list does not contain newPiid
+ mLoudnessHelper.startLoudnessCodecUpdates(newPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_D)));
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+
+ // triggers dispatch for new active apc with newPiid
+ mLoudnessHelper.updateCodecParameters(getApcListForPiids(newPiid));
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(newPiid), any());
+ }
+
+ @Test
+ public void updateCodecParameters_noStartedPiids_noDispatch() throws Exception {
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+ mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid,
+ getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_D));
+
+ mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+
+ // no dispatch since mInitialApcPiid was not started
+ verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void updateCodecParameters_removedCodecInfo_noDispatch() throws Exception {
+ final LoudnessCodecInfo info = getLoudnessInfo(/*mediaCodecHash=*/111,
+ /*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4);
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info));
+ mLoudnessHelper.removeLoudnessCodecInfo(mInitialApcPiid, info);
+
+ mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+
+ // no second dispatch since codec info was removed for updates
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void updateCodecParameters_stoppedPiids_noDispatch() throws Exception {
+ final LoudnessCodecInfo info = getLoudnessInfo(/*mediaCodecHash=*/111,
+ /*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4);
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info));
+ mLoudnessHelper.stopLoudnessCodecUpdates(mInitialApcPiid);
+
+ mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+
+ // no second dispatch since piid was removed for updates
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ private List<AudioPlaybackConfiguration> getApcListForPiids(int... piids) {
+ final ArrayList<AudioPlaybackConfiguration> apcList = new ArrayList<>();
+
+ AudioDeviceInfo[] devicesStatic = AudioManager.getDevicesStatic(GET_DEVICES_OUTPUTS);
+ assumeTrue(devicesStatic.length > 0);
+ int index = new Random().nextInt(devicesStatic.length);
+ Log.d(TAG, "Out devices number " + devicesStatic.length + ". Picking index " + index);
+ int deviceId = devicesStatic[index].getId();
+
+ for (int piid : piids) {
+ PlayerBase.PlayerIdCard idCard = Mockito.mock(PlayerBase.PlayerIdCard.class);
+ AudioPlaybackConfiguration apc =
+ new AudioPlaybackConfiguration(idCard, piid, /*uid=*/1, /*pid=*/1);
+ apc.handleStateEvent(PLAYER_UPDATE_DEVICE_ID, deviceId);
+
+ apcList.add(apc);
+ }
+ return apcList;
+ }
+
+ private static LoudnessCodecInfo getLoudnessInfo(int mediaCodecHash, boolean isDownmixing,
+ int metadataType) {
+ LoudnessCodecInfo info = new LoudnessCodecInfo();
+ info.isDownmixing = isDownmixing;
+ info.mediaCodecHashCode = mediaCodecHash;
+ info.metadataType = metadataType;
+
+ return info;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 57b1225..d70a4fd 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -60,7 +60,8 @@
.setShowInLauncher(21)
.setStartWithParent(false)
.setShowInSettings(45)
- .setHideInSettingsInQuietMode(false)
+ .setShowInSharingSurfaces(78)
+ .setShowInQuietMode(12)
.setInheritDevicePolicy(67)
.setUseParentsContacts(false)
.setCrossProfileIntentFilterAccessControl(10)
@@ -74,7 +75,8 @@
final UserProperties actualProps = new UserProperties(defaultProps);
actualProps.setShowInLauncher(14);
actualProps.setShowInSettings(32);
- actualProps.setHideInSettingsInQuietMode(true);
+ actualProps.setShowInSharingSurfaces(46);
+ actualProps.setShowInQuietMode(27);
actualProps.setInheritDevicePolicy(51);
actualProps.setUseParentsContacts(true);
actualProps.setCrossProfileIntentFilterAccessControl(20);
@@ -236,8 +238,10 @@
assertThat(expected.getShowInLauncher()).isEqualTo(actual.getShowInLauncher());
assertThat(expected.getStartWithParent()).isEqualTo(actual.getStartWithParent());
assertThat(expected.getShowInSettings()).isEqualTo(actual.getShowInSettings());
- assertThat(expected.getHideInSettingsInQuietMode())
- .isEqualTo(actual.getHideInSettingsInQuietMode());
+ assertThat(expected.getShowInSharingSurfaces()).isEqualTo(
+ actual.getShowInSharingSurfaces());
+ assertThat(expected.getShowInQuietMode())
+ .isEqualTo(actual.getShowInQuietMode());
assertThat(expected.getInheritDevicePolicy()).isEqualTo(actual.getInheritDevicePolicy());
assertThat(expected.getUseParentsContacts()).isEqualTo(actual.getUseParentsContacts());
assertThat(expected.getCrossProfileIntentFilterAccessControl())
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 48eb5c6..77f6939 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -29,6 +29,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
@@ -91,7 +92,8 @@
.setCredentialShareableWithParent(false)
.setAuthAlwaysRequiredToDisableQuietMode(true)
.setShowInSettings(900)
- .setHideInSettingsInQuietMode(true)
+ .setShowInSharingSurfaces(20)
+ .setShowInQuietMode(30)
.setInheritDevicePolicy(340)
.setDeleteAppWithParent(true)
.setAlwaysVisible(true);
@@ -107,9 +109,9 @@
.setIconBadge(28)
.setBadgePlain(29)
.setBadgeNoBackground(30)
- .setLabel(31)
.setMaxAllowedPerParent(32)
.setStatusBarIcon(33)
+ .setLabels(34, 35, 36)
.setDefaultRestrictions(restrictions)
.setDefaultSystemSettings(systemSettings)
.setDefaultSecureSettings(secureSettings)
@@ -124,9 +126,11 @@
assertEquals(28, type.getIconBadge());
assertEquals(29, type.getBadgePlain());
assertEquals(30, type.getBadgeNoBackground());
- assertEquals(31, type.getLabel());
assertEquals(32, type.getMaxAllowedPerParent());
assertEquals(33, type.getStatusBarIcon());
+ assertEquals(34, type.getLabel(0));
+ assertEquals(35, type.getLabel(1));
+ assertEquals(36, type.getLabel(2));
assertTrue(UserRestrictionsUtils.areEqual(restrictions, type.getDefaultRestrictions()));
assertNotSame(restrictions, type.getDefaultRestrictions());
@@ -164,7 +168,9 @@
assertTrue(type.getDefaultUserPropertiesReference()
.isAuthAlwaysRequiredToDisableQuietMode());
assertEquals(900, type.getDefaultUserPropertiesReference().getShowInSettings());
- assertTrue(type.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
+ assertEquals(20, type.getDefaultUserPropertiesReference().getShowInSharingSurfaces());
+ assertEquals(30,
+ type.getDefaultUserPropertiesReference().getShowInQuietMode());
assertEquals(340, type.getDefaultUserPropertiesReference()
.getInheritDevicePolicy());
assertTrue(type.getDefaultUserPropertiesReference().getDeleteAppWithParent());
@@ -203,7 +209,7 @@
assertEquals(Resources.ID_NULL, type.getStatusBarIcon());
assertEquals(Resources.ID_NULL, type.getBadgeLabel(0));
assertEquals(Resources.ID_NULL, type.getBadgeColor(0));
- assertEquals(Resources.ID_NULL, type.getLabel());
+ assertEquals(Resources.ID_NULL, type.getLabel(0));
assertTrue(type.getDefaultRestrictions().isEmpty());
assertTrue(type.getDefaultSystemSettings().isEmpty());
assertTrue(type.getDefaultSecureSettings().isEmpty());
@@ -222,7 +228,9 @@
assertFalse(props.isCredentialShareableWithParent());
assertFalse(props.getDeleteAppWithParent());
assertFalse(props.getAlwaysVisible());
- assertFalse(props.getHideInSettingsInQuietMode());
+ assertEquals(UserProperties.SHOW_IN_LAUNCHER_SEPARATE, props.getShowInSharingSurfaces());
+ assertEquals(UserProperties.SHOW_IN_QUIET_MODE_PAUSED,
+ props.getShowInQuietMode());
assertFalse(type.hasBadge());
}
@@ -311,8 +319,9 @@
.setCredentialShareableWithParent(true)
.setAuthAlwaysRequiredToDisableQuietMode(false)
.setShowInSettings(20)
- .setHideInSettingsInQuietMode(false)
.setInheritDevicePolicy(21)
+ .setShowInSharingSurfaces(22)
+ .setShowInQuietMode(24)
.setDeleteAppWithParent(true)
.setAlwaysVisible(false);
@@ -354,9 +363,11 @@
assertFalse(aospType.getDefaultUserPropertiesReference()
.isAuthAlwaysRequiredToDisableQuietMode());
assertEquals(20, aospType.getDefaultUserPropertiesReference().getShowInSettings());
- assertFalse(aospType.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
assertEquals(21, aospType.getDefaultUserPropertiesReference()
.getInheritDevicePolicy());
+ assertEquals(22, aospType.getDefaultUserPropertiesReference().getShowInSharingSurfaces());
+ assertEquals(24,
+ aospType.getDefaultUserPropertiesReference().getShowInQuietMode());
assertTrue(aospType.getDefaultUserPropertiesReference().getDeleteAppWithParent());
assertFalse(aospType.getDefaultUserPropertiesReference().getAlwaysVisible());
@@ -403,7 +414,10 @@
assertTrue(aospType.getDefaultUserPropertiesReference()
.isAuthAlwaysRequiredToDisableQuietMode());
assertEquals(23, aospType.getDefaultUserPropertiesReference().getShowInSettings());
- assertTrue(aospType.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
+ assertEquals(22,
+ aospType.getDefaultUserPropertiesReference().getShowInSharingSurfaces());
+ assertEquals(24,
+ aospType.getDefaultUserPropertiesReference().getShowInQuietMode());
assertEquals(450, aospType.getDefaultUserPropertiesReference()
.getInheritDevicePolicy());
assertFalse(aospType.getDefaultUserPropertiesReference().getDeleteAppWithParent());
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 2b6d8ed..c7d80ed 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -343,6 +343,8 @@
assertThat(mainUserId).isEqualTo(parentProfileInfo.id);
removeUser(userInfo.id);
assertThat(mUserManager.getProfileParent(mainUserId)).isNull();
+ assertThat(mUserManager.getProfileLabel()).isEqualTo(
+ Resources.getSystem().getString(userTypeDetails.getLabel(0)));
}
@MediumTest
diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
index ebe45a6..d000605 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
@@ -16,6 +16,8 @@
package com.android.server.webkit;
+import static android.webkit.Flags.updateServiceV2;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -25,6 +27,9 @@
import android.content.pm.Signature;
import android.os.Build;
import android.os.Bundle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Base64;
import android.webkit.UserPackage;
@@ -35,6 +40,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
@@ -55,11 +61,14 @@
public class WebViewUpdateServiceTest {
private final static String TAG = WebViewUpdateServiceTest.class.getSimpleName();
- private WebViewUpdateServiceImpl mWebViewUpdateServiceImpl;
+ private WebViewUpdateServiceInterface mWebViewUpdateServiceImpl;
private TestSystemImpl mTestSystemImpl;
private static final String WEBVIEW_LIBRARY_FLAG = "com.android.webview.WebViewLibrary";
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
/**
* Creates a new instance.
*/
@@ -92,8 +101,13 @@
TestSystemImpl testing = new TestSystemImpl(packages, numRelros, isDebuggable,
multiProcessDefault);
mTestSystemImpl = Mockito.spy(testing);
- mWebViewUpdateServiceImpl =
- new WebViewUpdateServiceImpl(null /*Context*/, mTestSystemImpl);
+ if (updateServiceV2()) {
+ mWebViewUpdateServiceImpl =
+ new WebViewUpdateServiceImpl2(null /*Context*/, mTestSystemImpl);
+ } else {
+ mWebViewUpdateServiceImpl =
+ new WebViewUpdateServiceImpl(null /*Context*/, mTestSystemImpl);
+ }
}
private void setEnabledAndValidPackageInfos(WebViewProviderInfo[] providers) {
@@ -1310,11 +1324,13 @@
}
@Test
+ @RequiresFlagsDisabled("android.webkit.update_service_v2")
public void testMultiProcessEnabledByDefault() {
testMultiProcessByDefault(true /* enabledByDefault */);
}
@Test
+ @RequiresFlagsDisabled("android.webkit.update_service_v2")
public void testMultiProcessDisabledByDefault() {
testMultiProcessByDefault(false /* enabledByDefault */);
}
@@ -1344,6 +1360,7 @@
}
@Test
+ @RequiresFlagsDisabled("android.webkit.update_service_v2")
public void testMultiProcessEnabledByDefaultWithSettingsValue() {
testMultiProcessByDefaultWithSettingsValue(
true /* enabledByDefault */, Integer.MIN_VALUE, false /* expectEnabled */);
@@ -1356,6 +1373,7 @@
}
@Test
+ @RequiresFlagsDisabled("android.webkit.update_service_v2")
public void testMultiProcessDisabledByDefaultWithSettingsValue() {
testMultiProcessByDefaultWithSettingsValue(
false /* enabledByDefault */, Integer.MIN_VALUE, false /* expectEnabled */);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index b45dcd4..09ffe71 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -241,7 +241,6 @@
import com.android.internal.R;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
-import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.Flag;
import com.android.internal.config.sysui.TestableFlagResolver;
import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.InstanceIdSequenceFake;
@@ -5296,7 +5295,7 @@
new Intent(Intent.ACTION_LOCALE_CHANGED));
verify(mZenModeHelper, times(1)).updateDefaultZenRules(
- anyInt(), anyBoolean());
+ anyInt());
}
@Test
@@ -8752,7 +8751,7 @@
// verify that zen mode helper gets passed in a package name of "android"
verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString(),
- anyInt(), eq(true)); // system call counts as "is system or system ui"
+ anyInt(), eq(ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI)); // system call
}
@Test
@@ -8774,7 +8773,7 @@
// verify that zen mode helper gets passed in a package name of "android"
verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString(),
- anyInt(), eq(true)); // system call counts as "system or system ui"
+ anyInt(), eq(ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI)); // system call
}
@Test
@@ -8795,7 +8794,7 @@
// verify that zen mode helper gets passed in the package name from the arg, not the owner
verify(mockZenModeHelper).addAutomaticZenRule(
eq("another.package"), eq(rule), anyString(), anyInt(),
- eq(false)); // doesn't count as a system/systemui call
+ eq(ZenModeHelper.FROM_APP)); // doesn't count as a system/systemui call
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 0313aaa..97b6b98 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -53,6 +53,9 @@
import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG;
import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW;
import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
+import static com.android.server.notification.ZenModeHelper.FROM_APP;
+import static com.android.server.notification.ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI;
+import static com.android.server.notification.ZenModeHelper.FROM_USER;
import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
import static com.google.common.truth.Truth.assertThat;
@@ -108,6 +111,7 @@
import android.provider.Settings;
import android.provider.Settings.Global;
import android.service.notification.Condition;
+import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.service.notification.ZenModeConfig.ZenRule;
@@ -1645,8 +1649,7 @@
ZenModeConfig config = new ZenModeConfig();
config.automaticRules = new ArrayMap<>();
mZenModeHelper.mConfig = config;
- mZenModeHelper.updateDefaultZenRules(
- Process.SYSTEM_UID, true); // shouldn't throw null pointer
+ mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID); // shouldn't throw null pointer
mZenModeHelper.pullRules(events); // shouldn't throw null pointer
}
@@ -1671,7 +1674,7 @@
autoRules.put(SCHEDULE_DEFAULT_RULE_ID, updatedDefaultRule);
mZenModeHelper.mConfig.automaticRules = autoRules;
- mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID, true);
+ mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID);
assertEquals(updatedDefaultRule,
mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID));
}
@@ -1697,7 +1700,7 @@
autoRules.put(SCHEDULE_DEFAULT_RULE_ID, updatedDefaultRule);
mZenModeHelper.mConfig.automaticRules = autoRules;
- mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID, true);
+ mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID);
assertEquals(updatedDefaultRule,
mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID));
}
@@ -1724,7 +1727,7 @@
autoRules.put(SCHEDULE_DEFAULT_RULE_ID, customDefaultRule);
mZenModeHelper.mConfig.automaticRules = autoRules;
- mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID, true);
+ mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID);
ZenModeConfig.ZenRule ruleAfterUpdating =
mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID);
assertEquals(customDefaultRule.enabled, ruleAfterUpdating.enabled);
@@ -1748,7 +1751,7 @@
// We need the package name to be something that's not "android" so there aren't any
// existing rules under that package.
String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
assertNotNull(id);
}
try {
@@ -1759,7 +1762,7 @@
new ZenPolicy.Builder().build(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
fail("allowed too many rules to be created");
} catch (IllegalArgumentException e) {
// yay
@@ -1780,7 +1783,7 @@
new ZenPolicy.Builder().build(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
assertNotNull(id);
}
try {
@@ -1791,7 +1794,7 @@
new ZenPolicy.Builder().build(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
fail("allowed too many rules to be created");
} catch (IllegalArgumentException e) {
// yay
@@ -1812,7 +1815,7 @@
new ZenPolicy.Builder().build(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
assertNotNull(id);
}
try {
@@ -1823,7 +1826,7 @@
new ZenPolicy.Builder().build(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
fail("allowed too many rules to be created");
} catch (IllegalArgumentException e) {
// yay
@@ -1839,7 +1842,7 @@
new ZenPolicy.Builder().build(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
assertTrue(id != null);
ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
@@ -1860,7 +1863,7 @@
ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
assertTrue(id != null);
ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
@@ -1884,7 +1887,7 @@
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
mZenModeHelper.setAutomaticZenRuleState(zenRule.getConditionId(),
new Condition(zenRule.getConditionId(), "", STATE_TRUE),
CUSTOM_PKG_UID, false);
@@ -1903,7 +1906,7 @@
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
AutomaticZenRule zenRule2 = new AutomaticZenRule("NEW",
null,
@@ -1912,7 +1915,7 @@
new ZenPolicy.Builder().build(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
- mZenModeHelper.updateAutomaticZenRule(id, zenRule2, "", CUSTOM_PKG_UID, false);
+ mZenModeHelper.updateAutomaticZenRule(id, zenRule2, "", CUSTOM_PKG_UID, FROM_APP);
ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
assertEquals("NEW", ruleInConfig.name);
@@ -1928,7 +1931,7 @@
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
assertTrue(id != null);
ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
@@ -1948,7 +1951,7 @@
new ZenPolicy.Builder().build(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test",
- CUSTOM_PKG_UID, false);
+ CUSTOM_PKG_UID, FROM_APP);
assertTrue(id != null);
ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
@@ -1972,13 +1975,13 @@
sharedUri,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
new ComponentName("android", "ScheduleConditionProvider"),
sharedUri,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
Condition condition = new Condition(sharedUri, "", STATE_TRUE);
mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition, Process.SYSTEM_UID, true);
@@ -2010,6 +2013,182 @@
}
@Test
+ public void addAutomaticZenRule_fromApp_ignoresHiddenEffects() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+ ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
+ .setShouldDisplayGrayscale(true)
+ .setShouldSuppressAmbientDisplay(true)
+ .setShouldDimWallpaper(true)
+ .setShouldUseNightMode(true)
+ .setShouldDisableAutoBrightness(true)
+ .setShouldDisableTapToWake(true)
+ .setShouldDisableTiltToWake(true)
+ .setShouldDisableTouch(true)
+ .setShouldMinimizeRadioUsage(true)
+ .setShouldMaximizeDoze(true)
+ .build();
+
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setOwner(OWNER)
+ .setDeviceEffects(zde)
+ .build(),
+ "reasons", 0, FROM_APP);
+
+ AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ assertThat(savedRule.getDeviceEffects()).isEqualTo(
+ new ZenDeviceEffects.Builder()
+ .setShouldDisplayGrayscale(true)
+ .setShouldSuppressAmbientDisplay(true)
+ .setShouldDimWallpaper(true)
+ .setShouldUseNightMode(true)
+ .build());
+ }
+
+ @Test
+ public void addAutomaticZenRule_fromSystem_respectsHiddenEffects() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+ ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
+ .setShouldDisplayGrayscale(true)
+ .setShouldSuppressAmbientDisplay(true)
+ .setShouldDimWallpaper(true)
+ .setShouldUseNightMode(true)
+ .setShouldDisableAutoBrightness(true)
+ .setShouldDisableTapToWake(true)
+ .setShouldDisableTiltToWake(true)
+ .setShouldDisableTouch(true)
+ .setShouldMinimizeRadioUsage(true)
+ .setShouldMaximizeDoze(true)
+ .build();
+
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setOwner(OWNER)
+ .setDeviceEffects(zde)
+ .build(),
+ "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+ AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ assertThat(savedRule.getDeviceEffects()).isEqualTo(zde);
+ }
+
+ @Test
+ public void addAutomaticZenRule_fromUser_respectsHiddenEffects() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+ ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
+ .setShouldDisplayGrayscale(true)
+ .setShouldSuppressAmbientDisplay(true)
+ .setShouldDimWallpaper(true)
+ .setShouldUseNightMode(true)
+ .setShouldDisableAutoBrightness(true)
+ .setShouldDisableTapToWake(true)
+ .setShouldDisableTiltToWake(true)
+ .setShouldDisableTouch(true)
+ .setShouldMinimizeRadioUsage(true)
+ .setShouldMaximizeDoze(true)
+ .build();
+
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setOwner(OWNER)
+ .setDeviceEffects(zde)
+ .build(),
+ "reasons", 0, FROM_USER);
+
+ AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ assertThat(savedRule.getDeviceEffects()).isEqualTo(zde);
+ }
+
+ @Test
+ public void updateAutomaticZenRule_fromApp_preservesPreviousHiddenEffects() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+ ZenDeviceEffects original = new ZenDeviceEffects.Builder()
+ .setShouldDisableTapToWake(true)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setOwner(OWNER)
+ .setDeviceEffects(original)
+ .build(),
+ "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+ ZenDeviceEffects updateFromApp = new ZenDeviceEffects.Builder()
+ .setShouldUseNightMode(true) // Good
+ .setShouldMaximizeDoze(true) // Bad
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId,
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setOwner(OWNER)
+ .setDeviceEffects(updateFromApp)
+ .build(),
+ "reasons", 0, FROM_APP);
+
+ AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ assertThat(savedRule.getDeviceEffects()).isEqualTo(
+ new ZenDeviceEffects.Builder()
+ .setShouldUseNightMode(true) // From update.
+ .setShouldDisableTapToWake(true) // From original.
+ .build());
+ }
+
+ @Test
+ public void updateAutomaticZenRule_fromSystem_updatesHiddenEffects() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+ ZenDeviceEffects original = new ZenDeviceEffects.Builder()
+ .setShouldDisableTapToWake(true)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setOwner(OWNER)
+ .setDeviceEffects(original)
+ .build(),
+ "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+ ZenDeviceEffects updateFromSystem = new ZenDeviceEffects.Builder()
+ .setShouldUseNightMode(true) // Good
+ .setShouldMaximizeDoze(true) // Also good
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId,
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setDeviceEffects(updateFromSystem)
+ .build(),
+ "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+ AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromSystem);
+ }
+
+ @Test
+ public void updateAutomaticZenRule_fromUser_updatesHiddenEffects() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+ ZenDeviceEffects original = new ZenDeviceEffects.Builder()
+ .setShouldDisableTapToWake(true)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setOwner(OWNER)
+ .setDeviceEffects(original)
+ .build(),
+ "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+ ZenDeviceEffects updateFromUser = new ZenDeviceEffects.Builder()
+ .setShouldUseNightMode(true) // Good
+ .setShouldMaximizeDoze(true) // Also good
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId,
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setDeviceEffects(updateFromUser)
+ .build(),
+ "reasons", 0, FROM_USER);
+
+ AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser);
+ }
+
+ @Test
public void testSetManualZenMode() {
mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
setupZenConfig();
@@ -2119,7 +2298,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
- "test", Process.SYSTEM_UID, true);
+ "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE
mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2128,7 +2307,8 @@
// Event 2: "User" turns off the automatic rule (sets it to not enabled)
zenRule.setEnabled(false);
- mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID, true);
+ mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID,
+ FROM_SYSTEM_OR_SYSTEMUI);
// Add a new system rule
AutomaticZenRule systemRule = new AutomaticZenRule("systemRule",
@@ -2138,7 +2318,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String systemId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), systemRule,
- "test", Process.SYSTEM_UID, true);
+ "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// Event 3: turn on the system rule
mZenModeHelper.setAutomaticZenRuleState(systemId,
@@ -2270,7 +2450,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// Rule 2, same as rule 1
AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
@@ -2280,7 +2460,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// Rule 3, has stricter settings than the default settings
ZenModeConfig ruleConfig = mZenModeHelper.mConfig.copy();
@@ -2294,7 +2474,7 @@
ruleConfig.toZenPolicy(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id3 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule3, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// First: turn on rule 1
mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2394,7 +2574,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// Rule 2, same as rule 1 but owned by the system
AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
@@ -2404,7 +2584,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// Turn on rule 1; call looks like it's from the system. Because setting a condition is
// typically an automatic (non-user-initiated) action, expect the calling UID to be
@@ -2422,7 +2602,8 @@
// Disable rule 1. Because this looks like a user action, the UID should not be modified
// from the system-provided one.
zenRule.setEnabled(false);
- mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID, true);
+ mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID,
+ FROM_SYSTEM_OR_SYSTEMUI);
// Add a manual rule. Any manual rule changes should not get calling uids reassigned.
mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null, "",
@@ -2553,7 +2734,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// enable the rule
mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2596,7 +2777,7 @@
customPolicy,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// enable the rule; this will update the consolidated policy
mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2632,7 +2813,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// enable rule 1
mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2656,7 +2837,7 @@
customPolicy,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
- "test", Process.SYSTEM_UID, true);
+ "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// enable rule 2; this will update the consolidated policy
mZenModeHelper.setAutomaticZenRuleState(id2,
@@ -2694,7 +2875,7 @@
customPolicy,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
- Process.SYSTEM_UID, true);
+ Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// enable the rule; this will update the consolidated policy
mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2716,7 +2897,7 @@
strictPolicy,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
- "test", Process.SYSTEM_UID, true);
+ "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// enable rule 2; this will update the consolidated policy
mZenModeHelper.setAutomaticZenRuleState(id2,
@@ -2784,7 +2965,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
- zenRule, "test", Process.SYSTEM_UID, true);
+ zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
CountDownLatch latch = new CountDownLatch(1);
final int[] actualStatus = new int[1];
@@ -2800,7 +2981,8 @@
mZenModeHelper.addCallback(callback);
zenRule.setEnabled(false);
- mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID, true);
+ mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID,
+ FROM_SYSTEM_OR_SYSTEMUI);
assertTrue(latch.await(500, TimeUnit.MILLISECONDS));
assertEquals(AUTOMATIC_RULE_STATUS_DISABLED, actualStatus[0]);
@@ -2818,7 +3000,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, false);
final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
- zenRule, "test", Process.SYSTEM_UID, true);
+ zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
CountDownLatch latch = new CountDownLatch(1);
final int[] actualStatus = new int[1];
@@ -2834,7 +3016,8 @@
mZenModeHelper.addCallback(callback);
zenRule.setEnabled(true);
- mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID, true);
+ mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID,
+ FROM_SYSTEM_OR_SYSTEMUI);
assertTrue(latch.await(500, TimeUnit.MILLISECONDS));
assertEquals(AUTOMATIC_RULE_STATUS_ENABLED, actualStatus[0]);
@@ -2853,7 +3036,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
- zenRule, "test", Process.SYSTEM_UID, true);
+ zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
CountDownLatch latch = new CountDownLatch(1);
final int[] actualStatus = new int[1];
@@ -2889,7 +3072,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
- zenRule, "test", Process.SYSTEM_UID, true);
+ zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
CountDownLatch latch = new CountDownLatch(1);
final int[] actualStatus = new int[2];
@@ -2929,7 +3112,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
- zenRule, "test", Process.SYSTEM_UID, true);
+ zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
CountDownLatch latch = new CountDownLatch(1);
final int[] actualStatus = new int[2];
@@ -2969,7 +3152,7 @@
null,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
- zenRule, "test", Process.SYSTEM_UID, true);
+ zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
// Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE
mZenModeHelper.setAutomaticZenRuleState(createdId,
@@ -2982,7 +3165,8 @@
// Event 3: "User" turns off the automatic rule (sets it to not enabled)
zenRule.setEnabled(false);
- mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID, true);
+ mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID,
+ FROM_SYSTEM_OR_SYSTEMUI);
assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 786432a..f2e54bc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -1175,10 +1175,12 @@
@Test
public void testFinishActivityIfPossible_nonVisibleNoAppTransition() {
registerTestTransitionPlayer();
+ spyOn(mRootWindowContainer.mTransitionController);
+ final ActivityRecord bottomActivity = createActivityWithTask();
+ bottomActivity.setVisibility(false);
+ bottomActivity.setState(STOPPED, "test");
+ bottomActivity.mLastSurfaceShowing = false;
final ActivityRecord activity = createActivityWithTask();
- // Put an activity on top of test activity to make it invisible and prevent us from
- // accidentally resuming the topmost one again.
- new ActivityBuilder(mAtm).build();
activity.setVisibleRequested(false);
activity.setState(STOPPED, "test");
@@ -1186,6 +1188,14 @@
verify(activity.mDisplayContent, never()).prepareAppTransition(eq(TRANSIT_CLOSE));
assertFalse(activity.inTransition());
+
+ // finishIfPossible -> completeFinishing -> addToFinishingAndWaitForIdle
+ // -> resumeFocusedTasksTopActivities
+ assertTrue(bottomActivity.isState(RESUMED));
+ assertTrue(bottomActivity.isVisible());
+ verify(mRootWindowContainer.mTransitionController).onVisibleWithoutCollectingTransition(
+ eq(bottomActivity), any());
+ assertTrue(bottomActivity.mLastSurfaceShowing);
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index b44d52e..2034751 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -201,6 +201,33 @@
verify(taskChangeNotifier, never()).notifyActivityDismissingDockedRootTask();
}
+ /** Verifies that the activity can be destroying after removing task. */
+ @Test
+ public void testRemoveTask() {
+ final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ activity1.setVisible(false);
+ activity1.finishing = true;
+ activity1.setState(ActivityRecord.State.STOPPING, "test");
+ activity1.addToStopping(false /* scheduleIdle */, false /* idleDelayed */, "test");
+ final ActivityRecord activity2 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ activity2.setState(ActivityRecord.State.RESUMED, "test");
+ // The state can happen from ActivityRecord#makeInvisible.
+ activity2.addToStopping(false /* scheduleIdle */, false /* idleDelayed */, "test");
+ mSupervisor.removeTask(activity1.getTask(), true /* killProcess */,
+ true /* removeFromRecents */, "testRemoveTask");
+ mSupervisor.removeTask(activity2.getTask(), true /* killProcess */,
+ true /* removeFromRecents */, "testRemoveTask");
+
+ assertEquals(ActivityRecord.State.DESTROYING, activity2.getState());
+ assertEquals(ActivityRecord.State.STOPPING, activity1.getState());
+ assertTrue(mSupervisor.mStoppingActivities.contains(activity1));
+ // Assume that it is called by scheduleIdle from addToStopping. And because
+ // mStoppingActivities remembers the finishing activity, it can continue to destroy.
+ mSupervisor.processStoppingAndFinishingActivities(null /* launchedActivity */,
+ false /* processPausingActivities */, "test");
+ assertEquals(ActivityRecord.State.DESTROYING, activity1.getState());
+ }
+
/** Ensures that the calling package name passed to client complies with package visibility. */
@Test
public void testFilteredReferred() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index c6fa8a1..acce2e2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -51,8 +51,6 @@
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
@@ -159,6 +157,10 @@
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
/**
* Tests for the {@link DisplayContent} class.
@@ -2287,7 +2289,7 @@
0 /* userId */);
dc.mCurrentUniqueDisplayId = mDisplayInfo.uniqueId + "-test";
// Trigger display changed.
- dc.onDisplayChanged();
+ updateDisplay(dc);
// Ensure overridden size and denisty match the most up-to-date values in settings for the
// display.
verifySizes(dc, forcedWidth, forcedHeight, forcedDensity);
@@ -2762,26 +2764,6 @@
mDisplayContent.getKeepClearAreas());
}
- @Test
- public void testMayImeShowOnLaunchingActivity_negativeWhenSoftInputModeHidden() {
- final ActivityRecord app = createActivityRecord(mDisplayContent);
- final WindowState appWin = createWindow(null, TYPE_BASE_APPLICATION, app, "appWin");
- createWindow(null, TYPE_APPLICATION_STARTING, app, "startingWin");
- app.mStartingData = mock(SnapshotStartingData.class);
- // Assume the app has shown IME before and warm launching with a snapshot window.
- doReturn(true).when(app.mStartingData).hasImeSurface();
-
- // Expect true when this IME focusable activity will show IME during launching.
- assertTrue(WindowManager.LayoutParams.mayUseInputMethod(appWin.mAttrs.flags));
- assertTrue(mDisplayContent.mayImeShowOnLaunchingActivity(app));
-
- // Not expect IME will be shown during launching if the app's softInputMode is hidden.
- appWin.mAttrs.softInputMode = SOFT_INPUT_STATE_ALWAYS_HIDDEN;
- assertFalse(mDisplayContent.mayImeShowOnLaunchingActivity(app));
- appWin.mAttrs.softInputMode = SOFT_INPUT_STATE_HIDDEN;
- assertFalse(mDisplayContent.mayImeShowOnLaunchingActivity(app));
- }
-
private void removeRootTaskTests(Runnable runnable) {
final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
final Task rootTask1 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
@@ -2852,7 +2834,7 @@
*/
private DisplayContent createDisplayNoUpdateDisplayInfo() {
final DisplayContent displayContent = createNewDisplay();
- doNothing().when(displayContent).updateDisplayInfo();
+ doNothing().when(displayContent).updateDisplayInfo(any());
return displayContent;
}
@@ -2882,6 +2864,16 @@
return result;
}
+ private void updateDisplay(DisplayContent displayContent) {
+ CompletableFuture<Object> future = new CompletableFuture<>();
+ displayContent.requestDisplayUpdate(() -> future.complete(new Object()));
+ try {
+ future.get(15, TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
private void tapOnDisplay(final DisplayContent dc) {
final DisplayMetrics dm = dc.getDisplayMetrics();
final float x = dm.widthPixels / 2;
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 9af5ba5..5738d24 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -69,6 +69,7 @@
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.util.Log;
+import android.view.DisplayInfo;
import android.view.InputChannel;
import android.view.SurfaceControl;
@@ -267,6 +268,12 @@
// DisplayManagerInternal
final DisplayManagerInternal dmi = mock(DisplayManagerInternal.class);
doReturn(dmi).when(() -> LocalServices.getService(eq(DisplayManagerInternal.class)));
+ doAnswer(invocation -> {
+ int displayId = invocation.getArgument(0);
+ DisplayInfo displayInfo = invocation.getArgument(1);
+ mWmService.mRoot.getDisplayContent(displayId).getDisplay().getDisplayInfo(displayInfo);
+ return null;
+ }).when(dmi).getNonOverrideDisplayInfo(anyInt(), any());
// ColorDisplayServiceInternal
final ColorDisplayService.ColorDisplayServiceInternal cds =
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
index 2d5b72b..d183cf7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
@@ -60,6 +60,11 @@
import org.mockito.Mock;
import org.mockito.Mockito;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
/**
* Build/Install/Run:
* atest WmTests:WindowContextListenerControllerTests
@@ -309,7 +314,7 @@
return null;
}).when(display).getDisplayInfo(any(DisplayInfo.class));
- mContainer.getDisplayContent().onDisplayChanged();
+ updateDisplay(mContainer.getDisplayContent());
assertThat(mockToken.mConfiguration).isEqualTo(config1);
assertThat(mockToken.mDisplayId).isEqualTo(mContainer.getDisplayContent().getDisplayId());
@@ -352,4 +357,14 @@
mRemoved = true;
}
}
+
+ private void updateDisplay(DisplayContent displayContent) {
+ CompletableFuture<Object> future = new CompletableFuture<>();
+ displayContent.requestDisplayUpdate(() -> future.complete(new Object()));
+ try {
+ future.get(15, TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index c124079..ede4885 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -8853,6 +8853,24 @@
KEY_PREFIX + "epdg_static_address_roaming_string";
/**
+ * Controls if the multiple SA proposals allowed for IKE session to include
+ * all the 3GPP TS 33.210 and RFC 8221 supported cipher suites in multiple
+ * IKE SA proposals as per RFC 7296.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_MULTIPLE_SA_PROPOSALS)
+ public static final String KEY_SUPPORTS_IKE_SESSION_MULTIPLE_SA_PROPOSALS_BOOL =
+ KEY_PREFIX + "supports_ike_session_multiple_sa_proposals_bool";
+
+ /**
+ * Controls if the multiple SA proposals allowed for Child session to include
+ * all the 3GPP TS 33.210 and RFC 8221 supported cipher suites in multiple
+ * Child SA proposals as per RFC 7296.
+ */
+ @FlaggedApi(Flags.FLAG_ENABLE_MULTIPLE_SA_PROPOSALS)
+ public static final String KEY_SUPPORTS_CHILD_SESSION_MULTIPLE_SA_PROPOSALS_BOOL =
+ KEY_PREFIX + "supports_child_session_multiple_sa_proposals_bool";
+
+ /**
* List of supported key sizes for AES Cipher Block Chaining (CBC) encryption mode of child
* session. Possible values are:
* {@link android.net.ipsec.ike.SaProposal#KEY_LEN_UNUSED},
@@ -9187,6 +9205,8 @@
defaults.putInt(KEY_IKE_REKEY_HARD_TIMER_SEC_INT, 14400);
defaults.putInt(KEY_CHILD_SA_REKEY_SOFT_TIMER_SEC_INT, 3600);
defaults.putInt(KEY_CHILD_SA_REKEY_HARD_TIMER_SEC_INT, 7200);
+ defaults.putBoolean(KEY_SUPPORTS_IKE_SESSION_MULTIPLE_SA_PROPOSALS_BOOL, false);
+ defaults.putBoolean(KEY_SUPPORTS_CHILD_SESSION_MULTIPLE_SA_PROPOSALS_BOOL, false);
defaults.putIntArray(
KEY_RETRANSMIT_TIMER_MSEC_INT_ARRAY, new int[] {500, 1000, 2000, 4000, 8000});
defaults.putInt(KEY_DPD_TIMER_SEC_INT, 120);