[flutter_releases] Flutter Stable 2.2.2 Framework Cherrypicks (#84364)

* [flutter_tools] throw a tool exit if pub cannot be run (#83293)
* Re-add the removed MediaQuery.removePadding of PopupMenuButton (#82986)
* import pkg:intl when DateFormat or NumberFormat is used (#83122)

Co-authored-by: Jonah Williams <jonahwilliams@google.com>
Co-authored-by: xubaolin <xubaolin@oppo.com>
Co-authored-by: Michael Goderbauer <goderbauer@google.com>
diff --git a/bin/internal/engine.version b/bin/internal/engine.version
index 1414ce1..8e7ff4c 100644
--- a/bin/internal/engine.version
+++ b/bin/internal/engine.version
@@ -1 +1 @@
-0fdb562ac8068ce3dda6b69aca3f355f4d1d2718
+91c9fc8fe011352879e3bb6660966eafc0847233
diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart
index cf7feb6..a54f207 100644
--- a/packages/flutter/lib/src/material/popup_menu.dart
+++ b/packages/flutter/lib/src/material/popup_menu.dart
@@ -618,8 +618,7 @@
     this.itemSizes,
     this.selectedItemIndex,
     this.textDirection,
-    this.topPadding,
-    this.bottomPadding,
+    this.padding,
   );
 
   // Rectangle of underlying button, relative to the overlay's dimensions.
@@ -636,11 +635,8 @@
   // Whether to prefer going to the left or to the right.
   final TextDirection textDirection;
 
-  // Top padding of unsafe area.
-  final double topPadding;
-
-  // Bottom padding of unsafe area.
-  final double bottomPadding;
+  // The padding of unsafe area.
+  EdgeInsets padding;
 
   // We put the child wherever position specifies, so long as it will fit within
   // the specified parent size padded (inset) by 8. If necessary, we adjust the
@@ -651,7 +647,8 @@
     // The menu can be at most the size of the overlay minus 8.0 pixels in each
     // direction.
     return BoxConstraints.loose(constraints.biggest).deflate(
-        const EdgeInsets.all(_kMenuScreenPadding) + EdgeInsets.only(top: topPadding, bottom: bottomPadding));
+      const EdgeInsets.all(_kMenuScreenPadding) + padding,
+    );
   }
 
   @override
@@ -694,14 +691,15 @@
 
     // Avoid going outside an area defined as the rectangle 8.0 pixels from the
     // edge of the screen in every direction.
-    if (x < _kMenuScreenPadding)
-      x = _kMenuScreenPadding;
-    else if (x + childSize.width > size.width - _kMenuScreenPadding)
-      x = size.width - childSize.width - _kMenuScreenPadding;
-    if (y < _kMenuScreenPadding + topPadding)
-      y = _kMenuScreenPadding + topPadding;
-    else if (y + childSize.height > size.height - _kMenuScreenPadding - bottomPadding)
-      y = size.height - bottomPadding - _kMenuScreenPadding - childSize.height ;
+    if (x < _kMenuScreenPadding + padding.left)
+      x = _kMenuScreenPadding + padding.left;
+    else if (x + childSize.width > size.width - _kMenuScreenPadding - padding.right)
+      x = size.width - childSize.width - _kMenuScreenPadding - padding.right  ;
+    if (y < _kMenuScreenPadding + padding.top)
+      y = _kMenuScreenPadding + padding.top;
+    else if (y + childSize.height > size.height - _kMenuScreenPadding - padding.bottom)
+      y = size.height - padding.bottom - _kMenuScreenPadding - childSize.height ;
+
     return Offset(x, y);
   }
 
@@ -716,8 +714,7 @@
       || selectedItemIndex != oldDelegate.selectedItemIndex
       || textDirection != oldDelegate.textDirection
       || !listEquals(itemSizes, oldDelegate.itemSizes)
-      || topPadding != oldDelegate.topPadding
-      || bottomPadding != oldDelegate.bottomPadding;
+      || padding != oldDelegate.padding;
   }
 }
 
@@ -777,22 +774,27 @@
     }
 
     final Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
-
-    return Builder(
-      builder: (BuildContext context) {
-        final MediaQueryData mediaQuery = MediaQuery.of(context);
-        return CustomSingleChildLayout(
-          delegate: _PopupMenuRouteLayout(
-            position,
-            itemSizes,
-            selectedItemIndex,
-            Directionality.of(context),
-            mediaQuery.padding.top,
-            mediaQuery.padding.bottom,
-          ),
-          child: capturedThemes.wrap(menu),
-        );
-      },
+    final MediaQueryData mediaQuery = MediaQuery.of(context);
+    return MediaQuery.removePadding(
+      context: context,
+      removeTop: true,
+      removeBottom: true,
+      removeLeft: true,
+      removeRight: true,
+      child: Builder(
+        builder: (BuildContext context) {
+          return CustomSingleChildLayout(
+            delegate: _PopupMenuRouteLayout(
+              position,
+              itemSizes,
+              selectedItemIndex,
+              Directionality.of(context),
+              mediaQuery.padding,
+            ),
+            child: capturedThemes.wrap(menu),
+          );
+        },
+      ),
     );
   }
 }
diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart
index 19349d7..10b44a0 100644
--- a/packages/flutter/test/material/popup_menu_test.dart
+++ b/packages/flutter/test/material/popup_menu_test.dart
@@ -2058,6 +2058,79 @@
     expect(popupMenu, Offset(button.dx - 8.0, button.dy + 8.0));
   });
 
+  // Regression test for https://github.com/flutter/flutter/issues/82874
+  testWidgets('PopupMenu position test when have unsafe area - left/right padding', (WidgetTester tester) async {
+    final GlobalKey buttonKey = GlobalKey();
+    const EdgeInsets padding = EdgeInsets.only(left: 300.0, top: 32.0, right: 310.0, bottom: 64.0);
+    EdgeInsets? mediaQueryPadding;
+
+    Widget buildFrame(double width, double height) {
+      return MaterialApp(
+        builder: (BuildContext context, Widget? child) {
+          return MediaQuery(
+            data: const MediaQueryData(
+              padding: padding,
+            ),
+            child: child!,
+          );
+        },
+        home: Scaffold(
+          appBar: AppBar(
+            title: const Text('PopupMenu Test'),
+            actions: <Widget>[PopupMenuButton<int>(
+              child: SizedBox(
+                key: buttonKey,
+                height: height,
+                width: width,
+                child: const ColoredBox(
+                  color: Colors.pink,
+                ),
+              ),
+              itemBuilder: (BuildContext context) {
+                return <PopupMenuEntry<int>>[
+                  PopupMenuItem<int>(
+                    value: 1,
+                    child: Builder(
+                      builder: (BuildContext context) {
+                        mediaQueryPadding = MediaQuery.of(context).padding;
+                        return Text('-1-' * 500); // A long long text string.
+                      },
+                    ),
+                  ),
+                  const PopupMenuItem<int>(value: 2, child: Text('-2-')),
+                ];
+              },
+            )],
+          ),
+          body: const SizedBox.shrink(),
+        ),
+      );
+    }
+
+    await tester.pumpWidget(buildFrame(20.0, 20.0));
+
+    await tester.tap(find.byKey(buttonKey));
+    await tester.pumpAndSettle();
+
+    final Offset button = tester.getTopRight(find.byKey(buttonKey));
+    expect(button, Offset(800.0 - padding.right, padding.top)); // The topPadding is 32.0.
+
+    final Offset popupMenuTopRight = tester.getTopRight(find.byType(SingleChildScrollView));
+
+    // The menu should be positioned directly next to the top of the button.
+    // The 8.0 pixels is [_kMenuScreenPadding].
+    expect(popupMenuTopRight, Offset(800.0 - padding.right - 8.0, padding.top + 8.0));
+
+    final Offset popupMenuTopLeft = tester.getTopLeft(find.byType(SingleChildScrollView));
+    expect(popupMenuTopLeft, Offset(padding.left + 8.0, padding.top + 8.0));
+
+    final Offset popupMenuBottomLeft = tester.getBottomLeft(find.byType(SingleChildScrollView));
+    expect(popupMenuBottomLeft, Offset(padding.left + 8.0, 600.0 - padding.bottom - 8.0));
+
+    // The `MediaQueryData.padding` should be removed.
+    expect(mediaQueryPadding, EdgeInsets.zero);
+  });
+
   group('feedback', () {
     late FeedbackTester feedback;
 
diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart
index f48658e..f9d358a 100644
--- a/packages/flutter_tools/lib/src/dart/pub.dart
+++ b/packages/flutter_tools/lib/src/dart/pub.dart
@@ -151,7 +151,8 @@
        _processUtils = ProcessUtils(
          logger: logger,
          processManager: processManager,
-       );
+       ),
+       _processManager = processManager;
 
   final FileSystem _fileSystem;
   final Logger _logger;
@@ -159,6 +160,7 @@
   final Platform _platform;
   final BotDetector _botDetector;
   final Usage _usage;
+  final ProcessManager _processManager;
 
   @override
   Future<void> get({
@@ -393,11 +395,15 @@
       'cache',
       'dart-sdk',
       'bin',
-      if (_platform.isWindows)
-        'pub.bat'
-      else
-        'pub'
+      'pub',
     ]);
+    if (!_processManager.canRun(sdkPath)) {
+      throwToolExit(
+        'Your Flutter SDK download may be corrupt or missing permissions to run. '
+        'Try re-downloading the Flutter SDK into a directory that has read/write '
+        'permissions for the current user.'
+      );
+    }
     return <String>[sdkPath, ...arguments];
   }
 
diff --git a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
index aeaec77..092c1b0 100644
--- a/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
+++ b/packages/flutter_tools/lib/src/localizations/gen_l10n.dart
@@ -1029,7 +1029,7 @@
       .replaceAll('@(class)', '$className${locale.camelCase()}')
       .replaceAll('@(localeName)', locale.toString())
       .replaceAll('@(methods)', methods.join('\n\n'))
-      .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '');
+      .replaceAll('@(requiresIntlImport)', _requiresIntlImport() ? "import 'package:intl/intl.dart' as intl;" : '');
   }
 
   String _generateSubclass(
@@ -1170,12 +1170,12 @@
       .replaceAll('@(messageClassImports)', sortedClassImports.join('\n'))
       .replaceAll('@(delegateClass)', delegateClass)
       .replaceAll('@(requiresFoundationImport)', _useDeferredLoading ? '' : "import 'package:flutter/foundation.dart';")
-      .replaceAll('@(requiresIntlImport)', _containsPluralMessage() ? "import 'package:intl/intl.dart' as intl;" : '')
+      .replaceAll('@(requiresIntlImport)', _requiresIntlImport() ? "import 'package:intl/intl.dart' as intl;" : '')
       .replaceAll('@(canBeNullable)', _usesNullableGetter ? '?' : '')
       .replaceAll('@(needsNullCheck)', _usesNullableGetter ? '' : '!');
   }
 
-  bool _containsPluralMessage() => _allMessages.any((Message message) => message.isPlural);
+  bool _requiresIntlImport() => _allMessages.any((Message message) => message.isPlural || message.placeholdersRequireFormatting);
 
   void writeOutputFiles(Logger logger, { bool isFromYaml = false }) {
     // First, generate the string contents of all necessary files.
diff --git a/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart b/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart
index 3fe2d05..f1c9051 100644
--- a/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart
+++ b/packages/flutter_tools/test/general.shard/dart/pub_get_test.dart
@@ -26,6 +26,31 @@
     Cache.flutterRoot = '';
   });
 
+  testWithoutContext('Throws a tool exit if pub cannot be run', () async {
+    final FakeProcessManager processManager = FakeProcessManager.any();
+    final BufferLogger logger = BufferLogger.test();
+    final MemoryFileSystem fileSystem = MemoryFileSystem.test();
+    processManager.excludedExecutables.add('bin/cache/dart-sdk/bin/pub');
+
+    fileSystem.file('pubspec.yaml').createSync();
+
+    final Pub pub = Pub(
+      fileSystem: fileSystem,
+      logger: logger,
+      processManager: processManager,
+      usage: TestUsage(),
+      platform: FakePlatform(
+        environment: const <String, String>{},
+      ),
+      botDetector: const BotDetectorAlwaysNo(),
+    );
+
+    await expectLater(() => pub.get(
+      context: PubContext.pubGet,
+      checkUpToDate: true,
+    ), throwsToolExit(message: 'Your Flutter SDK download may be corrupt or missing permissions to run'));
+  });
+
   testWithoutContext('checkUpToDate skips pub get if the package config is newer than the pubspec '
     'and the current framework version is the same as the last version', () async {
     final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[]);
@@ -717,5 +742,8 @@
   }
 
   @override
+  bool canRun(dynamic executable, {String workingDirectory}) => true;
+
+  @override
   dynamic noSuchMethod(Invocation invocation) => null;
 }
diff --git a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
index 28c8f33..9efa893 100644
--- a/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
+++ b/packages/flutter_tools/test/general.shard/generate_localizations_test.dart
@@ -1568,6 +1568,42 @@
     });
 
     group('DateTime tests', () {
+      testUsingContext('imports package:intl', () {
+        const String singleDateMessageArbFileString = '''
+{
+  "@@locale": "en",
+  "springBegins": "Spring begins on {springStartDate}",
+  "@springBegins": {
+      "description": "The first day of spring",
+      "placeholders": {
+          "springStartDate": {
+              "type": "DateTime",
+              "format": "yMd"
+          }
+      }
+  }
+}''';
+        fs.currentDirectory.childDirectory('lib').childDirectory('l10n')..createSync(recursive: true)
+          ..childFile(defaultTemplateArbFileName).writeAsStringSync(singleDateMessageArbFileString);
+
+        LocalizationsGenerator(
+          fs,
+        )
+          ..initialize(
+            inputPathString: defaultL10nPathString,
+            outputPathString: defaultL10nPathString,
+            templateArbFileName: defaultTemplateArbFileName,
+            outputFileString: defaultOutputFileString,
+            classNameString: defaultClassNameString)
+          ..loadResources()
+          ..writeOutputFiles(BufferLogger.test());
+
+        final String localizationsFile = fs.file(
+          fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
+        ).readAsStringSync();
+        expect(localizationsFile, contains(intlImportDartCode));
+      });
+
       testUsingContext('throws an exception when improperly formatted date is passed in', () {
         const String singleDateMessageArbFileString = '''
 {
@@ -1644,6 +1680,45 @@
 
         fail('Improper date formatting should throw an exception');
       });
+    });
+
+    group('NumberFormat tests', () {
+      testUsingContext('imports package:intl', () {
+        const String singleDateMessageArbFileString = '''
+{
+  "courseCompletion": "You have completed {progress} of the course.",
+  "@courseCompletion": {
+    "description": "The amount of progress the student has made in their class.",
+    "placeholders": {
+      "progress": {
+        "type": "double",
+        "format": "percentPattern"
+      }
+    }
+  }
+}''';
+        fs.currentDirectory.childDirectory('lib').childDirectory('l10n')
+          ..createSync(recursive: true)
+          ..childFile(defaultTemplateArbFileName).writeAsStringSync(
+              singleDateMessageArbFileString);
+
+        LocalizationsGenerator(
+          fs,
+        )
+          ..initialize(
+            inputPathString: defaultL10nPathString,
+            outputPathString: defaultL10nPathString,
+            templateArbFileName: defaultTemplateArbFileName,
+            outputFileString: defaultOutputFileString,
+            classNameString: defaultClassNameString)
+          ..loadResources()
+          ..writeOutputFiles(BufferLogger.test());
+
+        final String localizationsFile = fs.file(
+          fs.path.join(syntheticL10nPackagePath, 'output-localization-file_en.dart'),
+        ).readAsStringSync();
+        expect(localizationsFile, contains(intlImportDartCode));
+      });
 
       testUsingContext('throws an exception when improperly formatted number is passed in', () {
         const String singleDateMessageArbFileString = '''