В Unity3d есть классы для работы с пушами под iOS. Но нет стандартных средств сделать тоже самое и под Android. Приходится писать свой плагин на Java, который затем можно закинуть в Plugins/Android и делать native вызовы к нему. Если вас интересует, как создать свой плагин (в частности, плагин для отправки локальных пушей), заходим под кат.
- Как писать Android плагины для Unity3d
- AAR файл
- Проект плагина в Android Studio
- Настраиваем gradle build скрипт
- AndroidManifest.xml
- Ресурсы
- BroadcastReceiver
- Сборка AAR
- C# обёртка
Как писать Android плагины для Unity3d
У Unity есть доки о том, как написать свой плагин. Но он многим может показаться сложным, да и есть там неточности. Как минимум доки устарели в том плане, что ADT плагин скоро перестанет поддерживаться, что вынуждает нас использовать Android Studio. Я лично до этого всегда работал в Eclipse и меня всё устраивало. Единственное — перешёл с ant на maven в какой-то момент. Система сборки Gradle мне совсем не по душе, да и Android Studio не люблю (так как она базируется на IntelliJ IDEA, которую я недолюбливаю).
Начнём с того, что в Unity3d можно просто кинуть .jar’ник или aar’ку в Plugins/Android и затем использовать их в проекте. Так что, наша задача, по сути, собрать aar для Unity3d в Android Studio и написать врапер на C#, который будет дёргать нужные методы.
AAR
Да, собирать мы будем aar, а не jar, так как aar может содержать в себе ресурсы (layouts, drawables). Раньше Unity вполне нормально относился к тому, что пользователи кидали ресурсы в Plugins/Android/res. Нынче же такой метод помечен как deprecated, поэтому логичным решением будет использовать aar.
Как уже говорил, доки слегка устарели в этом плане. Там до сих пор указано, что можно использовать только .jar’ники:
There are several ways to create a Java plugin but the result in each case is that you end up with a .jar file containing the .class files for your plugin.
Подробнее про AAR формат можно почитать здесь.
Проект плагина в Android Studio
Просто создаём стандартный проект для Android. Для начала необходимо скопировать classes.jar из директории установки Unity. В доках сказано:
To do this, first locate the classes.jar shipped with Unity Android. It is found in the installation folder (usually C:\Program Files\Unity\Editor\Data (on Windows) or /Applications/Unity (on Mac)) in a sub-folder called PlaybackEngines/AndroidPlayer/Variations/mono or il2cpp/Development or Release/Classes/. Then add classes.jar to the classpath used to compile the new Activity.
У меня лично на Win7 этот файлик в C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes. В проекте создайте папочку libs и киньте туда classes.jar.
Настраиваем gradle build скрипт
После создания пустого проекта у вас будет создан build.gradle скрипт с примерно таким содержанием:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0.1" defaultConfig { applicationId "com.example.suvitruf.myapplication" minSdkVersion 15 targetSdkVersion 23 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.1.0' compile 'com.android.support:design:23.1.0' } |
Нам из него надо выпилить всё ненужное. И, самое главное, необходимо поменять тип проекта с com.android.application
на com.android.library
. В итоге наш скрипт будет иметь такой вид:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// проект как библиотека apply plugin: 'com.android.library' android { compileSdkVersion 23 buildToolsVersion "23.0.1" defaultConfig { minSdkVersion 19 targetSdkVersion 23 } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { // классы, которые мы скопировали из Unity директории // не включаем их в итоговую сборку, чтоб потом не было конфликтов provided files('libs/classes.jar') } |
AndroidManifest.xml
Так же удаляем всё ненужное. В итоге останется что-то такое:
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unnyworld.android.notifications" > <application android:allowBackup="true" android:supportsRtl="true" android:theme="@android:style/Theme.Light.NoTitleBar.Fullscreen" > </application> </manifest> |
Ресурсы
Для начала, удалите все ненужные файлы/папки вроде androidTest, Activity, слои и т.п. Из ресурсов нам необходимо создать только layout для отображения пуша:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="65sp" android:padding="10dp" android:orientation="vertical" > <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <ImageView android:id="@+id/notification_layout_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/notify_icon_big" android:layout_gravity="center_vertical" /> <TextView android:id="@+id/notification_layout_text" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:id="@+id/notification_layout_title" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout> |
И ещё необходимо картинки раскидать по нужным drawable директориям. Если верить сайту, то размеры для иконок пушей такие (размеры в пикселях):
22 × 22 area in 24 × 24 (mdpi) 33 × 33 area in 36 × 36 (hdpi) 44 × 44 area in 48 × 48 (xhdpi) 66 × 66 area in 72 × 72 (xxhdpi) 88 × 88 area in 96 × 96 (xxxhdpi)
Эта картинка показывается в status bar при получении пуша. Картина должна быть плоской (никаких градиентов и т.п.) и белой на прозрачном фоне.
BroadcastReceiver
Теперь остаётся только создать BroadcastReceiver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
package com.unnyworld.android.notifications; import android.app.Activity; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.BitmapFactory; import android.graphics.Color; import android.media.RingtoneManager; import android.os.Build; import com.unity3d.player.UnityPlayer; public class UnityNotificationManager extends BroadcastReceiver { public static void SetNotification(int id, long delayMs, String title, String message, String ticker, int sound, int vibrate, int lights, int bgColor, int executeMode, String unityClass) { Activity currentActivity = UnityPlayer.currentActivity; AlarmManager am = (AlarmManager)currentActivity.getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(currentActivity, UnityNotificationManager.class); intent.putExtra("ticker", ticker); intent.putExtra("title", title); intent.putExtra("message", message); intent.putExtra("id", id); intent.putExtra("color", bgColor); intent.putExtra("sound", sound == 1); intent.putExtra("vibrate", vibrate == 1); intent.putExtra("lights", lights == 1); intent.putExtra("activity", unityClass); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (executeMode == 2) am.setExactAndAllowWhileIdle(0, System.currentTimeMillis() + delayMs, PendingIntent.getBroadcast(currentActivity, id, intent, 0)); else if (executeMode == 1) am.setExact(0, System.currentTimeMillis() + delayMs, PendingIntent.getBroadcast(currentActivity, id, intent, 0)); else am.set(0, System.currentTimeMillis() + delayMs, PendingIntent.getBroadcast(currentActivity, id, intent, 0)); } else am.set(0, System.currentTimeMillis() + delayMs, PendingIntent.getBroadcast(currentActivity, id, intent, 0)); } public static void SetRepeatingNotification(int id, long delay, String title, String message, String ticker, long rep, int sound, int vibrate, int lights, int bgColor, String unityClass) { Activity currentActivity = UnityPlayer.currentActivity; AlarmManager am = (AlarmManager)currentActivity.getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(currentActivity, UnityNotificationManager.class); intent.putExtra("ticker", ticker); intent.putExtra("title", title); intent.putExtra("message", message); intent.putExtra("id", id); intent.putExtra("color", bgColor); intent.putExtra("sound", sound == 1); intent.putExtra("vibrate", vibrate == 1); intent.putExtra("lights", lights == 1); intent.putExtra("activity", unityClass); am.setRepeating(0, System.currentTimeMillis() + delay, rep, PendingIntent.getBroadcast(currentActivity, id, intent, 0)); } public void onReceive(Context context, Intent intent) { NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); String ticker = intent.getStringExtra("ticker"); String title = intent.getStringExtra("title"); String message = intent.getStringExtra("message"); int color = intent.getIntExtra("color", 0); String unityClass = intent.getStringExtra("activity"); Boolean sound = intent.getBooleanExtra("sound", false); Boolean vibrate = intent.getBooleanExtra("vibrate", false); Boolean lights = intent.getBooleanExtra("lights", false); int id = intent.getIntExtra("id", 0); Resources res = context.getResources(); Class<?> unityClassActivity = null; try { unityClassActivity = Class.forName(unityClass); } catch (ClassNotFoundException e) { e.printStackTrace(); // всё плохо return; } Intent notificationIntent = new Intent(context, unityClassActivity); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0); Notification.Builder builder = new Notification.Builder(context); builder.setContentIntent(contentIntent) .setWhen(System.currentTimeMillis()) .setAutoCancel(true) .setContentTitle(title) .setContentText(message); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) builder.setColor(color); if(ticker != null && ticker.length() > 0) builder.setTicker(ticker); // это та самая белая иконка из drawable builder.setSmallIcon(R.drawable.notify_icon_small); builder.setLargeIcon(BitmapFactory.decodeResource(res, R.drawable.notify_icon_big)); if(sound) builder.setSound(RingtoneManager.getDefaultUri(2)); if(vibrate) builder.setVibrate(new long[] { 1000L, 1000L }); if(lights) builder.setLights(Color.GREEN, 3000, 3000); Notification notification = builder.build(); notificationManager.notify(id, notification); } public static void CancelNotification(int id) { Activity currentActivity = UnityPlayer.currentActivity; AlarmManager am = (AlarmManager)currentActivity.getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(currentActivity, UnityNotificationManager.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(currentActivity, id, intent, 0); am.cancel(pendingIntent); } public static void CancelAll(){ NotificationManager notificationManager = (NotificationManager)UnityPlayer.currentActivity.getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancelAll(); } } |
Собственно, ничего примечательно, обычный BroadcastReceiver, с помощью которого можно отправлять обычные пуши (SetNotification
) и повторяющиеся (SetRepeatingNotification
). Ну и монжо отменить какой-то пуш по id (CancelNotification(int id)
)или же отменить их все (CancelAll
).
Сборка AAR
Собственно, остаётся только сбилдить проект. Пункт AssembleRelease справа. Теперь скопируем полученный aar в /Plugins/Android.
C# обёртка
Для начала добавить пермишены нужные в /Plugins/Android/AndroidManifest.xml:
1 |
<uses-permission android:name="android.permission.VIBRATE" /> |
И добавить наш UnityNotificationManager в application:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<application android:icon="@drawable/app_icon" android:label="@string/app_name" android:debuggable="true"> <!-- NOTIFY java --> <receiver android:name="com.unnyworld.android.notifications.UnityNotificationManager"></receiver> <!-- end --> <activity android:name="com.unity3d.player.UnityPlayerNativeActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> </intent-filter> <meta-data android:name="unityplayer.UnityActivity" android:value="true" /> <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="false" /> </activity> </application> |
Остаётся только написать немного кода на C#. Создадим простенький скриптик LocalNotification.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
using UnityEngine; using System.Collections; using System.Collections.Generic; using System; class LocalNotification{ /// <summary> /// Inexact uses `set` method /// Exact uses `setExact` method /// ExactAndAllowWhileIdle uses `setAndAllowWhileIdle` method /// Documentation: https://developer.android.com/intl/ru/reference/android/app/AlarmManager.html /// </summary> public enum NotificationExecuteMode{ Inexact = 0, Exact = 1, ExactAndAllowWhileIdle = 2 } #if UNITY_ANDROID && !UNITY_EDITOR private static string fullClassName = "com.unnyworld.android.notifications.UnityNotificationManager"; private static string mainActivityClassName = "com.unity3d.player.UnityPlayerNativeActivity"; #endif public static void SendNotification(int id, TimeSpan delay, string title, string message){ SendNotification(id, (int)delay.TotalSeconds, title, message, Color.white); } public static void SendNotification(int id, long delay, string title, string message, Color32 bgColor, bool sound = true, bool vibrate = true, bool lights = true, NotificationExecuteMode executeMode = NotificationExecuteMode.Inexact){ #if UNITY_ANDROID && !UNITY_EDITOR AndroidJavaClass pluginClass = new AndroidJavaClass(fullClassName); if (pluginClass != null) pluginClass.CallStatic("SetNotification", id, delay * 1000L, title, message, message, sound ? 1 : 0, vibrate ? 1 : 0, lights ? 1 : 0, bgColor.r * 65536 + bgColor.g * 256 + bgColor.b, (int)executeMode, mainActivityClassName); #endif } public static void SendRepeatingNotification(int id, long delay, long timeout, string title, string message, Color32 bgColor, bool sound = true, bool vibrate = true, bool lights = true){ #if UNITY_ANDROID && !UNITY_EDITOR AndroidJavaClass pluginClass = new AndroidJavaClass(fullClassName); if (pluginClass != null) pluginClass.CallStatic("SetRepeatingNotification", id, delay * 1000L, title, message, message, timeout * 1000, sound ? 1 : 0, vibrate ? 1 : 0, lights ? 1 : 0, bgColor.r * 65536 + bgColor.g * 256 + bgColor.b, mainActivityClassName); #endif } public static void CancelNotification(int id){ #if UNITY_ANDROID && !UNITY_EDITOR AndroidJavaClass pluginClass = new AndroidJavaClass(fullClassName); if (pluginClass != null) pluginClass.CallStatic("CancelNotification", id); #endif } public static void CancelAllNotifications(){ #if UNITY_ANDROID && !UNITY_EDITOR AndroidJavaClass pluginClass = new AndroidJavaClass(fullClassName); if (pluginClass != null) pluginClass.CallStatic("CancelAll"); #endif } } |
Собственно, всё. Как основу я испольщовал этот проект. Немного его допилил (перенёс проект с Eclipse на Android Studio). Пока что в тестовой ветке мой пул, но, думаю, в скором времени автор запушит в мастер ветку всё.