Proxy - collect screen recording/screenshot for multi-display device.

Collect only one screen recording at a time but optionally specify display. (Working on changes to allow multiple screen recordings to be collected simulatenously).
Allow for screenshots of multiple displays in one tracing session.

Bug: 264688932
Test: npm run test:unit:ci
Change-Id: I482cc54bad73cd6bf68ab9aff8cc3d52ae360d90
diff --git a/tools/winscope/src/adb/winscope_proxy.py b/tools/winscope/src/adb/winscope_proxy.py
index ebb5f13..ebe87c9 100644
--- a/tools/winscope/src/adb/winscope_proxy.py
+++ b/tools/winscope/src/adb/winscope_proxy.py
@@ -63,46 +63,13 @@
     return parser
 
 # Keep in sync with ProxyConnection#VERSION in Winscope
-VERSION = '3.0.0'
+VERSION = '3.1.0'
 
 PERFETTO_TRACE_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-trace.conf'
 PERFETTO_DUMP_CONFIG_FILE = '/data/misc/perfetto-configs/winscope-proxy-dump.conf'
 PERFETTO_TRACE_FILE = '/data/misc/perfetto-traces/winscope-proxy-trace.perfetto-trace'
 PERFETTO_DUMP_FILE = '/data/misc/perfetto-traces/winscope-proxy-dump.perfetto-trace'
 PERFETTO_UNIQUE_SESSION_NAME = 'winscope proxy perfetto tracing'
-PERFETTO_UTILS = f"""
-function is_perfetto_data_source_available {{
-    local data_source_name=$1
-    if perfetto --query | grep $data_source_name 2>&1 >/dev/null; then
-        return 0
-    else
-        return 1
-    fi
-}}
-
-function is_perfetto_tracing_session_running {{
-    if perfetto --query | grep "{PERFETTO_UNIQUE_SESSION_NAME}" 2>&1 >/dev/null; then
-        return 0
-    else
-        return 1
-    fi
-}}
-
-function is_any_perfetto_data_source_available {{
-    if is_perfetto_data_source_available android.inputmethod || \
-       is_perfetto_data_source_available android.protolog || \
-       is_perfetto_data_source_available android.surfaceflinger.layers || \
-       is_perfetto_data_source_available android.surfaceflinger.transactions || \
-       is_perfetto_data_source_available com.android.wm.shell.transition || \
-       is_perfetto_data_source_available android.viewcapture || \
-       is_perfetto_data_source_available android.windowmanager || \
-       is_perfetto_data_source_available android.input.inputevent; then
-        return 0
-    else
-        return 1
-    fi
-}}
-"""
 
 WINSCOPE_VERSION_HEADER = "Winscope-Proxy-Version"
 WINSCOPE_TOKEN_HEADER = "Winscope-Token"
@@ -131,10 +98,10 @@
         self.file = file
         self.type = filetype
 
-    def get_filepaths(self, device_id):
+    def get_filepaths(self, device_id) -> list[str]:
         return [self.file]
 
-    def get_filetype(self):
+    def get_filetype(self) -> str:
         return self.type
 
 
@@ -144,7 +111,7 @@
         self.matcher = matcher
         self.type = filetype
 
-    def get_filepaths(self, device_id):
+    def get_filepaths(self, device_id) -> list[str]:
         if len(self.matcher) > 0:
             matchingFiles = call_adb(
                 f"shell su root find {self.path} -name {self.matcher}", device_id)
@@ -156,7 +123,7 @@
         log.debug("Found files %s", files)
         return files
 
-    def get_filetype(self):
+    def get_filetype(self) -> str:
         return self.type
 
 
@@ -167,7 +134,7 @@
             WINSCOPE_EXTS))
         self.type = filetype
 
-    def get_filepaths(self, device_id):
+    def get_filepaths(self, device_id) -> list[str]:
         for matcher in self.internal_matchers:
             files = matcher.get_filepaths(device_id)
             if len(files) > 0:
@@ -176,201 +143,6 @@
         return []
 
 
-class TraceTarget:
-    """Defines a single parameter to trace.
-
-    Attributes:
-        file_matchers: the matchers used to identify the paths on the device the trace results are saved to.
-        trace_start: command to start the trace from adb shell, must not block.
-        trace_stop: command to stop the trace, should block until the trace is stopped.
-    """
-
-    def __init__(self, trace_name: str, files: list[File | FileMatcher], is_perfetto_available: Callable[[str], bool], trace_start: str, trace_stop: str) -> None:
-        self.trace_name = trace_name
-        if type(files) is not list:
-            files = [files]
-        self.files = files
-        self.is_perfetto_available = is_perfetto_available
-        self.trace_start = trace_start
-        self.trace_stop = trace_stop
-
-
-# Order of files matters as they will be expected in that order and decoded in that order
-TRACE_TARGETS = {
-    "view_capture_trace": TraceTarget(
-        "view_capture_trace",
-        File('/data/misc/wmtrace/view_capture_trace.zip', "view_capture_trace.zip"),
-        lambda res: is_perfetto_data_source_available("android.viewcapture", res),
-    """
-su root settings put global view_capture_enabled 1
-echo 'ViewCapture tracing (legacy) started.'
-""",
-    """
-su root sh -c 'cmd launcherapps dump-view-hierarchies >/data/misc/wmtrace/view_capture_trace.zip'
-su root settings put global view_capture_enabled 0
-echo 'ViewCapture tracing (legacy) stopped.'
-"""
-    ),
-    "window_trace": TraceTarget(
-        "window_trace",
-        WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"),
-        lambda res: is_perfetto_data_source_available('android.windowmanager', res),
-        """
-su root cmd window tracing start
-echo 'WM trace (legacy) started.'
-        """,
-        """
-su root cmd window tracing stop >/dev/null 2>&1
-echo 'WM trace (legacy) stopped.'
-        """
-    ),
-    "layers_trace": TraceTarget(
-        "layers_trace",
-        WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"),
-        lambda res: is_perfetto_data_source_available('android.surfaceflinger.layers', res),
-        """
-su root service call SurfaceFlinger 1025 i32 1
-echo 'SF layers trace (legacy) started.'
-        """,
-        """
-su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1
-echo 'SF layers trace (legacy) stopped.'
-"""
-),
-    "screen_recording": TraceTarget(
-        "screen_recording",
-        File(f'/data/local/tmp/screen.mp4', "screen_recording"),
-        lambda res: False,
-        f'''
-        settings put system show_touches 1 && \
-        settings put system pointer_location 1 && \
-        screenrecord --bugreport --bit-rate 8M /data/local/tmp/screen.mp4 & \
-        echo "ScreenRecorder started."
-        ''',
-        '''settings put system pointer_location 0 && \
-        settings put system show_touches 0 && \
-        pkill -l SIGINT screenrecord >/dev/null 2>&1
-        '''.strip()
-    ),
-    "transactions": TraceTarget(
-        "transactions",
-        WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"),
-        lambda res: is_perfetto_data_source_available('android.surfaceflinger.transactions', res),
-        """
-su root service call SurfaceFlinger 1041 i32 1
-echo 'SF transactions trace (legacy) started.'
-""",
-        "su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1"
-    ),
-    "transactions_legacy": TraceTarget(
-        "transactions_legacy",
-        [
-            WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"),
-            FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"),
-        ],
-        lambda res: False,
-        'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."',
-        'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1'
-    ),
-    "proto_log": TraceTarget(
-         "proto_log",
-        WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"),
-        lambda res: is_perfetto_data_source_available('android.protolog', res),
-        """
-su root cmd window logging start
-echo "ProtoLog (legacy) started."
-        """,
-        """
-su root cmd window logging stop >/dev/null 2>&1
-echo "ProtoLog (legacy) stopped."
-        """
-    ),
-    "ime": TraceTarget(
-        "ime",
-        [WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"),
-         WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"),
-         WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice")],
-        lambda res: is_perfetto_data_source_available('android.inputmethod', res),
-        """
-su root ime tracing start
-echo "IME tracing (legacy) started."
-""",
-    """
-su root ime tracing stop >/dev/null 2>&1
-echo "IME tracing (legacy) stopped."
-"""
-    ),
-    "wayland_trace": TraceTarget(
-        "wayland_trace",
-        WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"),
-        lambda res: False,
-        'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."',
-        'su root service call Wayland 26 i32 0 >/dev/null'
-    ),
-    "eventlog": TraceTarget(
-        "eventlog",
-        WinscopeFileMatcher("/data/local/tmp", "eventlog", "eventlog"),
-        lambda res: False,
-        'rm -f /data/local/tmp/eventlog.winscope && EVENT_LOG_TRACING_START_TIME=$EPOCHREALTIME\necho "Event Log trace started."',
-        'echo "EventLog\\n" > /data/local/tmp/eventlog.winscope && su root logcat -b events -v threadtime -v printable -v uid -v nsec -v epoch -b events -t $EVENT_LOG_TRACING_START_TIME >> /data/local/tmp/eventlog.winscope',
-    ),
-    "transition_traces": TraceTarget(
-        "transition_traces",
-        [WinscopeFileMatcher(WINSCOPE_DIR, "wm_transition_trace", "wm_transition_trace"),
-         WinscopeFileMatcher(WINSCOPE_DIR, "shell_transition_trace", "shell_transition_trace")],
-        lambda res: is_perfetto_data_source_available('com.android.wm.shell.transition', res),
-         f"""
-su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start
-echo "Transition traces (legacy) started."
-        """,
-        """
-su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1
-echo 'Transition traces (legacy) stopped.'
-"""
-    ),
-    "input": TraceTarget(
-        "input",
-        [WinscopeFileMatcher(WINSCOPE_DIR, "input_trace", "input_trace")],
-        lambda res: is_perfetto_data_source_available('android.input.inputevent', res),
-        "",
-        ""
-    ),
-    "perfetto_trace": TraceTarget(
-         "perfetto_trace",
-        File(PERFETTO_TRACE_FILE, "trace.perfetto-trace"),
-        lambda res: is_any_perfetto_data_source_available(res),
-        f"""
-cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
-buffers: {{
-    size_kb: 500000
-    fill_policy: RING_BUFFER
-}}
-duration_ms: 0
-file_write_period_ms: 999999999
-write_into_file: true
-unique_session_name: "{PERFETTO_UNIQUE_SESSION_NAME}"
-EOF
-
-if is_perfetto_tracing_session_running; then
-    perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
-    echo 'Stopped already-running winscope perfetto session.'
-fi
-
-echo 'Concurrent Perfetto Sessions'
-perfetto --query | sed -n '/^TRACING SESSIONS:$/,$p'
-
-rm -f {PERFETTO_TRACE_FILE}
-perfetto --out {PERFETTO_TRACE_FILE} --txt --config {PERFETTO_TRACE_CONFIG_FILE} --detach=WINSCOPE-PROXY-TRACING-SESSION
-echo 'Started perfetto trace.'
-""",
-        """
-perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
-echo 'Stopped perfetto trace.'
-""",
-    ),
-}
-
-
 def get_shell_args(device_id: str, type: str) -> list[str]:
     shell = ['adb', '-s', device_id, 'shell']
     log.debug(f"Starting {type} shell {' '.join(shell)}")
@@ -395,7 +167,7 @@
         self.is_perfetto = is_perfetto
 
     @abstractmethod
-    def add(self, config: str, value: str | None) -> None:
+    def add(self, config: str, value: str | list[str] | None) -> None:
         pass
 
     @abstractmethod
@@ -406,6 +178,12 @@
     def execute_command(self, server, device_id):
         pass
 
+    def get_trace_identifiers(self)-> list[str]:
+        return [""]
+
+    def get_optional_start_args(self, identifier)-> str:
+        return ""
+
     def execute_optional_config_command(self, server, device_id, shell, command, config_key, config_value):
         process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                     stdin=subprocess.PIPE, start_new_session=True)
@@ -481,8 +259,6 @@
         flags = "\n".join([f"""trace_flags: {SurfaceFlingerTraceConfig.PERFETTO_FLAGS_MAP[flag]}""" for flag in self.flags])
 
         return f"""
-{PERFETTO_UTILS}
-
 cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
 data_sources: {{
     config {{
@@ -494,7 +270,6 @@
     }}
 }}
 EOF
-echo 'SF trace (perfetto) configured.'
 """
 
     def _legacy_buffer_size_command(self) -> str:
@@ -532,10 +307,10 @@
             "tracingtype": "frame",
         }
 
-    def add(self, config_type, config_value) -> None:
+    def add(self, config_type: str, config_value: str | None) -> None:
         self.selected_configs[config_type] = config_value
 
-    def is_valid(self, config_type) -> bool:
+    def is_valid(self, config_type: str) -> bool:
         return config_type in self.selected_configs
 
     def execute_command(self, server, device_id):
@@ -699,44 +474,63 @@
             self.execute_perfetto_config_command(server, shell, InputConfig.COMMAND, "Input trace")
 
 
-TRACE_CONFIG: dict[str, Callable[[bool], TraceConfig]] = {
-    "window_trace": lambda is_perfetto: WindowManagerTraceConfig(is_perfetto),
-    "layers_trace": lambda is_perfetto: SurfaceFlingerTraceConfig(is_perfetto),
-    "view_capture_trace": lambda is_perfetto: ViewCaptureTraceConfig(is_perfetto),
-    "transactions": lambda is_perfetto: TransactionsConfig(is_perfetto),
-    "proto_log": lambda is_perfetto: ProtoLogConfig(is_perfetto),
-    "ime": lambda is_perfetto: ImeConfig(is_perfetto),
-    "transition_traces": lambda is_perfetto: TransitionTracesConfig(is_perfetto),
-    "input": lambda is_perfetto: InputConfig(is_perfetto),
-}
+class ScreenConfig(TraceConfig):
+    """Creates trace identifiers for Screen Recording traces and Screenshots."""
+    trace_identifiers = ["active"]
+
+    def get_trace_identifiers(self):
+        return self.trace_identifiers
+
+    def is_valid(self, config_type: str) -> bool:
+        return config_type == "displays"
+
+    def add(self, config_type: str, config_value: str | list[str] | None):
+        if config_type != "displays":
+           return
+        if config_value and len(config_value) > 0:
+            if type(config_value) == str:
+                self.trace_identifiers = [config_value.split(" ")[0]]
+            else:
+                self.trace_identifiers = list(map(lambda d: d.split(" ")[0], config_value))
+
+    def execute_command(self, server, device_id):
+        pass
+
+class ScreenRecordingConfig(ScreenConfig):
+    """Creates display args for Screen Recording traces."""
+    def get_optional_start_args(self, identifier):
+        if identifier == "active":
+            return ""
+        return f"--display-id {identifier}"
+
+class ScreenshotConfig(ScreenConfig):
+    """Creates display args for Screenshots."""
+    def get_optional_start_args(self, identifier):
+        if identifier == "active":
+            return ""
+        return f"-d {identifier}"
 
 
-class DumpTarget:
-    """Defines a single parameter to trace.
+class SurfaceFlingerDumpConfig(TraceConfig):
+    """Handles perfetto config for SurfaceFlinger dumps."""
+    def __init__(self, is_perfetto: bool) -> None:
+        super().__init__(is_perfetto)
 
-    Attributes:
-        file: the path on the device the dump results are saved to.
-        dump_command: command to dump state to file.
-    """
+    def add(self, config: str, value: str | None) -> None:
+        pass
 
-    def __init__(self, files: list[File | FileMatcher], dump_command: str) -> None:
-        if type(files) is not list:
-            files = [files]
-        self.files = files
-        self.dump_command = dump_command
+    def is_valid(self, config: str) -> bool:
+        return False
 
+    def execute_command(self, server, device_id):
+        shell = get_shell_args(device_id, "sf config")
 
-DUMP_TARGETS = {
-    "window_dump": DumpTarget(
-        File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"),
-        f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}'
-    ),
+        if self.is_perfetto:
+            self.execute_perfetto_config_command(server, shell, self._perfetto_config_command(), "SurfaceFlinger")
 
-    "layers_dump": DumpTarget(
-        File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"),
-        f"""
-if is_perfetto_data_source_available android.surfaceflinger.layers; then
-    cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
+    def _perfetto_config_command(self) -> str:
+        return f"""
+cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
 data_sources: {{
     config {{
         name: "android.surfaceflinger.layers"
@@ -751,23 +545,296 @@
     }}
 }}
 EOF
-    echo 'SF transactions trace (perfetto) configured to start along the other perfetto traces.'
-else
-    su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}
-fi
 """
+
+    def _legacy_buffer_size_command(self) -> str:
+        return f'su root service call SurfaceFlinger 1029 i32 {self.selected_configs["sfbuffersize"]}'
+
+    def _legacy_flags_command(self) -> str:
+        flags = 0
+        for flag in self.flags:
+            flags |= SurfaceFlingerTraceConfig.LEGACY_FLAGS_MAP[flag]
+
+        return f"su root service call SurfaceFlinger 1033 i32 {flags}"
+
+
+class TraceTarget:
+    """Defines a single parameter to trace.
+
+    Attributes:
+        trace_name: used as a key to access config and log statements during Start/End endpoints.
+        files: the matchers used to identify the paths on the device the trace results are saved to.
+        is_perfetto_available: callback to determine if perfetto tracing is available for target.
+        trace_start: command to start the trace from adb shell, must not block.
+        trace_stop: command to stop the trace, should block until the trace is stopped.
+        get_trace_config: getter for optional setup to execute pre-tracing adb commands and define start command arguments.
+    """
+
+    def __init__(
+            self,
+            trace_name: str,
+            files: list[File | FileMatcher],
+            is_perfetto_available: Callable[[str], bool],
+            trace_start: str,
+            trace_stop: str,
+            get_trace_config: Callable[[bool], TraceConfig] | None = None
+        ) -> None:
+        self.trace_name = trace_name
+        if type(files) is not list:
+            files = [files]
+        self.files = files
+        self.is_perfetto_available = is_perfetto_available
+        self.trace_start = trace_start
+        self.trace_stop = trace_stop
+        self.get_trace_config = get_trace_config
+
+
+# Order of files matters as they will be expected in that order and decoded in that order
+TRACE_TARGETS = {
+    "view_capture_trace": TraceTarget(
+        "view_capture_trace",
+        File('/data/misc/wmtrace/view_capture_trace.zip', "view_capture_trace.zip"),
+        lambda res: is_perfetto_data_source_available("android.viewcapture", res),
+        """
+su root settings put global view_capture_enabled 1
+echo 'ViewCapture tracing (legacy) started.'
+        """,
+        """
+su root sh -c 'cmd launcherapps dump-view-hierarchies >/data/misc/wmtrace/view_capture_trace.zip'
+su root settings put global view_capture_enabled 0
+echo 'ViewCapture tracing (legacy) stopped.'
+        """,
+        lambda is_perfetto: ViewCaptureTraceConfig(is_perfetto)
+    ),
+    "window_trace": TraceTarget(
+        "window_trace",
+        WinscopeFileMatcher(WINSCOPE_DIR, "wm_trace", "window_trace"),
+        lambda res: is_perfetto_data_source_available('android.windowmanager', res),
+        """
+su root cmd window tracing start
+echo 'WM trace (legacy) started.'
+        """,
+        """
+su root cmd window tracing stop >/dev/null 2>&1
+echo 'WM trace (legacy) stopped.'
+        """,
+        lambda is_perfetto: WindowManagerTraceConfig(is_perfetto)
+    ),
+    "layers_trace": TraceTarget(
+        "layers_trace",
+        WinscopeFileMatcher(WINSCOPE_DIR, "layers_trace", "layers_trace"),
+        lambda res: is_perfetto_data_source_available('android.surfaceflinger.layers', res),
+        """
+su root service call SurfaceFlinger 1025 i32 1
+echo 'SF layers trace (legacy) started.'
+        """,
+        """
+su root service call SurfaceFlinger 1025 i32 0 >/dev/null 2>&1
+echo 'SF layers trace (legacy) stopped.'
+        """,
+        lambda is_perfetto: SurfaceFlingerTraceConfig(is_perfetto)
+    ),
+    "screen_recording": TraceTarget(
+        "screen_recording",
+        File(
+            '/data/local/tmp/screen_{trace_identifier}.mp4',
+            "screen_recording_{trace_identifier}"),
+        lambda res: False,
+        '''
+        settings put system show_touches 1 && \
+        settings put system pointer_location 1 && \
+        screenrecord --bugreport --bit-rate 8M {options} /data/local/tmp/screen_{trace_identifier}.mp4 & \
+        echo "ScreenRecorder started."
+        ''',
+        '''settings put system pointer_location 0 && \
+        settings put system show_touches 0 && \
+        pkill -l SIGINT screenrecord >/dev/null 2>&1
+        '''.strip(),
+        lambda is_perfetto: ScreenRecordingConfig(is_perfetto)
+    ),
+    "transactions": TraceTarget(
+        "transactions",
+        WinscopeFileMatcher(WINSCOPE_DIR, "transactions_trace", "transactions"),
+        lambda res: is_perfetto_data_source_available('android.surfaceflinger.transactions', res),
+        """
+su root service call SurfaceFlinger 1041 i32 1
+echo 'SF transactions trace (legacy) started.'
+        """,
+        "su root service call SurfaceFlinger 1041 i32 0 >/dev/null 2>&1",
+        lambda is_perfetto: TransactionsConfig(is_perfetto)
+    ),
+    "transactions_legacy": TraceTarget(
+        "transactions_legacy",
+        [
+            WinscopeFileMatcher(WINSCOPE_DIR, "transaction_trace", "transactions_legacy"),
+            FileMatcher(WINSCOPE_DIR, f'transaction_merges_*', "transaction_merges"),
+        ],
+        lambda res: False,
+        'su root service call SurfaceFlinger 1020 i32 1\necho "SF transactions recording started."',
+        'su root service call SurfaceFlinger 1020 i32 0 >/dev/null 2>&1',
+    ),
+    "proto_log": TraceTarget(
+         "proto_log",
+        WinscopeFileMatcher(WINSCOPE_DIR, "wm_log", "proto_log"),
+        lambda res: is_perfetto_data_source_available('android.protolog', res),
+        """
+su root cmd window logging start
+echo "ProtoLog (legacy) started."
+        """,
+        """
+su root cmd window logging stop >/dev/null 2>&1
+echo "ProtoLog (legacy) stopped."
+        """,
+        lambda is_perfetto: ProtoLogConfig(is_perfetto)
+    ),
+    "ime": TraceTarget(
+        "ime",
+        [WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_clients", "ime_trace_clients"),
+         WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_service", "ime_trace_service"),
+         WinscopeFileMatcher(WINSCOPE_DIR, "ime_trace_managerservice", "ime_trace_managerservice")],
+        lambda res: is_perfetto_data_source_available('android.inputmethod', res),
+        """
+su root ime tracing start
+echo "IME tracing (legacy) started."
+        """,
+        """
+su root ime tracing stop >/dev/null 2>&1
+echo "IME tracing (legacy) stopped."
+        """,
+        lambda is_perfetto: ImeConfig(is_perfetto)
+    ),
+    "wayland_trace": TraceTarget(
+        "wayland_trace",
+        WinscopeFileMatcher("/data/misc/wltrace", "wl_trace", "wl_trace"),
+        lambda res: False,
+        'su root service call Wayland 26 i32 1 >/dev/null\necho "Wayland trace started."',
+        'su root service call Wayland 26 i32 0 >/dev/null'
+    ),
+    "eventlog": TraceTarget(
+        "eventlog",
+        WinscopeFileMatcher("/data/local/tmp", "eventlog", "eventlog"),
+        lambda res: False,
+        'rm -f /data/local/tmp/eventlog.winscope && EVENT_LOG_TRACING_START_TIME=$EPOCHREALTIME\necho "Event Log trace started."',
+        'echo "EventLog\\n" > /data/local/tmp/eventlog.winscope && su root logcat -b events -v threadtime -v printable -v uid -v nsec -v epoch -b events -t $EVENT_LOG_TRACING_START_TIME >> /data/local/tmp/eventlog.winscope',
+    ),
+    "transition_traces": TraceTarget(
+        "transition_traces",
+        [WinscopeFileMatcher(WINSCOPE_DIR, "wm_transition_trace", "wm_transition_trace"),
+         WinscopeFileMatcher(WINSCOPE_DIR, "shell_transition_trace", "shell_transition_trace")],
+        lambda res: is_perfetto_data_source_available('com.android.wm.shell.transition', res),
+        """
+su root cmd window shell tracing start && su root dumpsys activity service SystemUIService WMShell transitions tracing start
+echo "Transition traces (legacy) started."
+        """,
+        """
+su root cmd window shell tracing stop && su root dumpsys activity service SystemUIService WMShell transitions tracing stop >/dev/null 2>&1
+echo 'Transition traces (legacy) stopped.'
+        """,
+        lambda is_perfetto: TransitionTracesConfig(is_perfetto)
+    ),
+    "input": TraceTarget(
+        "input",
+        [WinscopeFileMatcher(WINSCOPE_DIR, "input_trace", "input_trace")],
+        lambda res: is_perfetto_data_source_available('android.input.inputevent', res),
+        "",
+        "",
+        lambda is_perfetto: InputConfig(is_perfetto)
+    ),
+    "perfetto_trace": TraceTarget(
+         "perfetto_trace",
+        File(PERFETTO_TRACE_FILE, "trace.perfetto-trace"),
+        lambda res: is_any_perfetto_data_source_available(res),
+        f"""
+cat << EOF >> {PERFETTO_TRACE_CONFIG_FILE}
+buffers: {{
+    size_kb: 500000
+    fill_policy: RING_BUFFER
+}}
+duration_ms: 0
+file_write_period_ms: 999999999
+write_into_file: true
+unique_session_name: "{PERFETTO_UNIQUE_SESSION_NAME}"
+EOF
+
+if is_perfetto_tracing_session_running; then
+    perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
+    echo 'Stopped already-running winscope perfetto session.'
+fi
+
+echo 'Concurrent Perfetto Sessions'
+perfetto --query | sed -n '/^TRACING SESSIONS:$/,$p'
+
+rm -f {PERFETTO_TRACE_FILE}
+perfetto --out {PERFETTO_TRACE_FILE} --txt --config {PERFETTO_TRACE_CONFIG_FILE} --detach=WINSCOPE-PROXY-TRACING-SESSION
+echo 'Started perfetto trace.'
+""",
+        """
+perfetto --attach=WINSCOPE-PROXY-TRACING-SESSION --stop
+echo 'Stopped perfetto trace.'
+""",
+    ),
+}
+
+
+class DumpTarget:
+    """Defines a single parameter to dump.
+
+    Attributes:
+        trace_name: used as a key to access config and log statements during Dump endpoint.
+        files: the matchers used to identify the paths on the device the dump results are saved to.
+        dump_command: command to dump state to file.
+        get_trace_config: getter for optional setup to execute pre-tracing adb commands and define start command arguments.
+    """
+
+    def __init__(
+            self,
+            trace_name: str,
+            files: list[File | FileMatcher],
+            is_perfetto_available: Callable[[str], bool],
+            dump_command: str,
+            get_trace_config: Callable[[bool], TraceConfig] | None = None
+        ) -> None:
+        self.trace_name = trace_name
+        if type(files) is not list:
+            files = [files]
+        self.files = files
+        self.is_perfetto_available = is_perfetto_available
+        self.dump_command = dump_command
+        self.get_trace_config = get_trace_config
+
+
+DUMP_TARGETS = {
+    "window_dump": DumpTarget(
+        "window_dump",
+        File(f'/data/local/tmp/wm_dump{WINSCOPE_EXT}', "window_dump"),
+        lambda res: False,
+        f'su root dumpsys window --proto > /data/local/tmp/wm_dump{WINSCOPE_EXT}'
+    ),
+
+    "layers_dump": DumpTarget(
+        "layers_dump",
+        File(f'/data/local/tmp/sf_dump{WINSCOPE_EXT}', "layers_dump"),
+        lambda res: is_perfetto_data_source_available('android.surfaceflinger.layers', res),
+        f"""
+su root dumpsys SurfaceFlinger --proto > /data/local/tmp/sf_dump{WINSCOPE_EXT}
+        """,
+        lambda is_perfetto: SurfaceFlingerDumpConfig(is_perfetto)
     ),
 
     "screenshot": DumpTarget(
-        File("/data/local/tmp/screenshot.png", "screenshot.png"),
-        "screencap -p > /data/local/tmp/screenshot.png"
+        "screenshot",
+        File("/data/local/tmp/screenshot_{trace_identifier}.png", "screenshot_{trace_identifier}.png"),
+        lambda res: False,
+        "screencap -p {options}> /data/local/tmp/screenshot_{trace_identifier}.png",
+        lambda is_perfetto: ScreenshotConfig(is_perfetto)
     ),
 
     "perfetto_dump": DumpTarget(
+        "perfetto_dump",
         File(PERFETTO_DUMP_FILE, "dump.perfetto-trace"),
+        lambda res: is_any_perfetto_data_source_available(res),
         f"""
-if is_any_perfetto_data_source_available; then
-    cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
+cat << EOF >> {PERFETTO_DUMP_CONFIG_FILE}
 buffers: {{
     size_kb: 500000
     fill_policy: RING_BUFFER
@@ -775,10 +842,9 @@
 duration_ms: 1
 EOF
 
-    rm -f {PERFETTO_DUMP_FILE}
-    perfetto --out {PERFETTO_DUMP_FILE} --txt --config {PERFETTO_DUMP_CONFIG_FILE}
-    echo 'Recorded perfetto dump.'
-fi
+rm -f {PERFETTO_DUMP_FILE}
+perfetto --out {PERFETTO_DUMP_FILE} --txt --config {PERFETTO_DUMP_CONFIG_FILE}
+echo 'Recorded perfetto dump.'
         """
     )
 }
@@ -880,8 +946,10 @@
             endpoint_name = path[0]
             try:
                 return self.endpoints[(method, endpoint_name)].process(self.request, path[1:])
-            except KeyError:
-                return self._bad_request("Unknown endpoint /{}/".format(endpoint_name))
+            except KeyError as ex:
+                if "RequestType" in repr(ex):
+                    return self._bad_request("Unknown endpoint /{}/".format(endpoint_name))
+                return self._internal_error(repr(ex))
             except AdbError as ex:
                 return self._internal_error(str(ex))
             except BadRequest as ex:
@@ -934,7 +1002,8 @@
         lines = list(filter(None, call_adb('devices -l').split('\n')))
         devices = {m.group(1): {
             'authorized': str(m.group(2)) != 'unauthorized',
-            'model': m.group(4).replace('_', ' ') if m.group(4) else ''
+            'model': m.group(4).replace('_', ' ') if m.group(4) else '',
+            'displays': list(filter(None, call_adb('shell su root dumpsys SurfaceFlinger --display-id', m.group(1)).split('\n')))
         } for m in [ListDevicesEndpoint.ADB_INFO_RE.match(d) for d in lines[1:]] if m}
         self.foundDevices = devices
         j = json.dumps(devices)
@@ -984,17 +1053,84 @@
             raise BadRequest("Content length unreadable\n" + str(err))
         return json.loads(server.rfile.read(length).decode("utf-8"))
 
-    def move_perfetto_target_to_end_of_list(self, targets):
+    def get_targets_and_prepare_for_tracing(self, server, device_id, perfetto_config_file, targets_map: dict[str, TraceTarget | DumpTarget], perfetto_name):
+        log.debug("Clearing perfetto config file for previous tracing session")
+        call_adb(f"shell su root rm -f {perfetto_config_file}", device_id)
+
+        trace_requests: list[dict] = self.get_request(server)
+        trace_types = [t.get("name") for t in trace_requests]
+        log.debug(f"Received client request of {trace_types} for {device_id}")
+        perfetto_query_result = call_adb("shell perfetto --query", device_id)
+
+        trace_targets: list[tuple[DumpTarget | TraceTarget, TraceConfig | None]] = []
+        for t in trace_requests:
+            try:
+                trace_name = t.get("name")
+                target = targets_map[trace_name]
+                is_perfetto = target.is_perfetto_available(perfetto_query_result)
+                config = None
+                if target.get_trace_config is not None:
+                    config = target.get_trace_config(is_perfetto)
+                    self.apply_config(config, t.get("config"), server, device_id)
+                if trace_name == perfetto_name or not is_perfetto:
+                    trace_targets.append((target, config))
+
+            except KeyError as err:
+                log.warning("Unsupported trace target\n" + str(err))
+        trace_targets = self.move_perfetto_target_to_end_of_list(trace_targets)
+
+        self.check_device_and_permissions(server, device_id)
+        self.clear_last_tracing_session(device_id)
+
+        log.debug("Trace requested for {} with targets {}".format(
+            device_id, ','.join([target.trace_name for target, config in trace_targets])))
+
+        return trace_targets
+
+    def apply_config(self, trace_config: TraceConfig, requested_configs: list[dict], server, device_id):
+        for requested_config in requested_configs:
+            config_key = requested_config.get("key")
+            if not trace_config.is_valid(config_key):
+                raise BadRequest(
+                    f"Unsupported config {config_key}\n")
+            trace_config.add(config_key, requested_config.get("value"))
+
+        if device_id in TRACE_THREADS:
+            BadRequest(f"Trace in progress for {device_id}")
+        if not self.check_root(device_id):
+            raise AdbError(
+                f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
+        trace_config.execute_command(server, device_id)
+
+    def check_root(self, device_id):
+        log.debug("Checking root access on {}".format(device_id))
+        return int(call_adb('shell su root id -u', device_id)) == 0
+
+    def move_perfetto_target_to_end_of_list(self, targets: list[tuple[TraceTarget, TraceConfig | None]]) -> list[tuple[TraceTarget, TraceConfig | None]]:
         # Make sure a perfetto target (if present) comes last in the list of targets, i.e. will
         # be processed last.
         # A perfetto target must be processed last, so that perfetto tracing is started only after
         # the other targets have been processed and have configured the perfetto config file.
-        def is_perfetto_target(target):
+        def is_perfetto_target(target: TraceTarget):
             return target == TRACE_TARGETS["perfetto_trace"] or target == DUMP_TARGETS["perfetto_dump"]
-        non_perfetto_targets = [t for t in targets if not is_perfetto_target(t)]
-        perfetto_targets = [t for t in targets if is_perfetto_target(t)]
+        non_perfetto_targets = [t for t in targets if not is_perfetto_target(t[0])]
+        perfetto_targets = [t for t in targets if is_perfetto_target(t[0])]
         return non_perfetto_targets + perfetto_targets
 
+    def check_device_and_permissions(self, server, device_id):
+        if device_id in TRACE_THREADS:
+            log.warning("Trace already in progress for {}", device_id)
+            server.respond(HTTPStatus.OK, b'', "text/plain")
+        if not self.check_root(device_id):
+            raise AdbError(
+                "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'"
+                .format( device_id))
+
+    def clear_last_tracing_session(self, device_id):
+        log.debug("Clearing previous tracing session files from device")
+        call_adb(f"shell su root rm -rf {WINSCOPE_BACKUP_DIR}", device_id)
+        call_adb(f"shell su root mkdir {WINSCOPE_BACKUP_DIR}", device_id)
+
 
 class FetchFilesEndpoint(DeviceRequestEndpoint):
     def process_with_device(self, server, path, device_id):
@@ -1032,17 +1168,13 @@
         log.warning("Proxy didn't find any file to fetch")
 
 
-def check_root(device_id):
-    log.debug("Checking root access on {}".format(device_id))
-    return int(call_adb('shell su root id -u', device_id)) == 0
-
-
 TRACE_THREADS = {}
 
 class TraceThread(threading.Thread):
-    def __init__(self, trace_name: str, device_id: str, command: str):
+    def __init__(self, trace_name: str, device_id: str, command: str, trace_identifier: str):
         self.trace_command = command
         self.trace_name = trace_name
+        self.trace_identifier = trace_identifier
         self._device_id = device_id
         self._keep_alive_timer = None
         self.out = None,
@@ -1113,18 +1245,10 @@
         return self._success
 
 
-def clear_last_tracing_session(device_id):
-    log.debug("Clearing previous tracing session files from device")
-    call_adb(f"shell su root rm -rf {WINSCOPE_BACKUP_DIR}", device_id)
-    call_adb(f"shell su root mkdir {WINSCOPE_BACKUP_DIR}", device_id)
-
-
 class StartTraceEndpoint(DeviceRequestEndpoint):
     TRACE_COMMAND = """
 set -e
 
-{perfetto_utils}
-
 echo "Starting trace..."
 echo "TRACE_START" > {winscope_status}
 
@@ -1153,91 +1277,57 @@
 """
 
     def process_with_device(self, server, path, device_id):
-        log.debug("Clearing perfetto config file for previous tracing session")
-        call_adb(f"shell su root rm -f {PERFETTO_TRACE_CONFIG_FILE}", device_id)
+        trace_targets = self.get_targets_and_prepare_for_tracing(
+            server, device_id, PERFETTO_TRACE_CONFIG_FILE, TRACE_TARGETS, "perfetto_trace")
 
-        trace_requests: list[dict] = self.get_request(server)
-        trace_types = [t.get("name") for t in trace_requests]
-        log.debug(f"Received client request of trace types {trace_types} for {device_id}")
-        trace_targets: list[TraceTarget] = []
-        perfetto_query_result = call_adb("shell perfetto --query", device_id)
-
-        for t in trace_requests:
-            try:
-                trace_name = t.get("name")
-                target = TRACE_TARGETS[trace_name]
-                get_trace_config = TRACE_CONFIG.get(trace_name)
-                is_perfetto = target.is_perfetto_available(perfetto_query_result)
-                if get_trace_config is not None:
-                    self.apply_config(get_trace_config(is_perfetto), t.get("config"), server, device_id)
-                if trace_name == "perfetto_trace" or not is_perfetto:
-                    trace_targets.append(target)
-            except KeyError as err:
-                log.warning("Unsupported trace target\n" + str(err))
-        trace_targets = self.move_perfetto_target_to_end_of_list(trace_targets)
-
-        if device_id in TRACE_THREADS:
-            log.warning("Trace already in progress for {}", device_id)
-            server.respond(HTTPStatus.OK, b'', "text/plain")
-        if not check_root(device_id):
-            raise AdbError(
-                "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'".format(
-                    device_id))
-
-        clear_last_tracing_session(device_id)
-
-        requested_trace_names = [t.trace_name for t in trace_targets]
-        log.debug("Trace requested for {} with targets {}".format(
-            device_id, ','.join(requested_trace_names)))
-
-        for t in trace_targets:
-            command = StartTraceEndpoint.TRACE_COMMAND.format(
-                perfetto_utils=PERFETTO_UTILS,
-                winscope_status=WINSCOPE_STATUS,
-                signal_handler_log=SIGNAL_HANDLER_LOG,
-                stop_commands=t.trace_stop,
-                perfetto_config_file=PERFETTO_TRACE_CONFIG_FILE,
-                start_commands=t.trace_start,
-            )
-            log.debug(f"Executing start command for {t.trace_name} on {device_id}...")
-            thread = TraceThread(t.trace_name, device_id, command.encode('utf-8'))
-            if device_id not in TRACE_THREADS:
-                TRACE_THREADS[device_id] = [thread]
+        for t, config in trace_targets:
+            if config:
+                trace_identifiers = config.get_trace_identifiers()
             else:
-                TRACE_THREADS[device_id].append(thread)
-            thread.start()
+                trace_identifiers = [""]
+
+            for trace_identifier in trace_identifiers:
+                if trace_identifier:
+                    if config:
+                        options = config.get_optional_start_args(trace_identifier)
+                    else:
+                        options = ""
+                    start_cmd = t.trace_start.format(trace_identifier=trace_identifier, options=options)
+                else:
+                    start_cmd = t.trace_start
+
+                command = StartTraceEndpoint.TRACE_COMMAND.format(
+                    winscope_status=WINSCOPE_STATUS,
+                    signal_handler_log=SIGNAL_HANDLER_LOG,
+                    stop_commands=t.trace_stop,
+                    perfetto_config_file=PERFETTO_TRACE_CONFIG_FILE,
+                    start_commands=start_cmd,
+                )
+                log.debug(f"Executing start command for {t.trace_name} on {device_id}...")
+                thread = TraceThread(t.trace_name, device_id, command.encode('utf-8'), trace_identifier)
+                if device_id not in TRACE_THREADS:
+                    TRACE_THREADS[device_id] = [thread]
+                else:
+                    TRACE_THREADS[device_id].append(thread)
+                thread.start()
 
         server.respond(HTTPStatus.OK, b'', "text/plain")
 
-    def apply_config(self, trace_config: TraceConfig, requested_configs: list[dict], server, device_id):
-        for requested_config in requested_configs:
-            config_key = requested_config.get("key")
-            if not trace_config.is_valid(config_key):
-                raise BadRequest(
-                    f"Unsupported config {config_key}\n")
-            trace_config.add(config_key, requested_config.get("value"))
 
-        if device_id in TRACE_THREADS:
-            BadRequest(f"Trace in progress for {device_id}")
-        if not check_root(device_id):
-            raise AdbError(
-                f"Unable to acquire root privileges on the device - check the output of 'adb -s {device_id} shell su root id'")
-        trace_config.execute_command(server, device_id)
-
-
-def move_collected_files(files: list[File | FileMatcher], device_id):
+def move_collected_files(files: list[File | FileMatcher], device_id, trace_identifier):
     for f in files:
         file_paths = f.get_filepaths(device_id)
-        file_type = f.get_filetype()
+        file_type = f.get_filetype().format(trace_identifier=trace_identifier)
 
         for file_path in file_paths:
-            log.debug(f"Moving file {file_path} to {WINSCOPE_BACKUP_DIR}{file_type} on device")
+            formatted_path = file_path.format(trace_identifier=trace_identifier)
+            log.debug(f"Moving file {formatted_path} to {WINSCOPE_BACKUP_DIR}{file_type} on device")
             try:
                 call_adb(
-                    f"shell su root [ ! -f {file_path} ] || su root mv {file_path} {WINSCOPE_BACKUP_DIR}{file_type}",
+                    f"shell su root [ ! -f {formatted_path} ] || su root mv {formatted_path} {WINSCOPE_BACKUP_DIR}{file_type}",
                     device_id)
             except AdbError as ex:
-                log.warning(f"Unable to move file {file_path} - {repr(ex)}")
+                log.warning(f"Unable to move file {formatted_path} - {repr(ex)}")
 
 
 class EndTraceEndpoint(DeviceRequestEndpoint):
@@ -1269,7 +1359,7 @@
                 errors.append("Error ending trace {} on the device: {}".format(thread.trace_name, thread.err))
             if thread.trace_name in TRACE_TARGETS:
                 files = TRACE_TARGETS[thread.trace_name].files
-                move_collected_files(files, device_id)
+                move_collected_files(files, device_id, thread.trace_identifier)
             else:
                 errors.append(f"File location unknown for {thread.trace_name}")
 
@@ -1289,45 +1379,38 @@
 
 class DumpEndpoint(DeviceRequestEndpoint):
     def process_with_device(self, server, path, device_id):
-        try:
-            requested_types = self.get_request(server)
-            requested_dumps: list[DumpTarget] = [DUMP_TARGETS[t] for t in requested_types]
-            requested_dumps = self.move_perfetto_target_to_end_of_list(requested_dumps)
-        except KeyError as err:
-            raise BadRequest("Unsupported trace target\n" + str(err))
-        if device_id in TRACE_THREADS:
-            BadRequest("Trace in progress for {}".format(device_id))
-        if not check_root(device_id):
-            raise AdbError(
-                "Unable to acquire root privileges on the device - check the output of 'adb -s {} shell su root id'"
-                .format(device_id))
+        dump_targets = self.get_targets_and_prepare_for_tracing(
+            server, device_id, PERFETTO_DUMP_CONFIG_FILE, DUMP_TARGETS, "perfetto_dump")
 
-        clear_last_tracing_session(device_id)
+        dump_commands = []
+        for t, config in dump_targets:
+            if config:
+                for trace_identifier in config.get_trace_identifiers():
+                    dump_commands.append(
+                        t.dump_command.format(trace_identifier=trace_identifier, options=config.get_optional_start_args(trace_identifier)))
+            else:
+                dump_commands.append(t.dump_command)
 
-        log.debug("Dump requested for {} with targets {}".format(
-            device_id, ','.join([t for t in requested_types])))
+        dump_commands = '\n'.join(dump_commands)
 
-        dump_commands = '\n'.join(t.dump_command for t in requested_dumps)
-        command = f"""
-{PERFETTO_UTILS}
-
-# Clear perfetto config file. The commands below are going to populate it.
-rm -f {PERFETTO_DUMP_CONFIG_FILE}
-
-{dump_commands}
-"""
         shell = get_shell_args(device_id, "dump")
         process = subprocess.Popen(shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                    stdin=subprocess.PIPE, start_new_session=True)
         log.debug("Starting dump on device {}".format(device_id))
-        out, err = process.communicate(command.encode('utf-8'))
+        out, err = process.communicate(dump_commands.encode('utf-8'))
         if process.returncode != 0:
             raise AdbError("Error executing dump command." + "\n\n### OUTPUT ###" + out.decode('utf-8') + "\n"
                            + err.decode('utf-8'))
         log.debug("Dump finished on device {}".format(device_id))
 
-        for dump in requested_dumps:
-            move_collected_files(dump.files, device_id)
+        for target, config in dump_targets:
+            if config:
+                trace_identifiers = config.get_trace_identifiers()
+                for trace_identifier in trace_identifiers:
+                    move_collected_files(target.files, device_id, trace_identifier)
+            else:
+                move_collected_files(target.files, device_id, "")
+
         server.respond(HTTPStatus.OK, b'', "text/plain")