fix:1、sdk更换。2、修复bug

This commit is contained in:
2026-07-07 14:03:12 +08:00
parent 63b530f245
commit e3b0b9b008
1617 changed files with 134668 additions and 62343 deletions
@@ -0,0 +1,80 @@
fileFormatVersion: 2
guid: 5a06b648d726ee0479a00bac712c49f1
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 0
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,44 @@
plugins {
id 'com.android.library'
}
def getCompileSdk(unityLib) {
def unityCompileSdk = unityLib.compileSdkVersion
def version = unityCompileSdk.find('\\d+').toInteger()
if (version < 33) {
return 33
}
return version
}
android {
// Checking if namespace exists is needed for 2021.3 (AGP 4.0.1)
// When 2021.3 is dropped, remove this check and package="com.unity.androidnotifications" from AndroidManifest
if (project.android.hasProperty("namespace")) {
namespace "com.unity.androidnotifications"
}
def unityLib = project(':unityLibrary').extensions.getByName('android')
compileSdkVersion getCompileSdk(unityLib)
buildToolsVersion unityLib.buildToolsVersion
defaultConfig {
minSdkVersion 22
consumerProguardFiles "proguard-rules.pro"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
api files('../libs/unity-classes.jar')
}
@@ -0,0 +1,3 @@
# Accessed from C# code
-keep class com.unity.androidnotifications.UnityNotificationManager { public *; }
-keep interface com.unity.androidnotifications.NotificationCallback { *; }
@@ -0,0 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unity.androidnotifications">
<application>
<receiver android:name="com.unity.androidnotifications.UnityNotificationManager" android:exported="false" />
<receiver android:name="com.unity.androidnotifications.UnityNotificationRestartReceiver" android:enabled="false" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<meta-data android:name="com.unity.androidnotifications.exact_scheduling" android:value="0" />
</application>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
@@ -0,0 +1,203 @@
package com.unity.androidnotifications;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_FIRE_TIME;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_ID;
import static com.unity.androidnotifications.UnityNotificationManager.TAG_UNITY;
import android.app.Notification;
import android.os.Bundle;
import android.util.Log;
import java.util.Calendar;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedTransferQueue;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class UnityNotificationBackgroundThread extends Thread {
private static abstract class Task {
// returns true if notificationIds was modified (needs to be saved)
public abstract boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications);
}
private static class ScheduleNotificationTask extends Task {
private int notificationId;
private Notification.Builder notificationBuilder;
private boolean isCustomized;
private boolean isNew;
public ScheduleNotificationTask(int id, Notification.Builder builder, boolean customized, boolean addedNew) {
notificationId = id;
notificationBuilder = builder;
isCustomized = customized;
isNew = addedNew;
}
@Override
public boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
String id = String.valueOf(notificationId);
Integer ID = Integer.valueOf(notificationId);
boolean didSchedule = false;
try {
UnityNotificationManager.mUnityNotificationManager.performNotificationScheduling(notificationId, notificationBuilder, isCustomized);
didSchedule = true;
} finally {
// if failed to schedule or replace, remove
if (!didSchedule) {
notifications.remove(notificationId);
manager.cancelPendingNotificationIntent(notificationId);
manager.deleteExpiredNotificationIntent(id);
}
}
return isNew;
}
}
private static class CancelNotificationTask extends Task {
private int notificationId;
public CancelNotificationTask(int id) {
notificationId = id;
}
@Override
public boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
manager.cancelPendingNotificationIntent(notificationId);
if (notifications.remove(notificationId) != null) {
manager.deleteExpiredNotificationIntent(String.valueOf(notificationId));
return true;
}
return false;
}
}
private static class CancelAllNotificationsTask extends Task {
@Override
public boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
if (notifications.isEmpty())
return false;
Enumeration<Integer> ids = notifications.keys();
while (ids.hasMoreElements()) {
Integer notificationId = ids.nextElement();
manager.cancelPendingNotificationIntent(notificationId);
manager.deleteExpiredNotificationIntent(String.valueOf(notificationId));
}
notifications.clear();
return true;
}
}
private static class HousekeepingTask extends Task {
UnityNotificationBackgroundThread thread;
public HousekeepingTask(UnityNotificationBackgroundThread th) {
thread = th;
}
@Override
public boolean run(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
HashSet<String> notificationIds = new HashSet<>();
Enumeration<Integer> ids = notifications.keys();
while (ids.hasMoreElements()) {
notificationIds.add(String.valueOf(ids.nextElement()));
}
thread.performHousekeeping(notificationIds);
return false;
}
}
private static final int TASKS_FOR_HOUSEKEEPING = 50;
private LinkedTransferQueue<Task> mTasks = new LinkedTransferQueue();
private ConcurrentHashMap<Integer, Notification.Builder> mScheduledNotifications;
private UnityNotificationManager mManager;
private int mTasksSinceHousekeeping = TASKS_FOR_HOUSEKEEPING; // we want hoursekeeping at the start
public UnityNotificationBackgroundThread(UnityNotificationManager manager, ConcurrentHashMap<Integer, Notification.Builder> scheduledNotifications) {
mManager = manager;
mScheduledNotifications = scheduledNotifications;
// rescheduling after reboot may have loaded, otherwise load here
if (mScheduledNotifications.size() == 0)
loadNotifications();
}
public void enqueueNotification(int id, Notification.Builder notificationBuilder, boolean customized, boolean addedNew) {
mTasks.add(new UnityNotificationBackgroundThread.ScheduleNotificationTask(id, notificationBuilder, customized, addedNew));
}
public void enqueueCancelNotification(int id) {
mTasks.add(new CancelNotificationTask(id));
}
public void enqueueCancelAllNotifications() {
mTasks.add(new CancelAllNotificationsTask());
}
private void enqueueHousekeeping() {
mTasks.add(new HousekeepingTask(this));
}
@Override
public void run() {
boolean haveChanges = false;
while (true) {
try {
Task task = mTasks.take();
haveChanges |= executeTask(mManager, task, mScheduledNotifications);
if (!(task instanceof HousekeepingTask))
++mTasksSinceHousekeeping;
if (mTasks.size() == 0 && haveChanges) {
haveChanges = false;
enqueueHousekeeping();
}
} catch (InterruptedException e) {
if (mTasks.isEmpty())
break;
}
}
}
private boolean executeTask(UnityNotificationManager manager, Task task, ConcurrentHashMap<Integer, Notification.Builder> notifications) {
try {
return task.run(manager, notifications);
} catch (Exception e) {
Log.e(TAG_UNITY, "Exception executing notification task", e);
return false;
}
}
private void performHousekeeping(Set<String> notificationIds) {
// don't do housekeeping if last task we did was housekeeping (other=1)
boolean performHousekeeping = mTasksSinceHousekeeping >= TASKS_FOR_HOUSEKEEPING;
mTasksSinceHousekeeping = 0;
if (performHousekeeping)
mManager.performNotificationHousekeeping(notificationIds);
mManager.saveScheduledNotificationIDs(notificationIds);
}
private void loadNotifications() {
List<Notification.Builder> notifications = mManager.loadSavedNotifications();
if (notifications == null || notifications.size() == 0)
return;
final long currentTime = Calendar.getInstance().getTime().getTime();
boolean needHousekeeping = false;
for (Notification.Builder builder : notifications) {
Bundle extras = builder.getExtras();
int id = extras.getInt(KEY_ID, -1);
long fireTime = extras.getLong(KEY_FIRE_TIME, -1);
boolean inFuture = fireTime - currentTime > 0;
if (inFuture)
mScheduledNotifications.put(id, builder);
else
needHousekeeping = true;
}
if (needHousekeeping)
enqueueHousekeeping();
}
}
@@ -0,0 +1,67 @@
package com.unity.androidnotifications;
import android.app.Notification;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_FIRE_TIME;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_ID;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_REPEAT_INTERVAL;
import static com.unity.androidnotifications.UnityNotificationManager.TAG_UNITY;
public class UnityNotificationRestartReceiver extends BroadcastReceiver {
private static final long EXPIRATION_TRESHOLD = 600000; // 10 minutes
@Override
public void onReceive(Context context, Intent received_intent) {
Log.d(TAG_UNITY, "Rescheduling notifications after restart");
if (Intent.ACTION_BOOT_COMPLETED.equals(received_intent.getAction())) {
AsyncTask.execute(() -> { rescheduleSavedNotifications(context); });
}
}
private static void rescheduleSavedNotifications(Context context) {
UnityNotificationManager manager = UnityNotificationManager.getNotificationManagerImpl(context);
List<Notification.Builder> saved_notifications = manager.loadSavedNotifications();
Date currentDate = Calendar.getInstance().getTime();
for (Notification.Builder notificationBuilder : saved_notifications) {
rescheduleNotification(manager, currentDate, notificationBuilder);
}
}
private static boolean rescheduleNotification(UnityNotificationManager manager, Date currentDate, Notification.Builder notificationBuilder) {
try {
Bundle extras = notificationBuilder.getExtras();
long repeatInterval = extras.getLong(KEY_REPEAT_INTERVAL, 0L);
long fireTime = extras.getLong(KEY_FIRE_TIME, 0L);
Date fireTimeDate = new Date(fireTime);
boolean isRepeatable = repeatInterval > 0;
if (fireTimeDate.after(currentDate) || isRepeatable) {
manager.scheduleAlarmWithNotification(notificationBuilder);
return true;
} else if (currentDate.getTime() - fireTime < EXPIRATION_TRESHOLD) {
// notification is in the past, but not by much, send now
int id = extras.getInt(KEY_ID);
manager.notify(id, notificationBuilder);
return true;
} else {
Log.d(TAG_UNITY, "Notification expired, not rescheduling, ID: " + extras.getInt(KEY_ID, -1));
return false;
}
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to reschedule notification", e);
return false;
}
}
}
@@ -0,0 +1,652 @@
package com.unity.androidnotifications;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import android.util.Log;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_CHANNEL_ID;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_FIRE_TIME;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_ID;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_INTENT_DATA;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_LARGE_ICON;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_NOTIFICATION;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_REPEAT_INTERVAL;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_SMALL_ICON;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_SHOW_IN_FOREGROUND;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_LARGE_ICON;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_PICTURE;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_CONTENT_TITLE;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_SUMMARY_TEXT;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_CONTENT_DESCRIPTION;
import static com.unity.androidnotifications.UnityNotificationManager.KEY_BIG_SHOW_WHEN_COLLAPSED;
import static com.unity.androidnotifications.UnityNotificationManager.TAG_UNITY;
class UnityNotificationUtilities {
/*
We serialize notifications and save them to shared prefs, so that if app is killed, we can recreate them.
The serialized BLOB starts with a four byte magic number descibing serialization type, followed by an integer version.
IMPORTANT: IF YOU DO A CHANGE THAT AFFECTS THE LAYOUT, BUMP THE VERSION, AND ENSURE OLD VERSION STILL DESERIALIZES. ADD TEST.
In real life app can get updated having old serialized notifications present, so we should be able to deserialize them.
*/
// magic stands for "Unity Mobile Notifications Notification"
static final byte[] UNITY_MAGIC_NUMBER = new byte[] { 'U', 'M', 'N', 'N'};
private static final byte[] UNITY_MAGIC_NUMBER_PARCELLED = new byte[] { 'U', 'M', 'N', 'P'};
private static final int NOTIFICATION_SERIALIZATION_VERSION = 3;
private static final int INTENT_SERIALIZATION_VERSION = 0;
static final String SAVED_NOTIFICATION_PRIMARY_KEY = "data";
static final String SAVED_NOTIFICATION_FALLBACK_KEY = "fallback.data";
protected static int findResourceIdInContextByName(Context context, String name) {
if (name == null)
return 0;
try {
Resources res = context.getResources();
if (res != null) {
int id = res.getIdentifier(name, "mipmap", context.getPackageName());
if (id == 0)
return res.getIdentifier(name, "drawable", context.getPackageName());
else
return id;
}
return 0;
} catch (Resources.NotFoundException e) {
return 0;
}
}
/* Originally we used to serialize a bundle with predefined list of values.
After we exposed entire Notification.Builder to users, this is not sufficient anymore.
Unfortunately, while Notification itself is Parcelable and can be marshalled to bytes,
it's contents are not guaranteed to be (Binder objects).
Hence what we try to do here is:
- serialize as is if notification is possibly customized by user
- otherwise serialize our stuff, since there is nothing more
*/
protected static void serializeNotification(SharedPreferences prefs, Notification notification, boolean serializeParcel) {
try {
String serialized;
ByteArrayOutputStream data = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(data);
if (serializeParcel) {
Intent intent = new Intent();
intent.putExtra(KEY_NOTIFICATION, notification);
if (serializeNotificationParcel(intent, out)) {
out.close();
byte[] bytes = data.toByteArray();
serialized = Base64.encodeToString(bytes, 0, bytes.length, 0);
} else {
return; // failed
}
}
else {
if (serializeNotificationCustom(notification, out)) {
out.flush();
byte[] bytes = data.toByteArray();
serialized = Base64.encodeToString(bytes, 0, bytes.length, 0);
} else {
return; // failed
}
}
SharedPreferences.Editor editor = prefs.edit().clear();
editor.putString(SAVED_NOTIFICATION_PRIMARY_KEY, serialized);
editor.apply();
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to serialize notification", e);
}
}
static boolean serializeNotificationParcel(Intent intent, DataOutputStream out) {
try {
byte[] bytes = serializeParcelable(intent);
if (bytes == null || bytes.length == 0)
return false;
out.write(UNITY_MAGIC_NUMBER_PARCELLED);
out.writeInt(INTENT_SERIALIZATION_VERSION);
out.writeInt(bytes.length);
out.write(bytes);
return true;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to serialize notification as Parcel", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to serialize notification as Parcel", e);
}
return false;
}
private static boolean serializeNotificationCustom(Notification notification, DataOutputStream out) {
try {
out.write(UNITY_MAGIC_NUMBER);
out.writeInt(NOTIFICATION_SERIALIZATION_VERSION);
// serialize extras
boolean showWhen = notification.extras.getBoolean(Notification.EXTRA_SHOW_WHEN, false);
out.writeInt(notification.extras.getInt(KEY_ID));
serializeString(out, notification.extras.getString(Notification.EXTRA_TITLE));
serializeString(out, notification.extras.getString(Notification.EXTRA_TEXT));
serializeString(out, notification.extras.getString(KEY_SMALL_ICON));
serializeString(out, notification.extras.getString(KEY_LARGE_ICON));
out.writeLong(notification.extras.getLong(KEY_FIRE_TIME, -1));
out.writeLong(notification.extras.getLong(KEY_REPEAT_INTERVAL, -1));
serializeString(out, notification.extras.getString(Notification.EXTRA_BIG_TEXT));
out.writeBoolean(notification.extras.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false));
out.writeBoolean(showWhen);
serializeString(out, notification.extras.getString(KEY_INTENT_DATA));
out.writeBoolean(notification.extras.getBoolean(KEY_SHOW_IN_FOREGROUND, true));
String bigPicture = notification.extras.getString(KEY_BIG_PICTURE);
serializeString(out, bigPicture);
if (bigPicture != null && bigPicture.length() > 0) {
// the following only need to be put in if big picture is there
serializeString(out, notification.extras.getString(KEY_BIG_LARGE_ICON));
serializeString(out, notification.extras.getString(KEY_BIG_CONTENT_TITLE));
serializeString(out, notification.extras.getString(KEY_BIG_CONTENT_DESCRIPTION));
serializeString(out, notification.extras.getString(KEY_BIG_SUMMARY_TEXT));
out.writeBoolean(notification.extras.getBoolean(KEY_BIG_SHOW_WHEN_COLLAPSED, false));
}
serializeString(out, Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? null : notification.getChannelId());
Integer color = UnityNotificationManager.getNotificationColor(notification);
out.writeBoolean(color != null);
if (color != null)
out.writeInt(color);
out.writeInt(notification.number);
out.writeBoolean(0 != (notification.flags & Notification.FLAG_AUTO_CANCEL));
serializeString(out, notification.getGroup());
out.writeBoolean(0 != (notification.flags & Notification.FLAG_GROUP_SUMMARY));
out.writeInt(UnityNotificationManager.getNotificationGroupAlertBehavior(notification));
serializeString(out, notification.getSortKey());
if (showWhen)
out.writeLong(notification.when);
return true;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to serialize notification", e);
return false;
}
}
static void serializeString(DataOutputStream out, String s) throws IOException {
if (s == null || s.length() == 0)
out.writeInt(0);
else {
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
out.writeInt(bytes.length);
out.write(bytes);
}
}
static byte[] serializeParcelable(Parcelable obj) {
try {
Parcel p = Parcel.obtain();
Bundle b = new Bundle();
b.putParcelable("obj", obj);
p.writeParcelable(b, 0);
byte[] result = p.marshall();
p.recycle();
return result;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to serialize Parcelable", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to serialize Parcelable", e);
}
return null;
}
protected static Object deserializeNotification(Context context, SharedPreferences prefs) {
String serializedIntentData = prefs.getString(SAVED_NOTIFICATION_PRIMARY_KEY, "");
if (null == serializedIntentData || serializedIntentData.length() <= 0)
return null;
byte[] bytes = Base64.decode(serializedIntentData, 0);
Object notification = deserializeNotification(context, bytes);
if (notification != null)
return notification;
serializedIntentData = prefs.getString(SAVED_NOTIFICATION_FALLBACK_KEY, "");
if (null == serializedIntentData || serializedIntentData.length() <= 0)
return null;
bytes = Base64.decode(serializedIntentData, 0);
return deserializeNotification(context, bytes);
}
/* See serialization method above for explaination of fallbacks.
This one matches it with one additional fallback: support for "old" bundle serialization.
*/
private static Object deserializeNotification(Context context, byte[] bytes) {
ByteArrayInputStream data = new ByteArrayInputStream(bytes);
DataInputStream in = new DataInputStream(data);
Notification notification = deserializeNotificationParcelable(in);
if (notification != null)
return notification;
data.reset();
Notification.Builder builder = deserializeNotificationCustom(context, in);
if (builder == null) {
builder = deserializedFromOldIntent(context, bytes);
}
return builder;
}
private static boolean readAndCheckMagicNumber(DataInputStream in, byte[] magic) {
try {
boolean magicNumberMatch = true;
for (int i = 0; i < magic.length; ++i) {
byte b = in.readByte();
if (b != magic[i]) {
magicNumberMatch = false;
break;
}
}
return magicNumberMatch;
} catch (Exception e) {
return false;
}
}
private static Notification deserializeNotificationParcelable(DataInputStream in) {
try {
if (!readAndCheckMagicNumber(in, UNITY_MAGIC_NUMBER_PARCELLED))
return null;
int version = in.readInt();
if (version < 0 || version > INTENT_SERIALIZATION_VERSION)
return null;
Intent intent = deserializeParcelable(in);
Notification notification = intent.getParcelableExtra(KEY_NOTIFICATION);
return notification;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to deserialize notification intent", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to deserialize notification intent", e);
}
return null;
}
private static Notification.Builder deserializeNotificationCustom(Context context, DataInputStream in) {
try {
if (!readAndCheckMagicNumber(in, UNITY_MAGIC_NUMBER))
return null;
int version = in.readInt();
if (version < 0 || version > NOTIFICATION_SERIALIZATION_VERSION)
return null;
// deserialize extras
int id = 0;
String title, text, smallIcon, largeIcon, bigText, intentData;
long fireTime, repeatInterval;
boolean usesStopWatch, showWhen, showInForeground = true;
Bundle extras = null;
String bigPicture = null, bigLargeIcon = null, bigContentTitle = null, bigSummary = null, bigContentDesc = null;
boolean bigShowWhenCollapsed = false;
if (version < 2) {
// no longer serialized since v2
extras = deserializeParcelable(in);
}
// before v2 it was extras or variables, since 2 always variables
if (extras == null) {
// extras serialized manually
id = in.readInt();
title = deserializeString(in);
text = deserializeString(in);
smallIcon = deserializeString(in);
largeIcon = deserializeString(in);
fireTime = in.readLong();
repeatInterval = in.readLong();
bigText = deserializeString(in);
usesStopWatch = in.readBoolean();
showWhen = in.readBoolean();
intentData = deserializeString(in);
if (version > 0)
showInForeground = in.readBoolean();
if (version >= 3) {
bigPicture = deserializeString(in);
if (bigPicture != null && bigPicture.length() > 0) {
// the following only need to be put in if big picture is there
bigLargeIcon = deserializeString(in);
bigContentTitle = deserializeString(in);
bigContentDesc = deserializeString(in);
bigSummary = deserializeString(in);
bigShowWhenCollapsed = in.readBoolean();
}
}
} else {
title = extras.getString(Notification.EXTRA_TITLE);
text = extras.getString(Notification.EXTRA_TEXT);
smallIcon = extras.getString(KEY_SMALL_ICON);
largeIcon = extras.getString(KEY_LARGE_ICON);
fireTime = extras.getLong(KEY_FIRE_TIME, -1);
repeatInterval = extras.getLong(KEY_REPEAT_INTERVAL, -1);
bigText = extras.getString(Notification.EXTRA_BIG_TEXT);
usesStopWatch = extras.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false);
showWhen = extras.getBoolean(Notification.EXTRA_SHOW_WHEN, false);
intentData = extras.getString(KEY_INTENT_DATA);
}
String channelId = deserializeString(in);
boolean haveColor = in.readBoolean();
int color = 0;
if (haveColor)
color = in.readInt();
int number = in.readInt();
boolean shouldAutoCancel = in.readBoolean();
String group = deserializeString(in);
boolean groupSummary = in.readBoolean();
int groupAlertBehavior = in.readInt();
String sortKey = deserializeString(in);
long when = showWhen ? in.readLong() : 0;
UnityNotificationManager manager = UnityNotificationManager.getNotificationManagerImpl(context);
Notification.Builder builder = manager.createNotificationBuilder(channelId);
if (extras != null)
builder.setExtras(extras);
else {
builder.getExtras().putInt(KEY_ID, id);
UnityNotificationManager.setNotificationIcon(builder, KEY_SMALL_ICON, smallIcon);
UnityNotificationManager.setNotificationIcon(builder, KEY_LARGE_ICON, largeIcon);
if (fireTime != -1)
builder.getExtras().putLong(KEY_FIRE_TIME, fireTime);
if (repeatInterval != -1)
builder.getExtras().putLong(KEY_REPEAT_INTERVAL, repeatInterval);
if (intentData != null)
builder.getExtras().putString(KEY_INTENT_DATA, intentData);
builder.getExtras().putBoolean(KEY_SHOW_IN_FOREGROUND, showInForeground);
}
if (title != null)
builder.setContentTitle(title);
if (text != null)
builder.setContentText(text);
if (bigText != null)
builder.setStyle(new Notification.BigTextStyle().bigText(bigText));
else if (bigPicture != null)
manager.setupBigPictureStyle(builder, bigLargeIcon, bigPicture, bigContentTitle, bigContentDesc, bigSummary, bigShowWhenCollapsed);
if (haveColor)
UnityNotificationManager.setNotificationColor(builder, color);
if (number >= 0)
builder.setNumber(number);
builder.setAutoCancel(shouldAutoCancel);
UnityNotificationManager.setNotificationUsesChronometer(builder, usesStopWatch);
if (group != null && group.length() > 0)
builder.setGroup(group);
builder.setGroupSummary(groupSummary);
UnityNotificationManager.setNotificationGroupAlertBehavior(builder, groupAlertBehavior);
if (sortKey != null && sortKey.length() > 0)
builder.setSortKey(sortKey);
if (showWhen) {
builder.setShowWhen(true);
builder.setWhen(when);
}
return builder;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to deserialize notification", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to deserialize notification", e);
}
return null;
}
private static Notification.Builder deserializedFromOldIntent(Context context, byte[] bytes) {
try {
Parcel p = Parcel.obtain();
p.unmarshall(bytes, 0, bytes.length);
p.setDataPosition(0);
Bundle bundle = new Bundle();
bundle.readFromParcel(p);
int id = bundle.getInt(KEY_ID, -1);
String channelId = bundle.getString("channelID");
String textTitle = bundle.getString("textTitle");
String textContent = bundle.getString("textContent");
String smallIcon = bundle.getString("smallIconStr");
boolean autoCancel = bundle.getBoolean("autoCancel", false);
boolean usesChronometer = bundle.getBoolean("usesChronometer", false);
long fireTime = bundle.getLong(KEY_FIRE_TIME, -1);
long repeatInterval = bundle.getLong(KEY_REPEAT_INTERVAL, -1);
String largeIcon = bundle.getString("largeIconStr");
int style = bundle.getInt("style", -1);
int color = bundle.getInt("color", 0);
int number = bundle.getInt("number", 0);
String intentData = bundle.getString(KEY_INTENT_DATA);
String group = bundle.getString("group");
boolean groupSummary = bundle.getBoolean("groupSummary", false);
String sortKey = bundle.getString("sortKey");
int groupAlertBehaviour = bundle.getInt("groupAlertBehaviour", -1);
boolean showTimestamp = bundle.getBoolean("showTimestamp", false);
Notification.Builder builder = UnityNotificationManager.getNotificationManagerImpl(context).createNotificationBuilder(channelId);
builder.getExtras().putInt(KEY_ID, id);
builder.setContentTitle(textTitle);
builder.setContentText(textContent);
UnityNotificationManager.setNotificationIcon(builder, KEY_SMALL_ICON, smallIcon);
builder.setAutoCancel(autoCancel);
builder.setUsesChronometer(usesChronometer);
builder.getExtras().putLong(KEY_FIRE_TIME, fireTime);
builder.getExtras().putLong(KEY_REPEAT_INTERVAL, repeatInterval);
UnityNotificationManager.setNotificationIcon(builder, KEY_LARGE_ICON, largeIcon);
if (style == 2)
builder.setStyle(new Notification.BigTextStyle().bigText(textContent));
if (color != 0)
UnityNotificationManager.setNotificationColor(builder, color);
if (number >= 0)
builder.setNumber(number);
if (intentData != null)
builder.getExtras().putString(KEY_INTENT_DATA, intentData);
if (null != group && group.length() > 0)
builder.setGroup(group);
builder.setGroupSummary(groupSummary);
if (null != sortKey && sortKey.length() > 0)
builder.setSortKey(sortKey);
UnityNotificationManager.setNotificationGroupAlertBehavior(builder, groupAlertBehaviour);
builder.setShowWhen(showTimestamp);
return builder;
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to deserialize old style notification", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to deserialize old style notification", e);
}
return null;
}
private static String deserializeString(DataInputStream in) throws IOException {
int length = in.readInt();
if (length <= 0)
return null;
byte[] bytes = new byte[length];
int didRead = in.read(bytes);
if (didRead != bytes.length)
throw new IOException("Insufficient amount of bytes read");
return new String(bytes, StandardCharsets.UTF_8);
}
private static <T extends Parcelable> T deserializeParcelable(DataInputStream in) throws IOException {
int length = in.readInt();
if (length <= 0)
return null;
byte[] bytes = new byte[length];
int didRead = in.read(bytes);
if (didRead != bytes.length)
throw new IOException("Insufficient amount of bytes read");
try {
Parcel p = Parcel.obtain();
p.unmarshall(bytes, 0, bytes.length);
p.setDataPosition(0);
Bundle b = p.readParcelable(UnityNotificationUtilities.class.getClassLoader());
p.recycle();
if (b != null) {
return b.getParcelable("obj");
}
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to deserialize parcelable", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to deserialize parcelable", e);
}
return null;
}
// Returns Activity class to be opened when notification is tapped
// Search is done in this order:
// * class specified in meta-data key custom_notification_android_activity
// * the only enabled activity with name ending in either .UnityPlayerActivity or .UnityPlayerGameActivity
// * the only enabled activity in the package
protected static Class<?> getOpenAppActivity(Context context) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
if (bundle.containsKey("custom_notification_android_activity")) {
try {
return Class.forName(bundle.getString("custom_notification_android_activity"));
} catch (ClassNotFoundException e) {
Log.e(TAG_UNITY, "Specified activity class for notifications not found: " + e.getMessage());
}
}
Log.w(TAG_UNITY, "No custom_notification_android_activity found, attempting to find app activity class");
ActivityInfo[] aInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES).activities;
if (aInfo == null) {
Log.e(TAG_UNITY, "Could not get package activities");
return null;
}
String activityClassName = null;
boolean activityIsUnity = false, activityConflict = false;
for (ActivityInfo info : aInfo) {
// activity alias not supported
if (!info.enabled || info.targetActivity != null)
continue;
boolean candidateIsUnity = isUnityActivity(info.name);
if (activityClassName == null) {
activityClassName = info.name;
activityIsUnity = candidateIsUnity;
continue;
}
// two Unity activities is a hard conflict
// two non-Unity activities is a conflict unless we find a Unity activity later on
if (activityIsUnity == candidateIsUnity) {
activityConflict = true;
if (activityIsUnity && candidateIsUnity)
break;
continue;
}
if (candidateIsUnity) {
activityClassName = info.name;
activityIsUnity = candidateIsUnity;
activityConflict = false;
}
}
if (activityConflict) {
Log.e(TAG_UNITY, "Multiple choices for activity for notifications, set activity explicitly in Notification Settings");
return null;
}
if (activityClassName == null) {
Log.e(TAG_UNITY, "Activity class for notifications not found");
return null;
}
return Class.forName(activityClassName);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
Log.e(TAG_UNITY, "Failed to find activity class: " + e.getMessage());
}
return null;
}
private static boolean isUnityActivity(String name) {
return name.endsWith(".UnityPlayerActivity") || name.endsWith(".UnityPlayerGameActivity");
}
protected static Notification.Builder recoverBuilder(Context context, Notification notification) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Notification.Builder builder = Notification.Builder.recoverBuilder(context, notification);
// extras not recovered, transfer manually
builder.setExtras(notification.extras);
return builder;
}
} catch (Exception e) {
Log.e(TAG_UNITY, "Failed to recover builder for notification!", e);
} catch (OutOfMemoryError e) {
Log.e(TAG_UNITY, "Failed to recover builder for notification!", e);
}
return recoverBuilderCustom(context, notification);
}
private static Notification.Builder recoverBuilderCustom(Context context, Notification notification) {
String channelID = notification.extras.getString(KEY_CHANNEL_ID);
Notification.Builder builder = UnityNotificationManager.getNotificationManagerImpl(context).createNotificationBuilder(channelID);
UnityNotificationManager.setNotificationIcon(builder, KEY_SMALL_ICON, notification.extras.getString(KEY_SMALL_ICON));
String largeIcon = notification.extras.getString(KEY_LARGE_ICON);
if (largeIcon != null && !largeIcon.isEmpty())
UnityNotificationManager.setNotificationIcon(builder, KEY_LARGE_ICON, largeIcon);
builder.setContentTitle(notification.extras.getString(Notification.EXTRA_TITLE));
builder.setContentText(notification.extras.getString(Notification.EXTRA_TEXT));
builder.setAutoCancel(0 != (notification.flags & Notification.FLAG_AUTO_CANCEL));
if (notification.number >= 0)
builder.setNumber(notification.number);
String bigText = notification.extras.getString(Notification.EXTRA_BIG_TEXT);
if (bigText != null)
builder.setStyle(new Notification.BigTextStyle().bigText(bigText));
builder.setWhen(notification.when);
String group = notification.getGroup();
if (group != null && !group.isEmpty())
builder.setGroup(group);
builder.setGroupSummary(0 != (notification.flags & Notification.FLAG_GROUP_SUMMARY));
String sortKey = notification.getSortKey();
if (sortKey != null && !sortKey.isEmpty())
builder.setSortKey(sortKey);
builder.setShowWhen(notification.extras.getBoolean(Notification.EXTRA_SHOW_WHEN, false));
Integer color = UnityNotificationManager.getNotificationColor(notification);
if (color != null)
UnityNotificationManager.setNotificationColor(builder, color);
UnityNotificationManager.setNotificationUsesChronometer(builder, notification.extras.getBoolean(Notification.EXTRA_SHOW_CHRONOMETER, false));
UnityNotificationManager.setNotificationGroupAlertBehavior(builder, UnityNotificationManager.getNotificationGroupAlertBehavior(notification));
builder.getExtras().putInt(KEY_ID, notification.extras.getInt(KEY_ID, 0));
builder.getExtras().putLong(KEY_REPEAT_INTERVAL, notification.extras.getLong(KEY_REPEAT_INTERVAL, 0));
builder.getExtras().putLong(KEY_FIRE_TIME, notification.extras.getLong(KEY_FIRE_TIME, 0));
String intentData = notification.extras.getString(KEY_INTENT_DATA);
if (intentData != null && !intentData.isEmpty())
builder.getExtras().putString(KEY_INTENT_DATA, intentData);
return builder;
}
}