[ui] Add screenshot track

Add ui support for visualizing screenshots in traces. In followup
change we will add a python script for capturing trace and screencapture
video simultaneously and then merging into a single trace. Using the
traces captured using the script and this UI feature we should be able
to see screenshots in perfetto like this: http://screen/BcWEtzCfAt7kb6m.

Bug: b/287934064
Change-Id: I01e528a9e12b8ceab72fdbec1f1ece9ce977a5cc
diff --git a/Android.bp b/Android.bp
index f66c143..4560f9f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -5370,6 +5370,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
@@ -7969,6 +7970,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
@@ -8002,6 +8004,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/log_message.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.gen.cc",
+        "external/perfetto/protos/perfetto/trace/track_event/screenshot.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/source_location.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/task_execution.gen.cc",
         "external/perfetto/protos/perfetto/trace/track_event/thread_descriptor.gen.cc",
@@ -8035,6 +8038,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
@@ -8068,6 +8072,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/log_message.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.gen.h",
+        "external/perfetto/protos/perfetto/trace/track_event/screenshot.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/source_location.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/task_execution.gen.h",
         "external/perfetto/protos/perfetto/trace/track_event/thread_descriptor.gen.h",
@@ -8105,6 +8110,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
@@ -8145,6 +8151,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
@@ -8177,6 +8184,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/log_message.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.pb.cc",
+        "external/perfetto/protos/perfetto/trace/track_event/screenshot.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/source_location.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/task_execution.pb.cc",
         "external/perfetto/protos/perfetto/trace/track_event/thread_descriptor.pb.cc",
@@ -8210,6 +8218,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
@@ -8242,6 +8251,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/log_message.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.pb.h",
+        "external/perfetto/protos/perfetto/trace/track_event/screenshot.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/source_location.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/task_execution.pb.h",
         "external/perfetto/protos/perfetto/trace/track_event/thread_descriptor.pb.h",
@@ -8279,6 +8289,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
@@ -8312,6 +8323,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/log_message.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/track_event/screenshot.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/source_location.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/task_execution.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/track_event/thread_descriptor.pbzero.cc",
@@ -8345,6 +8357,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
@@ -8378,6 +8391,7 @@
         "external/perfetto/protos/perfetto/trace/track_event/log_message.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/process_descriptor.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/range_of_interest.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/track_event/screenshot.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/source_location.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/task_execution.pbzero.h",
         "external/perfetto/protos/perfetto/trace/track_event/thread_descriptor.pbzero.h",
@@ -8521,6 +8535,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
@@ -10759,6 +10774,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/network_packets.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/slices.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/internal_startups_maxsdk28.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/startup/internal_startups_minsdk29.sql",
@@ -12176,6 +12192,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
diff --git a/BUILD b/BUILD
index a6e2834..4b3baee 100644
--- a/BUILD
+++ b/BUILD
@@ -2256,6 +2256,7 @@
         "src/trace_processor/perfetto_sql/stdlib/android/monitor_contention.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/network_packets.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql",
+        "src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/slices.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/statsd.sql",
         "src/trace_processor/perfetto_sql/stdlib/android/thread.sql",
@@ -4883,6 +4884,7 @@
         "protos/perfetto/trace/track_event/log_message.proto",
         "protos/perfetto/trace/track_event/process_descriptor.proto",
         "protos/perfetto/trace/track_event/range_of_interest.proto",
+        "protos/perfetto/trace/track_event/screenshot.proto",
         "protos/perfetto/trace/track_event/source_location.proto",
         "protos/perfetto/trace/track_event/task_execution.proto",
         "protos/perfetto/trace/track_event/thread_descriptor.proto",
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 75043d0..dff8d0b 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -10661,6 +10661,14 @@
 
 // End of protos/perfetto/trace/track_event/chrome_window_handle_event_info.proto
 
+// Begin of protos/perfetto/trace/track_event/screenshot.proto
+
+message Screenshot {
+  optional bytes jpg_image = 1;
+}
+
+// End of protos/perfetto/trace/track_event/screenshot.proto
+
 // Begin of protos/perfetto/trace/track_event/task_execution.proto
 
 // TrackEvent arguments describing the execution of a task.
@@ -10738,7 +10746,7 @@
 // their default track association) can be emitted as part of a
 // TrackEventDefaults message.
 //
-// Next reserved id: 13 (up to 15). Next id: 50.
+// Next reserved id: 13 (up to 15). Next id: 51.
 message TrackEvent {
   // Names of categories of the event. In the client library, categories are a
   // way to turn groups of individual events on or off.
@@ -10891,6 +10899,7 @@
   optional ChromeContentSettingsEventInfo chrome_content_settings_event_info =
       43;
   optional ChromeActiveProcesses chrome_active_processes = 49;
+  optional Screenshot screenshot = 50;
 
   // This field is used only if the source location represents the function that
   // executes during this event.
diff --git a/protos/perfetto/trace/track_event/BUILD.gn b/protos/perfetto/trace/track_event/BUILD.gn
index 7dc1aad..c101c46 100644
--- a/protos/perfetto/trace/track_event/BUILD.gn
+++ b/protos/perfetto/trace/track_event/BUILD.gn
@@ -37,6 +37,7 @@
     "log_message.proto",
     "process_descriptor.proto",
     "range_of_interest.proto",
+    "screenshot.proto",
     "source_location.proto",
     "task_execution.proto",
     "thread_descriptor.proto",
diff --git a/protos/perfetto/trace/track_event/screenshot.proto b/protos/perfetto/trace/track_event/screenshot.proto
new file mode 100644
index 0000000..02e658c
--- /dev/null
+++ b/protos/perfetto/trace/track_event/screenshot.proto
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+message Screenshot {
+  optional bytes jpg_image = 1;
+}
diff --git a/protos/perfetto/trace/track_event/track_event.proto b/protos/perfetto/trace/track_event/track_event.proto
index cde2fbb..325465d 100644
--- a/protos/perfetto/trace/track_event/track_event.proto
+++ b/protos/perfetto/trace/track_event/track_event.proto
@@ -33,6 +33,7 @@
 import "protos/perfetto/trace/track_event/chrome_renderer_scheduler_state.proto";
 import "protos/perfetto/trace/track_event/chrome_user_event.proto";
 import "protos/perfetto/trace/track_event/chrome_window_handle_event_info.proto";
+import "protos/perfetto/trace/track_event/screenshot.proto";
 import "protos/perfetto/trace/track_event/source_location.proto";
 
 package perfetto.protos;
@@ -102,7 +103,7 @@
 // their default track association) can be emitted as part of a
 // TrackEventDefaults message.
 //
-// Next reserved id: 13 (up to 15). Next id: 50.
+// Next reserved id: 13 (up to 15). Next id: 51.
 message TrackEvent {
   // Names of categories of the event. In the client library, categories are a
   // way to turn groups of individual events on or off.
@@ -255,6 +256,7 @@
   optional ChromeContentSettingsEventInfo chrome_content_settings_event_info =
       43;
   optional ChromeActiveProcesses chrome_active_processes = 49;
+  optional Screenshot screenshot = 50;
 
   // This field is used only if the source location represents the function that
   // executes during this event.
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 504e66f..06e5c39 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -21,6 +21,7 @@
 #include <string>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/base64.h"
 #include "perfetto/ext/base/string_writer.h"
 #include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
@@ -122,6 +123,10 @@
                      storage_.InternString(base::StringView(key.key)),
                      Variadic::Boolean(value));
   }
+  void AddBytes(const Key& key, const protozero::ConstBytes& value) final {
+    std::string b64_data = base::Base64Encode(value.data, value.size);
+    AddString(key, b64_data);
+  }
   bool AddJson(const Key& key, const protozero::ConstChars& value) final {
     auto json_value = json::ParseJsonString(value);
     if (!json_value)
diff --git a/src/trace_processor/importers/proto/track_event_parser.h b/src/trace_processor/importers/proto/track_event_parser.h
index a1cd114..6198dd5 100644
--- a/src/trace_processor/importers/proto/track_event_parser.h
+++ b/src/trace_processor/importers/proto/track_event_parser.h
@@ -44,8 +44,8 @@
 //
 // TODO(ddrone): replace with a predicate on field id to import new fields
 // automatically
-static constexpr uint16_t kReflectFields[] = {24, 25, 26, 27, 28, 29, 32, 33,
-                                              34, 35, 38, 39, 40, 41, 43, 49};
+static constexpr uint16_t kReflectFields[] = {
+    24, 25, 26, 27, 28, 29, 32, 33, 34, 35, 38, 39, 40, 41, 43, 49, 50};
 
 class PacketSequenceStateGeneration;
 class TraceProcessorContext;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
index 8a70e98..7567f75 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/android/BUILD.gn
@@ -25,6 +25,7 @@
     "monitor_contention.sql",
     "network_packets.sql",
     "process_metadata.sql",
+    "screenshots.sql",
     "slices.sql",
     "statsd.sql",
     "thread.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql b/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql
new file mode 100644
index 0000000..7efc08a
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/android/screenshots.sql
@@ -0,0 +1,32 @@
+-- Copyright 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.
+
+-- Screenshot slices, used in perfetto UI.
+--
+-- @column id               Slice id.
+-- @column ts               Slice timestamp.
+-- @column dur              Slice duration, should be typically 0 since
+--                            screeenshot slices are of instant type.
+-- @column name             Slice name.
+CREATE PERFETTO TABLE android_screenshots AS
+SELECT
+  slice.id as id,
+  slice.ts as ts,
+  slice.dur as dur,
+  slice.name as name
+FROM slice
+JOIN args USING(arg_set_id)
+WHERE slice.name = "Screenshot"
+  AND slice.category = "android_screenshot"
+  AND args.key = "screenshot.jpg_image";
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index 70702dc..3b9b236 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -700,3 +700,10 @@
     }
   }
 }
+
+.screenshot-panel {
+  height: 100%;
+  img {
+    max-height: 100%;
+  }
+}
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 9ccf890..d64c00b 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -66,6 +66,9 @@
   PROCESS_SCHEDULING_TRACK_KIND,
 } from '../tracks/process_scheduling';
 import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary';
+import {
+  decideTracks as screenshotDecideTracks,
+} from '../tracks/screenshots';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
 import {THREAD_STATE_TRACK_V2_KIND} from '../tracks/thread_state_v2';
 
@@ -1955,6 +1958,14 @@
   async decideTracks(): Promise<DeferredAction[]> {
     await this.defineMaxLayoutDepthSqlFunction();
 
+    {
+      const result = screenshotDecideTracks(this.engine);
+      if (result !== null) {
+        const {tracksToAdd} = await result;
+        this.tracksToAdd.push(...tracksToAdd);
+      }
+    }
+
     // Add first the global tracks that don't require per-process track groups.
     await this.addCpuSchedulingTracks();
     await this.addFtraceTrack(
diff --git a/ui/src/tracks/screenshots/index.ts b/ui/src/tracks/screenshots/index.ts
new file mode 100644
index 0000000..63ff916
--- /dev/null
+++ b/ui/src/tracks/screenshots/index.ts
@@ -0,0 +1,94 @@
+// 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.
+
+import {v4 as uuidv4} from 'uuid';
+
+import {AddTrackArgs} from '../../common/actions';
+import {Engine} from '../../common/engine';
+import {PrimaryTrackSortKey} from '../../common/state';
+import {
+  NamedSliceTrackTypes,
+} from '../../frontend/named_slice_track';
+import {NewTrackArgs, Track} from '../../frontend/track';
+import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {
+  CustomSqlDetailsPanelConfig,
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../custom_sql_table_slices';
+
+import {
+  ScreenshotTab,
+} from './screenshot_panel';
+
+export {Data} from '../chrome_slices';
+
+class ScreenshotsTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
+  static readonly kind = 'dev.perfetto.ScreenshotsTrack';
+  static create(args: NewTrackArgs): Track {
+    return new ScreenshotsTrack(args);
+  }
+
+  getSqlDataSource(): CustomSqlTableDefConfig {
+    return {
+      sqlTableName: 'android_screenshots',
+      columns: ['*'],
+    };
+  }
+
+  getDetailsPanel(): CustomSqlDetailsPanelConfig {
+    return {
+      kind: ScreenshotTab.kind,
+      config: {
+        sqlTableName: this.tableName,
+        title: 'Screenshots',
+      },
+    };
+  }
+}
+
+export type DecideTracksResult = {
+  tracksToAdd: AddTrackArgs[],
+};
+
+export async function decideTracks(engine: Engine):
+    Promise<DecideTracksResult> {
+  const result: DecideTracksResult = {
+    tracksToAdd: [],
+  };
+
+  await engine.query(`SELECT IMPORT('android.screenshots')`);
+
+  result.tracksToAdd.push({
+    id: uuidv4(),
+    engineId: engine.id,
+    kind: ScreenshotsTrack.kind,
+    trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
+    name: 'Screenshots',
+    config: {},
+    trackGroup: undefined,
+  });
+  return result;
+}
+
+class ScreenshotsPlugin implements Plugin {
+  onActivate(ctx: PluginContext): void {
+    ctx.registerTrack(ScreenshotsTrack);
+  }
+}
+
+export const plugin: PluginInfo = {
+  pluginId: 'perfetto.Screenshots',
+  plugin: ScreenshotsPlugin,
+};
diff --git a/ui/src/tracks/screenshots/screenshot_panel.ts b/ui/src/tracks/screenshots/screenshot_panel.ts
new file mode 100644
index 0000000..350357f
--- /dev/null
+++ b/ui/src/tracks/screenshots/screenshot_panel.ts
@@ -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.
+
+import m from 'mithril';
+
+import {assertTrue} from '../../base/logging';
+import {exists} from '../../base/utils';
+import {EngineProxy} from '../../common/engine';
+import {
+  BottomTab,
+  bottomTabRegistry,
+  NewBottomTabArgs,
+} from '../../frontend/bottom_tab';
+import {
+  GenericSliceDetailsTabConfig,
+} from '../../frontend/generic_slice_details_tab';
+import {getSlice, SliceDetails} from '../../frontend/sql/slice';
+import {asSliceSqlId} from '../../frontend/sql_types';
+
+async function getSliceDetails(
+    engine: EngineProxy, id: number): Promise<SliceDetails|undefined> {
+  return getSlice(engine, asSliceSqlId(id));
+}
+
+export class ScreenshotTab extends BottomTab<GenericSliceDetailsTabConfig> {
+  static readonly kind = 'dev.perfetto.ScreenshotDetailsPanel';
+
+  private sliceDetails?: SliceDetails;
+
+  static create(args: NewBottomTabArgs): ScreenshotTab {
+    return new ScreenshotTab(args);
+  }
+
+  constructor(args: NewBottomTabArgs) {
+    super(args);
+    getSliceDetails(this.engine, this.config.id)
+        .then((sliceDetails) => this.sliceDetails = sliceDetails);
+  }
+
+  renderTabCanvas() {}
+
+  getTitle() {
+    return this.config.title;
+  }
+
+  viewTab() {
+    if (!exists(this.sliceDetails) || !exists(this.sliceDetails.args) ||
+        this.sliceDetails.args.length == 0) {
+      return m('h2', 'Loading Screenshot');
+    }
+    assertTrue(this.sliceDetails.args[0].key == 'screenshot.jpg_image');
+    return m('.screenshot-panel', m('img', {
+               src: 'data:image/png;base64, ' +
+                   this.sliceDetails.args[0].displayValue,
+             }));
+  }
+}
+
+bottomTabRegistry.register(ScreenshotTab);