From 43a90d3f31caef9debf12f8853ae75aac96cfeac Mon Sep 17 00:00:00 2001
From: Colin Mallon <82714119+Mallon94@users.noreply.github.com>
Date: Sun, 1 Feb 2026 22:59:48 +0000
Subject: [PATCH 1/2] Add support for nested meta-data inside android manifest
components
---
.../Compilers/AndroidPluginCompiler.php | 45 ++++++++++++++++---
1 file changed, 40 insertions(+), 5 deletions(-)
diff --git a/src/Plugins/Compilers/AndroidPluginCompiler.php b/src/Plugins/Compilers/AndroidPluginCompiler.php
index a65b5da..7a1a8ef 100644
--- a/src/Plugins/Compilers/AndroidPluginCompiler.php
+++ b/src/Plugins/Compilers/AndroidPluginCompiler.php
@@ -548,7 +548,6 @@ protected function buildServiceEntry(array $service, Plugin $plugin): string
}
if (isset($service['foregroundServiceType'])) {
$type = $service['foregroundServiceType'];
- // Support both array and string formats
if (is_array($type)) {
$type = implode('|', $type);
}
@@ -557,17 +556,53 @@ protected function buildServiceEntry(array $service, Plugin $plugin): string
$attrString = implode("\n ", $attrs);
- // Support both snake_case and kebab-case
+ // Build nested content (intent-filters and meta-data)
+ $nestedContent = '';
+
+ // Support both snake_case and kebab-case for intent filters
$intentFilters = $service['intent_filters'] ?? $service['intent-filters'] ?? [];
- if (! empty($intentFilters)) {
- $filters = $this->buildIntentFilters($intentFilters);
+ if (!empty($intentFilters)) {
+ $nestedContent .= $this->buildIntentFilters($intentFilters);
+ }
+
+ // Add meta-data support at service level
+ $metaData = $service['meta_data'] ?? $service['meta-data'] ?? [];
+ if (!empty($metaData)) {
+ $nestedContent .= $this->buildComponentMetaData($metaData);
+ }
- return "\n{$filters} ";
+ if (!empty($nestedContent)) {
+ return "\n{$nestedContent} ";
}
return "";
}
+ /**
+ * Build meta-data XML entries for use inside manifest components
+ */
+ protected function buildComponentMetaData(array $metaDataEntries): string
+ {
+ $xml = '';
+
+ foreach ($metaDataEntries as $metaData) {
+ $name = $metaData['name'];
+ $value = $metaData['value'] ?? null;
+ $resource = $metaData['resource'] ?? null;
+
+ if ($resource !== null) {
+ $xml .= " \n";
+ } elseif ($value !== null) {
+ if (is_bool($value)) {
+ $value = $value ? 'true' : 'false';
+ }
+ $xml .= " \n";
+ }
+ }
+
+ return $xml;
+ }
+
/**
* Build a receiver XML entry
*/
From e289c9d1ae6481e49b38691ea426c445b881608b Mon Sep 17 00:00:00 2001
From: Colin Mallon
Date: Wed, 25 Feb 2026 20:31:52 +0000
Subject: [PATCH 2/2] Add tests for handling meta-data in Android service
components, including value and resource attributes, boolean values,
kebab-case keys, and intent filters.
---
tests/Feature/Plugins/AndroidCompilerTest.php | 299 ++++++++++++++++++
1 file changed, 299 insertions(+)
diff --git a/tests/Feature/Plugins/AndroidCompilerTest.php b/tests/Feature/Plugins/AndroidCompilerTest.php
index a428e3f..fe26f57 100644
--- a/tests/Feature/Plugins/AndroidCompilerTest.php
+++ b/tests/Feature/Plugins/AndroidCompilerTest.php
@@ -655,6 +655,305 @@ public function it_handles_plugins_without_bridge_functions(): void
$this->assertFileExists($generatedPath);
}
+ /**
+ * @test
+ *
+ * Should add meta-data entries inside a service component using value attribute.
+ */
+ public function it_adds_meta_data_with_value_inside_service(): void
+ {
+ $plugin = $this->createTestPlugin([
+ 'android' => [
+ 'permissions' => [],
+ 'dependencies' => [],
+ 'services' => [
+ [
+ 'name' => 'com.example.MyService',
+ 'exported' => false,
+ 'meta_data' => [
+ ['name' => 'com.example.API_KEY', 'value' => 'abc123'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->mockRegistry
+ ->shouldReceive('all')
+ ->andReturn(collect([$plugin]));
+
+ $this->compiler->compile();
+
+ $manifestPath = $this->testBasePath.'/android/app/src/main/AndroidManifest.xml';
+ $content = $this->files->get($manifestPath);
+
+ $this->assertStringContainsString('assertStringContainsString('', $content);
+ // Service should use closing tag (not self-closing) when it has nested content
+ $this->assertStringContainsString('', $content);
+ }
+
+ /**
+ * @test
+ *
+ * Should add meta-data entries inside a service component using resource attribute.
+ */
+ public function it_adds_meta_data_with_resource_inside_service(): void
+ {
+ $plugin = $this->createTestPlugin([
+ 'android' => [
+ 'permissions' => [],
+ 'dependencies' => [],
+ 'services' => [
+ [
+ 'name' => 'com.example.MyService',
+ 'exported' => false,
+ 'meta_data' => [
+ ['name' => 'com.example.ICON', 'resource' => '@drawable/icon'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->mockRegistry
+ ->shouldReceive('all')
+ ->andReturn(collect([$plugin]));
+
+ $this->compiler->compile();
+
+ $manifestPath = $this->testBasePath.'/android/app/src/main/AndroidManifest.xml';
+ $content = $this->files->get($manifestPath);
+
+ $this->assertStringContainsString('', $content);
+ }
+
+ /**
+ * @test
+ *
+ * Should handle boolean values in service meta-data.
+ */
+ public function it_handles_boolean_values_in_service_meta_data(): void
+ {
+ $plugin = $this->createTestPlugin([
+ 'android' => [
+ 'permissions' => [],
+ 'dependencies' => [],
+ 'services' => [
+ [
+ 'name' => 'com.example.MyService',
+ 'exported' => false,
+ 'meta_data' => [
+ ['name' => 'com.example.ENABLED', 'value' => true],
+ ['name' => 'com.example.DEBUG', 'value' => false],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->mockRegistry
+ ->shouldReceive('all')
+ ->andReturn(collect([$plugin]));
+
+ $this->compiler->compile();
+
+ $manifestPath = $this->testBasePath.'/android/app/src/main/AndroidManifest.xml';
+ $content = $this->files->get($manifestPath);
+
+ $this->assertStringContainsString('android:value="true"', $content);
+ $this->assertStringContainsString('android:value="false"', $content);
+ }
+
+ /**
+ * @test
+ *
+ * Should support kebab-case meta-data key in service definitions.
+ */
+ public function it_supports_kebab_case_meta_data_key_in_service(): void
+ {
+ $plugin = $this->createTestPlugin([
+ 'android' => [
+ 'permissions' => [],
+ 'dependencies' => [],
+ 'services' => [
+ [
+ 'name' => 'com.example.MyService',
+ 'exported' => false,
+ 'meta-data' => [
+ ['name' => 'com.example.SETTING', 'value' => 'kebab-works'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->mockRegistry
+ ->shouldReceive('all')
+ ->andReturn(collect([$plugin]));
+
+ $this->compiler->compile();
+
+ $manifestPath = $this->testBasePath.'/android/app/src/main/AndroidManifest.xml';
+ $content = $this->files->get($manifestPath);
+
+ $this->assertStringContainsString('', $content);
+ }
+
+ /**
+ * @test
+ *
+ * Should support both intent-filters and meta-data nested inside a service.
+ */
+ public function it_supports_both_intent_filters_and_meta_data_in_service(): void
+ {
+ $plugin = $this->createTestPlugin([
+ 'android' => [
+ 'permissions' => [],
+ 'dependencies' => [],
+ 'services' => [
+ [
+ 'name' => 'com.example.MyService',
+ 'exported' => true,
+ 'intent_filters' => [
+ [
+ 'action' => 'com.example.ACTION',
+ 'category' => 'android.intent.category.DEFAULT',
+ ],
+ ],
+ 'meta_data' => [
+ ['name' => 'com.example.API_KEY', 'value' => 'abc123'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->mockRegistry
+ ->shouldReceive('all')
+ ->andReturn(collect([$plugin]));
+
+ $this->compiler->compile();
+
+ $manifestPath = $this->testBasePath.'/android/app/src/main/AndroidManifest.xml';
+ $content = $this->files->get($manifestPath);
+
+ $this->assertStringContainsString('', $content);
+ $this->assertStringContainsString('', $content);
+ $this->assertStringContainsString('', $content);
+ }
+
+ /**
+ * @test
+ *
+ * Service without meta-data or intent-filters should remain self-closing.
+ */
+ public function it_keeps_service_self_closing_without_nested_content(): void
+ {
+ $plugin = $this->createTestPlugin([
+ 'android' => [
+ 'permissions' => [],
+ 'dependencies' => [],
+ 'services' => [
+ [
+ 'name' => 'com.example.SimpleService',
+ 'exported' => false,
+ ],
+ ],
+ ],
+ ]);
+
+ $this->mockRegistry
+ ->shouldReceive('all')
+ ->andReturn(collect([$plugin]));
+
+ $this->compiler->compile();
+
+ $manifestPath = $this->testBasePath.'/android/app/src/main/AndroidManifest.xml';
+ $content = $this->files->get($manifestPath);
+
+ $this->assertStringContainsString('com.example.SimpleService', $content);
+ // Self-closing service tag
+ $this->assertMatchesRegularExpression('/]*\/>/', $content);
+ $this->assertStringNotContainsString('', $content);
+ }
+
+ /**
+ * @test
+ *
+ * Resource attribute should take precedence over value in service meta-data.
+ */
+ public function it_prefers_resource_over_value_in_service_meta_data(): void
+ {
+ $plugin = $this->createTestPlugin([
+ 'android' => [
+ 'permissions' => [],
+ 'dependencies' => [],
+ 'services' => [
+ [
+ 'name' => 'com.example.MyService',
+ 'exported' => false,
+ 'meta_data' => [
+ ['name' => 'com.example.ICON', 'resource' => '@drawable/icon', 'value' => 'ignored'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->mockRegistry
+ ->shouldReceive('all')
+ ->andReturn(collect([$plugin]));
+
+ $this->compiler->compile();
+
+ $manifestPath = $this->testBasePath.'/android/app/src/main/AndroidManifest.xml';
+ $content = $this->files->get($manifestPath);
+
+ // Resource should be used, not value
+ $this->assertStringContainsString('android:resource="@drawable/icon"', $content);
+ $this->assertStringNotContainsString('android:value="ignored"', $content);
+ }
+
+ /**
+ * @test
+ *
+ * Should handle multiple meta-data entries inside a single service.
+ */
+ public function it_handles_multiple_meta_data_entries_in_service(): void
+ {
+ $plugin = $this->createTestPlugin([
+ 'android' => [
+ 'permissions' => [],
+ 'dependencies' => [],
+ 'services' => [
+ [
+ 'name' => 'com.example.MyService',
+ 'exported' => false,
+ 'meta_data' => [
+ ['name' => 'com.example.KEY_ONE', 'value' => 'first'],
+ ['name' => 'com.example.KEY_TWO', 'value' => 'second'],
+ ['name' => 'com.example.KEY_THREE', 'resource' => '@string/third'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ $this->mockRegistry
+ ->shouldReceive('all')
+ ->andReturn(collect([$plugin]));
+
+ $this->compiler->compile();
+
+ $manifestPath = $this->testBasePath.'/android/app/src/main/AndroidManifest.xml';
+ $content = $this->files->get($manifestPath);
+
+ $this->assertStringContainsString('android:name="com.example.KEY_ONE" android:value="first"', $content);
+ $this->assertStringContainsString('android:name="com.example.KEY_TWO" android:value="second"', $content);
+ $this->assertStringContainsString('android:name="com.example.KEY_THREE" android:resource="@string/third"', $content);
+ }
+
/**
* Helper method to create a test Plugin instance.
*/