Adding same name entries into AndroidManifest.xml in managed workflow

Hey there, I would like to add more than one <activity> tags to my AndroidManifest, but I think since I have to add it under the main <application> tag I’m restricted to unique key names in JS’ object model

Is there a way around this? I have explored the xml2js docs and it seems like there is a method to do so in that, but still a bit clueless on how I’d pass that to the manifest in my custom expo plugin.

This is what I am attempting to add to Android Manifest:

      <activity
            android:exported="true"
            android:name="com.spotify.sdk.android.authentication.AuthCallbackActivity"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">

            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>

                <data
                    android:scheme="<YOUR_APPLICATION_SCHEME>"
                    android:host="<YOUR_APPLICATION_CALLBACK>"/>
            </intent-filter>
        </activity>

        <activity
            android:name="com.spotify.sdk.android.authentication.LoginActivity"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">
        </activity>

I am using an example this thread as a basis

  1. SDK Version: latest
  2. Platforms(Android/iOS/web/all): android
  3. Add the appropriate “Tag” based on what Expo library you have a question on.

Hi @lewistyler

The activities must be added to an array, not an object, so there’s no need for unique names. Although the way xml2js represents the XML is quite verbose, which makes it tricky to see what you need to do, you can take snippets of XML, parse it with xml2js and print it out to see what you need to build.

e.g.:

var parseString = require("xml2js").parseString;

var intentFilterXml = `
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>

                <data
                    android:scheme="<YOUR_APPLICATION_SCHEME>"
                    android:host="<YOUR_APPLICATION_CALLBACK>"/>
            </intent-filter>
`;

parseString(intentFilterXml, function (err, result) {
  console.log(JSON.stringify(result, null, 2));
});

The above prints out the following:

{
  "intent-filter": {
    "action": [
      {
        "$": {
          "android:name": "android.intent.action.VIEW"
        }
      }
    ],
    "category": [
      {
        "$": {
          "android:name": "android.intent.category.DEFAULT"
        }
      },
      {
        "$": {
          "android:name": "android.intent.category.BROWSABLE"
        }
      }
    ],
    "data": [
      {
        "$": {
          "android:scheme": "<YOUR_APPLICATION_SCHEME>",
          "android:host": "<YOUR_APPLICATION_CALLBACK>"
        }
      }
    ]
  }
}

As you can see there is an action array, a category array and a data array, which is how you are able to add more than one <category> tag to the <intent-filter> tag.

You could build up this intent datastructure something like this, for example:

function actionAttribute(action) {
  return {
    "android:name": `android.intent.action.${action}`,
  };
}

function intentFilterAction(action) {
  return {
    $: actionAttribute(action),
  };
}

function categoryAttribute(category) {
  return {
    "android:name": `android.intent.category.${category}`,
  };
}

function intentFilterCategory(category) {
  return {
    $: categoryAttribute(category),
  };
}

function intentFilterData(scheme, host) {
  return {
    $: {
      "android:scheme": scheme,
      "android:host": host,
    },
  };
}

function intentFilter(scheme, host) {
  return {
    "intent-filter": {
      action: [intentFilterAction("VIEW")],
      category: [
        intentFilterCategory("DEFAULT"),
        intentFilterCategory("BROWSABLE"),
      ],
      data: [intentFilterData(scheme, host)],
    },
  };
}

const authCallbackIntentFilter = intentFilter(
  "<YOUR_APPLICATION_SCHEME>",
  "<YOUR_APPLICATION_CALLBACK>"
);

console.log(JSON.stringify(authCallbackIntentFilter, null, 2));

If you log that, you’ll see that it produces the same output as the parsed XML.

You can build up the two activities in a similar way.

Then, like in my code from the other thread, you can find the activity array like this:

// [...]
  // Check if there are any activity tags
  let activities = application["activity"];
  if (!Array.isArray(activities)) {
    console.warn('addCustomActivityToMainApplication: No activity array in .MainApplication?');
    return androidManifest;
  }

And then you can push the two activities you built onto the array. But it’s probably best to check if they have already been added (for people using “expo prebuild”).

  // If we don't find the custom activity, add it
  // If we do find it, assume it's correct
  if (
    !activities.find(
      (item) =>
        item.$["android:name"] === "com.spotify.sdk.android.authentication.AuthCallbackActivity"
    )
  ) {
    activities.push(authCallbackActivity);
  }

  // If we don't find the custom activity, add it
  // If we do find it, assume it's correct
  if (
    !activities.find(
      (item) =>
        item.$["android:name"] === "com.spotify.sdk.android.authentication.LoginActivity"
    )
  ) {
    activities.push(loginActivity);
  }
1 Like

@wodin awesome, thank you. Have done a quick test and works a charm.

I’ll conclude the thread with a full example soon.

Thanks again for the great write-up - Expo has been a first class experience so far, it’s making mobile development very comfortable and fun and it is good to know help is available here, so this is really appreciated

1 Like