|
| 1 | +/* |
| 2 | + * Copyright 2023 Google LLC |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | +using System.IO; |
| 17 | +using System.Linq; |
| 18 | +using UnityEngine; |
| 19 | +using UnityEditor; |
| 20 | +using UnityEditor.Build; |
| 21 | +using UnityEditor.Build.Reporting; |
| 22 | + |
| 23 | +namespace Firebase.Messaging.Editor { |
| 24 | + |
| 25 | +// Handles the generation of the MessagingUnityPlayerActivity java file. |
| 26 | +// Note this regenerates the file every time an Android build occurs, |
| 27 | +// but local changes can be preserved by using the PreserveTag below. |
| 28 | +// This is needed because the source code needs to be present to work across |
| 29 | +// different Unity versions, due to changes in mUnityPlayer. |
| 30 | +// It also adjusts the base class of the file based on if GameActivity is being |
| 31 | +// used (a new feature in Unity 2023). |
| 32 | +public class FirebaseMessagingActivityGenerator : IPreprocessBuildWithReport { |
| 33 | + // TODO: Ideally this should use a template file, the tricky part is locating |
| 34 | + // the template file when it is either in the Assets path, or the Packages path. |
| 35 | + // There are some similar cases in EDM4U, so a solution might be to use that. |
| 36 | + private readonly string[] ActivityClassContents = new string[]{ |
| 37 | +"/*", |
| 38 | +" * This file is generated by the FirebaseMessagingActivityGenerator script.", |
| 39 | +" * Refer to that script for more information.", |
| 40 | +" */", |
| 41 | +"", |
| 42 | +"package com.google.firebase;", |
| 43 | +"", |
| 44 | +"import android.content.Intent;", |
| 45 | +"import android.os.Bundle;", |
| 46 | +"import com.google.firebase.messaging.MessageForwardingService;", |
| 47 | +"import com.unity3d.player.{0};", |
| 48 | +"", |
| 49 | +"/**", |
| 50 | +" * MessagingUnityPlayerActivity is a {0} that updates its intent when new intents", |
| 51 | +" * are sent to it.", |
| 52 | +" *", |
| 53 | +" * This is a workaround for a known issue that prevents Firebase Cloud Messaging from responding to", |
| 54 | +" * data payloads when both a data and notification payload are sent to the app while it is in the", |
| 55 | +" * background.", |
| 56 | +" */", |
| 57 | +"public class MessagingUnityPlayerActivity extends {0} {{", |
| 58 | +" // The key in the intent's extras that maps to the incoming message's message ID. Only sent by", |
| 59 | +" // the server, GmsCore sends EXTRA_MESSAGE_ID_KEY below. Server can't send that as it would get", |
| 60 | +" // stripped by the client.", |
| 61 | +" private static final String EXTRA_MESSAGE_ID_KEY_SERVER = \"message_id\";", |
| 62 | +"", |
| 63 | +" // An alternate key value in the intent's extras that also maps to the incoming message's message", |
| 64 | +" // ID. Used by upstream, and set by GmsCore.", |
| 65 | +" private static final String EXTRA_MESSAGE_ID_KEY = \"google.message_id\";", |
| 66 | +"", |
| 67 | +" // The key in the intent's extras that maps to the incoming message's sender value.", |
| 68 | +" private static final String EXTRA_FROM = \"google.message_id\";", |
| 69 | +"", |
| 70 | +" /**", |
| 71 | +" * Workaround for when a message is sent containing both a Data and Notification payload.", |
| 72 | +" *", |
| 73 | +" * <p>When the app is in the background, if a message with both a data and notification payload is", |
| 74 | +" * received the data payload is stored on the Intent passed to onNewIntent. By default, that", |
| 75 | +" * intent does not get set as the Intent that started the app, so when the app comes back online", |
| 76 | +" * it doesn't see a new FCM message to respond to. As a workaround, we override onNewIntent so", |
| 77 | +" * that it sends the intent to the MessageForwardingService which forwards the message to the", |
| 78 | +" * FirebaseMessagingService which in turn sends the message to the application.", |
| 79 | +" */", |
| 80 | +" @Override", |
| 81 | +" protected void onNewIntent(Intent intent) {{", |
| 82 | +" super.onNewIntent(intent);", |
| 83 | +"", |
| 84 | +" // If we do not have a 'from' field this intent was not a message and should not be handled. It", |
| 85 | +" // probably means this intent was fired by tapping on the app icon.", |
| 86 | +" Bundle extras = intent.getExtras();", |
| 87 | +" if (extras == null) {{", |
| 88 | +" return;", |
| 89 | +" }}", |
| 90 | +" String from = extras.getString(EXTRA_FROM);", |
| 91 | +" String messageId = extras.getString(EXTRA_MESSAGE_ID_KEY);", |
| 92 | +" if (messageId == null) {{", |
| 93 | +" messageId = extras.getString(EXTRA_MESSAGE_ID_KEY_SERVER);", |
| 94 | +" }}", |
| 95 | +" if (from != null && messageId != null) {{", |
| 96 | +" Intent message = new Intent(this, MessageForwardingService.class);", |
| 97 | +" message.setAction(MessageForwardingService.ACTION_REMOTE_INTENT);", |
| 98 | +" message.putExtras(intent);", |
| 99 | +" message.setData(intent.getData());", |
| 100 | +" MessageForwardingService.enqueueWork(this, message);", |
| 101 | +" }}", |
| 102 | +" setIntent(intent);", |
| 103 | +" }}", |
| 104 | +"", |
| 105 | +" /**", |
| 106 | +" * Dispose of the mUnityPlayer when restarting the app.", |
| 107 | +" *", |
| 108 | +" * <p>This ensures that when the app starts up again it does not start with stale data.", |
| 109 | +" */", |
| 110 | +" @Override", |
| 111 | +" protected void onCreate(Bundle savedInstanceState) {{", |
| 112 | +" if (mUnityPlayer != null) {{", |
| 113 | +" mUnityPlayer.quit();", |
| 114 | +" mUnityPlayer = null;", |
| 115 | +" }}", |
| 116 | +" super.onCreate(savedInstanceState);", |
| 117 | +" }}", |
| 118 | +"}}" |
| 119 | + }; |
| 120 | + private readonly string BaseActivityClass = "UnityPlayerActivity"; |
| 121 | + private readonly string BaseGameActivityClass = "UnityPlayerGameActivity"; |
| 122 | + |
| 123 | + private readonly string GeneratedFileTag = "FirebaseMessagingActivityGenerated"; |
| 124 | + // If this tag is present on the generated file, it will not be replaced. |
| 125 | + private readonly string PreserveTag = "FirebasePreserve"; |
| 126 | + |
| 127 | + private readonly string OutputPath = Path.Combine("Plugins", "Android"); |
| 128 | + private readonly string OutputFilename = "MessagingUnityPlayerActivity.java"; |
| 129 | + |
| 130 | + public int callbackOrder { get { return 0; } } |
| 131 | + public void OnPreprocessBuild(BuildReport report) { |
| 132 | + // Only run this logic when building for Android. |
| 133 | + if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) { |
| 134 | + return; |
| 135 | + } |
| 136 | + |
| 137 | + // Determine what the contents of the generated file should be. |
| 138 | + string baseClass = BaseActivityClass; |
| 139 | +#if UNITY_2023_1_OR_NEWER |
| 140 | + // If using the new GameActivity logic, we want to generate with that base class. |
| 141 | + if (PlayerSettings.Android.applicationEntry.HasFlag(AndroidApplicationEntry.GameActivity)) { |
| 142 | + baseClass = BaseGameActivityClass; |
| 143 | + } |
| 144 | +#endif |
| 145 | + string fileContents = System.String.Format(System.String.Join("\n", ActivityClassContents), baseClass); |
| 146 | + |
| 147 | + // Check if the file has already been generated. |
| 148 | + string[] oldAssetGuids = AssetDatabase.FindAssets("l:" + GeneratedFileTag); |
| 149 | + if (oldAssetGuids != null && oldAssetGuids.Length > 0) { |
| 150 | + if (oldAssetGuids.Length != 1) { |
| 151 | + Debug.LogWarning("FirebaseMessagingActivityEditor found multiple generated files with the label: " + |
| 152 | + GeneratedFileTag + " \n" + |
| 153 | + "No changes will be made, but this can potentially cause problems on Android with duplicate classes.\n" + |
| 154 | + "Please check for duplicate classes, and remove any unnecessary uses of the label."); |
| 155 | + return; |
| 156 | + } |
| 157 | + string oldAssetPath = AssetDatabase.GUIDToAssetPath(oldAssetGuids[0]); |
| 158 | + Object oldAsset = AssetDatabase.LoadMainAssetAtPath(oldAssetPath); |
| 159 | + if (oldAsset != null) { |
| 160 | + string oldAssetFullPath = Path.Combine(Application.dataPath, "..", oldAssetPath); |
| 161 | + string oldFileContents = System.IO.File.ReadAllText(oldAssetFullPath); |
| 162 | + // If the old file matches what we would generate, exit early. |
| 163 | + if (oldFileContents == fileContents) { |
| 164 | + return; |
| 165 | + } |
| 166 | + // If the generated file has been tagged to be preserved, don't change it. |
| 167 | + string[] labelList = AssetDatabase.GetLabels(oldAsset); |
| 168 | + if (labelList.Contains(PreserveTag)) { |
| 169 | + return; |
| 170 | + } |
| 171 | + // Delete the old asset. |
| 172 | + Debug.Log("Changes detected, regenerating " + oldAssetPath + "\n" + |
| 173 | + "To preserve local changes to that file, add the label: " + PreserveTag); |
| 174 | + AssetDatabase.DeleteAsset(oldAssetPath); |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + // Generate the new file. |
| 179 | + string newAssetFullDirectory = Path.Combine(Application.dataPath, OutputPath); |
| 180 | + System.IO.Directory.CreateDirectory(newAssetFullDirectory); |
| 181 | + System.IO.File.WriteAllText(Path.Combine(newAssetFullDirectory, OutputFilename), fileContents); |
| 182 | + string newAssetLocalPath = Path.Combine("Assets", OutputPath, OutputFilename); |
| 183 | + AssetDatabase.ImportAsset(newAssetLocalPath); |
| 184 | + Object newAsset = AssetDatabase.LoadMainAssetAtPath(newAssetLocalPath); |
| 185 | + AssetDatabase.SetLabels(newAsset, new[]{GeneratedFileTag}); |
| 186 | + } |
| 187 | +} |
| 188 | + |
| 189 | +} // namespace Firebase.Messaging.Editor |
0 commit comments