From 08538d22a3c2f8682550b4bca94ecd7142c8d7e4 Mon Sep 17 00:00:00 2001 From: Eric Seidel Date: Fri, 30 Jan 2026 22:54:14 -0800 Subject: [PATCH] feat: don't reformat AndroidManifest.xml (#3490) --- ...android_internet_permission_validator.dart | 35 ++++- ...id_internet_permission_validator_test.dart | 124 ++++++++++++++++++ 2 files changed, 153 insertions(+), 6 deletions(-) diff --git a/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart b/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart index de078098..46ce93bd 100644 --- a/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart +++ b/packages/shorebird_cli/lib/src/validators/android_internet_permission_validator.dart @@ -90,12 +90,35 @@ The command you are running must be run within a Flutter app project that suppor } void _addInternetPermissionToFile(String path) { - final xmlDocument = XmlDocument.parse(File(path).readAsStringSync()); - xmlDocument.rootElement.children.add( - XmlElement(XmlName('uses-permission'), [ - XmlAttribute(XmlName('android:name'), 'android.permission.INTERNET'), - ]), + final file = File(path); + final contents = file.readAsStringSync(); + // Insert the permission after the opening tag to preserve + // the existing formatting of the file. + final manifestTagEnd = RegExp(']*>'); + final match = manifestTagEnd.firstMatch(contents); + if (match == null) return; + final indent = _detectIndent(contents); + final permissionLine = + '$indent'; + final updated = contents.replaceRange( + match.end, + match.end, + '\n$permissionLine', ); - File(path).writeAsStringSync(xmlDocument.toXmlString(pretty: true)); + file.writeAsStringSync(updated); + } + + /// Detects the indentation used in the manifest by looking at the first + /// indented line after the `` tag. + String _detectIndent(String contents) { + final lines = contents.split('\n'); + for (final line in lines) { + final stripped = line.trimLeft(); + if (stripped.isEmpty || stripped.startsWith(' +'''; + + // A stock AndroidManifest.xml from `flutter create`. + const stockFlutterManifest = r''' + + + + + + + + + + + + + + + + + + + + + '''; group(AndroidInternetPermissionValidator, () { @@ -239,6 +288,81 @@ void main() { ); expect(results, isEmpty); }); + + test('preserves formatting of stock Flutter manifest', () async { + final manifestPath = p.join( + projectRoot.path, + 'android', + 'app', + 'src', + 'main', + ); + writeManifestToPath(stockFlutterManifest, manifestPath); + + final results = await runWithOverrides( + AndroidInternetPermissionValidator().validate, + ); + expect(results, hasLength(1)); + await runWithOverrides(() => results.first.fix!()); + + final updated = File( + p.join(manifestPath, 'AndroidManifest.xml'), + ).readAsStringSync(); + final originalLines = stockFlutterManifest.split('\n'); + final updatedLines = updated.split('\n'); + // Should only add one line. + expect(updatedLines.length, originalLines.length + 1); + // First line is the manifest tag, unchanged. + expect(updatedLines[0], originalLines[0]); + // Second line is the new permission. + expect( + updatedLines[1], + ' ', + ); + // Remaining lines are identical to the original. + for (var i = 1; i < originalLines.length; i++) { + expect(updatedLines[i + 1], originalLines[i]); + } + }); + + test('preserves existing formatting', () async { + final manifestPath = p.join( + projectRoot.path, + 'android', + 'app', + 'src', + 'main', + ); + writeManifestToPath(manifestWithNoPermissions, manifestPath); + + final results = await runWithOverrides( + AndroidInternetPermissionValidator().validate, + ); + await runWithOverrides(() => results.first.fix!()); + + final updated = File( + p.join(manifestPath, 'AndroidManifest.xml'), + ).readAsStringSync(); + // The permission should be inserted after with matching + // indentation, and the rest of the file should be unchanged. + expect( + updated, + contains( + ' ', + ), + ); + // Original closing tag should remain untouched. + expect(updated, contains('')); + // Should not collapse attributes onto one line (no reformatting). + expect( + updated, + contains( + ' package="dev.shorebird.u_shorebird_clock">', + ), + ); + }); }); }); }