Skip to content

[Messaging] Include theme in AndroidManifest #1267

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 2 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ Release Notes
- Messaging (Android): Fix issue with `SubscribeAsync` task not completing when
a cached token is available.
([#1245](https://github.com/firebase/firebase-unity-sdk/issues/1245)).
- Messaging (Android): Fix issue with missing theme causing a crash on Unity 6.
([#1229](https://github.com/firebase/firebase-unity-sdk/issues/1229))

### 12.9.0
- Changes
Expand Down
1 change: 1 addition & 0 deletions messaging/activity/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
UnityPlayerActivity to work around a known issue when receiving
notification data payloads in the background. -->
<activity android:name="com.google.firebase.MessagingUnityPlayerActivity"
android:theme="@style/UnityThemeSelector"
android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
215 changes: 144 additions & 71 deletions messaging/activity/FirebaseMessagingActivityGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,26 @@
*/
using System.IO;
using System.Linq;
using System.Xml;
using UnityEngine;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;

namespace Firebase.Messaging.Editor {

// Handles the generation of the MessagingUnityPlayerActivity java file.
// Note this regenerates the file every time an Android build occurs,
// but local changes can be preserved by using the PreserveTag below.
// This is needed because the source code needs to be present to work across
// different Unity versions, due to changes in mUnityPlayer.
// It also adjusts the base class of the file based on if GameActivity is being
// used (a new feature in Unity 2023).
public class FirebaseMessagingActivityGenerator : IPreprocessBuildWithReport {
// TODO: Ideally this should use a template file, the tricky part is locating
// the template file when it is either in the Assets path, or the Packages path.
// There are some similar cases in EDM4U, so a solution might be to use that.
private readonly string[] ActivityClassContents = new string[]{
// Handles the generation of the MessagingUnityPlayerActivity java file.
// Note this regenerates the file every time an Android build occurs,
// but local changes can be preserved by using the PreserveTag below.
// This is needed because the source code needs to be present to work across
// different Unity versions, due to changes in mUnityPlayer.
// It also adjusts the base class of the file based on if GameActivity is being
// used (a new feature in Unity 2023).
public class FirebaseMessagingActivityGenerator : IPreprocessBuildWithReport {
// TODO: Ideally this should use a template file, the tricky part is locating
// the template file when it is either in the Assets path, or the Packages path.
// There are some similar cases in EDM4U, so a solution might be to use that.
private readonly string[] ActivityClassContents = new string[]{
"/*",
" * This file is generated by the FirebaseMessagingActivityGenerator script.",
" * Refer to that script for more information.",
Expand Down Expand Up @@ -116,83 +117,155 @@ public class FirebaseMessagingActivityGenerator : IPreprocessBuildWithReport {
" super.onCreate(savedInstanceState);",
" }}",
"}}"
};
private readonly string BaseActivityClass = "UnityPlayerActivity";
};
private readonly string BaseActivityClass = "UnityPlayerActivity";
#if UNITY_2023_1_OR_NEWER
private readonly string BaseGameActivityClass = "UnityPlayerGameActivity";
private readonly string BaseGameActivityClass = "UnityPlayerGameActivity";
#endif

#if UNITY_2023_1_OR_NEWER
private readonly string UnityPlayerQuitFunction = "destroy";
private readonly string UnityPlayerQuitFunction = "destroy";
#else
private readonly string UnityPlayerQuitFunction = "quit";
private readonly string UnityPlayerQuitFunction = "quit";
#endif

private readonly string GeneratedFileTag = "FirebaseMessagingActivityGenerated";
// If this tag is present on the generated file, it will not be replaced.
private readonly string PreserveTag = "FirebasePreserve";
private readonly string GeneratedFileTag = "FirebaseMessagingActivityGenerated";
// If this tag is present on the generated file, it will not be replaced.
private readonly string PreserveTag = "FirebasePreserve";

private readonly string OutputPath = Path.Combine("Plugins", "Android");
private readonly string OutputFilename = "MessagingUnityPlayerActivity.java";
private readonly string OutputPath = Path.Combine("Plugins", "Android");
private readonly string OutputFilename = "MessagingUnityPlayerActivity.java";

public int callbackOrder { get { return 0; } }
public void OnPreprocessBuild(BuildReport report) {
// Only run this logic when building for Android.
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) {
return;
}
public int callbackOrder { get { return 0; } }
public void OnPreprocessBuild(BuildReport report) {
// Only run this logic when building for Android.
if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) {
return;
}

// Determine what the contents of the generated file should be.
string baseClass = BaseActivityClass;
// Determine what the contents of the generated file should be.
string baseClass = BaseActivityClass;
#if UNITY_2023_1_OR_NEWER
// If using the new GameActivity logic, we want to generate with that base class.
if (PlayerSettings.Android.applicationEntry.HasFlag(AndroidApplicationEntry.GameActivity)) {
baseClass = BaseGameActivityClass;
// If using the new GameActivity logic, we want to generate with that base class.
if (PlayerSettings.Android.applicationEntry.HasFlag(AndroidApplicationEntry.GameActivity)) {
baseClass = BaseGameActivityClass;
}
#endif
string fileContents = System.String.Format(System.String.Join("\n", ActivityClassContents),
baseClass, UnityPlayerQuitFunction);

// Check if the file has already been generated.
string[] oldAssetGuids = AssetDatabase.FindAssets("l:" + GeneratedFileTag);
if (oldAssetGuids != null && oldAssetGuids.Length > 0) {
if (oldAssetGuids.Length != 1) {
Debug.LogWarning("FirebaseMessagingActivityEditor found multiple generated files with the label: " +
GeneratedFileTag + " \n" +
"No changes will be made, but this can potentially cause problems on Android with duplicate classes.\n" +
"Please check for duplicate classes, and remove any unnecessary uses of the label.");
return;
}
string oldAssetPath = AssetDatabase.GUIDToAssetPath(oldAssetGuids[0]);
Object oldAsset = AssetDatabase.LoadMainAssetAtPath(oldAssetPath);
if (oldAsset != null) {
string oldAssetFullPath = Path.Combine(Application.dataPath, "..", oldAssetPath);
string oldFileContents = System.IO.File.ReadAllText(oldAssetFullPath);
// If the old file matches what we would generate, exit early.
if (oldFileContents == fileContents) {
return;
}
// If the generated file has been tagged to be preserved, don't change it.
string[] labelList = AssetDatabase.GetLabels(oldAsset);
if (labelList.Contains(PreserveTag)) {
return;
}
// Delete the old asset.
Debug.Log("Changes detected, regenerating " + oldAssetPath + "\n" +
"To preserve local changes to that file, add the label: " + PreserveTag);
AssetDatabase.DeleteAsset(oldAssetPath);
}
}

// Generate the new file.
string newAssetFullDirectory = Path.Combine(Application.dataPath, OutputPath);
System.IO.Directory.CreateDirectory(newAssetFullDirectory);
System.IO.File.WriteAllText(Path.Combine(newAssetFullDirectory, OutputFilename), fileContents);
string newAssetLocalPath = Path.Combine("Assets", OutputPath, OutputFilename);
AssetDatabase.ImportAsset(newAssetLocalPath);
Object newAsset = AssetDatabase.LoadMainAssetAtPath(newAssetLocalPath);
AssetDatabase.SetLabels(newAsset, new[]{GeneratedFileTag});
}
}

// Handles adding the android:theme to the AndroidManifest file if it isn't present,
// and swaps it on Unity versions that support GameActivity, which uses a different theme
// by default.
public class FirebaseMessagingAndroidManifestModifier : IPreprocessBuildWithReport {
// Hard coded directories and file names.
private static readonly string ANDROID_MANIFEST_DIRECTORY =
Path.Combine(Path.Combine("Assets", "Plugins"), "Android");
private static readonly string ANDROID_MANIFEST_FILE = "AndroidManifest.xml";
private static readonly string ANDROID_MANIFEST_PATH =
Path.Combine(ANDROID_MANIFEST_DIRECTORY, ANDROID_MANIFEST_FILE);

private static readonly string MESSAGING_ACTIVITY = "com.google.firebase.MessagingUnityPlayerActivity";

private static readonly string DEFAULT_THEME = "@style/UnityThemeSelector";
#if UNITY_2023_1_OR_NEWER
private static readonly string DEFAULT_GAME_THEME = "@style/BaseUnityGameActivityTheme";
#endif
string fileContents = System.String.Format(System.String.Join("\n", ActivityClassContents),
baseClass, UnityPlayerQuitFunction);

// Check if the file has already been generated.
string[] oldAssetGuids = AssetDatabase.FindAssets("l:" + GeneratedFileTag);
if (oldAssetGuids != null && oldAssetGuids.Length > 0) {
if (oldAssetGuids.Length != 1) {
Debug.LogWarning("FirebaseMessagingActivityEditor found multiple generated files with the label: " +
GeneratedFileTag + " \n" +
"No changes will be made, but this can potentially cause problems on Android with duplicate classes.\n" +
"Please check for duplicate classes, and remove any unnecessary uses of the label.");

public int callbackOrder { get { return 0; } }
public void OnPreprocessBuild(BuildReport report) {
string projectDir = Path.Combine(Application.dataPath, "..");
string manifestFile = Path.Combine(projectDir, ANDROID_MANIFEST_PATH);

if (!File.Exists(manifestFile)) {
// Unable to locate the file, so exit early.
return;
}

var manifest = new XmlDocument();
manifest.Load(manifestFile);

CheckTheme(manifest);

manifest.Save(manifestFile);
}

private void CheckTheme(XmlDocument manifest) {
// Create a NamespaceManager with the 'android' namespace.
var nsManager = new XmlNamespaceManager(manifest.NameTable);
var androidNamespace = manifest.DocumentElement.GetNamespaceOfPrefix("android");
if (string.IsNullOrEmpty(androidNamespace)) {
// If unable to find the android namespace, just exit out early, since we depend on it
return;
}
string oldAssetPath = AssetDatabase.GUIDToAssetPath(oldAssetGuids[0]);
Object oldAsset = AssetDatabase.LoadMainAssetAtPath(oldAssetPath);
if (oldAsset != null) {
string oldAssetFullPath = Path.Combine(Application.dataPath, "..", oldAssetPath);
string oldFileContents = System.IO.File.ReadAllText(oldAssetFullPath);
// If the old file matches what we would generate, exit early.
if (oldFileContents == fileContents) {
return;
nsManager.AddNamespace("android", androidNamespace);

// Find the activity node
XmlNode activityNode = manifest.SelectSingleNode(
$"//application/activity[@android:name='{MESSAGING_ACTIVITY}']",
nsManager);

if (activityNode != null && activityNode is XmlElement activityElement) {
string existingTheme = activityElement.GetAttribute("theme", androidNamespace);

string defaultTheme = DEFAULT_THEME;
bool setTheme = string.IsNullOrEmpty(existingTheme);
#if UNITY_2023_1_OR_NEWER
string otherTheme = DEFAULT_GAME_THEME;
// If using the new GameActivity logic, we want to use the default game theme.
if (PlayerSettings.Android.applicationEntry.HasFlag(AndroidApplicationEntry.GameActivity)) {
defaultTheme = DEFAULT_GAME_THEME;
otherTheme = DEFAULT_THEME;
}
// If the generated file has been tagged to be preserved, don't change it.
string[] labelList = AssetDatabase.GetLabels(oldAsset);
if (labelList.Contains(PreserveTag)) {
return;
// We also want to change the theme if is is using the other default.
setTheme |= existingTheme == otherTheme;
#endif
if (setTheme) {
activityElement.SetAttribute("theme", androidNamespace, defaultTheme);
}
// Delete the old asset.
Debug.Log("Changes detected, regenerating " + oldAssetPath + "\n" +
"To preserve local changes to that file, add the label: " + PreserveTag);
AssetDatabase.DeleteAsset(oldAssetPath);
}
}

// Generate the new file.
string newAssetFullDirectory = Path.Combine(Application.dataPath, OutputPath);
System.IO.Directory.CreateDirectory(newAssetFullDirectory);
System.IO.File.WriteAllText(Path.Combine(newAssetFullDirectory, OutputFilename), fileContents);
string newAssetLocalPath = Path.Combine("Assets", OutputPath, OutputFilename);
AssetDatabase.ImportAsset(newAssetLocalPath);
Object newAsset = AssetDatabase.LoadMainAssetAtPath(newAssetLocalPath);
AssetDatabase.SetLabels(newAsset, new[]{GeneratedFileTag});
}
}

} // namespace Firebase.Messaging.Editor