/* * Copyright 2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.IO; using System.Linq; 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[]{ "/*", " * This file is generated by the FirebaseMessagingActivityGenerator script.", " * Refer to that script for more information.", " */", "", "package com.google.firebase;", "", "import android.content.Intent;", "import android.os.Bundle;", "import com.google.firebase.messaging.MessageForwardingService;", "import com.unity3d.player.{0};", "", "/**", " * MessagingUnityPlayerActivity is a {0} that updates its intent when new intents", " * are sent to it.", " *", " * This is a workaround for a known issue that prevents Firebase Cloud Messaging from responding to", " * data payloads when both a data and notification payload are sent to the app while it is in the", " * background.", " */", "public class MessagingUnityPlayerActivity extends {0} {{", " // The key in the intent's extras that maps to the incoming message's message ID. Only sent by", " // the server, GmsCore sends EXTRA_MESSAGE_ID_KEY below. Server can't send that as it would get", " // stripped by the client.", " private static final String EXTRA_MESSAGE_ID_KEY_SERVER = \"message_id\";", "", " // An alternate key value in the intent's extras that also maps to the incoming message's message", " // ID. Used by upstream, and set by GmsCore.", " private static final String EXTRA_MESSAGE_ID_KEY = \"google.message_id\";", "", " // The key in the intent's extras that maps to the incoming message's sender value.", " private static final String EXTRA_FROM = \"google.message_id\";", "", " /**", " * Workaround for when a message is sent containing both a Data and Notification payload.", " *", " *
When the app is in the background, if a message with both a data and notification payload is", " * received the data payload is stored on the Intent passed to onNewIntent. By default, that", " * intent does not get set as the Intent that started the app, so when the app comes back online", " * it doesn't see a new FCM message to respond to. As a workaround, we override onNewIntent so", " * that it sends the intent to the MessageForwardingService which forwards the message to the", " * FirebaseMessagingService which in turn sends the message to the application.", " */", " @Override", " protected void onNewIntent(Intent intent) {{", " super.onNewIntent(intent);", "", " // If we do not have a 'from' field this intent was not a message and should not be handled. It", " // probably means this intent was fired by tapping on the app icon.", " Bundle extras = intent.getExtras();", " if (extras == null) {{", " return;", " }}", " String from = extras.getString(EXTRA_FROM);", " String messageId = extras.getString(EXTRA_MESSAGE_ID_KEY);", " if (messageId == null) {{", " messageId = extras.getString(EXTRA_MESSAGE_ID_KEY_SERVER);", " }}", " if (from != null && messageId != null) {{", " Intent message = new Intent(this, MessageForwardingService.class);", " message.setAction(MessageForwardingService.ACTION_REMOTE_INTENT);", " message.putExtras(intent);", " message.setData(intent.getData());", " MessageForwardingService.enqueueWork(this, message);", " }}", " setIntent(intent);", " }}", "", " /**", " * Dispose of the mUnityPlayer when restarting the app.", " *", " *
This ensures that when the app starts up again it does not start with stale data.", " */", " @Override", " protected void onCreate(Bundle savedInstanceState) {{", " if (mUnityPlayer != null) {{", " mUnityPlayer.{1}();", " mUnityPlayer = null;", " }}", " super.onCreate(savedInstanceState);", " }}", "}}" }; private readonly string BaseActivityClass = "UnityPlayerActivity"; #if UNITY_2023_1_OR_NEWER private readonly string BaseGameActivityClass = "UnityPlayerGameActivity"; #endif #if UNITY_2023_1_OR_NEWER private readonly string UnityPlayerQuitFunction = "destroy"; #else 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 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; } // 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; } #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}); } } } // namespace Firebase.Messaging.Editor