1import { ExpoConfig } from '@expo/config-types'; 2 3import { ConfigPlugin } from '../Plugin.types'; 4import { withAndroidManifest } from '../plugins/android-plugins'; 5import { AndroidManifest, ensureToolsAvailable, ManifestUsesPermission } from './Manifest'; 6 7const USES_PERMISSION = 'uses-permission'; 8 9export const withPermissions: ConfigPlugin<string[] | void> = (config, permissions) => { 10 if (Array.isArray(permissions)) { 11 permissions = permissions.filter(Boolean); 12 if (!config.android) config.android = {}; 13 if (!config.android.permissions) config.android.permissions = []; 14 config.android.permissions = [ 15 // @ts-ignore 16 ...new Set(config.android.permissions.concat(permissions)), 17 ]; 18 } 19 return withAndroidManifest(config, async (config) => { 20 config.modResults = await setAndroidPermissions(config, config.modResults); 21 return config; 22 }); 23}; 24 25/** Given a permission or list of permissions, block permissions in the final `AndroidManifest.xml` to ensure no installed library or plugin can add them. */ 26export const withBlockedPermissions: ConfigPlugin<string[] | string> = (config, permissions) => { 27 const resolvedPermissions = prefixAndroidPermissionsIfNecessary( 28 (Array.isArray(permissions) ? permissions : [permissions]).filter(Boolean) 29 ); 30 31 if (config?.android?.permissions && Array.isArray(config.android.permissions)) { 32 // Remove any static config permissions 33 config.android.permissions = prefixAndroidPermissionsIfNecessary( 34 config.android.permissions 35 ).filter((permission) => !resolvedPermissions.includes(permission)); 36 } 37 38 return withAndroidManifest(config, async (config) => { 39 config.modResults = ensureToolsAvailable(config.modResults); 40 config.modResults = addBlockedPermissions(config.modResults, resolvedPermissions); 41 return config; 42 }); 43}; 44 45export const withInternalBlockedPermissions: ConfigPlugin = (config) => { 46 // Only add permissions if the user defined the property and added some values 47 // this ensures we don't add the `tools:*` namespace extraneously. 48 if (config.android?.blockedPermissions?.length) { 49 return withBlockedPermissions(config, config.android.blockedPermissions); 50 } 51 52 return config; 53}; 54 55export function addBlockedPermissions(androidManifest: AndroidManifest, permissions: string[]) { 56 if (!Array.isArray(androidManifest.manifest['uses-permission'])) { 57 androidManifest.manifest['uses-permission'] = []; 58 } 59 60 for (const permission of prefixAndroidPermissionsIfNecessary(permissions)) { 61 androidManifest.manifest['uses-permission'] = ensureBlockedPermission( 62 androidManifest.manifest['uses-permission'], 63 permission 64 ); 65 } 66 67 return androidManifest; 68} 69 70/** 71 * Filter any existing permissions matching the provided permission name, then add a 72 * restricted permission to overwrite any extra permissions that may be added in a 73 * third-party package's AndroidManifest.xml. 74 * 75 * @param manifestPermissions manifest `uses-permissions` array. 76 * @param permission `android:name` of the permission to restrict 77 * @returns 78 */ 79function ensureBlockedPermission( 80 manifestPermissions: ManifestUsesPermission[], 81 permission: string 82) { 83 // Remove permission if it currently exists 84 manifestPermissions = manifestPermissions.filter((e) => e.$['android:name'] !== permission); 85 86 // Add a permission with tools:node to overwrite any existing permission and ensure it's removed upon building. 87 manifestPermissions.push({ 88 $: { 'android:name': permission, 'tools:node': 'remove' }, 89 }); 90 return manifestPermissions; 91} 92 93function prefixAndroidPermissionsIfNecessary(permissions: string[]): string[] { 94 return permissions.map((permission) => { 95 if (!permission.includes('.')) { 96 return `android.permission.${permission}`; 97 } 98 return permission; 99 }); 100} 101 102export function getAndroidPermissions(config: Pick<ExpoConfig, 'android'>): string[] { 103 return config.android?.permissions ?? []; 104} 105 106export function setAndroidPermissions( 107 config: Pick<ExpoConfig, 'android'>, 108 androidManifest: AndroidManifest 109) { 110 const permissions = getAndroidPermissions(config); 111 const providedPermissions = prefixAndroidPermissionsIfNecessary(permissions); 112 const permissionsToAdd = [...providedPermissions]; 113 114 if (!androidManifest.manifest.hasOwnProperty('uses-permission')) { 115 androidManifest.manifest['uses-permission'] = []; 116 } 117 // manifest.manifest['uses-permission'] = []; 118 119 const manifestPermissions = androidManifest.manifest['uses-permission'] ?? []; 120 121 permissionsToAdd.forEach((permission) => { 122 if (!isPermissionAlreadyRequested(permission, manifestPermissions)) { 123 addPermissionToManifest(permission, manifestPermissions); 124 } 125 }); 126 127 return androidManifest; 128} 129 130export function isPermissionAlreadyRequested( 131 permission: string, 132 manifestPermissions: ManifestUsesPermission[] 133): boolean { 134 return manifestPermissions.some((e) => e.$['android:name'] === permission); 135} 136 137export function addPermissionToManifest( 138 permission: string, 139 manifestPermissions: ManifestUsesPermission[] 140) { 141 manifestPermissions.push({ $: { 'android:name': permission } }); 142 return manifestPermissions; 143} 144 145export function removePermissions(androidManifest: AndroidManifest, permissionNames?: string[]) { 146 const targetNames = permissionNames ? permissionNames.map(ensurePermissionNameFormat) : null; 147 const permissions = androidManifest.manifest[USES_PERMISSION] || []; 148 const nextPermissions = []; 149 for (const attribute of permissions) { 150 if (targetNames) { 151 // @ts-ignore: name isn't part of the type 152 const value = attribute.$['android:name'] || attribute.$.name; 153 if (!targetNames.includes(value)) { 154 nextPermissions.push(attribute); 155 } 156 } 157 } 158 159 androidManifest.manifest[USES_PERMISSION] = nextPermissions; 160} 161 162export function addPermission(androidManifest: AndroidManifest, permissionName: string): void { 163 const usesPermissions: ManifestUsesPermission[] = androidManifest.manifest[USES_PERMISSION] || []; 164 usesPermissions.push({ 165 $: { 'android:name': permissionName }, 166 }); 167 androidManifest.manifest[USES_PERMISSION] = usesPermissions; 168} 169 170export function ensurePermissions( 171 androidManifest: AndroidManifest, 172 permissionNames: string[] 173): { [permission: string]: boolean } { 174 const permissions = getPermissions(androidManifest); 175 176 const results: { [permission: string]: boolean } = {}; 177 for (const permissionName of permissionNames) { 178 const targetName = ensurePermissionNameFormat(permissionName); 179 if (!permissions.includes(targetName)) { 180 addPermission(androidManifest, targetName); 181 results[permissionName] = true; 182 } else { 183 results[permissionName] = false; 184 } 185 } 186 return results; 187} 188 189export function ensurePermission( 190 androidManifest: AndroidManifest, 191 permissionName: string 192): boolean { 193 const permissions = getPermissions(androidManifest); 194 const targetName = ensurePermissionNameFormat(permissionName); 195 196 if (!permissions.includes(targetName)) { 197 addPermission(androidManifest, targetName); 198 return true; 199 } 200 return false; 201} 202 203export function ensurePermissionNameFormat(permissionName: string): string { 204 if (permissionName.includes('.')) { 205 const com = permissionName.split('.'); 206 const name = com.pop() as string; 207 return [...com, name.toUpperCase()].join('.'); 208 } else { 209 // If shorthand form like `WRITE_CONTACTS` is provided, expand it to `android.permission.WRITE_CONTACTS`. 210 return ensurePermissionNameFormat(`android.permission.${permissionName}`); 211 } 212} 213 214export function getPermissions(androidManifest: AndroidManifest): string[] { 215 const usesPermissions: { [key: string]: any }[] = androidManifest.manifest[USES_PERMISSION] || []; 216 const permissions = usesPermissions.map((permissionObject) => { 217 return permissionObject.$['android:name'] || permissionObject.$.name; 218 }); 219 return permissions; 220} 221