diff options
176 files changed, 7156 insertions, 1522 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 16389b3bcc3c..941d842d8ec4 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -755,6 +755,13 @@ java_aconfig_library { 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 3a114173869a..d3b4f23477da 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 @@ public class InternalWindowOperationPerfTest extends WindowManagerPerfTestBase 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 ec2b1f4db521..a78a46504684 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 @@ JNIEnv* DeviceCallback::getJNIEnv() { 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 @@ std::unique_ptr<UinputDevice> UinputDevice::open(int32_t id, const char* name, i 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 @@ std::vector<int32_t> toVector(JNIEnv* env, jintArray javaArray) { 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 @@ static jlong openUinputDevice(JNIEnv* env, jclass /* clazz */, jstring rawName, 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 jint getEvdevInputPropByLabel(JNIEnv* env, jclass /* clazz */, jstring ra 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 6da3d7968ed0..9769a75bd9ef 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 @@ private: 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 b0fa34c68092..787055c8cd89 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 @@ public class Device { 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 @@ public class Device { 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 @@ public class Device { 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 @@ public class Device { 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 b89e2cdbd905..7652f2403f6e 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 @@ package com.android.commands.uinput; 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 @@ public class EvemuParser implements EventParser { 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 @@ public class EvemuParser implements EventParser { /** 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 @@ public class EvemuParser implements EventParser { 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 @@ public class EvemuParser implements EventParser { 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 @@ public class EvemuParser implements EventParser { 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 @@ public class EvemuParser implements EventParser { } 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 @@ public class EvemuParser implements EventParser { } 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 @@ public class EvemuParser implements EventParser { 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 @@ public class EvemuParser implements EventParser { 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 @@ public class EvemuParser implements EventParser { } 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 @@ public class EvemuParser implements EventParser { } } - /** - * 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 5ec40e5d04e3..0f16a27aac1d 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 @@ public class Event { 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 class Event { } 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 @@ public class Event { 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 @@ public class Event { mEvent.mConfiguration = configuration; } - public void setVid(int vid) { - mEvent.mVid = vid; + public void setVendorId(int vendorId) { + mEvent.mVendorId = vendorId; + } + + public void setProductId(int productId) { + mEvent.mProductId = productId; } - public void setPid(int pid) { - mEvent.mPid = pid; + 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 888ec5a1d33a..ed3ff33f7e52 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 @@ public class JsonStyleParser implements EventParser { 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 684a12fc8f37..04df27987d58 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 @@ public class Uinput { 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 @@ public class Uinput { "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 6ee987fe07f1..06b0aac271ad 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 @@ package com.android.commands.uinput.tests; 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 @@ public class EvemuParserTest { 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 @@ public class EvemuParserTest { } @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 d7341e0100a5..9d13d8ad7905 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -43742,7 +43742,9 @@ package android.telephony { 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 01ca6d924605..ce5752fdbd8b 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4211,10 +4211,18 @@ package android.content.pm { 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 @@ package android.os { 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 47a152aa6cca..0f7a0700b370 100644 --- a/core/java/android/app/time/TEST_MAPPING +++ b/core/java/android/app/time/TEST_MAPPING @@ -7,12 +7,20 @@ "include-filter": "android.app." } ] + }, + { + "name": "CtsTimeTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] } ], // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows. "postsubmit": [ { - "name": "FrameworksServicesTests", + "name": "FrameworksTimeServicesTests", "options": [ { "include-filter": "com.android.server.timezonedetector." @@ -21,14 +29,6 @@ "include-filter": "com.android.server.timedetector." } ] - }, - { - "name": "CtsTimeTestCases", - "options": [ - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - } - ] } ] } diff --git a/core/java/android/app/timedetector/TEST_MAPPING b/core/java/android/app/timedetector/TEST_MAPPING index 9517fb99b04a..43dd82f8a26e 100644 --- a/core/java/android/app/timedetector/TEST_MAPPING +++ b/core/java/android/app/timedetector/TEST_MAPPING @@ -7,23 +7,23 @@ "include-filter": "android.app." } ] - } - ], - // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows. - "postsubmit": [ + }, { - "name": "FrameworksServicesTests", + "name": "CtsTimeTestCases", "options": [ { - "include-filter": "com.android.server.timedetector." + "exclude-annotation": "androidx.test.filters.FlakyTest" } ] - }, + } + ], + // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows. + "postsubmit": [ { - "name": "CtsTimeTestCases", + "name": "FrameworksTimeServicesTests", "options": [ { - "exclude-annotation": "androidx.test.filters.FlakyTest" + "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 fd41b869efaf..2be5614b54a5 100644 --- a/core/java/android/app/timezonedetector/TEST_MAPPING +++ b/core/java/android/app/timezonedetector/TEST_MAPPING @@ -7,23 +7,23 @@ "include-filter": "android.app." } ] - } - ], - // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows. - "postsubmit": [ + }, { - "name": "FrameworksServicesTests", + "name": "CtsTimeTestCases", "options": [ { - "include-filter": "com.android.server.timezonedetector." + "exclude-annotation": "androidx.test.filters.FlakyTest" } ] - }, + } + ], + // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows. + "postsubmit": [ { - "name": "CtsTimeTestCases", + "name": "FrameworksTimeServicesTests", "options": [ { - "exclude-annotation": "androidx.test.filters.FlakyTest" + "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 f532c4cffcc4..445ca0c98416 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -19,6 +19,7 @@ package android.content.pm; 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 final class UserProperties implements Parcelable { */ 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { setShowInLauncher(orig.getShowInLauncher()); setMediaSharedWithParent(orig.isMediaSharedWithParent()); setCredentialShareableWithParent(orig.isCredentialShareableWithParent()); + setShowInQuietMode(orig.getShowInQuietMode()); + setShowInSharingSurfaces(orig.getShowInSharingSurfaces()); } /** @@ -419,40 +499,59 @@ public final class UserProperties implements Parcelable { 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. - * - * <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> The caller must have {@link android.Manifest.permission#MANAGE_USERS} to query this - * property. + * 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. * - * @return true if a profile should be shown in the Settings only when the user is not in the - * quiet mode. + * <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}. * - * See also {@link #getShowInSettings()}, {@link #setShowInSettings(int)}, - * {@link ShowInSettings} + * @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. + */ + @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 ShowInQuietMode"); + } + /** @hide */ + public void setShowInQuietMode(@ShowInQuietMode int showInQuietMode) { + this.mShowInQuietMode = showInQuietMode; + setPresent(INDEX_SHOW_IN_QUIET_MODE); + } + 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. * - * @hide + * @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. */ - 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 @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 HideInSettingsInQuietMode"); + "You don't have permission to query ShowInSharingSurfaces"); } /** @hide */ - public void setHideInSettingsInQuietMode(boolean hideInSettingsInQuietMode) { - this.mHideInSettingsInQuietMode = hideInSettingsInQuietMode; - setPresent(INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE); + public void setShowInSharingSurfaces(@ShowInSharingSurfaces int showInSharingSurfaces) { + this.mShowInSharingSurfaces = showInSharingSurfaces; + setPresent(INDEX_SHOW_IN_SHARING_SURFACES); } - private boolean mHideInSettingsInQuietMode; + private int mShowInSharingSurfaces; /** * Returns whether a profile should be started when its parent starts (unless in quiet mode). @@ -799,8 +898,11 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { 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 @@ public final class UserProperties implements Parcelable { mShowInLauncher, mStartWithParent, mShowInSettings, - mHideInSettingsInQuietMode, + mShowInQuietMode, + mShowInSharingSurfaces, mInheritDevicePolicy, mUseParentsContacts, mUpdateCrossProfileIntentFiltersOnOTA, @@ -1100,7 +1217,8 @@ public final class UserProperties implements Parcelable { @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 @@ public final class UserProperties implements Parcelable { 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 9ec082ab8eea..6c6b33b8d716 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -42,3 +42,10 @@ flag { 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 330b992ef308..c0d1fb9c6a88 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -131,6 +131,7 @@ interface IUserManager { 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 08d6e028f08c..ec6d20fbc0f5 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -5693,6 +5693,44 @@ public class UserManager { } /** + * 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 7b456007e4ae..7a6c2929c706 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -130,7 +130,6 @@ import android.graphics.FrameInfo; 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.IAccessibilityInteractionConnection; 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 @@ public final class ViewRootImpl implements ViewParent, } 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 5a058ff3de99..a8297472445f 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -18,6 +18,7 @@ package android.view.contentcapture; 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 android.view.contentcapture.ContentCaptureSession.FlushReason; 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 @@ public final class ContentCaptureManager { @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 @@ public final class ContentCaptureManager { 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 final class ContentCaptureManager { 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 d9b0f8035a6d..542c783c9dff 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -34,7 +34,6 @@ import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALS 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.Spannable; 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } else { binder = null; } - mainSession.mHandler.post(() -> mainSession.onSessionStarted(resultCode, binder)); + mainSession.mHandler.post(() -> + mainSession.onSessionStarted(resultCode, binder)); } } @@ -244,9 +247,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** * 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * @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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** @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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } } - @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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** @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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { * 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { /** 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { // 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { 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 @@ public final class MainContentCaptureSession extends ContentCaptureSession { && (!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 aaf90bd00535..858401a9ec1d 100644 --- a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java +++ b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java @@ -18,7 +18,6 @@ package android.view.contentprotection; 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 @@ public class ContentProtectionEventProcessor { } /** 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 @@ public class ContentProtectionEventProcessor { } } - @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 @@ public class ContentProtectionEventProcessor { mEventBuffer.append(event); } - @UiThread private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) { ViewNode viewNode = event.getViewNode(); String eventText = ContentProtectionUtils.getEventTextLower(event); @@ -154,7 +150,6 @@ public class ContentProtectionEventProcessor { } } - @UiThread private void loginDetected() { if (mLastFlushTime == null || Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) { @@ -163,13 +158,11 @@ public class ContentProtectionEventProcessor { 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 @@ public class ContentProtectionEventProcessor { } } - @UiThread private void flush() { mLastFlushTime = Instant.now(); @@ -192,7 +184,6 @@ public class ContentProtectionEventProcessor { 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 1b9ff44c9185..8e89541647c3 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 @@ public final class WebViewDelegate { * 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 bc7a5fda6f7a..e14ae72ee7a5 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 @@ public class WebViewZygote { 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 class WebViewZygote { 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 0da03fb5aaeb..7f93213bf884 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -35,8 +35,8 @@ flag { 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 6f114e34b21c..d0d2354e7007 100644 --- a/core/java/com/android/internal/os/MonotonicClock.java +++ b/core/java/com/android/internal/os/MonotonicClock.java @@ -30,6 +30,7 @@ import org.xmlpull.v1.XmlPullParserException; 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 @@ public class MonotonicClock { 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 eed186ad3702..73a7e4296a57 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -6350,4 +6350,20 @@ ul.</string> <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 24b39bc1225e..38f1f6756d17 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 d47d7891d0e4..1cdcb376effc 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.ComponentName; 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 @@ import java.util.List; * <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 @@ public class MainContentCaptureSessionTest { 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 @@ public class MainContentCaptureSessionTest { @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 @@ public class MainContentCaptureSessionTest { session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); + mTestableLooper.processAllMessages(); assertThat(session.mContentProtectionEventProcessor).isNull(); verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -122,6 +142,7 @@ public class MainContentCaptureSessionTest { session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); + mTestableLooper.processAllMessages(); assertThat(session.mContentProtectionEventProcessor).isNull(); verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -142,6 +163,7 @@ public class MainContentCaptureSessionTest { session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); + mTestableLooper.processAllMessages(); assertThat(session.mContentProtectionEventProcessor).isNull(); verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -153,6 +175,7 @@ public class MainContentCaptureSessionTest { session.mComponentName = null; session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); + mTestableLooper.processAllMessages(); assertThat(session.mContentProtectionEventProcessor).isNull(); } @@ -166,6 +189,7 @@ public class MainContentCaptureSessionTest { session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; session.sendEvent(EVENT); + mTestableLooper.processAllMessages(); verifyZeroInteractions(mMockContentProtectionEventProcessor); assertThat(session.mEvents).isNull(); @@ -180,6 +204,7 @@ public class MainContentCaptureSessionTest { session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; session.sendEvent(EVENT); + mTestableLooper.processAllMessages(); verify(mMockContentProtectionEventProcessor).processEvent(EVENT); assertThat(session.mEvents).isNull(); @@ -194,6 +219,7 @@ public class MainContentCaptureSessionTest { session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; session.sendEvent(EVENT); + mTestableLooper.processAllMessages(); verifyZeroInteractions(mMockContentProtectionEventProcessor); assertThat(session.mEvents).isNotNull(); @@ -206,6 +232,7 @@ public class MainContentCaptureSessionTest { session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; session.sendEvent(EVENT); + mTestableLooper.processAllMessages(); verify(mMockContentProtectionEventProcessor).processEvent(EVENT); assertThat(session.mEvents).isNotNull(); @@ -220,6 +247,7 @@ public class MainContentCaptureSessionTest { /* enableContentProtectionReceiver= */ true); session.sendEvent(EVENT); + mTestableLooper.processAllMessages(); verifyZeroInteractions(mMockContentProtectionEventProcessor); assertThat(session.mEvents).isNull(); @@ -236,6 +264,7 @@ public class MainContentCaptureSessionTest { session.mDirectServiceInterface = mMockContentCaptureDirectManager; session.flush(REASON); + mTestableLooper.processAllMessages(); verifyZeroInteractions(mMockContentProtectionEventProcessor); verifyZeroInteractions(mMockContentCaptureDirectManager); @@ -252,6 +281,7 @@ public class MainContentCaptureSessionTest { session.mDirectServiceInterface = mMockContentCaptureDirectManager; session.flush(REASON); + mTestableLooper.processAllMessages(); verifyZeroInteractions(mMockContentProtectionEventProcessor); verifyZeroInteractions(mMockContentCaptureDirectManager); @@ -269,6 +299,7 @@ public class MainContentCaptureSessionTest { session.mDirectServiceInterface = mMockContentCaptureDirectManager; session.flush(REASON); + mTestableLooper.processAllMessages(); verifyZeroInteractions(mMockContentProtectionEventProcessor); assertThat(session.mEvents).isEmpty(); @@ -286,6 +317,7 @@ public class MainContentCaptureSessionTest { session.mDirectServiceInterface = mMockContentCaptureDirectManager; session.flush(REASON); + mTestableLooper.processAllMessages(); verifyZeroInteractions(mMockContentProtectionEventProcessor); assertThat(session.mEvents).isEmpty(); @@ -298,6 +330,7 @@ public class MainContentCaptureSessionTest { session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; session.destroySession(); + mTestableLooper.processAllMessages(); verify(mMockSystemServerInterface).finishSession(anyInt()); verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -311,6 +344,7 @@ public class MainContentCaptureSessionTest { session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; session.resetSession(/* newState= */ 0); + mTestableLooper.processAllMessages(); verifyZeroInteractions(mMockSystemServerInterface); verifyZeroInteractions(mMockContentProtectionEventProcessor); @@ -318,6 +352,111 @@ public class MainContentCaptureSessionTest { 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 @@ public class MainContentCaptureSessionTest { 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 4a9ea6fed73f..144555dd70c3 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_HOME 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 @@ class DesktopTasksController( } /** 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 dd6ca8da56eb..03006f920072 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 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { 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 fdda5974d1f9..05f937ab6795 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 b55f4ecdb6a4..67316d2d7c0f 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 @@ data_sources: { 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 fde6acb9bfe5..94c862bd7a4f 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.sysui.ShellInit 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 @@ class DesktopTasksControllerTest : ShellTestCase() { 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 @@ class DesktopTasksControllerTest : ShellTestCase() { 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 @@ class DesktopTasksControllerTest : ShellTestCase() { 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 @@ class DesktopTasksControllerTest : ShellTestCase() { 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 a9da832b2a5a..8f5f1f6a4794 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 9f63dfdc0ccb..9ae6f8deb98b 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -19,8 +19,6 @@ package android.media; 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 @@ public class AudioManager { } //==================================================================== - // 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 61b5fd5fb0ec..367b38a152fc 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -2462,6 +2462,8 @@ public class AudioSystem 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 @@ public class AudioSystem 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 b4ca485eb764..42400d1d5d82 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -52,7 +52,7 @@ import android.media.ISpatializerHeadToSoundStagePoseCallback; 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 @@ interface IAudioService { 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 409abc211cb6..92f337244daf 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.annotation.FlaggedApi; 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 @@ public class LoudnessCodecConfigurator { @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} * - * @param executor {@link Executor} to handle the callbacks - * @param listener used to receive updates + * <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 {@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); - - return checkStartLoudnessConfigurator(); + public static @NonNull LoudnessCodecConfigurator create() { + return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()), + Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {}); } /** - * 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 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 for receiving updates + * + * @return the {@link LoudnessCodecConfigurator} instance * * TODO: remove hide once API is final * @hide */ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) - public boolean startLoudnessCodecUpdates() { - mLcDispatcher.addLoudnessCodecListener(this, - Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {}); - return checkStartLoudnessConfigurator(); + 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 new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()), + executor, listener); } /** - * Stops receiving asynchronous loudness updates. + * Creates a new instance of {@link LoudnessCodecConfigurator} + * + * <p>This method should be used only in testing + * + * @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 * - * TODO: remove hide once API is final * @hide */ - @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) - public void stopLoudnessCodecUpdates() { - mLcDispatcher.removeLoudnessCodecListener(this); + 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"); } /** - * Adds a new {@link MediaCodec} that will stream data to an {@link AudioTrack} - * which is registered through {@link #setAudioTrack(AudioTrack)}. + * 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 addMediaCodec(@NonNull MediaCodec mediaCodec) { - mMediaCodecs.add(Objects.requireNonNull(mediaCodec, - "MediaCodec for addMediaCodec must not be null")); + 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); + } } /** - * Removes the {@link MediaCodec} from receiving loudness updates. + * Adds a new {@link MediaCodec} that will stream data to an {@link 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 removeMediaCodec(@NonNull MediaCodec mediaCodec) { - mMediaCodecs.remove(Objects.requireNonNull(mediaCodec, - "MediaCodec for removeMediaCodec must not be null")); + public void addMediaCodec(@NonNull MediaCodec mediaCodec) { + 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); + } + } } /** - * 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)}) + * 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 setAudioTrack(@NonNull AudioTrack audioTrack) { - mAudioTrack = Objects.requireNonNull(audioTrack, - "AudioTrack for setAudioTrack must not be null"); + public void removeMediaCodec(@NonNull MediaCodec mediaCodec) { + 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); + } + } } /** - * 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. + * Gets synchronous loudness updates when no listener is required. The provided + * {@link MediaCodec} streams audio data to the passed {@link AudioTrack}. + * + * @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 @@ public class LoudnessCodecConfigurator { */ @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); + } + + /** @hide */ + /*package*/ int getAssignedTrackPiid() { + int piid = PLAYER_PIID_INVALID; + + synchronized (mConfiguratorLock) { + if (mAudioTrack == null) { + return piid; + } + piid = mAudioTrack.getPlayerIId(); + } + + return piid; } - private boolean checkStartLoudnessConfigurator() { - if (mAudioTrack == null) { - Log.w(TAG, "Cannot start loudness configurator without an AudioTrack"); - return false; + /** @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; } - if (mMediaCodecs.isEmpty()) { - Log.w(TAG, "Cannot start loudness configurator without at least one MediaCodec"); - return false; + 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; } - return true; + 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 fc5c354b98f5..be881b11e545 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); - } - } catch (RemoteException e) { - e.rethrowFromSystemServer(); + 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; } - } - private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener> - mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>(); + 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); + } - @NonNull private final LoudnessCodecUpdatesDispatcherStub mLoudnessCodecStub; + void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) { + Objects.requireNonNull(configurator); - private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> - mConfiguratorListener = new HashMap<>(); + for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e : + mConfiguratorListener.entrySet()) { + if (e.getValue() == configurator) { + final OnLoudnessCodecUpdateListener listener = e.getKey(); + mConfiguratorListener.remove(listener); + mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener"); + break; + } + } + } + } - @NonNull private final AudioManager mAm; + @NonNull private final IAudioService mAudioService; - protected LoudnessCodecDispatcher(@NonNull AudioManager am) { - mAm = Objects.requireNonNull(am); - mLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub(); + /** @hide */ + public LoudnessCodecDispatcher(@NonNull IAudioService audioService) { + mAudioService = Objects.requireNonNull(audioService); } - /** @hide */ - public LoudnessCodecConfigurator createLoudnessCodecConfigurator() { - return new LoudnessCodecConfigurator(this); + @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); - - for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e : - mConfiguratorListener.entrySet()) { - if (e.getValue() == configurator) { - final OnLoudnessCodecUpdateListener listener = e.getKey(); - mConfiguratorListener.remove(listener); - mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener"); - break; - } + LoudnessCodecUpdatesDispatcherStub.getInstance().removeLoudnessCodecListener(configurator); + } + + /** @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 75c906060d43..000000000000 --- 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 000000000000..fd695179057d --- /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 000000000000..5ca0fc9661c2 --- /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 000000000000..91a671fd6eef --- /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 000000000000..0099d986ac75 --- /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 000000000000..17fdba6f7c15 --- /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 Binary files differnew file mode 100644 index 000000000000..acba4b354066 --- /dev/null +++ b/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a diff --git a/media/tests/LoudnessCodecApiTest/res/values/strings.xml b/media/tests/LoudnessCodecApiTest/res/values/strings.xml new file mode 100644 index 000000000000..0c4227c364ca --- /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 000000000000..65a9799431e7 --- /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 3a2b1ce3373e..ef218fdd38f3 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 e1888382ae0a..fe05237bdc57 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.graphics.drawable.Drawable; 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 @@ public class PackageUtil { } /** + * 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 000000000000..79e00dfdd6df --- /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 6533c505d739..2e43b75e5123 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 000000000000..9aea6b18214b --- /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 000000000000..6ed8883570e3 --- /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/button/package-info.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java index 8e55695bc032..0108cb471b5a 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/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.button; +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 000000000000..5df6b020cef5 --- /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 000000000000..f5156cb676e9 --- /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 000000000000..b6001493ade9 --- /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 index fd6a5dda5ca8..b8a93559d782 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 5d47da1a93d4..7638e917c7d5 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 @@ public class UninstallLaunch extends FragmentActivity{ // 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 000000000000..1b0885ea684a --- /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 000000000000..305daba14f26 --- /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 000000000000..23cc421890ac --- /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 8187ea51fc59..3f7bce8f85d0 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 0b703c99884b..365dcb255852 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 83d7549551ce..23fdc013acdd 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/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 afe3f07a492e..000000000000 --- 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 0089c2e26e34..000000000000 --- 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 45210abad88e..000000000000 --- 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 223e99e61204..56796945f015 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.getUserGroups(): List<UserGroup> { 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 5dacba5357cd..52b51d7e42e9 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 @@ public abstract class InfoMediaManager extends MediaManager { */ @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 3514932d4e8d..02ec90d968b1 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 @@ public class MediaOutputConstants { "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 faccf2f15f49..90140d4540be 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 @@ public class ProviderTileTest { } @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 0cabab241be4..542f1010c492 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 @@ public class LicenseHtmlGeneratorFromXmlTest { 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 721e69d2eb8d..f0f53d6e82ad 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 android.widget.TextView; 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 e218308758ab..0a71cda4d6dd 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 a745ab5cbdd9..a9dc145afabd 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -143,8 +143,17 @@ flag { } 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 000000000000..800420b4ff60 --- /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 d9f4b7961636..8916e427a1d5 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 daf6cb3d683d..b20fa89273ed 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 befee2b3eeb7..3db56c5a681c 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 4da48f697b0f..706aba3c0505 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 @@ constructor( 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 0c5a14f5720a..48f432e6a6ea 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.core.graphics.drawable.IconCompat; 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 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements 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 abstract class MediaOutputBaseDialog extends SystemUIDialog implements } } - 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 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements mListPaddingTop = mContext.getResources().getDimensionPixelSize( R.dimen.media_output_dialog_list_padding_top); mExecutor = Executors.newSingleThreadExecutor(); + mIncludePlaybackAndAppMetadata = includePlaybackAndAppMetadata; } @Override @@ -354,7 +365,10 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements 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 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog implements 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 ac64300a6570..8e0191ec330f 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.annotation.NonNull; 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 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog { 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 426a497fa329..375a0ce55ac0 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.InfoMediaManager; 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.flags.FeatureFlags; 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 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } Drawable getAppSourceIconFromPackage() { - if (mPackageName.isEmpty()) { + if (TextUtils.isEmpty(mPackageName)) { return null; } try { @@ -372,7 +372,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } String getAppSourceName() { - if (mPackageName.isEmpty()) { + if (TextUtils.isEmpty(mPackageName)) { return null; } final PackageManager packageManager = mContext.getPackageManager(); @@ -391,7 +391,7 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } 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 4640a5d4d801..d40699ca088c 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 androidx.core.graphics.drawable.IconCompat; 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 @@ public class MediaOutputDialog extends MediaOutputBaseDialog { 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 2b38edb3c47e..b04a7a4fd155 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 @@ open class MediaOutputDialogFactory @Inject constructor( /** 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 @@ open class MediaOutputDialogFactory @Inject constructor( 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 132bf99c3f62..1002cc3bd3bb 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 @@ package com.android.systemui.media.dialog 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 @@ class MediaOutputDialogReceiver @Inject constructor( 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 654fffe89471..1983a670b5a8 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.View 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 @@ abstract class BaseMediaProjectionPermissionDialogDelegate<T : AlertDialog>( 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 0299114e0afc..e0eee96d8f0a 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 @@ object FooterViewBinder { 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 ea5abeff7042..244555a3d73b 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 @@ package com.android.systemui.statusbar.notification.footer.ui.viewmodel 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 bc912fb106f0..85cd397a3749 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 @@ package com.android.systemui.statusbar.notification.footer.ui.viewmodel 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 721bea1086e6..e6b0abcfad65 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.Module 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 @@ class FooterViewModel( 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 4554085c35c0..aa0d3acbec3b 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.lifecycle.repeatWhenAttached 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 @@ constructor( 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 5a9f5d5a72d2..886fa70d715d 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 @@ package com.android.systemui.theme; 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.dump.DumpManager; 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 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { 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 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { 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 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { 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 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { 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 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { mUserTracker = userTracker; mResources = resources; mWakefulnessLifecycle = wakefulnessLifecycle; + mJavaAdapter = javaAdapter; + mKeyguardTransitionInteractor = keyguardTransitionInteractor; mUiModeManager = uiModeManager; dumpManager.registerDumpable(TAG, this); } @@ -494,21 +506,34 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { } 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); - - handleWallpaperColors(colors, flags, userId); - } + + Runnable whenAsleepHandler = () -> { + 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); + + 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 9dfb5a5dcb3d..e082ca81ba4f 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.CachedBluetoothDeviceManager; 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 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase { 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 379136b0586f..d5dc502b1e6c 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.LocalBluetoothManager; 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 @@ public class MediaOutputDialogTest extends SysuiTestCase { @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 94dcf7a18514..0ba820f0972a 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 @@ class FooterViewModelTest : SysuiTestCase() { @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 @@ class FooterViewModelTest : SysuiTestCase() { ) 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 @@ class FooterViewModelTest : SysuiTestCase() { ) 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 @@ class FooterViewModelTest : SysuiTestCase() { 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 @@ class FooterViewModelTest : SysuiTestCase() { 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 c454b45a7312..112368895888 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.dump.DumpManager; 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 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { 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 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { .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 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { 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 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { 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 041cd75f82e7..71878954e9ec 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_CLEAR_ACCE 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ return; } final long identity = Binder.clearCallingIdentity(); - if (removeCaptureDisplay()) { + if (deleteCaptureDisplay()) { try { ScreenCapture.ScreenCaptureListener screenCaptureListener = new ScreenCapture.ScreenCaptureListener( @@ -1485,7 +1485,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 5295ec82e3c3..4fe0592f9075 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 c64fb2366dd6..54c8ed38bb1c 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 @@ public class ActivityManagerService extends IActivityManager.Stub } 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 e7ea0bedb2cb..9701fc87b2a6 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.AudioPlaybackConfiguration; 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.IStrategyNonDefaultDevicesDispatcher; 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 @@ public class AudioService extends IAudioService.Stub } /*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 @@ public class AudioService extends IAudioService.Stub private final SoundDoseHelper mSoundDoseHelper; + private final LoudnessCodecHelper mLoudnessCodecHelper; + private final Object mSupportedSystemUsagesLock = new Object(); @GuardedBy("mSupportedSystemUsagesLock") private @AttributeSystemUsage int[] mSupportedSystemUsages = @@ -1275,6 +1280,8 @@ public class AudioService extends IAudioService.Stub readPersistedSettings(); readUserRestrictions(); + mLoudnessCodecHelper = new LoudnessCodecHelper(this); + mPlaybackMonitor = new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM], device -> onMuteAwaitConnectionTimeout(device)); @@ -4366,6 +4373,8 @@ public class AudioService extends IAudioService.Stub 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 @@ public class AudioService extends IAudioService.Stub @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 000000000000..3c67e9dd116b --- /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 af33de0426b1..50ab3f8b8b6c 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 @@ public interface DeviceStateProvider { */ 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 ba321ae5d807..db636d619bd3 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 android.view.SurfaceControl; 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 @@ final class LogicalDisplay { 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 aa8061222444..5cfbf26338e3 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 @@ public class DisplayManagerFlags { 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 @@ public class DisplayManagerFlags { return mBrightnessIntRangeUserPerceptionFlagState.isEnabled(); } + public boolean isExternalVsyncProximityVoteEnabled() { + return mVsyncProximityVote.isEnabled(); + } + /** * dumps all flagstates * @param pw printWriter @@ -188,6 +196,7 @@ public class DisplayManagerFlags { 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 e28b415e6488..d95bdae7514d 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 @@ flag { 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 000000000000..c04df64fc15a --- /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 000000000000..f24fe3a7eb04 --- /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 000000000000..2fc5590a4b94 --- /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 9587f55b54dd..8eb03ec0d3bd 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 @@ public class DisplayModeDirector { 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 class DisplayModeDirector { 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 @@ public class DisplayModeDirector { 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 @@ public class DisplayModeDirector { 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 @@ public class DisplayModeDirector { + "]"); } - 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 000000000000..173b3c58cd95 --- /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 000000000000..a9b18a54a105 --- /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 000000000000..b31461fc66b8 --- /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 b6a6069b5a63..c1cdd6952dcc 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 @@ final class Vote { 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 @@ final class Vote { // 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 @@ final class Vote { 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 49c587aa5596..95fb8fc0947a 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 @@ class VotesStorage { } } - 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 6a43697770cf..4821fbe1e6c0 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 @@ class MediaRouter2ServiceImpl { 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 @@ class MediaRouter2ServiceImpl { 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 1b640fcb7e20..2707b4502f54 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { 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 2ef0ca64c9d6..89d820050b03 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 android.service.notification.NotificationServiceProto.ROOT_CONFIG; 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; 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 org.xmlpull.v1.XmlPullParserException; 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 @@ public class ZenModeHelper { @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 class ZenModeHelper { } 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 @@ public class ZenModeHelper { } 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 class ZenModeHelper { } 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 @@ public class ZenModeHelper { } } - 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 @@ public class ZenModeHelper { } } - 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 @@ public class ZenModeHelper { // 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 @@ public class ZenModeHelper { } 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 @@ public class ZenModeHelper { 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 @@ public class ZenModeHelper { } } + /** " + * 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 85563172cf05..f90bf4b47644 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 @@ public class UserManagerService extends IUserManager.Stub { 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 7bdcd685a2e9..56c400a0caf8 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 @@ public final class UserTypeDetails { /** 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 @@ public final class UserTypeDetails { 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 @@ public final class UserTypeDetails { 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 @@ public final class UserTypeDetails { 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 @@ public final class UserTypeDetails { 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 @@ public final class UserTypeDetails { 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 @@ public final class UserTypeDetails { 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 @@ public final class UserTypeDetails { return this; } - public Builder setLabel(int label) { - mLabel = label; + public Builder setLabels(@StringRes int ... labels) { + mLabels = labels; return this; } @@ -562,7 +576,7 @@ public final class UserTypeDetails { 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 7da76c18216e..4ef8cb780734 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 @@ public final class UserTypeFactory { .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 @@ public final class UserTypeFactory { 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 @@ public final class UserTypeFactory { .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 @@ public final class UserTypeFactory { .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 @@ public final class UserTypeFactory { .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 @@ public final class UserTypeFactory { .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 @@ public final class UserTypeFactory { .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 @@ public final class UserTypeFactory { .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 986735f5f2ee..73c422490330 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 eea13f179b9e..eb401043af03 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 @@ public class BatteryStatsImpl extends BatteryStats { @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); + } + if (mProcStateScreenOffTimeMs == null) { + mProcStateScreenOffTimeMs = + new TimeInFreqMultiStateCounter(mBsi.mOnBatteryScreenOffTimeBase, + 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); } @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 68f554cb2758..ea8a801ff697 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 @@ public class SystemImpl implements SystemInterface { @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 b3672ecb194c..b12da61d0b3f 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 class WebViewUpdateService extends SystemService { 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 @@ public class WebViewUpdateService extends SystemService { @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 e618c7e2a80c..89cb4c802410 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.content.pm.Signature; 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 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { 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 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { 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 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { @Override public void prepareWebViewInSystemServer() { - mSystemInterface.notifyZygote(isMultiProcessEnabled()); try { synchronized (mLock) { mCurrentWebViewPackage = findPreferredWebViewPackage(); @@ -366,14 +359,10 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { // 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 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { @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 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { 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 000000000000..ce95b1857b27 --- /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 d90d4ff6bbd6..849836828d94 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } 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 class ActivityRecord extends WindowToken implements WindowManagerService.A final StartingSurfaceController.StartingSurface surface; final boolean animate; + final boolean hasImeSurface; if (mStartingData != null) { if (mStartingData.mWaitForSyncTransactionCommit || mTransitionController.isCollecting(this)) { @@ -2907,6 +2908,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A this); return; } - surface.remove(animate); + surface.remove(animate, hasImeSurface); } /** @@ -5380,11 +5382,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // 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 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A 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 c61d86355c8b..1a197875ba31 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 @@ class ActivityRecordInputSink { || 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 90eeed288240..a21b9b488004 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.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS; 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 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { 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 8cc197c2f3d0..39e900a97021 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 @@ public class BackgroundActivityStartController { 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 @@ public class BackgroundActivityStartController { 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 @@ public class BackgroundActivityStartController { 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 2c224e458a2d..07cbd58744cb 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.FLAG_NOT_TOUCH_MODAL; 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_LEFT; 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp @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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp computeSizeRanges(mDisplayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig); - mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId, - mDisplayInfo); + setDisplayInfoOverride(); if (isDefaultDisplay) { mCompatibleScreenScale = CompatibilityInfo.computeCompatibleScaling(mDisplayMetrics, @@ -2314,6 +2301,20 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // 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 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp if (physicalDisplayChanged) { mDisplayPolicy.physicalDisplayUpdated(); - mDisplaySwitchTransitionLauncher.onDisplayUpdated(currentRotation, getRotation(), - getDisplayAreaInfo()); + mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged(currentRotation, + getRotation(), getDisplayAreaInfo()); } } } @@ -5494,8 +5498,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplayReady = true; if (mWmService.mDisplayManagerInternal != null) { - mWmService.mDisplayManagerInternal - .setDisplayInfoOverrideFromWindowManager(mDisplayId, getDisplayInfo()); + setDisplayInfoOverride(); configureDisplayPolicy(); } @@ -6138,9 +6141,17 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp 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 708ee7f59726..b862d7c28b52 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 @@ public class DisplayPolicy { 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 000000000000..e611177210e8 --- /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 000000000000..72e8fcb05bb9 --- /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 0c235bae2006..d65d778549cc 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 @@ class RootWindowContainer extends WindowContainer<DisplayContent> 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 5c84cb07e891..e7bffdfd448b 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.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATI 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 @@ class ScreenRotationAnimation { 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 28a35b9ceefe..303211083f29 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 @@ public class StartingSurfaceController { /** * 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 17ab00d64924..3a711b2c7046 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 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { 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 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { 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 a736874f178d..bacfda5fc528 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 @@ class TransitionController { 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 fd22f15fb798..750fd509e50f 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 @@ public class WindowAnimator { 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 a69a07f9aee0..575ae69be12b 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 @@ public class WindowManagerService extends IWindowManager.Stub 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 @@ public class WindowManagerService extends IWindowManager.Stub 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 3e43908994ad..6d6bcc88e8aa 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 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // 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 000000000000..8c8f6a6cb386 --- /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 34737eff8e6d..56daea772cfd 100644 --- a/services/foldables/devicestateprovider/Android.bp +++ b/services/foldables/devicestateprovider/Android.bp @@ -5,9 +5,12 @@ package { 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 aea46d1ce329..4c487a70390d 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.INVALID_DEVICE_STA 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.Sensor; 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.annotations.VisibleForTesting; 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, // 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, @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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, } } + 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, } listener = mListener; for (DeviceState deviceState : mOrderedStates) { - if (isThermalStatusCriticalOrAbove(mThermalStatus) - && deviceState.hasFlag( - DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) { - continue; - } - if (mPowerSaveModeEnabled && deviceState.hasFlag( - DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) { - continue; + if (isStateSupported(deviceState)) { + supportedStates.add(deviceState); } - supportedStates.add(deviceState); } } @@ -206,6 +243,26 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, @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 final class FoldableDeviceStateProvider implements DeviceStateProvider, */ 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( + @NonNull DeviceState deviceState, + @NonNull Predicate<FoldableDeviceStateProvider> predicate) { + this(deviceState, predicate, ALLOWED); + } + + private DeviceStateConfiguration( + @NonNull DeviceState deviceState, + @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate, + @NonNull Predicate<FoldableDeviceStateProvider> availabilityPredicate) { - private DeviceStateConfiguration(DeviceState deviceState, - Function<FoldableDeviceStateProvider, Boolean> predicate) { 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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, } /** + * @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 @@ public final class FoldableDeviceStateProvider implements DeviceStateProvider, 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 5f2cf3cb5060..5968b6346d35 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 android.hardware.display.DisplayManager; 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 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { 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 class TentModeDeviceStatePolicy extends DeviceStatePolicy { */ 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 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { 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 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { 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 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { 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 000000000000..6ad8d790485c --- /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 000000000000..47c2a1b079f8 --- /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 8fa4ce592777..ddf4a089e76e 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.assertEquals; 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.SensorEvent; 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 @@ public final class FoldableDeviceStateProviderTest { @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 @@ public final class FoldableDeviceStateProviderTest { 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 @@ public final class FoldableDeviceStateProviderTest { } 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 000000000000..3f72364005ab --- /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 000000000000..7f8da88ca996 --- /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 000000000000..c624325d773c --- /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 6798a2da4c54..d0859232778d 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_R 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 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 appRequestRefreshRateRange = - director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNull(appRequestRefreshRateRange); + 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 = 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 = 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); - - 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); - - appRequestRefreshRateRange = - director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); - assertNull(appRequestRefreshRateRange); + 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); + + 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); + + vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE); + assertNull(vote); } @Test @@ -1945,17 +1940,12 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 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 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 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_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 = 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 @@ public class DisplayModeDirectorTest { 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 000000000000..547008e2fe56 --- /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 000000000000..868a89393d5f --- /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 000000000000..1c631b07a2ff --- /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 9ab6ee5bd230..f6774017c523 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 @@ public class SkinThermalStatusObserverTest { 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 @@ public class SkinThermalStatusObserverTest { 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 @@ public class SkinThermalStatusObserverTest { 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 000000000000..cc8800395d1e --- /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 000000000000..749b07d16ebe --- /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 57b12251c207..d70a4fd555ec 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 @@ public class UserManagerServiceUserPropertiesTest { .setShowInLauncher(21) .setStartWithParent(false) .setShowInSettings(45) - .setHideInSettingsInQuietMode(false) + .setShowInSharingSurfaces(78) + .setShowInQuietMode(12) .setInheritDevicePolicy(67) .setUseParentsContacts(false) .setCrossProfileIntentFilterAccessControl(10) @@ -74,7 +75,8 @@ public class UserManagerServiceUserPropertiesTest { 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 @@ public class UserManagerServiceUserPropertiesTest { 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 48eb5c64f3d1..77f693917574 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 com.android.server.pm.UserTypeDetails.UNLIMITED_NUMBER_OF_USERS; 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 @@ public class UserManagerServiceUserTypeTest { .setCredentialShareableWithParent(false) .setAuthAlwaysRequiredToDisableQuietMode(true) .setShowInSettings(900) - .setHideInSettingsInQuietMode(true) + .setShowInSharingSurfaces(20) + .setShowInQuietMode(30) .setInheritDevicePolicy(340) .setDeleteAppWithParent(true) .setAlwaysVisible(true); @@ -107,9 +109,9 @@ public class UserManagerServiceUserTypeTest { .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 @@ public class UserManagerServiceUserTypeTest { 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 @@ public class UserManagerServiceUserTypeTest { 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 @@ public class UserManagerServiceUserTypeTest { 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 @@ public class UserManagerServiceUserTypeTest { 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 @@ public class UserManagerServiceUserTypeTest { .setCredentialShareableWithParent(true) .setAuthAlwaysRequiredToDisableQuietMode(false) .setShowInSettings(20) - .setHideInSettingsInQuietMode(false) .setInheritDevicePolicy(21) + .setShowInSharingSurfaces(22) + .setShowInQuietMode(24) .setDeleteAppWithParent(true) .setAlwaysVisible(false); @@ -354,9 +363,11 @@ public class UserManagerServiceUserTypeTest { 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 @@ public class UserManagerServiceUserTypeTest { 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 2b6d8eda6bb0..c7d80edc9c2d 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 @@ public final class UserManagerTest { 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 ebe45a6fa1e8..d00060564e74 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.PackageInfo; 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 android.webkit.WebViewProviderResponse; 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 @@ import java.util.concurrent.CountDownLatch; 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 @@ public class WebViewUpdateServiceTest { 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 @@ public class WebViewUpdateServiceTest { } @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 @@ public class WebViewUpdateServiceTest { } @Test + @RequiresFlagsDisabled("android.webkit.update_service_v2") public void testMultiProcessEnabledByDefaultWithSettingsValue() { testMultiProcessByDefaultWithSettingsValue( true /* enabledByDefault */, Integer.MIN_VALUE, false /* expectEnabled */); @@ -1356,6 +1373,7 @@ public class WebViewUpdateServiceTest { } @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 b45dcd4d5403..09ffe71a6758 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 androidx.test.InstrumentationRegistry; 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { new Intent(Intent.ACTION_LOCALE_CHANGED)); verify(mZenModeHelper, times(1)).updateDefaultZenRules( - anyInt(), anyBoolean()); + anyInt()); } @Test @@ -8752,7 +8751,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // 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 0313aaa6e3a7..97b6b98a0b08 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.PEOPLE_STARRED; 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.platform.test.flag.junit.SetFlagsRule; 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { 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 @@ public class ZenModeHelperTest extends UiServiceTestCase { // 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 786432a5dc88..f2e54bc572ae 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 @@ public class ActivityRecordTests extends WindowTestsBase { @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 @@ public class ActivityRecordTests extends WindowTestsBase { 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 b44d52e97d1c..203475156491 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 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { 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 c6fa8a1b5a98..acce2e2633bd 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_M 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.Arrays; 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 @@ public class DisplayContentTests extends WindowTestsBase { 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 @@ public class DisplayContentTests extends WindowTestsBase { 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 @@ public class DisplayContentTests extends WindowTestsBase { */ private DisplayContent createDisplayNoUpdateDisplayInfo() { final DisplayContent displayContent = createNewDisplay(); - doNothing().when(displayContent).updateDisplayInfo(); + doNothing().when(displayContent).updateDisplayInfo(any()); return displayContent; } @@ -2882,6 +2864,16 @@ public class DisplayContentTests extends WindowTestsBase { 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 9af5ba57d6c9..5738d246dea9 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.StrictMode; 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 @@ public class SystemServicesTestRule implements TestRule { // 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 2d5b72b3c680..d183cf720491 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.junit.runner.RunWith; 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 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { 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 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { 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 c124079ca2e3..ede4885df097 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -8853,6 +8853,24 @@ public class CarrierConfigManager { 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 @@ public class CarrierConfigManager { 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); |