[MV3 Debug Extension] Update Chrome APIs to be backwards compatible with MV2 (#1951)

diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart
index cc31a42..36410a5 100644
--- a/dwds/debug_extension_mv3/web/background.dart
+++ b/dwds/debug_extension_mv3/web/background.dart
@@ -6,7 +6,6 @@
 library background;
 
 import 'dart:async';
-import 'dart:html';
 
 import 'package:dwds/data/debug_info.dart';
 import 'package:js/js.dart';
@@ -39,7 +38,7 @@
     _updateIcon(info.tabId);
   }));
   chrome.windows.onFocusChanged.addListener(allowInterop((_) async {
-    final currentTab = await _getTab();
+    final currentTab = await activeTab;
     if (currentTab?.id != null) {
       _updateIcon(currentTab!.id);
     }
@@ -48,7 +47,7 @@
       .addListener(allowInterop(_detectNavigationAwayFromDartApp));
 
   // Detect clicks on the Dart Debug Extension icon.
-  chrome.action.onClicked.addListener(allowInterop(
+  onExtensionIconClicked(allowInterop(
     (Tab tab) => attachDebugger(
       tab.id,
       trigger: Trigger.extensionIcon,
@@ -94,7 +93,7 @@
         await setStorageObject<DebugInfo>(
             type: StorageObject.debugInfo, value: debugInfo, tabId: dartTab.id);
         // Update the icon to show that a Dart app has been detected:
-        final currentTab = await _getTab();
+        final currentTab = await activeTab;
         if (currentTab?.id == dartTab.id) {
           _setDebuggableIcon();
         }
@@ -139,15 +138,14 @@
 }
 
 void _setDebuggableIcon() {
-  chrome.action
-      .setIcon(IconInfo(path: 'static_assets/dart.png'), /*callback*/ null);
+  setExtensionIcon(IconInfo(path: 'static_assets/dart.png'));
 }
 
 void _setDefaultIcon() {
   final iconPath = isDevMode()
       ? 'static_assets/dart_dev.png'
       : 'static_assets/dart_grey.png';
-  chrome.action.setIcon(IconInfo(path: iconPath), /*callback*/ null);
+  setExtensionIcon(IconInfo(path: iconPath));
 }
 
 Future<DebugInfo?> _fetchDebugInfo(int tabId) {
@@ -156,9 +154,3 @@
     tabId: tabId,
   );
 }
-
-Future<Tab?> _getTab() async {
-  final query = QueryInfo(active: true, currentWindow: true);
-  final tabs = List<Tab>.from(await promiseToFuture(chrome.tabs.query(query)));
-  return tabs.isNotEmpty ? tabs.first : null;
-}
diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart
index 77fc960..5039db5 100644
--- a/dwds/debug_extension_mv3/web/chrome_api.dart
+++ b/dwds/debug_extension_mv3/web/chrome_api.dart
@@ -292,13 +292,14 @@
 @JS()
 @anonymous
 class Tabs {
-  external Object query(QueryInfo queryInfo);
+  external dynamic query(
+      QueryInfo queryInfo, void Function(List<Tab>) callback);
 
-  external Object create(TabInfo tabInfo);
+  external dynamic create(TabInfo tabInfo, void Function(Tab) callback);
 
-  external Object get(int tabId);
+  external dynamic get(int tabId, void Function(Tab?) callback);
 
-  external Object remove(int tabId);
+  external dynamic remove(int tabId, void Function()? callback);
 
   external OnActivatedHandler get onActivated;
 
@@ -378,7 +379,7 @@
 @JS()
 @anonymous
 class Windows {
-  external Object create(WindowInfo? createData);
+  external dynamic create(WindowInfo? createData, Function(WindowObj) callback);
 
   external OnFocusChangedHandler get onFocusChanged;
 }
diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart
index 54de63d..378f18c 100644
--- a/dwds/debug_extension_mv3/web/debug_session.dart
+++ b/dwds/debug_extension_mv3/web/debug_session.dart
@@ -267,7 +267,7 @@
   );
   _debugSessions.add(debugSession);
   // Create a connection with the lifeline port to keep the debug session alive:
-  maybeCreateLifelinePort(dartAppTabId);
+  await maybeCreateLifelinePort(dartAppTabId);
   // Send a DevtoolsRequest to the event stream:
   final tabUrl = await _getTabUrl(dartAppTabId);
   debugSession.sendEvent(DevToolsRequest((b) => b
@@ -401,7 +401,7 @@
   final devToolsTab = await getTab(devToolsTabId);
   if (devToolsTab != null) {
     debugLog('Closing DevTools tab...');
-    chrome.tabs.remove(devToolsTabId);
+    await removeTab(devToolsTabId);
   }
 }
 
diff --git a/dwds/debug_extension_mv3/web/lifeline_ports.dart b/dwds/debug_extension_mv3/web/lifeline_ports.dart
index 0f4ee73..b082002 100644
--- a/dwds/debug_extension_mv3/web/lifeline_ports.dart
+++ b/dwds/debug_extension_mv3/web/lifeline_ports.dart
@@ -14,11 +14,12 @@
 import 'chrome_api.dart';
 import 'debug_session.dart';
 import 'logger.dart';
+import 'utils.dart';
 
 Port? _lifelinePort;
 int? _lifelineTab;
 
-void maybeCreateLifelinePort(int tabId) {
+Future<void> maybeCreateLifelinePort(int tabId) async {
   // Don't create a lifeline port if we already have one (meaning another Dart
   // app is currently being debugged):
   if (_lifelinePort != null) {
@@ -31,13 +32,7 @@
   // will connect to the port:
   debugLog('Creating lifeline port.');
   _lifelineTab = tabId;
-  chrome.scripting.executeScript(
-    InjectDetails(
-      target: Target(tabId: tabId),
-      files: ['lifeline_connection.dart.js'],
-    ),
-    /*callback*/ null,
-  );
+  await injectScript('lifeline_connection.dart.js', tabId: tabId);
 }
 
 void maybeRemoveLifelinePort(int removedTabId) {
diff --git a/dwds/debug_extension_mv3/web/utils.dart b/dwds/debug_extension_mv3/web/utils.dart
index 5504e7f..d122140 100644
--- a/dwds/debug_extension_mv3/web/utils.dart
+++ b/dwds/debug_extension_mv3/web/utils.dart
@@ -12,29 +12,80 @@
 
 import 'chrome_api.dart';
 
-Future<Tab> createTab(String url, {bool inNewWindow = false}) async {
+Future<Tab> createTab(String url, {bool inNewWindow = false}) {
+  final completer = Completer<Tab>();
   if (inNewWindow) {
-    final windowPromise = chrome.windows.create(
+    chrome.windows.create(
       WindowInfo(focused: true, url: url),
+      allowInterop(
+        (WindowObj windowObj) {
+          completer.complete(windowObj.tabs.first);
+        },
+      ),
     );
-    final windowObj = await promiseToFuture<WindowObj>(windowPromise);
-    return windowObj.tabs.first;
+  } else {
+    chrome.tabs.create(
+      TabInfo(
+        active: true,
+        url: url,
+      ),
+      allowInterop(
+        (Tab tab) {
+          completer.complete(tab);
+        },
+      ),
+    );
   }
-  final tabPromise = chrome.tabs.create(TabInfo(
-    active: true,
-    url: url,
-  ));
-  return promiseToFuture<Tab>(tabPromise);
+  return completer.future;
 }
 
 Future<Tab?> getTab(int tabId) {
-  return promiseToFuture<Tab?>(chrome.tabs.get(tabId));
+  final completer = Completer<Tab?>();
+  chrome.tabs.get(tabId, allowInterop((tab) {
+    completer.complete(tab);
+  }));
+  return completer.future;
 }
 
-Future<Tab?> getActiveTab() async {
+Future<Tab?> get activeTab async {
+  final completer = Completer<Tab?>();
   final query = QueryInfo(active: true, currentWindow: true);
-  final tabs = List<Tab>.from(await promiseToFuture(chrome.tabs.query(query)));
-  return tabs.isNotEmpty ? tabs.first : null;
+  chrome.tabs.query(query, allowInterop((List tabs) {
+    if (tabs.isNotEmpty) {
+      completer.complete(tabs.first as Tab);
+    } else {
+      completer.complete(null);
+    }
+  }));
+  return completer.future;
+}
+
+Future<bool> removeTab(int tabId) {
+  final completer = Completer<bool>();
+  chrome.tabs.remove(tabId, allowInterop(() {
+    completer.complete(true);
+  }));
+  return completer.future;
+}
+
+Future<bool> injectScript(String scriptName, {required int tabId}) {
+  final completer = Completer<bool>();
+  chrome.scripting.executeScript(
+      InjectDetails(
+        target: Target(tabId: tabId),
+        files: [scriptName],
+      ), allowInterop(() {
+    completer.complete(true);
+  }));
+  return completer.future;
+}
+
+void onExtensionIconClicked(void Function(Tab) callback) {
+  chrome.action.onClicked.addListener(callback);
+}
+
+void setExtensionIcon(IconInfo info) {
+  chrome.action.setIcon(info, /*callback*/ null);
 }
 
 bool? _isDevMode;