1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types'; 2082815dcSEvan Bacon 3*8a424bebSJames Ideimport { AndroidManifest, ensureToolsAvailable, ManifestUsesPermission } from './Manifest'; 4082815dcSEvan Baconimport { ConfigPlugin } from '../Plugin.types'; 5082815dcSEvan Baconimport { withAndroidManifest } from '../plugins/android-plugins'; 6082815dcSEvan Bacon 7082815dcSEvan Baconconst USES_PERMISSION = 'uses-permission'; 8082815dcSEvan Bacon 9082815dcSEvan Baconexport const withPermissions: ConfigPlugin<string[] | void> = (config, permissions) => { 10082815dcSEvan Bacon if (Array.isArray(permissions)) { 11082815dcSEvan Bacon permissions = permissions.filter(Boolean); 12082815dcSEvan Bacon if (!config.android) config.android = {}; 13082815dcSEvan Bacon if (!config.android.permissions) config.android.permissions = []; 14082815dcSEvan Bacon config.android.permissions = [ 15082815dcSEvan Bacon // @ts-ignore 16082815dcSEvan Bacon ...new Set(config.android.permissions.concat(permissions)), 17082815dcSEvan Bacon ]; 18082815dcSEvan Bacon } 19082815dcSEvan Bacon return withAndroidManifest(config, async (config) => { 20082815dcSEvan Bacon config.modResults = await setAndroidPermissions(config, config.modResults); 21082815dcSEvan Bacon return config; 22082815dcSEvan Bacon }); 23082815dcSEvan Bacon}; 24082815dcSEvan Bacon 25082815dcSEvan Bacon/** Given a permission or list of permissions, block permissions in the final `AndroidManifest.xml` to ensure no installed library or plugin can add them. */ 26082815dcSEvan Baconexport const withBlockedPermissions: ConfigPlugin<string[] | string> = (config, permissions) => { 278828a4a3SCedric van Putten const resolvedPermissions = prefixAndroidPermissionsIfNecessary( 288828a4a3SCedric van Putten (Array.isArray(permissions) ? permissions : [permissions]).filter(Boolean) 29082815dcSEvan Bacon ); 30082815dcSEvan Bacon 31082815dcSEvan Bacon if (config?.android?.permissions && Array.isArray(config.android.permissions)) { 32082815dcSEvan Bacon // Remove any static config permissions 338828a4a3SCedric van Putten config.android.permissions = prefixAndroidPermissionsIfNecessary( 348828a4a3SCedric van Putten config.android.permissions 358828a4a3SCedric van Putten ).filter((permission) => !resolvedPermissions.includes(permission)); 36082815dcSEvan Bacon } 37082815dcSEvan Bacon 38082815dcSEvan Bacon return withAndroidManifest(config, async (config) => { 39082815dcSEvan Bacon config.modResults = ensureToolsAvailable(config.modResults); 40082815dcSEvan Bacon config.modResults = addBlockedPermissions(config.modResults, resolvedPermissions); 41082815dcSEvan Bacon return config; 42082815dcSEvan Bacon }); 43082815dcSEvan Bacon}; 44082815dcSEvan Bacon 45082815dcSEvan Baconexport const withInternalBlockedPermissions: ConfigPlugin = (config) => { 46082815dcSEvan Bacon // Only add permissions if the user defined the property and added some values 47082815dcSEvan Bacon // this ensures we don't add the `tools:*` namespace extraneously. 48082815dcSEvan Bacon if (config.android?.blockedPermissions?.length) { 49082815dcSEvan Bacon return withBlockedPermissions(config, config.android.blockedPermissions); 50082815dcSEvan Bacon } 51082815dcSEvan Bacon 52082815dcSEvan Bacon return config; 53082815dcSEvan Bacon}; 54082815dcSEvan Bacon 55082815dcSEvan Baconexport function addBlockedPermissions(androidManifest: AndroidManifest, permissions: string[]) { 56082815dcSEvan Bacon if (!Array.isArray(androidManifest.manifest['uses-permission'])) { 57082815dcSEvan Bacon androidManifest.manifest['uses-permission'] = []; 58082815dcSEvan Bacon } 59082815dcSEvan Bacon 608828a4a3SCedric van Putten for (const permission of prefixAndroidPermissionsIfNecessary(permissions)) { 61082815dcSEvan Bacon androidManifest.manifest['uses-permission'] = ensureBlockedPermission( 62082815dcSEvan Bacon androidManifest.manifest['uses-permission'], 63082815dcSEvan Bacon permission 64082815dcSEvan Bacon ); 65082815dcSEvan Bacon } 66082815dcSEvan Bacon 67082815dcSEvan Bacon return androidManifest; 68082815dcSEvan Bacon} 69082815dcSEvan Bacon 70082815dcSEvan Bacon/** 71082815dcSEvan Bacon * Filter any existing permissions matching the provided permission name, then add a 72082815dcSEvan Bacon * restricted permission to overwrite any extra permissions that may be added in a 73082815dcSEvan Bacon * third-party package's AndroidManifest.xml. 74082815dcSEvan Bacon * 75082815dcSEvan Bacon * @param manifestPermissions manifest `uses-permissions` array. 76082815dcSEvan Bacon * @param permission `android:name` of the permission to restrict 77082815dcSEvan Bacon * @returns 78082815dcSEvan Bacon */ 79082815dcSEvan Baconfunction ensureBlockedPermission( 80082815dcSEvan Bacon manifestPermissions: ManifestUsesPermission[], 81082815dcSEvan Bacon permission: string 82082815dcSEvan Bacon) { 83082815dcSEvan Bacon // Remove permission if it currently exists 84082815dcSEvan Bacon manifestPermissions = manifestPermissions.filter((e) => e.$['android:name'] !== permission); 85082815dcSEvan Bacon 86082815dcSEvan Bacon // Add a permission with tools:node to overwrite any existing permission and ensure it's removed upon building. 87082815dcSEvan Bacon manifestPermissions.push({ 88082815dcSEvan Bacon $: { 'android:name': permission, 'tools:node': 'remove' }, 89082815dcSEvan Bacon }); 90082815dcSEvan Bacon return manifestPermissions; 91082815dcSEvan Bacon} 92082815dcSEvan Bacon 93082815dcSEvan Baconfunction prefixAndroidPermissionsIfNecessary(permissions: string[]): string[] { 94082815dcSEvan Bacon return permissions.map((permission) => { 95082815dcSEvan Bacon if (!permission.includes('.')) { 96082815dcSEvan Bacon return `android.permission.${permission}`; 97082815dcSEvan Bacon } 98082815dcSEvan Bacon return permission; 99082815dcSEvan Bacon }); 100082815dcSEvan Bacon} 101082815dcSEvan Bacon 102082815dcSEvan Baconexport function getAndroidPermissions(config: Pick<ExpoConfig, 'android'>): string[] { 103082815dcSEvan Bacon return config.android?.permissions ?? []; 104082815dcSEvan Bacon} 105082815dcSEvan Bacon 106082815dcSEvan Baconexport function setAndroidPermissions( 107082815dcSEvan Bacon config: Pick<ExpoConfig, 'android'>, 108082815dcSEvan Bacon androidManifest: AndroidManifest 109082815dcSEvan Bacon) { 110082815dcSEvan Bacon const permissions = getAndroidPermissions(config); 111082815dcSEvan Bacon const providedPermissions = prefixAndroidPermissionsIfNecessary(permissions); 112082815dcSEvan Bacon const permissionsToAdd = [...providedPermissions]; 113082815dcSEvan Bacon 114082815dcSEvan Bacon if (!androidManifest.manifest.hasOwnProperty('uses-permission')) { 115082815dcSEvan Bacon androidManifest.manifest['uses-permission'] = []; 116082815dcSEvan Bacon } 117082815dcSEvan Bacon // manifest.manifest['uses-permission'] = []; 118082815dcSEvan Bacon 119082815dcSEvan Bacon const manifestPermissions = androidManifest.manifest['uses-permission'] ?? []; 120082815dcSEvan Bacon 121082815dcSEvan Bacon permissionsToAdd.forEach((permission) => { 122082815dcSEvan Bacon if (!isPermissionAlreadyRequested(permission, manifestPermissions)) { 123082815dcSEvan Bacon addPermissionToManifest(permission, manifestPermissions); 124082815dcSEvan Bacon } 125082815dcSEvan Bacon }); 126082815dcSEvan Bacon 127082815dcSEvan Bacon return androidManifest; 128082815dcSEvan Bacon} 129082815dcSEvan Bacon 130082815dcSEvan Baconexport function isPermissionAlreadyRequested( 131082815dcSEvan Bacon permission: string, 132082815dcSEvan Bacon manifestPermissions: ManifestUsesPermission[] 133082815dcSEvan Bacon): boolean { 134082815dcSEvan Bacon return manifestPermissions.some((e) => e.$['android:name'] === permission); 135082815dcSEvan Bacon} 136082815dcSEvan Bacon 137082815dcSEvan Baconexport function addPermissionToManifest( 138082815dcSEvan Bacon permission: string, 139082815dcSEvan Bacon manifestPermissions: ManifestUsesPermission[] 140082815dcSEvan Bacon) { 141082815dcSEvan Bacon manifestPermissions.push({ $: { 'android:name': permission } }); 142082815dcSEvan Bacon return manifestPermissions; 143082815dcSEvan Bacon} 144082815dcSEvan Bacon 145082815dcSEvan Baconexport function removePermissions(androidManifest: AndroidManifest, permissionNames?: string[]) { 146082815dcSEvan Bacon const targetNames = permissionNames ? permissionNames.map(ensurePermissionNameFormat) : null; 147082815dcSEvan Bacon const permissions = androidManifest.manifest[USES_PERMISSION] || []; 148082815dcSEvan Bacon const nextPermissions = []; 149082815dcSEvan Bacon for (const attribute of permissions) { 150082815dcSEvan Bacon if (targetNames) { 151082815dcSEvan Bacon // @ts-ignore: name isn't part of the type 152082815dcSEvan Bacon const value = attribute.$['android:name'] || attribute.$.name; 153082815dcSEvan Bacon if (!targetNames.includes(value)) { 154082815dcSEvan Bacon nextPermissions.push(attribute); 155082815dcSEvan Bacon } 156082815dcSEvan Bacon } 157082815dcSEvan Bacon } 158082815dcSEvan Bacon 159082815dcSEvan Bacon androidManifest.manifest[USES_PERMISSION] = nextPermissions; 160082815dcSEvan Bacon} 161082815dcSEvan Bacon 162082815dcSEvan Baconexport function addPermission(androidManifest: AndroidManifest, permissionName: string): void { 163082815dcSEvan Bacon const usesPermissions: ManifestUsesPermission[] = androidManifest.manifest[USES_PERMISSION] || []; 164082815dcSEvan Bacon usesPermissions.push({ 165082815dcSEvan Bacon $: { 'android:name': permissionName }, 166082815dcSEvan Bacon }); 167082815dcSEvan Bacon androidManifest.manifest[USES_PERMISSION] = usesPermissions; 168082815dcSEvan Bacon} 169082815dcSEvan Bacon 170082815dcSEvan Baconexport function ensurePermissions( 171082815dcSEvan Bacon androidManifest: AndroidManifest, 172082815dcSEvan Bacon permissionNames: string[] 173082815dcSEvan Bacon): { [permission: string]: boolean } { 174082815dcSEvan Bacon const permissions = getPermissions(androidManifest); 175082815dcSEvan Bacon 176082815dcSEvan Bacon const results: { [permission: string]: boolean } = {}; 177082815dcSEvan Bacon for (const permissionName of permissionNames) { 178082815dcSEvan Bacon const targetName = ensurePermissionNameFormat(permissionName); 179082815dcSEvan Bacon if (!permissions.includes(targetName)) { 180082815dcSEvan Bacon addPermission(androidManifest, targetName); 181082815dcSEvan Bacon results[permissionName] = true; 182082815dcSEvan Bacon } else { 183082815dcSEvan Bacon results[permissionName] = false; 184082815dcSEvan Bacon } 185082815dcSEvan Bacon } 186082815dcSEvan Bacon return results; 187082815dcSEvan Bacon} 188082815dcSEvan Bacon 189082815dcSEvan Baconexport function ensurePermission( 190082815dcSEvan Bacon androidManifest: AndroidManifest, 191082815dcSEvan Bacon permissionName: string 192082815dcSEvan Bacon): boolean { 193082815dcSEvan Bacon const permissions = getPermissions(androidManifest); 194082815dcSEvan Bacon const targetName = ensurePermissionNameFormat(permissionName); 195082815dcSEvan Bacon 196082815dcSEvan Bacon if (!permissions.includes(targetName)) { 197082815dcSEvan Bacon addPermission(androidManifest, targetName); 198082815dcSEvan Bacon return true; 199082815dcSEvan Bacon } 200082815dcSEvan Bacon return false; 201082815dcSEvan Bacon} 202082815dcSEvan Bacon 203082815dcSEvan Baconexport function ensurePermissionNameFormat(permissionName: string): string { 204082815dcSEvan Bacon if (permissionName.includes('.')) { 205082815dcSEvan Bacon const com = permissionName.split('.'); 206082815dcSEvan Bacon const name = com.pop() as string; 207082815dcSEvan Bacon return [...com, name.toUpperCase()].join('.'); 208082815dcSEvan Bacon } else { 209082815dcSEvan Bacon // If shorthand form like `WRITE_CONTACTS` is provided, expand it to `android.permission.WRITE_CONTACTS`. 210082815dcSEvan Bacon return ensurePermissionNameFormat(`android.permission.${permissionName}`); 211082815dcSEvan Bacon } 212082815dcSEvan Bacon} 213082815dcSEvan Bacon 214082815dcSEvan Baconexport function getPermissions(androidManifest: AndroidManifest): string[] { 215082815dcSEvan Bacon const usesPermissions: { [key: string]: any }[] = androidManifest.manifest[USES_PERMISSION] || []; 216082815dcSEvan Bacon const permissions = usesPermissions.map((permissionObject) => { 217082815dcSEvan Bacon return permissionObject.$['android:name'] || permissionObject.$.name; 218082815dcSEvan Bacon }); 219082815dcSEvan Bacon return permissions; 220082815dcSEvan Bacon} 221