494 lines
19 KiB
Java
494 lines
19 KiB
Java
package com.epicgames.unreal.notifications;
|
|
|
|
import android.app.NotificationChannel;
|
|
import android.app.NotificationManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.content.SharedPreferences;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.core.app.NotificationCompat;
|
|
import android.text.TextUtils;
|
|
|
|
import com.epicgames.unreal.GameActivity;
|
|
import com.epicgames.unreal.GameApplication;
|
|
import com.epicgames.unreal.LocalNotificationReceiver;
|
|
import com.epicgames.unreal.Logger;
|
|
import com.google.android.gms.tasks.OnCompleteListener;
|
|
import com.google.android.gms.tasks.Task;
|
|
import com.google.android.gms.tasks.Tasks;
|
|
import com.google.firebase.FirebaseApp;
|
|
import com.google.firebase.FirebaseOptions;
|
|
import com.google.firebase.messaging.FirebaseMessaging;
|
|
import com.google.firebase.messaging.FirebaseMessagingService;
|
|
import com.google.firebase.messaging.RemoteMessage;
|
|
import com.google.firebase.analytics.FirebaseAnalytics;
|
|
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Random;
|
|
import java.io.InputStreamReader;
|
|
import java.io.BufferedReader;
|
|
import java.io.InputStream;
|
|
import java.util.stream.Collectors;
|
|
|
|
import static com.epicgames.unreal.GameActivity.Get;
|
|
import static com.epicgames.unreal.GameActivity.LocalNotificationGetID;
|
|
import static com.epicgames.unreal.LocalNotificationReceiver.KEY_LOCAL_NOTIFICATION_ACTION;
|
|
import static com.epicgames.unreal.LocalNotificationReceiver.KEY_LOCAL_NOTIFICATION_BODY;
|
|
import static com.epicgames.unreal.LocalNotificationReceiver.KEY_LOCAL_NOTIFICATION_ID;
|
|
import static com.epicgames.unreal.LocalNotificationReceiver.KEY_LOCAL_NOTIFICATION_TITLE;
|
|
|
|
public class EpicFirebaseMessagingService extends FirebaseMessagingService {
|
|
|
|
private static final Logger Log = new Logger("UE-" + EpicFirebaseMessagingService.class.getSimpleName());
|
|
|
|
private static final String ATTR_TYPE = "type";
|
|
private static final String PAYLOAD_P_KEY = "p";
|
|
|
|
private static final String FIREBASE_ICON_NAME = "ic_notification";
|
|
private static final String FIREBASE_ICON_TYPE = "mipmap";
|
|
|
|
private static final String COMPONENT = "MessagingService";
|
|
private static final String SYSTEM = "Notification";
|
|
private static final String INCOMING_MESSAGE_ERROR = "IncomingMessageError";
|
|
private static final String INCOMING_MESSAGE_WARNING = "IncomingMessageWarning";
|
|
private static final String ATTR_ERROR = "Error";
|
|
private static final String ATTR_WARNING = "Warning";
|
|
|
|
// Firebase intent keys
|
|
public static final String NOTIFICATION_ACTION = "ue4_fb.notificationAction";
|
|
public static final String KEY_PUSH_NOTIFICATION = "ue4_fb.push";
|
|
public static final String KEY_NOTIFICATION_BODY = "ue4_fb.body";
|
|
private static final String KEY_MESSAGE_ID = "ue4_fb.messageId";
|
|
private static final String KEY_NOTIFICATION_TYPE = "ue4_fb.type";
|
|
|
|
public static native void OnFirebaseTokenChange(String PreviousToken, String NewToken);
|
|
|
|
@Override
|
|
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
|
|
Log.verbose("Firebase onMessageReceived");
|
|
super.onMessageReceived(remoteMessage);
|
|
final String messageId = remoteMessage.getMessageId();
|
|
final Map<String, String> data = remoteMessage.getData();
|
|
logNotificationInfo(remoteMessage.getNotification());
|
|
if (data != null) {
|
|
EpicFirebaseNotificationMeta meta = null;
|
|
try {
|
|
meta = getContentInfo(messageId, data);
|
|
} catch (JSONException e) {
|
|
Log.error("Firebase unable to parse payload", e);
|
|
}
|
|
if (meta != null && messageId != null) {
|
|
doNotify(messageId, meta);
|
|
} else {
|
|
Log.debug("containerType is empty");
|
|
sendLocalNotification(remoteMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void doNotify(String messageId, EpicFirebaseNotificationMeta meta) {
|
|
createChannel(meta);
|
|
Intent intent = getDefaultIntent(messageId, meta);
|
|
NotificationCompat.Builder notificationBuilder = getNotificationBuilder(meta, intent);
|
|
NotificationManager notificationManager;
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
notificationManager = getSystemService(NotificationManager.class);
|
|
} else {
|
|
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
}
|
|
if (notificationManager != null) {
|
|
if (GameApplication.isAppInBackground()) {
|
|
notificationManager.notify(new Random(System.currentTimeMillis()).nextInt(), notificationBuilder.build());
|
|
Log.verbose("Push notification notify");
|
|
} else {
|
|
this.startActivity(intent);
|
|
Log.verbose("Push notification sent to activity while app is in the foreground");
|
|
}
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
private NotificationCompat.Builder getNotificationBuilder(@NonNull EpicFirebaseNotificationMeta meta, @NonNull Intent defaultIntent) {
|
|
return new NotificationCompat.Builder(this, meta.getChannelType())
|
|
.setSmallIcon(meta.getNotificationResId())
|
|
.setContentTitle(meta.getTitle())
|
|
.setContentText(meta.getMessage())
|
|
.setContentInfo(meta.getContentInfo())
|
|
.setContentIntent(getPendingIntentIntent(defaultIntent))
|
|
.setAutoCancel(true)
|
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
|
|
}
|
|
|
|
@Nullable
|
|
private EpicFirebaseNotificationMeta getContentInfo(@Nullable String messageId, @NonNull Map<String, String> messageData) throws JSONException {
|
|
@Nullable EpicFirebaseNotificationMeta meta = null;
|
|
String payload = messageData.get(PAYLOAD_P_KEY);
|
|
logMessageData(messageData);
|
|
if (!TextUtils.isEmpty(payload)) {
|
|
String type = getType(payload);
|
|
Log.verbose("Message Received ( " + messageId + ") type = " + type + " : " + payload);
|
|
|
|
// meta = new EpicFirebaseNotificationMeta();
|
|
} else {
|
|
Log.warn("Firebase message received ( " + messageId + ") - NO PAYLOAD");
|
|
}
|
|
return meta;
|
|
}
|
|
|
|
private void logNotificationInfo(@Nullable RemoteMessage.Notification notification) {
|
|
if(notification == null) {
|
|
Log.verbose("Firebase no notification data");
|
|
} else {
|
|
JSONObject notificationData = new JSONObject();
|
|
try {
|
|
notificationData.put("title", getSafeString(notification.getTitle()));
|
|
notificationData.put("body", getSafeString(notification.getBody()));
|
|
notificationData.put("body_loc_key", getSafeString(notification.getBodyLocalizationKey()));
|
|
notificationData.put("click_action", getSafeString(notification.getClickAction()));
|
|
notificationData.put("color", getSafeString(notification.getColor()));
|
|
notificationData.put("icon", getSafeString(notification.getIcon()));
|
|
notificationData.put("sound", getSafeString(notification.getSound()));
|
|
notificationData.put("tag", getSafeString(notification.getTag()));
|
|
notificationData.put("title_loc_key", getSafeString(notification.getTitleLocalizationKey()));
|
|
String [] bodyArgs = notification.getBodyLocalizationArgs();
|
|
if(bodyArgs != null) {
|
|
int i = 0;
|
|
for(String arg : bodyArgs) {
|
|
notificationData.put("bodyArg" + i, getSafeString(arg));
|
|
i++;
|
|
}
|
|
}
|
|
Uri link = notification.getLink();
|
|
if(link != null) {
|
|
notificationData.put("title_loc_key", getSafeString(link.toString()));
|
|
}
|
|
String [] titleArgs = notification.getTitleLocalizationArgs();
|
|
if(titleArgs != null) {
|
|
int i = 0;
|
|
for(String arg : titleArgs) {
|
|
notificationData.put("titleArg" + i, getSafeString(arg));
|
|
i++;
|
|
}
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
Log.error("Unable to log notification", e);
|
|
}
|
|
Log.verbose("Firebase Notification data " + getSafeString(notificationData.toString()));
|
|
}
|
|
}
|
|
|
|
private void logMessageData(Map<String, String> messageData) {
|
|
JSONObject jsonObject = new JSONObject();
|
|
for (Map.Entry<String, String> entry : messageData.entrySet()) {
|
|
if (!"p".equals(entry.getKey())) {
|
|
try {
|
|
jsonObject.put(entry.getKey(), entry.getValue());
|
|
} catch (JSONException e) {
|
|
Log.error("Unable to add key:" + getSafeString(entry.getKey()) + " value:" + getSafeString(entry.getValue()));
|
|
}
|
|
}
|
|
}
|
|
Log.verbose("Firebase notification meta: " + jsonObject.toString());
|
|
}
|
|
|
|
public static String getSafeString(@Nullable String string) {
|
|
return Objects.toString(string, "<null>");
|
|
}
|
|
|
|
public static int getNotificationIconId(@NonNull Context context) {
|
|
int notificationIconID = context.getResources().getIdentifier(FIREBASE_ICON_NAME, FIREBASE_ICON_TYPE, context.getPackageName());
|
|
if (notificationIconID == 0) {
|
|
notificationIconID = LocalNotificationReceiver.getNotificationIconID(context);
|
|
}
|
|
return notificationIconID;
|
|
}
|
|
|
|
@NonNull
|
|
private Intent getDefaultIntent(@NonNull String messageId, @NonNull EpicFirebaseNotificationMeta meta) {
|
|
Intent defaultIntent = new Intent(this, GameActivity.class);
|
|
defaultIntent.setAction(NOTIFICATION_ACTION);
|
|
defaultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
|
defaultIntent.putExtra(KEY_MESSAGE_ID, messageId);
|
|
defaultIntent.putExtra(KEY_PUSH_NOTIFICATION, true);
|
|
defaultIntent.putExtra(KEY_NOTIFICATION_TYPE, meta.getType());
|
|
defaultIntent.putExtra(KEY_NOTIFICATION_BODY, meta.getPayload());
|
|
return defaultIntent;
|
|
}
|
|
|
|
|
|
@NonNull
|
|
private PendingIntent getPendingIntentIntent(@NonNull Intent defaultIntent) {
|
|
return PendingIntent.getActivity(this, 1, defaultIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
|
}
|
|
|
|
public void createChannel(@NonNull EpicFirebaseNotificationMeta meta) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
|
if (notificationManager != null) {
|
|
NotificationChannel channel = notificationManager.getNotificationChannel(meta.getChannelType());
|
|
if (channel == null) {
|
|
int importance = NotificationManager.IMPORTANCE_DEFAULT;
|
|
channel = new NotificationChannel(meta.getChannelType(), meta.getChannelTitle(), importance);
|
|
channel.setDescription(meta.getChannelDescription());
|
|
notificationManager.createNotificationChannel(channel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void sendLocalNotification(@NonNull RemoteMessage remoteMessage) {
|
|
RemoteMessage.Notification notification = remoteMessage.getNotification();
|
|
if (notification != null) {
|
|
Intent notificationIntent = new Intent(this, LocalNotificationReceiver.class);
|
|
int notificationID = LocalNotificationGetID(this);
|
|
notificationIntent.putExtra(KEY_LOCAL_NOTIFICATION_ID, notificationID);
|
|
notificationIntent.putExtra(KEY_LOCAL_NOTIFICATION_TITLE, notification.getTitle());
|
|
notificationIntent.putExtra(KEY_LOCAL_NOTIFICATION_BODY, notification.getBody());
|
|
notificationIntent.putExtra(KEY_LOCAL_NOTIFICATION_ACTION, notification.getClickAction());
|
|
sendBroadcast(notificationIntent);
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
static String getType(@NonNull String payload) throws JSONException {
|
|
JSONObject o = new JSONObject(payload);
|
|
return o.optString(ATTR_TYPE);
|
|
}
|
|
|
|
// Formally in EpicFirebaseInstanceIDService.java
|
|
private static final String PREFS_FILE_FIREBASE = "com.epicgames.firebase";
|
|
private static final String KEY_FIREBASE_TOKEN = "firebasetoken";
|
|
private static final String KEY_FIREBASE_PROJECT_ID = "firebaseProjectID";
|
|
private static final String KEY_IS_UPDATED_TOKEN = "isUpdatedToken";
|
|
private static final String KEY_IS_REGISTERED = "isRegistered";
|
|
|
|
@Override
|
|
public void onNewToken(@NonNull String firebaseToken) {
|
|
Log.debug("Refreshed Firebase token: " + firebaseToken);
|
|
if (TextUtils.isEmpty(firebaseToken)) {
|
|
Log.error("Firebase token is empty or null");
|
|
} else {
|
|
saveFirebaseToken(this, firebaseToken);
|
|
setLastFirebaseProjectID(this, FirebaseApp.getInstance().getOptions().getGcmSenderId());
|
|
}
|
|
}
|
|
|
|
private static void saveFirebaseToken(@NonNull Context context, @NonNull String firebaseToken) {
|
|
Log.debug("Saving Firebase token");
|
|
String storedToken = getFirebaseTokenFromCache(context);
|
|
OnFirebaseTokenChange(storedToken == null ? "" : storedToken, firebaseToken);
|
|
boolean isUpdatedToken = !TextUtils.isEmpty(storedToken);
|
|
SharedPreferences sharedPreferences = context.getSharedPreferences(PREFS_FILE_FIREBASE, Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
|
Log.debug("Firebase token isUpdated : " + isUpdatedToken);
|
|
editor.putBoolean(KEY_IS_UPDATED_TOKEN, isUpdatedToken).apply();
|
|
editor.putBoolean(KEY_IS_REGISTERED, false).apply();
|
|
editor.putString(KEY_FIREBASE_TOKEN, firebaseToken).apply();
|
|
}
|
|
|
|
@SuppressWarnings("unused")
|
|
static boolean isFirebaseTokenUpdated(@NonNull Context context) {
|
|
SharedPreferences sharedPreferences = context.getSharedPreferences(PREFS_FILE_FIREBASE, Context.MODE_PRIVATE);
|
|
boolean isUpdated = sharedPreferences.getBoolean(KEY_IS_UPDATED_TOKEN, true);
|
|
Log.debug("Firebase token isUpdatedToken is " + isUpdated);
|
|
return isUpdated;
|
|
}
|
|
|
|
public static boolean isFirebaseTokenRegistered(@NonNull Context context) {
|
|
SharedPreferences sharedPreferences = context.getSharedPreferences(PREFS_FILE_FIREBASE, Context.MODE_PRIVATE);
|
|
return sharedPreferences.getBoolean(KEY_IS_REGISTERED, false);
|
|
}
|
|
|
|
public static void setFirebaseTokenRegistered(@NonNull Context context, boolean isRegistered) {
|
|
Log.debug("Firebase token isRegistered setting to " + isRegistered);
|
|
SharedPreferences sharedPreferences = context.getSharedPreferences(PREFS_FILE_FIREBASE, Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
|
editor.putBoolean(KEY_IS_REGISTERED, isRegistered);
|
|
editor.apply();
|
|
}
|
|
|
|
public static void unregisterFirebaseToken(@NonNull Context context) {
|
|
setFirebaseTokenRegistered(context, false);
|
|
SharedPreferences sharedPreferences = context.getSharedPreferences(PREFS_FILE_FIREBASE, Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
|
editor.remove(KEY_FIREBASE_TOKEN);
|
|
editor.apply();
|
|
Log.debug("Firebase token cleared");
|
|
}
|
|
|
|
private static String getFirebaseTokenFromCache(@NonNull Context context) {
|
|
SharedPreferences sharedPreferences = context.getSharedPreferences(PREFS_FILE_FIREBASE, Context.MODE_PRIVATE);
|
|
return sharedPreferences.getString(KEY_FIREBASE_TOKEN, null);
|
|
}
|
|
|
|
private static String getLastFirebaseProjectID(@NonNull Context context)
|
|
{
|
|
SharedPreferences sharedPreferences = context.getSharedPreferences(PREFS_FILE_FIREBASE, Context.MODE_PRIVATE);
|
|
return sharedPreferences.getString(KEY_FIREBASE_PROJECT_ID, null);
|
|
}
|
|
|
|
private static void setLastFirebaseProjectID(@NonNull Context context, String projectID)
|
|
{
|
|
SharedPreferences sharedPreferences = context.getSharedPreferences(PREFS_FILE_FIREBASE, Context.MODE_PRIVATE);
|
|
SharedPreferences.Editor editor = sharedPreferences.edit();
|
|
editor.putString(KEY_FIREBASE_PROJECT_ID, projectID);
|
|
editor.apply();
|
|
}
|
|
|
|
private static Task<String> queryFirebaseToken(@NonNull Context context, String projectID)
|
|
{
|
|
Task<String> tokenTask = FirebaseMessaging.getInstance().getToken();
|
|
tokenTask.addOnCompleteListener(new OnCompleteListener<String>() {
|
|
@Override
|
|
public void onComplete(@NonNull Task<String> task) {
|
|
if (task.isSuccessful()) {
|
|
String token = task.getResult();
|
|
if(!TextUtils.isEmpty(token)) {
|
|
Log.debug("Firebase token retrieved from Firebase");
|
|
saveFirebaseToken(context, token);
|
|
setLastFirebaseProjectID(context, projectID);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return tokenTask;
|
|
}
|
|
|
|
@Nullable
|
|
public static String getFirebaseToken(@NonNull Context context) {
|
|
EnsureFirebaseIsInitialized(context);
|
|
|
|
String projectID = FirebaseApp.getInstance().getOptions().getGcmSenderId();
|
|
String lastProjectID = getLastFirebaseProjectID(context);
|
|
String token;
|
|
if (lastProjectID == null || !projectID.contentEquals(lastProjectID))
|
|
{
|
|
Log.debug("Firebase project changed, querying new token");
|
|
token = null;
|
|
saveFirebaseToken(context, "");
|
|
}
|
|
else
|
|
{
|
|
token = getFirebaseTokenFromCache(context);
|
|
}
|
|
|
|
if(TextUtils.isEmpty(token)) {
|
|
Task<String> tokenTask = queryFirebaseToken(context, projectID);
|
|
// wait on task
|
|
try {
|
|
Tasks.await(tokenTask);
|
|
token = getFirebaseTokenFromCache(context);
|
|
token = (token == null) ? "" : token;
|
|
} catch (Exception e) {
|
|
Log.error("Failed to retrieve Firebase token", e);
|
|
}
|
|
} else {
|
|
OnFirebaseTokenChange("", token);
|
|
Log.debug("Firebase token retrieved from cache");
|
|
|
|
// Schedule token query anyway in case cached token is stale, do it after OnFirebaseTokenChange to make sure we notify with cached token first
|
|
queryFirebaseToken(context, projectID);
|
|
}
|
|
|
|
if (!GameActivity.IS_SHIPPING_CONFIG)
|
|
{
|
|
Log.debug("Firebase token is " + token);
|
|
}
|
|
|
|
return token;
|
|
}
|
|
|
|
public static void enableFirebaseAutoInit(@NonNull Context context, boolean enableAnalytics) {
|
|
EnsureFirebaseIsInitialized(context);
|
|
FirebaseMessaging.getInstance().setAutoInitEnabled(true);
|
|
if (enableAnalytics) {
|
|
FirebaseAnalytics.getInstance(context).setAnalyticsCollectionEnabled(true);
|
|
}
|
|
}
|
|
|
|
public static void EnsureFirebaseIsInitialized(@NonNull Context context) {
|
|
if (FirebaseApp.getApps(context).isEmpty()) {
|
|
FirebaseApp.initializeApp(context);
|
|
}
|
|
}
|
|
|
|
public static void deleteFirebaseToken(@NonNull Context context) {
|
|
saveFirebaseToken(context, "");
|
|
FirebaseMessaging.getInstance().deleteToken();
|
|
}
|
|
|
|
public static void initializeFirebaseWithCustomFile(@NonNull Context context, String fileName) {
|
|
if (!FirebaseApp.getApps(context).isEmpty())
|
|
{
|
|
Log.error("Failed to initialize Firebase with custom file. Firebase is already initialized!");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
InputStream inputStream = context.getAssets().open(fileName);
|
|
String result = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining("\n"));
|
|
|
|
/*
|
|
Expected JSON format :
|
|
{
|
|
"project_info": {
|
|
"project_number": "X",
|
|
"firebase_url": "X",
|
|
"project_id": "X",
|
|
"storage_bucket": "X"
|
|
},
|
|
"client": [
|
|
{
|
|
"client_info": {
|
|
"mobilesdk_app_id": "X"
|
|
},
|
|
"api_key": [
|
|
{
|
|
"current_key": "X"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
*/
|
|
|
|
JSONObject root = new JSONObject(result);
|
|
JSONObject projectInfo = root.getJSONObject("project_info");
|
|
JSONObject client = root.getJSONArray("client").getJSONObject(0);
|
|
|
|
String projectNumber = projectInfo.getString("project_number");
|
|
String firebaseUrl = projectInfo.getString("firebase_url");
|
|
String projectId = projectInfo.getString("project_id");
|
|
String storageBucket = projectInfo.getString("storage_bucket");
|
|
String apiKey = client.getJSONArray("api_key").getJSONObject(0).getString("current_key");
|
|
String appID = client.getJSONObject("client_info").getString("mobilesdk_app_id");
|
|
|
|
FirebaseOptions.Builder optionsBuilder = new FirebaseOptions.Builder();
|
|
optionsBuilder.setApiKey(apiKey);
|
|
optionsBuilder.setApplicationId(appID);
|
|
optionsBuilder.setDatabaseUrl(firebaseUrl);
|
|
optionsBuilder.setGcmSenderId(projectNumber);
|
|
optionsBuilder.setProjectId(projectId);
|
|
optionsBuilder.setStorageBucket(storageBucket);
|
|
FirebaseOptions options = optionsBuilder.build();
|
|
|
|
FirebaseApp.initializeApp(context, options);
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
Log.error("Failed to initialize Firebase with custom file, using default initialization, exception : ", e);
|
|
FirebaseApp.initializeApp(context);
|
|
}
|
|
}
|
|
}
|