Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.

[config] android plugins #2849

Merged
merged 21 commits into from
Nov 10, 2020
Merged
Show file tree
Hide file tree
Changes from 17 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
40 changes: 38 additions & 2 deletions packages/config/src/Plugin.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { ExpoConfig } from '@expo/config-types';
import { JSONObject } from '@expo/json-file';
import { XcodeProject } from 'xcode';

import { AndroidManifest } from './android/Manifest';
import * as AndroidPaths from './android/Paths';
import { ResourceXML } from './android/Resources';
import { InfoPlist } from './ios/IosConfig.types';

type OptionalPromise<T> = Promise<T> | T;
Expand Down Expand Up @@ -51,19 +54,52 @@ export interface ExportedConfigWithProps<Data = any> extends ExpoConfig {
modRequest: ModProps<Data>;
}

// TODO: Change any to void
export type ConfigPlugin<Props = any> = (config: ExpoConfig, props: Props) => ExpoConfig;

export type Mod<Props = any> = (
config: ExportedConfigWithProps<Props>
) => OptionalPromise<ExportedConfigWithProps<Props>>;

export interface ModConfig {
// android?: {
// };
android?: {
/**
* Modify the `android/app/src/main/AndroidManifest.xml` as JSON (parsed with [`xml2js`](https://www.npmjs.com/package/xml2js)).
*/
manifest?: Mod<AndroidManifest>;
/**
* Modify the `android/app/src/main/res/values/strings.xml` as JSON (parsed with [`xml2js`](https://www.npmjs.com/package/xml2js)).
*/
strings?: Mod<ResourceXML>;
/**
* Modify the `android/app/src/main/<package>/MainActivity.java` as a string.
*/
mainActivity?: Mod<AndroidPaths.ApplicationProjectFile>;
/**
* Modify the `android/app/build.gradle` as a string.
*/
appBuildGradle?: Mod<AndroidPaths.GradleProjectFile>;
/**
* Modify the `android/build.gradle` as a string.
*/
projectBuildGradle?: Mod<AndroidPaths.GradleProjectFile>;
};
ios?: {
/**
* Modify the `ios/<name>/Info.plist` as JSON (parsed with [`@expo/plist`](https://www.npmjs.com/package/@expo/plist)).
*/
infoPlist?: Mod<InfoPlist>;
/**
* Modify the `ios/<name>/<product-name>.entitlements` as JSON (parsed with [`@expo/plist`](https://www.npmjs.com/package/@expo/plist)).
*/
entitlements?: Mod<Plist>;
/**
* Modify the `ios/<name>/Expo.plist` as JSON (Expo updates config for iOS) (parsed with [`@expo/plist`](https://www.npmjs.com/package/@expo/plist)).
*/
expoPlist?: Mod<Plist>;
/**
* Modify the `ios/<name>.xcodeproj` as an `XcodeProject` (parsed with [`xcode`](https://www.npmjs.com/package/xcode))
*/
xcodeproj?: Mod<XcodeProject>;
};
}
Expand Down
3 changes: 3 additions & 0 deletions packages/config/src/android/AllowBackup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ExpoConfig } from '../Config.types';
import { createAndroidManifestPlugin } from '../plugins/android-plugins';
import { AndroidManifest, getMainApplication, StringBoolean } from './Manifest';

export const withAllowBackup = createAndroidManifestPlugin(setAllowBackup);

export function getAllowBackup(config: Pick<ExpoConfig, 'android'>) {
// Defaults to true.
// https://docs.expo.io/versions/latest/config/app/#allowbackup
Expand Down
3 changes: 3 additions & 0 deletions packages/config/src/android/Branch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExpoConfig } from '../Config.types';
import { createAndroidManifestPlugin } from '../plugins/android-plugins';
import {
addMetaDataItemToMainApplication,
AndroidManifest,
Expand All @@ -8,6 +9,8 @@ import {

const META_BRANCH_KEY = 'io.branch.sdk.BranchKey';

export const withBranch = createAndroidManifestPlugin(setBranchApiKey);

export function getBranchApiKey(config: ExpoConfig) {
return config.android?.config?.branch?.apiKey ?? null;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/config/src/android/Facebook.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ExpoConfig } from '../Config.types';
import { assert } from '../Errors';
import { createAndroidManifestPlugin, createStringsXmlPlugin } from '../plugins/android-plugins';
import {
addMetaDataItemToMainApplication,
AndroidManifest,
Expand All @@ -21,6 +22,9 @@ const META_AUTO_INIT = 'com.facebook.sdk.AutoInitEnabled';
const META_AUTO_LOG_APP_EVENTS = 'com.facebook.sdk.AutoLogAppEventsEnabled';
const META_AD_ID_COLLECTION = 'com.facebook.sdk.AdvertiserIDCollectionEnabled';

export const withFacebookAppIdString = createStringsXmlPlugin(applyFacebookAppIdString);
export const withFacebookManifest = createAndroidManifestPlugin(setFacebookConfig);

function buildXMLItem({
head,
children,
Expand Down
3 changes: 3 additions & 0 deletions packages/config/src/android/GoogleMapsApiKey.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExpoConfig } from '../Config.types';
import { createAndroidManifestPlugin } from '../plugins/android-plugins';
import {
addMetaDataItemToMainApplication,
addUsesLibraryItemToMainApplication,
Expand All @@ -11,6 +12,8 @@ import {
const META_API_KEY = 'com.google.android.geo.API_KEY';
const LIB_HTTP = 'org.apache.http.legacy';

export const withGoogleMapsApiKey = createAndroidManifestPlugin(setGoogleMapsApiKey);

export function getGoogleMapsApiKey(config: Pick<ExpoConfig, 'android'>) {
return config.android?.config?.googleMaps?.apiKey ?? null;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/config/src/android/GoogleMobileAds.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExpoConfig } from '../Config.types';
import { createAndroidManifestPlugin } from '../plugins/android-plugins';
import {
addMetaDataItemToMainApplication,
AndroidManifest,
Expand All @@ -9,6 +10,8 @@ import {
const META_APPLICATION_ID = 'com.google.android.gms.ads.APPLICATION_ID';
const META_DELAY_APP_MEASUREMENT_INIT = 'com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT';

export const withGoogleMobileAdsConfig = createAndroidManifestPlugin(setGoogleMobileAdsConfig);

export function getGoogleMobileAdsAppId(config: Pick<ExpoConfig, 'android'>) {
return config.android?.config?.googleMobileAdsAppId ?? null;
}
Expand Down
45 changes: 45 additions & 0 deletions packages/config/src/android/GoogleServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import fs from 'fs-extra';
import { resolve } from 'path';

import { ExpoConfig } from '../Config.types';
import { ConfigPlugin } from '../Plugin.types';
import { addWarningAndroid } from '../WarningAggregator';
import {
withAppBuildGradle,
withDangerousAndroidMod,
withProjectBuildGradle,
} from '../plugins/android-plugins';

const DEFAULT_TARGET_PATH = './android/app/google-services.json';

Expand All @@ -11,6 +18,44 @@ const googleServicesPlugin = 'com.google.gms.google-services';
// NOTE(brentvatne): This may be annoying to keep up to date...
const googleServicesVersion = '4.3.3';

export const withClassPath: ConfigPlugin<void> = config => {
return withProjectBuildGradle(config, config => {
if (config.modResults.language === 'groovy') {
config.modResults.contents = setClassPath(config, config.modResults.contents);
} else {
addWarningAndroid(
'android-google-services',
`Cannot automatically configure project build.gradle if it's not groovy`
);
}
return config;
});
};

export const withApplyPlugin: ConfigPlugin<void> = config => {
return withAppBuildGradle(config, config => {
if (config.modResults.language === 'groovy') {
config.modResults.contents = applyPlugin(config, config.modResults.contents);
} else {
addWarningAndroid(
'android-google-services',
`Cannot automatically configure app build.gradle if it's not groovy`
);
}
return config;
});
};

/**
* Add `google-services.json` to project
*/
export const withGoogleServicesFile: ConfigPlugin<void> = config => {
return withDangerousAndroidMod(config, async config => {
await setGoogleServicesFile(config, config.modRequest.projectRoot);
return config;
});
};

export function getGoogleServicesFilePath(config: Pick<ExpoConfig, 'android'>) {
return config.android?.googleServicesFile ?? null;
}
Expand Down
109 changes: 57 additions & 52 deletions packages/config/src/android/Icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import fs from 'fs-extra';
import path from 'path';

import { ExpoConfig } from '../Config.types';
import { ConfigPlugin } from '../Plugin.types';
import { withDangerousAndroidMod } from '../plugins/android-plugins';
import * as Colors from './Colors';
import { buildResourceItem, readResourcesXMLAsync } from './Resources';
import { writeXMLAsync } from './XML';
Expand All @@ -28,6 +30,13 @@ const IC_LAUNCHER_FOREGROUND_PNG = 'ic_launcher_foreground.png';
const IC_LAUNCHER_XML = 'ic_launcher.xml';
const IC_LAUNCHER_ROUND_XML = 'ic_launcher_round.xml';

export const withIcons: ConfigPlugin<void> = config => {
return withDangerousAndroidMod(config, async config => {
await setIconAsync(config, config.modRequest.projectRoot);
return config;
});
};

export function getIcon(config: ExpoConfig) {
return config.icon || config.android?.icon || null;
}
Expand Down Expand Up @@ -85,76 +94,72 @@ async function configureLegacyIconAsync(
// backgroundImage overrides backgroundColor
backgroundColor = backgroundImage ? 'transparent' : backgroundColor ?? 'transparent';

try {
let squareIconImage: Buffer = (
let squareIconImage: Buffer = (
await generateImageAsync(
{ projectRoot, cacheType: 'android-standard-square' },
{
src: icon,
width: iconSizePx,
height: iconSizePx,
resizeMode: 'cover',
backgroundColor,
}
)
).source;
let roundIconImage: Buffer = (
await generateImageAsync(
{ projectRoot, cacheType: 'android-standard-circle' },
{
src: icon,
width: iconSizePx,
height: iconSizePx,
resizeMode: 'cover',
backgroundColor,
borderRadius: iconSizePx / 2,
}
)
).source;

if (backgroundImage) {
// Layer the buffers we just created on top of the background image that's provided
const squareBackgroundLayer = (
await generateImageAsync(
{ projectRoot, cacheType: 'android-standard-square' },
{ projectRoot, cacheType: 'android-standard-square-background' },
{
src: icon,
src: backgroundImage,
width: iconSizePx,
height: iconSizePx,
resizeMode: 'cover',
backgroundColor,
backgroundColor: 'transparent',
}
)
).source;
let roundIconImage: Buffer = (
const roundBackgroundLayer = (
await generateImageAsync(
{ projectRoot, cacheType: 'android-standard-circle' },
{ projectRoot, cacheType: 'android-standard-round-background' },
{
src: icon,
src: backgroundImage,
width: iconSizePx,
height: iconSizePx,
resizeMode: 'cover',
backgroundColor,
backgroundColor: 'transparent',
borderRadius: iconSizePx / 2,
}
)
).source;

if (backgroundImage) {
// Layer the buffers we just created on top of the background image that's provided
const squareBackgroundLayer = (
await generateImageAsync(
{ projectRoot, cacheType: 'android-standard-square-background' },
{
src: backgroundImage,
width: iconSizePx,
height: iconSizePx,
resizeMode: 'cover',
backgroundColor: 'transparent',
}
)
).source;
const roundBackgroundLayer = (
await generateImageAsync(
{ projectRoot, cacheType: 'android-standard-round-background' },
{
src: backgroundImage,
width: iconSizePx,
height: iconSizePx,
resizeMode: 'cover',
backgroundColor: 'transparent',
borderRadius: iconSizePx / 2,
}
)
).source;
squareIconImage = await compositeImagesAsync({
foreground: squareIconImage,
background: squareBackgroundLayer,
});
roundIconImage = await compositeImagesAsync({
foreground: roundIconImage,
background: roundBackgroundLayer,
});
}

await fs.ensureDir(dpiFolderPath);
await fs.writeFile(path.resolve(dpiFolderPath, IC_LAUNCHER_PNG), squareIconImage);
await fs.writeFile(path.resolve(dpiFolderPath, IC_LAUNCHER_ROUND_PNG), roundIconImage);
} catch (e) {
throw new Error('Encountered an issue resizing app icon: ' + e);
squareIconImage = await compositeImagesAsync({
foreground: squareIconImage,
background: squareBackgroundLayer,
});
roundIconImage = await compositeImagesAsync({
foreground: roundIconImage,
background: roundBackgroundLayer,
});
}

await fs.ensureDir(dpiFolderPath);
await fs.writeFile(path.resolve(dpiFolderPath, IC_LAUNCHER_PNG), squareIconImage);
await fs.writeFile(path.resolve(dpiFolderPath, IC_LAUNCHER_ROUND_PNG), roundIconImage);
})
);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/config/src/android/IntentFilters.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Android, AndroidIntentFiltersData, ExpoConfig } from '@expo/config-types';
import { Parser } from 'xml2js';

import { createAndroidManifestPlugin } from '../plugins/android-plugins';
import { AndroidManifest, getMainActivity } from './Manifest';

type AndroidIntentFilters = NonNullable<Android['intentFilters']>;
// TODO: make it so intent filters aren't written again if you run the command again

export const withAndroidIntentFilters = createAndroidManifestPlugin(setAndroidIntentFilters);

export function getIntentFilters(config: Pick<ExpoConfig, 'android'>): AndroidIntentFilters {
return config.android?.intentFilters ?? [];
}
Expand Down
Loading