[Howto] Hack your way to support Datadog RUM RN module on Android with Expo SDK44

Hello,

I figured I’m not alone facing this, even though it should be fixed with the next Expo SDK release which is due sometime by next month.

I did install Datadog’s RN RUM Collection following their doc:

It did work right away with iOS, but it crashed the build for Android:

Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.5.1, expected version is 1.1.15.
[stderr] The class is loaded from /home/expo/.gradle/caches/transforms-3/ed0646599df2d1ac2019df6e9af2e8ac/transformed/jetified-kotlin-stdlib-1.5.31.jar!/kotlin/Unit.class
w: Detected multiple Kotlin daemon sessions at build/kotlin/sessions
[stderr] FAILURE: Build failed with an exception.

I figured that DD’s team wrote their plugin for the latest everything (RN/Kotlin/…), whereas Expo is a bit behind (as it’s a full ecosystem to keep in balance, it’s near impossible for them to keep it all bleeding edge.

First I followed an advice to use android.(production|staging|development).image = "latest" in eas.json, to make sure I’m using Java 11.

So looking it up on the interwebs, I came up with the following solution, which is to force kotlin’s version:

buildscript {
  ext {
    kotlinVersion = "1.5.10"
    kotlin_version = "1.5.10"
  }
}

in the project’s build.gradle (I put both because I’m not sure which one works, and I’ve seen both in the answers… :roll_eyes:). Hopefully, thanks to expo’s amazing work, you don’t need to eject to make it work, you can stay in managed mode and hack your way through using a plugin, so I wrote the following plugin:

import {
  ConfigPlugin,
  withProjectBuildGradle,
  withPlugins,
  createRunOncePlugin,
  WarningAggregator,
} from "@expo/config-plugins";

const setKotlinVersion = (buildGradle: string) => {
  // buildscript
  // https://stackoverflow.com/questions/67699823/module-was-compiled-with-an-incompatible-version-of-kotlin-the-binary-version-o
  return `${buildGradle}
buildscript {
  ext {
    kotlinVersion = "1.5.10"
    kotlin_version = "1.5.10"
  }
}
  `;
};

const withAndroidBuildGradleMods: ConfigPlugin = config => {
  return withProjectBuildGradle(config, config => {
    if (config.modResults.language === "groovy") {
      config.modResults.contents = setKotlinVersion(config.modResults.contents);
    } else {
      WarningAggregator.addWarningAndroid(
        "with-android-build-gradle",
        `Cannot automatically configure project build.gradle if it's not groovy`,
      );
    }
    return config;
  });
};

const withKotlinFix: ConfigPlugin = config => {
  return withPlugins(config, [
    withAndroidBuildGradleMods,
  ]);
};

export default createRunOncePlugin(withKotlinFix, "withKotlinFix", "0.0.1");

which I placed within $PROJECT_PATH/plugin/src/withKotlinFix.ts.

I was happy and thought the issue was fixed.


Until it crashed again with:

[stderr] > A failure occurred while executing com.android.build.gradle.internal.tasks.CheckAarMetadataWorkAction
[stderr]    > The minCompileSdk (31) specified in a
[stderr]      dependency's AAR metadata (META-INF/com/android/build/gradle/aar-metadata.properties)
[stderr]      is greater than this module's compileSdkVersion (android-30).
[stderr]      Dependency: androidx.work:work-runtime:2.7.0.
[stderr]      AAR metadata file: /home/expo/.gradle/caches/transforms-3/ffee51ff6db99f884b0e8a6a5d65a88b/transformed/work-runtime-2.7.0/META-INF/com/android/build/gradle/aar-metadata.properties.

Which I solved with another plugin, fixing the SDK version, plugins/src/withCustomAndroidVersion.ts:

import {
  withProjectBuildGradle,
  withPlugins,
  createRunOncePlugin,
  ConfigPlugin,
} from "@expo/config-plugins";

function setMinSdkVersion(buildGradle: string, targetVersion: number) {
  const regexpCurrentVersion = /\bminSdkVersion\s*=\s*(\d+)/;
  const match = buildGradle.match(regexpCurrentVersion);

  if (match) {
    const version = parseInt(match[1], 10);

    if (version < targetVersion) {
      buildGradle = buildGradle.replace(
        /\bminSdkVersion\s*=\s*\d+/,
        `minSdkVersion = ${targetVersion}`
      );
    } else {
      console.warn(`WARN: minSdkVersion is already >= ${version}`);
    }
  }

  return buildGradle;
}

function setCompileSdkVersion(buildGradle: string, targetVersion: number) {
  const regexpCurrentVersion = /\bcompileSdkVersion\s*=\s*(\d+)/;
  const match = buildGradle.match(regexpCurrentVersion);

  if (match) {
    const version = parseInt(match[1], 10);

    if (version < targetVersion) {
      buildGradle = buildGradle.replace(
        /\bcompileSdkVersion\s*=\s*\d+/,
        `compileSdkVersion = ${targetVersion}`
      );
    } else {
      console.warn(`WARN: compileSdkVersion is already >= ${version}`);
    }
  }

  return buildGradle;
}

function setTargetSdkVersion(buildGradle: string, targetVersion: number) {
  const regexpCurrentVersion = /\btargetSdkVersion\s*=\s*(\d+)/;
  const match = buildGradle.match(regexpCurrentVersion);

  if (match) {
    const version = parseInt(match[1], 10);

    if (version < targetVersion) {
      buildGradle = buildGradle.replace(
        /\btargetSdkVersion\s*=\s*\d+/,
        `targetSdkVersion = ${targetVersion}`
      );
    } else {
      console.warn(`WARN: targetSdkVersion is already >= ${version}`);
    }
  }

  return buildGradle;
}

const withAndroidSdkVersions: ConfigPlugin<{
  minSdkVersion?: number;
  compileSdkVersion?: number;
  targetSdkVersion?: number;
}> = (config, {
  minSdkVersion,
  compileSdkVersion,
  targetSdkVersion
}) => {
  return withProjectBuildGradle(config, (config) => {
    if (config.modResults.language !== 'groovy')
      throw new Error("Can't use withAndroidSdkVersions EAS Plugin as build.gradle is not groovy");

    if (minSdkVersion)
      config.modResults.contents = setMinSdkVersion(config.modResults.contents, minSdkVersion);
    if (compileSdkVersion)
      config.modResults.contents = setCompileSdkVersion(config.modResults.contents, compileSdkVersion);
    if (targetSdkVersion)
      config.modResults.contents = setTargetSdkVersion(config.modResults.contents, targetSdkVersion);

    return config;
  });
};

const withCustomAndroidVersion: ConfigPlugin = (config, props) => {
  return withPlugins(config, [
    [withAndroidSdkVersions, props],
  ]);
}

export default createRunOncePlugin(withCustomAndroidVersion, "withCustomAndroidVersion", "0.0.1");

N.B.: here’s an alternative way to do the above

But that was not enough, so I also forced the WorkManager version:

import {
  ConfigPlugin,
  withAppBuildGradle,
  withPlugins,
  createRunOncePlugin,
  WarningAggregator,
} from "@expo/config-plugins";

const setWorkManagerVersion = (buildGradle: string) => {
  return `${buildGradle}
dependencies {
  def work_version = "2.7.1"
  // Force WorkManager 2.6.0 for transitive dependency
  implementation("androidx.work:work-runtime-ktx:$work_version") {
      force = true
  }
  implementation("androidx.work:work-runtime:$work_version") {
      force = true
  }
}
  `;
}

const withAndroidBuildGradleMods: ConfigPlugin = config => {
  return withAppBuildGradle(config, config => {
    if (config.modResults.language === "groovy") {
      config.modResults.contents = setWorkManagerVersion(config.modResults.contents);
    } else {
      WarningAggregator.addWarningAndroid(
        "with-android-build-gradle",
        `Cannot automatically configure project build.gradle if it's not groovy`,
      );
    }
    return config;
  });
};

const withWorkManagerVersionFix: ConfigPlugin = config => {
  return withPlugins(config, [
    withAndroidBuildGradleMods,
  ]);
};

export default createRunOncePlugin(withWorkManagerVersionFix, "withWorkManagerVersionFix", "0.0.1");

Hopefully that’d be all.


And it was, I finally had the green light on the Android build… but a huge fail when submitting to Google Play. It rejected me saying:

Google Api Error: Invalid request - You uploaded an APK or Android App Bundle which has an activity, activity alias, service or broadcast receiver with intent filter, but without 'android:exported' property set

:face_with_symbols_over_mouth:

So I did two patches, one is to add android:exported in my app’s AndroidManifest.xml:

import {
  ConfigPlugin,
  withAndroidManifest,
  withPlugins,
  createRunOncePlugin,
  WarningAggregator,
} from "@expo/config-plugins";

const withAndroidManifestFix: ConfigPlugin = config => {
  return withAndroidManifest(config, config => {
    if (config.modResults.manifest.application?.[0]?.activity?.[0]?.$) {
      config.modResults.manifest.application[0].activity[0].$["android:exported"] = "true";
    } else {
      WarningAggregator.addWarningAndroid(
        "with-android-manifest",
        `Cannot automatically configure AndroidManifest if there's no activity`,
      );
    }
    return config;
  });
};

const withAndroidManifestFix: ConfigPlugin = config => {
  return withPlugins(config, [
    withAndroidManifestFix,
  ]);
};

export default createRunOncePlugin(withAndroidManifestFix, "withAndroidManifestFix", "0.0.1");

And the other was to use patch-package, edit the expo-dev-launcher's AndroidManifest.xml file, and insert android:exported="true" within the <activity /> tag, i.e.:

% yarn add patch-package postinstall-postinstall
% vim node_modules/expo-dev-launcher/android/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="expo.modules.devlauncher">

  <application>
    <activity
      android:name="expo.modules.devlauncher.launcher.DevLauncherActivity"
      android:screenOrientation="portrait"
      android:theme="@style/Theme.DevLauncher.LauncherActivity"
      android:launchMode="singleTask"
      android:exported="true"
      >
      <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
% yarn run patch-package expo-dev-launcher
% git add yarn.lock package.json patches
% git commit -m "Adds patch on expo-dev-launcher"

Then I did try again, and voilà the build and submit worked! :partying_face:

And to orchestrate all my plugins, here’s the plugins field of my app.config.js:

plugins: [
    /*
     * The following four plugins are there to fix issues with RN DD RUM
     * They should be solved (hopefully) with the next SDK release of Expo.
     */
    "./plugin/build/withKotlinFix",
    [
      "./plugin/build/withCustomAndroidVersion",
      {
        // minSdkVersion: 24,
        "compileSdkVersion": 31,
        "targetSdkVersion": 31
      }
    ],
    "./plugin/build/withWorkManagerVersionFix",
    "./plugin/build/withAndroidManifestFix",
],

The above changes are what I did to make it work, it might be possible some steps are not necessary, but because it takes a lot of time to do a full build cycle to test it through, and because I was in a hurry to publish my app, I did not take time to have a scientific approach and make sure what steps really are necessary. I leave that as an exercice to the reader :joy:.


N.B.: Because I always do TS whenever possible, to use the above plugins, you need to add the following tsconfig.json within ./plugins and make sure tsc --build plugins is ran in a yarn postinstall hook :

{
  "extends": "expo-module-scripts/tsconfig.plugin",
  "compilerOptions": {
    "outDir": "build",
    "rootDir": "src",
    "declaration": true
  },
  "include": ["./src"]
}

update: after one day of test with my team, we noticed that the app crashes on Android 12, with the following error:

04-20 15:47:50.951 15318 15344 E AndroidRuntime: java.lang.IllegalArgumentException: com.app.expo.my: Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent.
04-20 15:47:50.951 15318 15344 E AndroidRuntime: Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality depends on the PendingIntent being mutable, e.g. if it needs to be used with inline replies or bubbles.
04-20 15:47:50.951 15318 15344 E AndroidRuntime:    at android.app.PendingIntent.checkFlags(PendingIntent.java:378)
04-20 15:47:50.951 15318 15344 E AndroidRuntime:    at android.app.PendingIntent.getBroadcastAsUser(PendingIntent.java:648)
04-20 15:47:50.951 15318 15344 E AndroidRuntime:    at android.app.PendingIntent.getBroadcast(PendingIntent.java:635)

:face_with_symbols_over_mouth:

I’m still investigating

the solution has been to force work manager stuff to 2.7.1. I’m updating the origin post.

Have y’all tried pinning the sdk to 1.0.0-rc3?