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. */