Skip to content
Closed
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
10 changes: 6 additions & 4 deletions packages/platform-android/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
"@react-native-community/cli-tools": "^3.0.0",
"chalk": "^2.4.2",
"execa": "^1.0.0",
"fast-xml-parser": "^3.15.1",
"flat": "^5.0.0",
"gradle-to-js": "^2.0.0",
"jetifier": "^1.6.2",
"logkitty": "^0.6.0",
"slash": "^3.0.0",
"xmldoc": "^1.1.2"
"slash": "^3.0.0"
},
"files": [
"build",
Expand All @@ -22,8 +24,8 @@
"devDependencies": {
"@react-native-community/cli-types": "^3.0.0",
"@types/execa": "^0.9.0",
"@types/flat": "^0.0.28",
"@types/fs-extra": "^8.0.0",
"@types/glob": "^7.1.1",
"@types/xmldoc": "^1.1.4"
"@types/glob": "^7.1.1"
}
}
48 changes: 48 additions & 0 deletions packages/platform-android/src/commands/runAndroid/getApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @ts-ignore
import g2js from 'gradle-to-js/lib/parser';
import flatten from 'flat';
import readManifest, {Manifest} from '../../config/readManifest';

interface VariantMap {
[k: string]: string;
}

interface App extends Manifest {
packageName: string;
mainActivity: string;
name: string;
variants: VariantMap;
}

// runtime cache
let app: App = {
packageName: '',
mainActivity: '',
name: '',
variants: {},
};

export async function getApp(appFolder: string): Promise<App> {
if (app.packageName) {
return app;
}
const manifestPath = `${appFolder}/src/main/AndroidManifest.xml`;
const appGradlePath = `${appFolder}/build.gradle`;
const manifest = readManifest(manifestPath);
let variants: VariantMap = {};
try {
const appBuildConfig = flatten(await g2js.parseFile(appGradlePath)) as any;
Object.keys(appBuildConfig).forEach(key => {
if (key.endsWith('applicationId')) {
// get defaultConfig and flavor
// key: android.defaultConfig.applicationId | android.productFlavor.flavor.applicationId
const variant = key.split('.').slice(-2)[0] as string;
variants[variant] = appBuildConfig[key];
}
});
} catch (error) {}
return {
...manifest,
variants,
};
}
29 changes: 23 additions & 6 deletions packages/platform-android/src/commands/runAndroid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
CLIError,
} from '@react-native-community/cli-tools';
import warnAboutManuallyLinkedLibs from '../../link/warnAboutManuallyLinkedLibs';
import {getApp} from './getApp';

// Verifies this is an Android project
function checkAndroid(root: string) {
Expand Down Expand Up @@ -118,16 +119,32 @@ function getPackageNameWithSuffix(
}

// Builds the app and runs it on a connected emulator / device.
function buildAndRun(args: Flags) {
async function buildAndRun(args: Flags) {
process.chdir(path.join(args.root, 'android'));
const cmd = process.platform.startsWith('win') ? 'gradlew.bat' : './gradlew';

// "app" is usually the default value for Android apps with only 1 app
const {appFolder} = args;
// @ts-ignore
const packageName = fs
.readFileSync(`${appFolder}/src/main/AndroidManifest.xml`, 'utf8')
.match(/package="(.+?)"/)[1];
const app = await getApp(appFolder);

const {packageName, mainActivity, variants} = app;

if (!args.mainActivity) {
// `mainActivity` start with `.`, remove it
args.mainActivity = mainActivity.replace('.', '');
}

if (!args.appId) {
if (args.variant) {
// variant always end with `Debug` or `Release`
const flavor = args.variant.replace(/Debug$/, '').replace(/Release$/, '');
const appId = variants[flavor] || variants.defaultConfig;

if (appId) {
args.appId = appId;
}
}
}

const packageNameWithSuffix = getPackageNameWithSuffix(
args.appId,
Expand Down Expand Up @@ -403,7 +420,7 @@ export default {
{
name: '--main-activity [string]',
description: 'Name of the activity to start',
default: 'MainActivity',
default: '',
},
{
name: '--deviceId [string]',
Expand Down
9 changes: 9 additions & 0 deletions packages/platform-android/src/config/__fixtures__/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const manifest = fs.readFileSync(
const mainJavaClass = fs.readFileSync(
path.join(__dirname, './files/Main.java'),
);
const oneActivityManifest = fs.readFileSync(
path.join(__dirname, './files/AndroidManifest-one-activity.xml'),
);

function generateValidFileStructure(classFileName: string) {
return {
Expand Down Expand Up @@ -232,3 +235,9 @@ export const findPackagesClassNameJavaNotValid = [
}
`,
];

export const oneActivity = {
src: {
'AndroidManifest.xml': oneActivityManifest,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.some.example">

<uses-permission android:name="android.permission.INTERNET" />

<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme">
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.some.example">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.some.example">

<uses-permission android:name="android.permission.INTERNET" />

<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme">
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize" android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,32 @@ describe('android::readManifest', () => {
app: mocks.valid,
},
},
one: {
android: {
app: mocks.oneActivity,
},
},
});
});

it('returns manifest content if file exists in the folder', () => {
const manifestPath = findManifest('/nested');
expect(readManifest(manifestPath)).not.toBeNull();
expect(typeof readManifest(manifestPath)).toBe('object');
const manifest = readManifest(manifestPath);
expect(manifest).not.toBeNull();
expect(typeof manifest).toBe('object');
expect(manifest.packageName).toBe('com.some.example');
expect(manifest.mainActivity).toBe('.MainActivity');
expect(manifest.name).toBe('.MainApplication');
});

it('returns manifest content if only one activity in manifest', () => {
const manifestPath = findManifest('/one');
const manifest = readManifest(manifestPath);
expect(manifest).not.toBeNull();
expect(typeof manifest).toBe('object');
expect(manifest.packageName).toBe('com.some.example');
expect(manifest.mainActivity).toBe('.MainActivity');
expect(manifest.name).toBe('.MainApplication');
});

it('throws an error if there is no manifest in the folder', () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/platform-android/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ import {
AndroidProjectParams,
AndroidDependencyParams,
} from '@react-native-community/cli-types';
import {XmlDocument} from 'xmldoc';

const getPackageName = (manifest: XmlDocument) => manifest.attr.package;

/**
* Gets android project config by analyzing given folder and taking some
Expand Down Expand Up @@ -45,7 +42,7 @@ export function projectConfig(

const manifest = readManifest(manifestPath);

const packageName = userConfig.packageName || getPackageName(manifest);
const packageName = userConfig.packageName || manifest.packageName;

if (!packageName) {
throw new Error(`Package name not found in ${manifestPath}`);
Expand Down Expand Up @@ -119,7 +116,7 @@ export function dependencyConfig(
}

const manifest = readManifest(manifestPath);
const packageName = userConfig.packageName || getPackageName(manifest);
const packageName = userConfig.packageName || manifest.packageName;
const packageClassName = findPackageClassName(sourceDir);

/**
Expand Down
61 changes: 58 additions & 3 deletions packages/platform-android/src/config/readManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,63 @@
*/

import fs from 'fs';
import xml from 'xmldoc';
import xmlParser from 'fast-xml-parser';

export default function readManifest(manifestPath: string) {
return new xml.XmlDocument(fs.readFileSync(manifestPath, 'utf8'));
const MAIN_ACTION = 'android.intent.action.MAIN';
const LAUNCHER = 'android.intent.category.LAUNCHER';

interface Activity {
[x: string]: any;
}

export interface Manifest {
packageName: string;
mainActivity: string;
name: string;
}

export default function readManifest(manifestPath: string): Manifest {
const manifestContent = fs.readFileSync(manifestPath, {encoding: 'utf8'});
// generally, validate will always return `true`
if (xmlParser.validate(manifestContent)) {
const {manifest} = xmlParser.parse(manifestContent, {
attributeNamePrefix: '',
ignoreAttributes: false,
ignoreNameSpace: true,
});

const {package: packageName, application = {}} = manifest;
const {activity = [], name} = application;

let activities: Activity[] = [];

if (!Array.isArray(activity)) {
activities = [activity];
} else {
activities = activity;
}

const mainActivity = activities.find((act: Activity) => {
const intentFilter = act['intent-filter'];
if (intentFilter) {
const {action, category} = intentFilter;
if (action && category) {
return action.name === MAIN_ACTION && category.name === LAUNCHER;
}
}
return false;
});
return {
packageName,
name,
mainActivity: mainActivity ? mainActivity.name : '',
};
} else {
// no error throw, but return empty string
return {
packageName: '',
mainActivity: '',
name: '',
};
}
}
Loading