import { ExpoConfig } from '@expo/config-types' ; import { createAndroidManifestPlugin } from '../plugins/android-plugins' ; import { addWarningAndroid } from '../utils/warnings' ; import { AndroidManifest , ManifestActivity } from './Manifest' ; export type IntentFilterProps = { actions : string [ ] ; categories : string [ ] ; schemes : string [ ] ; } ; export const withScheme = createAndroidManifestPlugin ( setScheme , 'withScheme' ) ; export function getScheme ( config ) { if ( Array . isArray ( config . scheme ) ) { const validate = ( value ) : value is string => typeof value === 'string' ; return config . scheme . filter < string > ( validate ) ; } else if ( typeof config . scheme === 'string' ) { return [ config . scheme ] ; } return [ ] ; } export function setScheme ( config , androidManifest ) { const schemes = [ ... getScheme ( config ) , ... getScheme ( config . android ?? { } ) , ] ; if ( config . android ?. package ) { schemes . push ( config . android . package ) ; } if ( schemes . length === 0 ) { return androidManifest ; } if ( ! ensureManifestHasValidIntentFilter ( androidManifest ) ) { addWarningAndroid ( 'scheme' , `Cannot add schemes because the provided manifest does not have a valid Activity with \`android:launchMode="singleTask"\`` , 'https://expo.fyi/setup-android-uri-scheme' ) ; return androidManifest ; } const currentSchemes = getSchemesFromManifest ( androidManifest ) ; for ( const uri of currentSchemes ) { const index = schemes . indexOf ( uri ) ; if ( index > - 1 ) schemes . splice ( index , 1 ) ; } for ( const uri of schemes ) { androidManifest = appendScheme ( uri , androidManifest ) ; } return androidManifest ; } function isValidRedirectIntentFilter ( { actions , categories , schemes } ) { return ( actions . includes ( 'android.intent.action.VIEW' ) && ! categories . includes ( 'android.intent.category.LAUNCHER' ) ) ; } function propertiesFromIntentFilter ( intentFilter ) { const actions = intentFilter ?. action ?. map ( ( data ) => data ?. $ ?. [ 'android:name' ] ) ?? [ ] ; const categories = intentFilter ?. category ?. map ( ( data ) => data ?. $ ?. [ 'android:name' ] ) ?? [ ] ; const schemes = intentFilter ?. data ?. map ( ( data ) => data ?. $ ?. [ 'android:scheme' ] ) ?? [ ] ; return { schemes , actions , categories } ; } function getSingleTaskIntentFilters ( androidManifest ) { if ( ! Array . isArray ( androidManifest . manifest . application ) ) return [ ] ; let outputSchemes = [ ] ; for ( const application of androidManifest . manifest . application ) { const { activity } = application ; const activities = Array . isArray ( activity ) ? activity : [ activity ] ; const singleTaskActivities = ( activities as ManifestActivity [ ] ) . filter ( activity => activity ?. $ ?. [ 'android:launchMode' ] === 'singleTask' ) ; for ( const activity of singleTaskActivities ) { const intentFilters = activity [ 'intent-filter' ] ; outputSchemes = outputSchemes . concat ( intentFilters ) ; } } return outputSchemes ; } export function getSchemesFromManifest ( androidManifest ) { const outputSchemes = [ ] ; const singleTaskIntentFilters = getSingleTaskIntentFilters ( androidManifest ) ; for ( const intentFilter of singleTaskIntentFilters ) { const properties = propertiesFromIntentFilter ( intentFilter ) ; if ( isValidRedirectIntentFilter ( properties ) ) { outputSchemes . push ( properties ) ; } } return outputSchemes . reduce < string [ ] > ( ( prev , { schemes } ) => [ ... prev , ... schemes ] , [ ] ) ; } export function ensureManifestHasValidIntentFilter ( androidManifest ) { if ( ! Array . isArray ( androidManifest . manifest . application ) ) { return false ; } for ( const application of androidManifest . manifest . application ) { for ( const activity of application . activity || [ ] ) { if ( activity ?. $ ?. [ 'android:launchMode' ] === 'singleTask' ) { for ( const intentFilter of activity [ 'intent-filter' ] || [ ] ) { const properties = propertiesFromIntentFilter ( intentFilter ) ; if ( isValidRedirectIntentFilter ( properties ) ) { return true ; } } if ( ! activity [ 'intent-filter' ] ) { activity [ 'intent-filter' ] = [ ] ; } activity [ 'intent-filter' ] . push ( { action : [ { $ : { 'android:name' : 'android.intent.action.VIEW' } } ] , category : [ { $ : { 'android:name' : 'android.intent.category.DEFAULT' } } , { $ : { 'android:name' : 'android.intent.category.BROWSABLE' } } , ] } ) ; return true ; } } } return false ; } export function hasScheme ( scheme , androidManifest ) { const schemes = getSchemesFromManifest ( androidManifest ) ; return schemes . includes ( scheme ) ; } export function appendScheme ( scheme , androidManifest ) { if ( ! Array . isArray ( androidManifest . manifest . application ) ) { return androidManifest ; } for ( const application of androidManifest . manifest . application ) { for ( const activity of application . activity || [ ] ) { if ( activity ?. $ ?. [ 'android:launchMode' ] === 'singleTask' ) { for ( const intentFilter of activity [ 'intent-filter' ] || [ ] ) { const properties = propertiesFromIntentFilter ( intentFilter ) ; if ( isValidRedirectIntentFilter ( properties ) ) { if ( ! intentFilter . data ) intentFilter . data = [ ] ; intentFilter . data . push ( { $ : { 'android:scheme' : scheme } } ) ; } } break ; } } } return androidManifest ; } export function removeScheme ( scheme , androidManifest ) { if ( ! Array . isArray ( androidManifest . manifest . application ) ) { return androidManifest ; } for ( const application of androidManifest . manifest . application ) { for ( const activity of application . activity || [ ] ) { if ( activity ?. $ ?. [ 'android:launchMode' ] === 'singleTask' ) { for ( const intentFilter of activity [ 'intent-filter' ] || [ ] ) { const properties = propertiesFromIntentFilter ( intentFilter ) ; if ( isValidRedirectIntentFilter ( properties ) ) { for ( const dataKey in intentFilter ?. data || [ ] ) { const data = intentFilter . data ?. [ dataKey ] ; if ( data ?. $ ?. [ 'android:scheme' ] === scheme ) { delete intentFilter . data ?. [ dataKey ] ; } } } } break ; } } } return androidManifest ; }