Wires up deeplink validation tool backend (#6536)

diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart
index 7816f3f..9d91886 100644
--- a/packages/devtools_app/lib/devtools_app.dart
+++ b/packages/devtools_app/lib/devtools_app.dart
@@ -19,6 +19,8 @@
 export 'src/screens/debugger/program_explorer_model.dart';
 export 'src/screens/debugger/span_parser.dart';
 export 'src/screens/debugger/syntax_highlighter.dart';
+export 'src/screens/deep_link_validation/deep_links_controller.dart';
+export 'src/screens/deep_link_validation/deep_links_screen.dart';
 export 'src/screens/inspector/inspector_controller.dart';
 export 'src/screens/inspector/inspector_screen.dart';
 export 'src/screens/inspector/inspector_tree_controller.dart';
diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart
new file mode 100644
index 0000000..8f1508c
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_link_list_view.dart
@@ -0,0 +1,384 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:math';
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import '../../shared/common_widgets.dart';
+import '../../shared/primitives/utils.dart';
+import '../../shared/table/table.dart';
+import '../../shared/table/table_data.dart';
+import '../../shared/ui/colors.dart';
+import '../../shared/utils.dart';
+import 'deep_links_controller.dart';
+import 'deep_links_model.dart';
+
+enum TableViewType {
+  domainView,
+  pathView,
+  singleUrlView,
+}
+
+/// A view that display all deep links for the app.
+class DeepLinkListView extends StatefulWidget {
+  const DeepLinkListView({super.key});
+
+  @override
+  State<DeepLinkListView> createState() => _DeepLinkListViewState();
+}
+
+class _DeepLinkListViewState extends State<DeepLinkListView>
+    with ProvidedControllerMixin<DeepLinksController, DeepLinkListView> {
+  List<String> get androidVariants =>
+      controller.selectedProject.value!.androidVariants;
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    initController();
+    callWhenControllerReady((_) {
+      int releaseVariantIndex = controller
+          .selectedProject.value!.androidVariants
+          .indexWhere((variant) => variant.toLowerCase().contains('release'));
+      // If not found, default to 0.
+      releaseVariantIndex = max(releaseVariantIndex, 0);
+      controller.selectedVariantIndex.value = releaseVariantIndex;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return DefaultTabController(
+      length: TableViewType.values.length,
+      child: const Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          _DeepLinkListViewTopPanel(),
+          SizedBox(height: denseSpacing),
+          Expanded(child: _DeepLinkListViewMainPanel()),
+        ],
+      ),
+    );
+  }
+}
+
+class _DeepLinkListViewMainPanel extends StatelessWidget {
+  const _DeepLinkListViewMainPanel();
+
+  @override
+  Widget build(BuildContext context) {
+    final controller = Provider.of<DeepLinksController>(context);
+    return ValueListenableBuilder<List<LinkData>?>(
+      valueListenable: controller.linkDatasNotifier,
+      builder: (context, linkDatas, _) {
+        if (linkDatas == null) {
+          return const CenteredCircularProgressIndicator();
+        }
+        return Column(
+          children: [
+            AreaPaneHeader(
+              title: Text(
+                'All deep links',
+                style: Theme.of(context).textTheme.bodyLarge,
+              ),
+              actions: [
+                SizedBox(
+                  width: wideSearchFieldWidth,
+                  child: DevToolsClearableTextField(
+                    labelText: '',
+                    hintText: 'Search a URL, domain or path',
+                    prefixIcon: const Icon(Icons.search),
+                    onChanged: (value) {
+                      controller.searchContent = value;
+                    },
+                  ),
+                ),
+              ],
+            ),
+            const SizedBox(height: denseSpacing),
+            const TabBar(
+              tabs: [
+                Text('Domain view'),
+                Text('Path view'),
+                Text('Single URL view'),
+              ],
+              tabAlignment: TabAlignment.start,
+              isScrollable: true,
+            ),
+            Expanded(
+              child: ValueListenableBuilder<bool>(
+                valueListenable: controller.showSpitScreenNotifier,
+                builder: (context, showSpitScreen, _) => TabBarView(
+                  children: [
+                    _DataTableWithValidationDetails(
+                      tableView: TableViewType.domainView,
+                      linkDatas: controller.getLinkDatasByDomain,
+                      controller: controller,
+                      showSpitScreen: showSpitScreen,
+                    ),
+                    _DataTableWithValidationDetails(
+                      tableView: TableViewType.pathView,
+                      linkDatas: controller.getLinkDatasByPath,
+                      controller: controller,
+                      showSpitScreen: showSpitScreen,
+                    ),
+                    _DataTableWithValidationDetails(
+                      tableView: TableViewType.singleUrlView,
+                      linkDatas: linkDatas,
+                      controller: controller,
+                      showSpitScreen: showSpitScreen,
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}
+
+class _DataTableWithValidationDetails extends StatelessWidget {
+  const _DataTableWithValidationDetails({
+    required this.linkDatas,
+    required this.tableView,
+    required this.controller,
+    required this.showSpitScreen,
+  });
+  final List<LinkData> linkDatas;
+  final TableViewType tableView;
+  final DeepLinksController controller;
+  final bool showSpitScreen;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        Expanded(
+          child: _DataTable(
+            tableView: tableView,
+            linkDatas: linkDatas,
+            controller: controller,
+          ),
+        ),
+        if (showSpitScreen)
+          Expanded(
+            child: ValueListenableBuilder<LinkData?>(
+              valueListenable: controller.selectedLink,
+              builder: (context, selectedLink, _) => _ValidationDetailScreen(
+                tableView: tableView,
+                linkData: selectedLink!,
+                controller: controller,
+              ),
+            ),
+          ),
+      ],
+    );
+  }
+}
+
+class _DataTable extends StatelessWidget {
+  const _DataTable({
+    required this.linkDatas,
+    required this.tableView,
+    required this.controller,
+  });
+  final List<LinkData> linkDatas;
+  final TableViewType tableView;
+  final DeepLinksController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    final ColumnData<LinkData> domain = DomainColumn();
+    final ColumnData<LinkData> path = PathColumn();
+
+    return FlatTable(
+      keyFactory: (node) => ValueKey(node.toString),
+      data: linkDatas,
+      dataKey: 'deep-links',
+      autoScrollContent: true,
+      columns: <ColumnData>[
+        if (tableView != TableViewType.pathView) domain,
+        if (tableView != TableViewType.domainView) path,
+        SchemeColumn(),
+        OSColumn(),
+        if (!controller.showSpitScreen) ...[
+          StatusColumn(),
+          NavigationColumn(),
+        ],
+      ],
+      selectionNotifier: controller.selectedLink,
+      defaultSortColumn: tableView == TableViewType.pathView ? path : domain,
+      defaultSortDirection: SortDirection.ascending,
+      onItemSelected: (item) => controller.showSpitScreenNotifier.value = true,
+    );
+  }
+}
+
+class _ValidationDetailScreen extends StatelessWidget {
+  const _ValidationDetailScreen({
+    required this.linkData,
+    required this.tableView,
+    required this.controller,
+  });
+
+  final LinkData linkData;
+  final TableViewType tableView;
+  final DeepLinksController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: largeSpacing),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              const Text('Selected deep link validation details'),
+              IconButton(
+                onPressed: () =>
+                    controller.showSpitScreenNotifier.value = false,
+                icon: const Icon(Icons.close),
+              ),
+            ],
+          ),
+          Text(
+            'This tool helps you diagnose Universal Links, App Links,'
+            ' and Custom Schemes in your app. Web checks are done for the web association'
+            ' files on your website. App checks are done for the intent filters in'
+            ' the manifest and info.plist file, routing issues, URL format, etc.',
+            style: Theme.of(context).textTheme.bodySmall,
+          ),
+          const Text('Domain check'),
+          Expanded(child: _DomainCheckTable(linkData: linkData)),
+        ],
+      ),
+    );
+  }
+}
+
+class _DomainCheckTable extends StatelessWidget {
+  const _DomainCheckTable({
+    required this.linkData,
+  });
+
+  final LinkData linkData;
+
+  @override
+  Widget build(BuildContext context) {
+    return DataTable(
+      columns: const [
+        DataColumn(label: Text('OS')),
+        DataColumn(label: Text('Issue type')),
+        DataColumn(label: Text('Status')),
+      ],
+      rows: [
+        if (linkData.os.contains(PlatformOS.android))
+          DataRow(
+            cells: [
+              const DataCell(Text('Android')),
+              const DataCell(Text('Digital assets link file')),
+              DataCell(
+                linkData.domainError
+                    ? Text(
+                        'Check failed',
+                        style: TextStyle(
+                          color: Theme.of(context).colorScheme.error,
+                        ),
+                      )
+                    : Text(
+                        'No issues found',
+                        style: TextStyle(
+                          color: Theme.of(context).colorScheme.green,
+                        ),
+                      ),
+              ),
+            ],
+          ),
+        if (linkData.os.contains(PlatformOS.ios))
+          DataRow(
+            cells: [
+              const DataCell(Text('iOS')),
+              const DataCell(Text('Apple-App-Site-Association file')),
+              DataCell(
+                Text(
+                  'No issues found',
+                  style: TextStyle(color: Theme.of(context).colorScheme.green),
+                ),
+              ),
+            ],
+          ),
+      ],
+    );
+  }
+}
+
+class _DeepLinkListViewTopPanel extends StatelessWidget {
+  const _DeepLinkListViewTopPanel();
+
+  @override
+  Widget build(BuildContext context) {
+    final controller = Provider.of<DeepLinksController>(context);
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.end,
+      children: [
+        Padding(
+          padding: const EdgeInsets.all(defaultSpacing),
+          child: ValueListenableBuilder(
+            valueListenable: controller.selectedVariantIndex,
+            builder: (_, value, __) {
+              return _AndroidVariantDropdown(
+                androidVariants:
+                    controller.selectedProject.value!.androidVariants,
+                index: value,
+                onVariantIndexSelected: (index) {
+                  controller.selectedVariantIndex.value = index;
+                },
+              );
+            },
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class _AndroidVariantDropdown extends StatelessWidget {
+  const _AndroidVariantDropdown({
+    required this.androidVariants,
+    required this.index,
+    required this.onVariantIndexSelected,
+  });
+
+  final List<String> androidVariants;
+  final int index;
+  final ValueChanged<int> onVariantIndexSelected;
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: [
+        const Text('Android Variant:'),
+        RoundedDropDownButton<int>(
+          value: index,
+          items: [
+            for (int i = 0; i < androidVariants.length; i++)
+              DropdownMenuItem<int>(
+                value: i,
+                child: Text(androidVariants[i]),
+              ),
+          ],
+          onChanged: (int? index) {
+            onVariantIndexSelected(index!);
+          },
+        ),
+      ],
+    );
+  }
+}
diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart
index 961e82b..f742733 100644
--- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart
+++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_controller.dart
@@ -2,17 +2,26 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
+
+import 'package:devtools_shared/devtools_deeplink.dart';
 import 'package:flutter/foundation.dart';
 
+import '../../shared/config_specific/server/server.dart' as server;
 import 'deep_links_model.dart';
-import 'fake_data.dart';
+
+typedef _DomainAndPath = ({String domain, String path});
 
 class DeepLinksController {
+  DeepLinksController() {
+    selectedVariantIndex.addListener(_handleSelectedVariantIndexChanged);
+  }
+
   bool get showSpitScreen => showSpitScreenNotifier.value;
 
   List<LinkData> get getLinkDatasByPath {
     final linkDatasByPath = <String, LinkData>{};
-    for (var linkData in linkDatasNotifier.value) {
+    for (var linkData in linkDatasNotifier.value!) {
       linkDatasByPath[linkData.path] = linkData;
     }
     return linkDatasByPath.values.toList();
@@ -20,14 +29,61 @@
 
   List<LinkData> get getLinkDatasByDomain {
     final linkDatasByDomain = <String, LinkData>{};
-    for (var linkData in linkDatasNotifier.value) {
+    for (var linkData in linkDatasNotifier.value!) {
       linkDatasByDomain[linkData.domain] = linkData;
     }
     return linkDatasByDomain.values.toList();
   }
 
+  final Map<int, AppLinkSettings> _androidAppLinks = <int, AppLinkSettings>{};
+
+  late final selectedVariantIndex = ValueNotifier<int>(0);
+  void _handleSelectedVariantIndexChanged() {
+    linkDatasNotifier.value = null;
+    unawaited(_loadAndroidAppLinks());
+  }
+
+  Future<void> _loadAndroidAppLinks() async {
+    if (!_androidAppLinks.containsKey(selectedVariantIndex.value)) {
+      final variant =
+          selectedProject.value!.androidVariants[selectedVariantIndex.value];
+      final result = await server.requestAndroidAppLinkSettings(
+        selectedProject.value!.path,
+        buildVariant: variant,
+      );
+      _androidAppLinks[selectedVariantIndex.value] = result;
+    }
+    _updateLinks();
+  }
+
+  List<LinkData> get _allLinkDatas {
+    final appLinks = _androidAppLinks[selectedVariantIndex.value]?.deeplinks;
+    if (appLinks == null) {
+      return const <LinkData>[];
+    }
+    final domainPathToScheme = <_DomainAndPath, Set<String>>{};
+    for (final appLink in appLinks) {
+      final schemes = domainPathToScheme.putIfAbsent(
+        (domain: appLink.host, path: appLink.path),
+        () => <String>{},
+      );
+      schemes.add(appLink.scheme);
+    }
+    return domainPathToScheme.entries
+        .map(
+          (entry) => LinkData(
+            domain: entry.key.domain,
+            path: entry.key.path,
+            os: [PlatformOS.android],
+            scheme: entry.value.toList(),
+          ),
+        )
+        .toList();
+  }
+
+  final selectedProject = ValueNotifier<FlutterProject?>(null);
   final selectedLink = ValueNotifier<LinkData?>(null);
-  final linkDatasNotifier = ValueNotifier<List<LinkData>>(allLinkDatas);
+  final linkDatasNotifier = ValueNotifier<List<LinkData>?>(null);
   final showSpitScreenNotifier = ValueNotifier<bool>(false);
 
   final _searchContentNotifier = ValueNotifier<String>('');
@@ -35,7 +91,7 @@
   void _updateLinks() {
     final searchContent = _searchContentNotifier.value;
     final List<LinkData> linkDatas = searchContent.isNotEmpty
-        ? allLinkDatas
+        ? _allLinkDatas
             .where(
               (linkData) => linkData.matchesSearchToken(
                 RegExp(
@@ -45,8 +101,7 @@
               ),
             )
             .toList()
-        : allLinkDatas;
-
+        : _allLinkDatas;
     linkDatasNotifier.value = linkDatas;
   }
 
diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart
index 8c47c6a..5ef2d74 100644
--- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart
+++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_model.dart
@@ -13,6 +13,11 @@
 
 const kDeeplinkTableCellDefaultWidth = 200.0;
 
+enum PlatformOS {
+  android,
+  ios,
+}
+
 /// Contains all data relevant to a deep link.
 class LinkData with SearchableDataMixin {
   LinkData({
@@ -26,7 +31,7 @@
 
   final String path;
   final String domain;
-  final List<String> os;
+  final List<PlatformOS> os;
   final List<String> scheme;
   final bool domainError;
   final bool pathError;
@@ -214,3 +219,12 @@
     return const Icon(Icons.arrow_forward);
   }
 }
+
+class FlutterProject {
+  FlutterProject({
+    required this.path,
+    required this.androidVariants,
+  });
+  final String path;
+  final List<String> androidVariants;
+}
diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart
index 0dc00c2..b723885 100644
--- a/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart
+++ b/packages/devtools_app/lib/src/screens/deep_link_validation/deep_links_screen.dart
@@ -2,19 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:devtools_app_shared/ui.dart';
-import 'package:devtools_app_shared/utils.dart';
 import 'package:flutter/material.dart';
 
-import '../../shared/common_widgets.dart';
-import '../../shared/primitives/utils.dart';
 import '../../shared/screen.dart';
-import '../../shared/table/table.dart';
-import '../../shared/table/table_data.dart';
-import '../../shared/ui/colors.dart';
 import '../../shared/utils.dart';
+import 'deep_link_list_view.dart';
 import 'deep_links_controller.dart';
 import 'deep_links_model.dart';
+import 'select_project_view.dart';
 
 enum TableViewType {
   domainView,
@@ -45,10 +40,7 @@
 }
 
 class _DeepLinkPageState extends State<DeepLinkPage>
-    with
-        AutoDisposeMixin,
-        SingleTickerProviderStateMixin,
-        ProvidedControllerMixin<DeepLinksController, DeepLinkPage> {
+    with ProvidedControllerMixin<DeepLinksController, DeepLinkPage> {
   @override
   void didChangeDependencies() {
     super.didChangeDependencies();
@@ -57,249 +49,13 @@
 
   @override
   Widget build(BuildContext context) {
-    return DefaultTabController(
-      length: TableViewType.values.length,
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          AreaPaneHeader(
-            title: Text(
-              'All deep links',
-              style: Theme.of(context).textTheme.bodyLarge,
-            ),
-            actions: [
-              SizedBox(
-                width: wideSearchFieldWidth,
-                child: DevToolsClearableTextField(
-                  labelText: '',
-                  hintText: 'Search a URL, domain or path',
-                  prefixIcon: const Icon(Icons.search),
-                  onChanged: (value) {
-                    controller.searchContent = value;
-                  },
-                ),
-              ),
-            ],
-          ),
-          const SizedBox(height: denseSpacing),
-          const TabBar(
-            tabs: [
-              Text('Domain view'),
-              Text('Path view'),
-              Text('Single URL view'),
-            ],
-            tabAlignment: TabAlignment.start,
-            isScrollable: true,
-          ),
-          Expanded(
-            child: ValueListenableBuilder<List<LinkData>>(
-              valueListenable: controller.linkDatasNotifier,
-              builder: (context, linkDatas, _) => ValueListenableBuilder<bool>(
-                valueListenable: controller.showSpitScreenNotifier,
-                builder: (context, showSpitScreen, _) => TabBarView(
-                  children: [
-                    _DataTableWithValidationDetails(
-                      tableView: TableViewType.domainView,
-                      linkDatas: controller.getLinkDatasByDomain,
-                      controller: controller,
-                      showSpitScreen: showSpitScreen,
-                    ),
-                    _DataTableWithValidationDetails(
-                      tableView: TableViewType.pathView,
-                      linkDatas: controller.getLinkDatasByPath,
-                      controller: controller,
-                      showSpitScreen: showSpitScreen,
-                    ),
-                    _DataTableWithValidationDetails(
-                      tableView: TableViewType.singleUrlView,
-                      linkDatas: linkDatas,
-                      controller: controller,
-                      showSpitScreen: showSpitScreen,
-                    ),
-                  ],
-                ),
-              ),
-            ),
-          ),
-        ],
-      ),
-    );
-  }
-}
-
-class _DataTableWithValidationDetails extends StatelessWidget {
-  const _DataTableWithValidationDetails({
-    required this.linkDatas,
-    required this.tableView,
-    required this.controller,
-    required this.showSpitScreen,
-  });
-  final List<LinkData> linkDatas;
-  final TableViewType tableView;
-  final DeepLinksController controller;
-  final bool showSpitScreen;
-
-  @override
-  Widget build(BuildContext context) {
-    return Row(
-      children: [
-        Expanded(
-          child: _DataTable(
-            tableView: tableView,
-            linkDatas: linkDatas,
-            controller: controller,
-          ),
-        ),
-        if (showSpitScreen)
-          Expanded(
-            child: ValueListenableBuilder<LinkData?>(
-              valueListenable: controller.selectedLink,
-              builder: (context, selectedLink, _) => _ValidationDetailScreen(
-                tableView: tableView,
-                linkData: selectedLink!,
-                controller: controller,
-              ),
-            ),
-          ),
-      ],
-    );
-  }
-}
-
-class _DataTable extends StatelessWidget {
-  const _DataTable({
-    required this.linkDatas,
-    required this.tableView,
-    required this.controller,
-  });
-  final List<LinkData> linkDatas;
-  final TableViewType tableView;
-  final DeepLinksController controller;
-
-  @override
-  Widget build(BuildContext context) {
-    final ColumnData<LinkData> domain = DomainColumn();
-    final ColumnData<LinkData> path = PathColumn();
-
-    return FlatTable(
-      keyFactory: (node) => ValueKey(node.toString),
-      data: linkDatas,
-      dataKey: 'deep-links',
-      autoScrollContent: true,
-      columns: <ColumnData>[
-        if (tableView != TableViewType.pathView) domain,
-        if (tableView != TableViewType.domainView) path,
-        SchemeColumn(),
-        OSColumn(),
-        if (!controller.showSpitScreen) ...[
-          StatusColumn(),
-          NavigationColumn(),
-        ],
-      ],
-      selectionNotifier: controller.selectedLink,
-      defaultSortColumn: tableView == TableViewType.pathView ? path : domain,
-      defaultSortDirection: SortDirection.ascending,
-      onItemSelected: (item) => controller.showSpitScreenNotifier.value = true,
-    );
-  }
-}
-
-class _ValidationDetailScreen extends StatelessWidget {
-  const _ValidationDetailScreen({
-    required this.linkData,
-    required this.tableView,
-    required this.controller,
-  });
-
-  final LinkData linkData;
-  final TableViewType tableView;
-  final DeepLinksController controller;
-
-  @override
-  Widget build(BuildContext context) {
-    return Padding(
-      padding: const EdgeInsets.symmetric(horizontal: largeSpacing),
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          Row(
-            mainAxisAlignment: MainAxisAlignment.spaceBetween,
-            children: [
-              const Text('Selected Deep link validation details'),
-              IconButton(
-                onPressed: () =>
-                    controller.showSpitScreenNotifier.value = false,
-                icon: const Icon(Icons.close),
-              ),
-            ],
-          ),
-          Text(
-            'This tool assistants helps you diagnose Universal Links, App Links,'
-            ' and Custom Schemes in your app. Web check are done for the web association'
-            ' file on your website. App checks are done for the intent filters in'
-            ' the manifest and info.plist file, routing issues, URL format, etc.',
-            style: Theme.of(context).textTheme.bodySmall,
-          ),
-          const Text('Domain check'),
-          Expanded(child: _DomainCheckTable(linkData: linkData)),
-        ],
-      ),
-    );
-  }
-}
-
-class _DomainCheckTable extends StatelessWidget {
-  const _DomainCheckTable({
-    required this.linkData,
-  });
-
-  final LinkData linkData;
-
-  @override
-  Widget build(BuildContext context) {
-    return DataTable(
-      columns: const [
-        DataColumn(label: Text('OS')),
-        DataColumn(label: Text('Issue type')),
-        DataColumn(label: Text('Status')),
-      ],
-      rows: [
-        if (linkData.os.contains('Android'))
-          DataRow(
-            cells: [
-              const DataCell(Text('Android')),
-              const DataCell(Text('Digital assets link file')),
-              DataCell(
-                linkData.domainError
-                    ? Text(
-                        'Check failed',
-                        style: TextStyle(
-                          color: Theme.of(context).colorScheme.error,
-                        ),
-                      )
-                    : Text(
-                        'No issues found',
-                        style: TextStyle(
-                          color: Theme.of(context).colorScheme.green,
-                        ),
-                      ),
-              ),
-            ],
-          ),
-        if (linkData.os.contains('iOS'))
-          DataRow(
-            cells: [
-              const DataCell(Text('iOS')),
-              const DataCell(Text('Apple-App-Site-Association file')),
-              DataCell(
-                Text(
-                  'No issues found',
-                  style: TextStyle(color: Theme.of(context).colorScheme.green),
-                ),
-              ),
-            ],
-          ),
-      ],
+    return ValueListenableBuilder(
+      valueListenable: controller.selectedProject,
+      builder: (_, FlutterProject? project, __) {
+        return project == null
+            ? const SelectProjectView()
+            : const DeepLinkListView();
+      },
     );
   }
 }
diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart
index 2e16a18..5595652 100644
--- a/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart
+++ b/packages/devtools_app/lib/src/screens/deep_link_validation/fake_data.dart
@@ -17,7 +17,7 @@
 final allLinkDatas = <LinkData>[
   for (var path in paths)
     LinkData(
-      os: ['Android', 'iOS'],
+      os: [PlatformOS.android, PlatformOS.ios],
       domain: 'm.shopping.com',
       path: path,
       domainError: true,
@@ -25,14 +25,14 @@
     ),
   for (var path in paths)
     LinkData(
-      os: ['iOS'],
+      os: [PlatformOS.ios],
       domain: 'm.french.shopping.com',
       path: path,
       pathError: path.contains('shoe'),
     ),
   for (var path in paths)
     LinkData(
-      os: ['Android'],
+      os: [PlatformOS.android],
       domain: 'm.chinese.shopping.com',
       path: path,
       pathError: path.contains('shoe'),
diff --git a/packages/devtools_app/lib/src/screens/deep_link_validation/select_project_view.dart b/packages/devtools_app/lib/src/screens/deep_link_validation/select_project_view.dart
new file mode 100644
index 0000000..9d05e20
--- /dev/null
+++ b/packages/devtools_app/lib/src/screens/deep_link_validation/select_project_view.dart
@@ -0,0 +1,97 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/material.dart';
+
+import '../../shared/common_widgets.dart';
+import '../../shared/config_specific/server/server.dart' as server;
+import '../../shared/directory_picker.dart';
+import '../../shared/utils.dart';
+import 'deep_links_controller.dart';
+import 'deep_links_model.dart';
+
+/// A view for selecting a Flutter project.
+class SelectProjectView extends StatefulWidget {
+  const SelectProjectView({super.key});
+
+  @override
+  State<SelectProjectView> createState() => _SelectProjectViewState();
+}
+
+class _SelectProjectViewState extends State<SelectProjectView>
+    with ProvidedControllerMixin<DeepLinksController, SelectProjectView> {
+  static const _kMessageSize = 24.0;
+  bool _retrievingFlutterProject = false;
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    if (!initController()) return;
+  }
+
+  void _handleDirectoryPicked(String directory) async {
+    setState(() {
+      _retrievingFlutterProject = true;
+    });
+    final List<String> androidVariants =
+        await server.requestAndroidBuildVariants(directory);
+    if (!context.mounted) {
+      return;
+    }
+    if (androidVariants.isEmpty) {
+      await showDialog(
+        context: context,
+        builder: (_) {
+          return const AlertDialog(
+            title: Text('You selected a non Flutter project'),
+            content: Text(
+              'Seems you selected a non Flutter project. If it is not intended, please reselect a Flutter project.',
+            ),
+            actions: [
+              DialogCloseButton(),
+            ],
+          );
+        },
+      );
+    } else {
+      controller.selectedProject.value =
+          FlutterProject(path: directory, androidVariants: androidVariants);
+    }
+    setState(() {
+      _retrievingFlutterProject = false;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    Widget? child;
+    if (_retrievingFlutterProject) {
+      child = const CenteredCircularProgressIndicator(size: _kMessageSize);
+    } else {
+      child = Text(
+        'Pick a flutter project from your local file to check all deep links status',
+        textAlign: TextAlign.center,
+        style: TextStyle(
+          color: Theme.of(context).textTheme.displayLarge!.color,
+        ),
+      );
+    }
+    return Center(
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Padding(
+            padding: const EdgeInsets.all(defaultSpacing),
+            child: child,
+          ),
+          DirectoryPicker(
+            onDirectoryPicked: _handleDirectoryPicked,
+            enabled: !_retrievingFlutterProject,
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart
index 4546930..82040cc 100644
--- a/packages/devtools_app/lib/src/shared/analytics/constants.dart
+++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart
@@ -32,6 +32,7 @@
 final vmTools = ScreenMetaData.vmTools.id;
 const console = 'console';
 final simple = ScreenMetaData.simple.id;
+final deeplink = ScreenMetaData.deepLinks.id;
 
 // GA events not associated with a any screen e.g., hotReload, hotRestart, etc
 const devToolsMain = 'main';
diff --git a/packages/devtools_app/lib/src/shared/directory_picker.dart b/packages/devtools_app/lib/src/shared/directory_picker.dart
new file mode 100644
index 0000000..1f84aa1
--- /dev/null
+++ b/packages/devtools_app/lib/src/shared/directory_picker.dart
@@ -0,0 +1,67 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:devtools_app_shared/ui.dart';
+import 'package:flutter/material.dart';
+
+class DirectoryPicker extends StatefulWidget {
+  const DirectoryPicker({
+    required this.onDirectoryPicked,
+    this.enabled = true,
+    super.key,
+  });
+
+  final bool enabled;
+
+  final ValueChanged<String> onDirectoryPicked;
+
+  @override
+  State<DirectoryPicker> createState() => _DirectoryPickerState();
+}
+
+class _DirectoryPickerState extends State<DirectoryPicker> {
+  final TextEditingController controller = TextEditingController();
+
+  @override
+  void dispose() {
+    controller.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final rowHeight = defaultButtonHeight;
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: [
+        const Spacer(),
+        Flexible(
+          flex: 4,
+          fit: FlexFit.tight,
+          child: RoundedOutlinedBorder(
+            child: Container(
+              height: rowHeight,
+              padding: const EdgeInsets.symmetric(horizontal: defaultSpacing),
+              child: TextField(
+                controller: controller,
+                enabled: widget.enabled,
+                onSubmitted: (String path) {
+                  widget.onDirectoryPicked(path.trim());
+                },
+                decoration: const InputDecoration(
+                  hintText: 'Enter path to a Flutter project here',
+                ),
+                style: TextStyle(
+                  color: Theme.of(context).textTheme.displayLarge!.color,
+                ),
+                textAlign: TextAlign.left,
+              ),
+            ),
+          ),
+        ),
+        const Spacer(),
+      ],
+    );
+  }
+}
diff --git a/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart
new file mode 100644
index 0000000..52c207d
--- /dev/null
+++ b/packages/devtools_app/test/deep_link_vlidation/deep_links_screen_test.dart
@@ -0,0 +1,100 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:devtools_app/devtools_app.dart';
+import 'package:devtools_app/src/screens/deep_link_validation/deep_link_list_view.dart';
+import 'package:devtools_app/src/screens/deep_link_validation/deep_links_model.dart';
+import 'package:devtools_app/src/shared/directory_picker.dart';
+import 'package:devtools_app_shared/ui.dart';
+import 'package:devtools_app_shared/utils.dart';
+import 'package:devtools_test/devtools_test.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  setUp(() {
+    setGlobal(ServiceConnectionManager, FakeServiceConnectionManager());
+    setGlobal(
+      DevToolsEnvironmentParameters,
+      ExternalDevToolsEnvironmentParameters(),
+    );
+    setGlobal(PreferencesController, PreferencesController());
+    setGlobal(IdeTheme, IdeTheme());
+    setGlobal(NotificationService, NotificationService());
+  });
+
+  late DeepLinksScreen screen;
+  late DeepLinksController deepLinksController;
+
+  const windowSize = Size(2560.0, 1338.0);
+
+  Future<void> pumpDeepLinkScreen(
+    WidgetTester tester, {
+    required DeepLinksController controller,
+  }) async {
+    await tester.pumpWidget(
+      wrapWithControllers(
+        const DeepLinkPage(),
+        deepLink: controller,
+      ),
+    );
+    deferredLoadingSupportEnabled = true;
+    await tester.pumpAndSettle(const Duration(seconds: 1));
+    expect(find.byType(DeepLinkPage), findsOneWidget);
+  }
+
+  group('DeepLinkScreen', () {
+    setUp(() {
+      screen = DeepLinksScreen();
+      deepLinksController = DeepLinksController();
+    });
+
+    testWidgets('builds its tab', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        wrapWithControllers(
+          Builder(builder: screen.buildTab),
+          deepLink: deepLinksController,
+        ),
+      );
+      expect(find.text('Deep Links'), findsOneWidget);
+    });
+
+    testWidgetsWithWindowSize(
+      'builds initial content',
+      windowSize,
+      (WidgetTester tester) async {
+        await pumpDeepLinkScreen(
+          tester,
+          controller: deepLinksController,
+        );
+
+        expect(find.byType(DeepLinkPage), findsOneWidget);
+        expect(find.byType(DirectoryPicker), findsOneWidget);
+      },
+    );
+
+    testWidgetsWithWindowSize(
+      'builds deeplink list page',
+      windowSize,
+      (WidgetTester tester) async {
+        deepLinksController.selectedProject.value =
+            FlutterProject(path: '/abc', androidVariants: ['debug', 'release']);
+        deepLinksController.linkDatasNotifier.value = [
+          LinkData(
+            domain: 'www.google.com',
+            path: '/',
+            os: [PlatformOS.android],
+          ),
+        ];
+        await pumpDeepLinkScreen(
+          tester,
+          controller: deepLinksController,
+        );
+
+        expect(find.byType(DeepLinkPage), findsOneWidget);
+        expect(find.byType(DeepLinkListView), findsOneWidget);
+      },
+    );
+  });
+}
diff --git a/packages/devtools_shared/lib/src/deeplink/app_link_settings.dart b/packages/devtools_shared/lib/src/deeplink/app_link_settings.dart
index 73d3e40..9220df6 100644
--- a/packages/devtools_shared/lib/src/deeplink/app_link_settings.dart
+++ b/packages/devtools_shared/lib/src/deeplink/app_link_settings.dart
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// This file handles json object.
+// ignore_for_file: avoid-dynamic
+
 import 'dart:convert';
 
 /// The app link related settings of a Android build of a Flutter project.
@@ -12,8 +15,9 @@
     final jsonObject = jsonDecode(json);
     return AppLinkSettings._(
       jsonObject[_kApplicationIdKey] as String,
-      jsonObject[_kDeeplinksKey]
-          .map<AndroidDeeplink>(AndroidDeeplink._fromJsonObject),
+      (jsonObject[_kDeeplinksKey] as List<dynamic>)
+          .map<AndroidDeeplink>(AndroidDeeplink._fromJsonObject)
+          .toList(),
     );
   }
 
@@ -39,11 +43,11 @@
 class AndroidDeeplink {
   AndroidDeeplink._(this.scheme, this.host, this.path);
 
-  factory AndroidDeeplink._fromJsonObject(Map<String, Object?> object) {
+  factory AndroidDeeplink._fromJsonObject(dynamic json) {
     return AndroidDeeplink._(
-      object[_kSchemeKey] as String,
-      object[_kHostKey] as String,
-      object[_kPathKey] as String,
+      json[_kSchemeKey] as String,
+      json[_kHostKey] as String,
+      json[_kPathKey] as String,
     );
   }
 
diff --git a/packages/devtools_shared/lib/src/deeplink/deeplink_manager.dart b/packages/devtools_shared/lib/src/deeplink/deeplink_manager.dart
index aed1e39..10c6183 100644
--- a/packages/devtools_shared/lib/src/deeplink/deeplink_manager.dart
+++ b/packages/devtools_shared/lib/src/deeplink/deeplink_manager.dart
@@ -190,7 +190,6 @@
 }
 
 class _FlutterProcessError extends Error {
-  /// Constructs a [GoError]
   _FlutterProcessError(this.message);
 
   /// The error message.
diff --git a/packages/devtools_shared/lib/src/server/server_api.dart b/packages/devtools_shared/lib/src/server/server_api.dart
index 18fb7b3..528307c 100644
--- a/packages/devtools_shared/lib/src/server/server_api.dart
+++ b/packages/devtools_shared/lib/src/server/server_api.dart
@@ -492,7 +492,7 @@
   ) {
     final error = result[DeeplinkManager.kErrorField] as String?;
     if (error != null) {
-      api.serverError(error);
+      return api.serverError(error);
     }
     return api.getCompleted(
       result[DeeplinkManager.kOutputJsonField]! as String,
diff --git a/packages/devtools_test/lib/src/utils.dart b/packages/devtools_test/lib/src/utils.dart
index 775f875..6f590e8 100644
--- a/packages/devtools_test/lib/src/utils.dart
+++ b/packages/devtools_test/lib/src/utils.dart
@@ -17,6 +17,7 @@
 final screenIds = <String>[
   AppSizeScreen.id,
   DebuggerScreen.id,
+  DeepLinksScreen.id,
   InspectorScreen.id,
   LoggingScreen.id,
   MemoryScreen.id,
diff --git a/packages/devtools_test/lib/src/wrappers.dart b/packages/devtools_test/lib/src/wrappers.dart
index 4814998..2e00c23 100644
--- a/packages/devtools_test/lib/src/wrappers.dart
+++ b/packages/devtools_test/lib/src/wrappers.dart
@@ -86,6 +86,7 @@
   PerformanceController? performance,
   ProfilerScreenController? profiler,
   DebuggerController? debugger,
+  DeepLinksController? deepLink,
   NetworkController? network,
   AppSizeController? appSize,
   AnalyticsController? analytics,
@@ -104,6 +105,7 @@
       Provider<ProfilerScreenController>.value(value: profiler),
     if (network != null) Provider<NetworkController>.value(value: network),
     if (debugger != null) Provider<DebuggerController>.value(value: debugger),
+    if (deepLink != null) Provider<DeepLinksController>.value(value: deepLink),
     if (appSize != null) Provider<AppSizeController>.value(value: appSize),
     if (analytics != null)
       Provider<AnalyticsController>.value(value: analytics),