Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'@config-plugins/react-native-callkeep',
'./customGradle.plugin.js',
'./customManifest.plugin.js',
'./plugins/withMediaButtonModule.js',
'./plugins/withInCallAudioModule.js',
'./plugins/withResourceBundleDeploymentTarget.js',
[
Expand Down
107 changes: 107 additions & 0 deletions plugins/__tests__/register-android-package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { registerAndroidPackage } from '../utils/register-android-package';

// Expo SDK 57 template (what `expo prebuild` generates today)
const applyTemplate = `package wtdt.resgrid.andriod.development

import android.app.Application

import com.facebook.react.PackageList
import com.facebook.react.ReactApplication

class MainApplication : Application(), ReactApplication {

override val reactHost: ReactHost by lazy {
ExpoReactHostFactory.getDefaultReactHost(
context = applicationContext,
packageList =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}
)
}
}
`;

// Older template that declares the package list as a local
const valTemplate = `package com.example

import com.facebook.react.PackageList

class MainApplication : Application(), ReactApplication {
override fun getPackages(): List<ReactPackage> {
val packages = PackageList(this).packages
return packages
}
}
`;

const countOf = (contents: string, text: string) => contents.split(text).length - 1;

describe('registerAndroidPackage', () => {
it('adds the package inside the packages.apply block of the current Expo template', () => {
const result = registerAndroidPackage(applyTemplate, 'com.resgridresponder', 'MediaButtonPackage');
const lines = result.split('\n');

expect(lines[1]).toBe('import com.resgridresponder.MediaButtonPackage');
const applyLine = lines.findIndex((line) => line.includes('PackageList(this).packages.apply {'));
expect(lines[applyLine + 1]).toBe(' add(MediaButtonPackage())');
});

it('adds the package to a val packages list, making it mutable', () => {
const result = registerAndroidPackage(valTemplate, 'com.example', 'MediaButtonPackage');

expect(result).toContain('import com.example.MediaButtonPackage');
expect(result).toContain(' val packages = PackageList(this).packages.toMutableList()\n packages.add(MediaButtonPackage())');
});

it('registers several packages once each', () => {
for (const template of [applyTemplate, valTemplate]) {
const result = registerAndroidPackage(registerAndroidPackage(template, 'com.resgridresponder', 'InCallAudioPackage'), 'com.resgridresponder', 'MediaButtonPackage');

expect(countOf(result, 'add(InCallAudioPackage())')).toBe(1);
expect(countOf(result, 'add(MediaButtonPackage())')).toBe(1);
expect(countOf(result, 'toMutableList()')).toBeLessThanOrEqual(1);
}
});

it('leaves an already registered package unchanged across repeated prebuilds', () => {
const once = registerAndroidPackage(applyTemplate, 'com.resgridresponder', 'MediaButtonPackage');
const twice = registerAndroidPackage(once, 'com.resgridresponder', 'MediaButtonPackage');

expect(twice).toBe(once);
});

it('registers a package that is imported but not yet registered', () => {
const importedOnly = applyTemplate.replace('package wtdt.resgrid.andriod.development\n', 'package wtdt.resgrid.andriod.development\nimport com.resgridresponder.InCallAudioPackage\n');

const result = registerAndroidPackage(importedOnly, 'com.resgridresponder', 'InCallAudioPackage');

expect(countOf(result, 'import com.resgridresponder.InCallAudioPackage')).toBe(1);
expect(result).toContain('add(InCallAudioPackage())');
});

it('registers a package whose only registration is commented out', () => {
const commentedOut = applyTemplate
.replace('// add(MyReactNativePackage())', '// add(MediaButtonPackage())')
.replace(' }\n )', ' /* add(MediaButtonPackage()) */\n }\n )');

const result = registerAndroidPackage(commentedOut, 'com.resgridresponder', 'MediaButtonPackage');

expect(result.split('\n')).toContain(' add(MediaButtonPackage())');
});

it('adds the import when the only one is commented out', () => {
const commentedImport = applyTemplate.replace('package wtdt.resgrid.andriod.development\n', 'package wtdt.resgrid.andriod.development\n// import com.resgridresponder.MediaButtonPackage\n');

const result = registerAndroidPackage(commentedImport, 'com.resgridresponder', 'MediaButtonPackage');

expect(result.split('\n')).toContain('import com.resgridresponder.MediaButtonPackage');
});

it('throws when MainApplication.kt has no recognised package list', () => {
const unknown = 'package com.example\n\nclass MainApplication : Application()\n';

expect(() => registerAndroidPackage(unknown, 'com.example', 'MediaButtonPackage')).toThrow('Could not find where to register MediaButtonPackage in MainApplication.kt');
});
});
51 changes: 51 additions & 0 deletions plugins/utils/register-android-package.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-env node */

/** Kotlin source without its comments, so a commented-out import or `add(...)` is not taken for a real one. */
const withoutComments = (source) => source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');

/**
* Registers a manually linked ReactPackage in MainApplication.kt.
*
* The import uses the BASE package name (where our plugins write the native module files), not the
* variant-specific package of MainApplication.kt (e.g. `.development`).
*
* Handles both MainApplication shapes:
* - Current Expo templates: `PackageList(this).packages.apply { ... }` gets `add(Package())` inside the block
* - Older templates: `val packages = PackageList(this).packages` gets `packages.add(Package())`
*
* Throws when neither shape is found, so a template change fails the prebuild instead of shipping a
* build where the native module is silently missing.
*
* @param {string} contents - MainApplication.kt source
* @param {string} basePackageName - Package the native module files are written to
* @param {string} packageClass - ReactPackage class name, e.g. `MediaButtonPackage`
* @returns {string} Updated MainApplication.kt source
*/
function registerAndroidPackage(contents, basePackageName, packageClass) {
let result = contents;

const importStatement = `import ${basePackageName}.${packageClass}`;
if (!withoutComments(result).includes(importStatement)) {
result = result.replace(/^(package\s+[^\n]+\n)/, `$1${importStatement}\n`);
}

// Checked separately from the import: a MainApplication.kt that already imports the package
// without registering it must still get the registration
if (withoutComments(result).includes(`add(${packageClass}())`)) {
return result;
}

const applyPattern = /^([ \t]*)(.*PackageList\(this\)\.packages\.apply\s*\{)/m;
if (applyPattern.test(result)) {
return result.replace(applyPattern, (_match, indent, line) => `${indent}${line}\n${indent} add(${packageClass}())`);
}

const packagesPattern = /^([ \t]*)val packages = PackageList\(this\)\.packages(\.toMutableList\(\))?/m;
if (packagesPattern.test(result)) {
return result.replace(packagesPattern, (_match, indent) => `${indent}val packages = PackageList(this).packages.toMutableList()\n${indent}packages.add(${packageClass}())`);
}

throw new Error(`Could not find where to register ${packageClass} in MainApplication.kt`);
}

module.exports = { registerAndroidPackage };
34 changes: 7 additions & 27 deletions plugins/withInCallAudioModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const { withDangerousMod, withMainApplication } = require('@expo/config-plugins'
const fs = require('fs');
const path = require('path');

const { registerAndroidPackage } = require('./utils/register-android-package');

/**
* Android InCallAudioModule.kt content
* Uses SoundPool to play sounds on the VOICE_COMMUNICATION stream.
Expand Down Expand Up @@ -222,34 +224,12 @@ const withInCallAudioModule = (config) => {

// 3. Register Package in MainApplication.kt
config = withMainApplication(config, (config) => {
const mainApplication = config.modResults;
const projectRoot = config.modRequest.projectRoot;

if (!mainApplication.contents.includes('InCallAudioPackage')) {
const basePackageName = resolveBasePackageName(projectRoot);
const importStatement = `import ${basePackageName}.InCallAudioPackage`;

if (!mainApplication.contents.includes(importStatement)) {
mainApplication.contents = mainApplication.contents.replace(/^(package\s+[^\n]+\n)/, `$1${importStatement}\n`);
}

const packagesPattern = /val packages = PackageList\(this\)\.packages(\.toMutableList\(\))?/;
const packagesMatch = mainApplication.contents.match(packagesPattern);
const basePackageName = resolveBasePackageName(config.modRequest.projectRoot);
const contents = config.modResults.contents;

if (packagesMatch) {
// Using the simplest replacement that ensures toMutableList()
const replacement = `val packages = PackageList(this).packages.toMutableList()\n packages.add(InCallAudioPackage())`;

// Avoid double adding if MediaButtonPackage logic already changed it to mutable
if (mainApplication.contents.includes('packages.add(MediaButtonPackage()')) {
// Add ours after MediaButtonPackage
mainApplication.contents = mainApplication.contents.replace('packages.add(MediaButtonPackage())', 'packages.add(MediaButtonPackage())\n packages.add(InCallAudioPackage())');
} else {
// Standard replacement
mainApplication.contents = mainApplication.contents.replace(packagesPattern, replacement);
}
console.log('[withInCallAudioModule] Registered InCallAudioPackage in MainApplication.kt');
}
config.modResults.contents = registerAndroidPackage(contents, basePackageName, 'InCallAudioPackage');
if (config.modResults.contents !== contents) {
console.log('[withInCallAudioModule] Registered InCallAudioPackage in MainApplication.kt');
}

return config;
Expand Down
Loading
Loading