Как отправлять push-уведомления на Windows Universal app

    Мы делаем сервис для студентов, основной задачей которого является оповещение одногруппников о различных событиях. Для этого в первую очередь мы используем механизм push-уведомлений. Приложение, разработанное для iOS и Android, будет работать как на планшетах, так и на телефонах с этими операционными системами и механизм отправки push-уведомлений не зависит от класса устройтва. До недавнего времени для Windows и Windows Phone приходилось писать два раздельных приложения, однако теперь есть возможность создавать Universal Windows app — универсальные приложения, которые работают как на Windows 8.1, так и на Windows Phone 8.1. Мы решили не отставать от прогресса и разработали Universal Windows app, отправку push-уведомлений на которое мы также хотели сделать универсальной с точки зрения кода.



    Сервис Edusty работает на ASP .NET MVC WebAPI и располагается в Windows Azure. Изначально для отправки push-уведомлений мы решили использовать библиотеку PushSharp. Для iOS и Android она работает отлично, тоже самое касается Windows Phone 7.5/8.0, а также Windows 8.0/8.1. Когда же мы решили писать Universal Windows app, то столкнулись с проблемой отправки push-уведомлений на устройства с Windows Phone 8.1. Максимальная версия Windows Phone, которую PushSharp поддерживал на тот момент была 8.0. На устройства с Windows Phone 8.1 удавалось отправить push-уведомления только через класс, предназначеный для Windows 8.х, но живые плитки таким образом не работали. Так как быстрого обновления библиотеки мы не дождались, то было принято решение написать код отправки push-уведомлений самостоятельно.
    До прихода универсальных Windows приложений push-уведомления для Windows Phone отправлялись через Microsoft Push Notification Service (MPNS), тогда как для Windows 8.0 приложений был создан новый сервис Windows Notification Service (WNS). Универсальные приложения Windows принесли WNS и для Windows Phone 8.1. Тем самым, отправлять push-уведомления для обеих платформ стало возможным одним способом.

    Формирование контента уведомления


    Универсальное приложение Windows поддерживает три типа уведомлений: toast (всплывающее уведомление), live tile (живая плитка), badge (бляха с цифрой).Push-уведомление формируется XML-разметкой, корневой тег которой определяет тип уведомления.

    <toast>
           <visual>
                 <...>
           </visual>
    </toast>
    

    <tile>
           <visual>
                 <...>
           </visual>
    </tile>
    

    <badge> 
           <...>
    </badge>
    

    Тег visual обязателен для toast и tile. Объединять разные типы в один xml нельзя, так что придётся делать три отправки для трёх типов. Для toast и tile существует множество шаблонов, которые позволяют по-разному оформить всплывающие уведомления и живые плитки. Некоторые шаблоны поддерживаются только на Windows, другие только на Windows Phone, а есть те, которые поддерживаются на обеих платформах — среди них мы и выбирали.

    Для toast и tile внутри тега visual должен быть вложен тег binding с указанием выбранного шаблона, причём в tile можно объединять несколько шаблонов для поддержки разных размеров плиток. Полный список шаблонов можно взять здесь и здесь.

    <toast>
           <visual>
                 <binding template="ToastText02">
                            <text id="1">text1</text>
                            <text id="2">text2</text>
                 </binding>
           </visual>
    </toast>
    

    <tile>
           <visual>
               <binding template="TileSquare150x150Text02" fallback="TileSquareText02">
                         <text id="1">text1</text>
                         <text id="2">text2</text>
               </binding>
               <binding template="TileWide310x150Text09" fallback="TileWideText09">
                         <text id="1">text1</text>
                         <text id="2">text2</text>
               </binding>    
           </visual>
    </tile>
    

    Для тега badge необходимо указать лишь параметр value, значениями которого являются числа или глифы, отображаемые на плитке приложения.

    <badge value="1"> 
    </badge>
    

    Получение ключей авторизации


    Перед тем, как отправить push-уведомление, нам необходимо получить Uri канала уведомлений (PushNotificationChannel.Uri) и токен доступа (access_token).

    Uri канала уведомлений сервис получает от клиента, а чтобы получить токен доступа, необходимо отправить POST-запрос по адресу https://login.live.com/accesstoken.srf. В запросе должен присутствовать заголовок «Content-Type: application/x-www-form-urlencoded», а тело содержать следующие параметры: grant_type (значение всегда «client_credentials»), client_id, client_secret, scope (значение всегда «notify.windows.com»). Значения client_id и client_secret можно узнать в центре разработки для учётных записей Майкрософт на странице вашего приложения, где client_id имеет вид «ms-app://s-1-15-....», а client_secret имеет вид «Z9qiptLV.....». В ответ на запрос придёт json с двумя полями: access_token и token_type, где первое поле и является необходимым нам токеном доступа.

    Отправка push-уведомления


    Теперь можно начинать отправлять push-уведомления. Для этого сформируем POST-запрос по Uri канала уведомлений, полученному с устройства клиента. В запрос необходимо добавить следующие заголовки:
    • «X-WNS-Type», который может принимать 3 различных значения: «wns/toast», «wns/tile», «wns/badge»,
    • «ContentType» со значением «text/xml»,
    • «Authorization» со значением «Bearer ваш токен доступа».
    Добавляем в тело запроса сформированный ранее XML и отправляем. Для каждого типа уведомлений нужно делать отдельные запросы (токен доступа можно не перезапрашивать, если эти запросы идут подряд в течение короткого времени).

    Заключение


    Таким образом мы решили вопрос универсальной отправки push-уведомлений для устройств под управлением Windows 8.1 и Windows Phone 8.1. Через некоторое время после этого мы узнали, что в Windows Azure есть такой замечатльный сервис как Azure Notification Hub, в котором это всё уже реализовано, в том числе отправка для iOS, Android и других платформ. Поскольку у нас и так всё работает, мы решили пока не пользоваться этой функцией Windows Azure.

    Специально для лентяев
    var accessToken = GetAccessToken("Z9qiptL...","ms-app://s-1-15...");
    
                                        var xml = @"<toast>
                                                    <visual>
                                                        <binding template=""ToastText02"">
                                                            <text id=""1"">" + text1 + @"</text>
                                                            <text id=""2"">" + text2 + @"</text>
                                                        </binding>
                                                    </visual>
                                                </toast>";
                                        byte[] content = Encoding.UTF8.GetBytes(xml);
                                        SendWindowsPush(pushDevice, accessToken, content, "wns/toast");
    
                                        xml = @"<tile>
                                                <visual version=""2"">
                                                    <binding template=""TileSquare150x150Text02"" fallback=""TileSquareText02"">
                                                        <text id=""1"">" + text1 + @"</text>
                                                        <text id=""2"">" + text2 + @"</text>
                                                    </binding>
                                                    <binding template=""TileWide310x150Text09"" fallback=""TileWideText09"">
                                                       <text id=""1"">" + text1 + @"</text>
                                                       <text id=""2"">" + text2 + @"</text>
                                                    </binding>    
                                                </visual>                                          
                                            </tile>";
    
                                        content = Encoding.UTF8.GetBytes(xml);
                                        SendWindowsPush(pushDevice, accessToken, content, "wns/tile");
    
                                        xml = @"<badge value=""" + pushDevice.User.UnreadMessagesCount + @"""/>";
                                        content = Encoding.UTF8.GetBytes(xml);
                                        SendWindowsPush(pushDevice, accessToken, content, "wns/badge");
    

    private static OAuthToken GetAccessToken(string secret, string sid)
            {
                HttpContent content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
                {
                    new KeyValuePair<string, string>("grant_type","client_credentials"),
                    new KeyValuePair<string, string>("client_id",sid),
                    new KeyValuePair<string, string>("client_secret",secret),
                    new KeyValuePair<string, string>("scope","notify.windows.com")
                });
                var client = new HttpClient();
                client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded");
                var response = client.PostAsync(new Uri("https://login.live.com/accesstoken.srf"), content).Result;
                var json = response.Content.ReadAsStringAsync().Result;
                return GetOAuthTokenFromJson(json);
            }
    

    private static OAuthToken GetOAuthTokenFromJson(string jsonString)
            {
                using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(jsonString)))
                {
                    var ser = new DataContractJsonSerializer(typeof(OAuthToken));
                    var oAuthToken = (OAuthToken)ser.ReadObject(ms);
                    return oAuthToken;
                }
            }
    

     [DataContract]
        public class OAuthToken
        {
            [DataMember(Name = "access_token")]
            public string AccessToken { get; set; }
            [DataMember(Name = "token_type")]
            public string TokenType { get; set; }
        }
    

     private static void SendWindowsPush(PushDevice pushDevice, OAuthToken accessToken, byte[] content, string type)
            {
                var request = HttpWebRequest.Create(pushDevice.PushCode) as HttpWebRequest;
                request.Method = "POST";
                request.Headers.Add("X-WNS-Type", type);
                request.ContentType = "text/xml";
                request.Headers.Add("Authorization", String.Format("Bearer {0}", accessToken.AccessToken));
    
                using (Stream requestStream = request.GetRequestStream())
                    requestStream.Write(content, 0, content.Length);
                var result = request.GetResponse();
                result.GetResponseStream();
    
    
            }
    
    • +13
    • 7.6k
    • 6
    Edusty
    33.25
    Компания
    Share post

    Comments 6

      0
      Ваша реализация
      1) шлёт пуши синхронно
      2) не обрабатывает протухшие токены
      3) не вызывает Dispose для нескольких объектов, создаваемых в процессе работы с HttpClient и HttpWebRequest, что приводит к утечкам ресурсов. Б-га ради, купите уже ReSharper, он вам кучу косяков в коде выявит.
        0
        1) Так ведь это лишь метод отправки пушей, а сам метод вызывается асинхронно.
        2) Протухшие токены там вряд ли будут, ведь мы отправляем пуш сразу после получения токена, а при следующем вызове этого метода будет получен новый токен.
        3) Ну тут согласен, надо допилить чуток, спасибо.
          +1
          1) Так ведь это лишь метод отправки пушей, а сам метод вызывается асинхронно.
          У вас не просто не используется асинхронная обвязка HttpWebRequest, но ещё и происходит синхронное ожидание асинхронной операции с HttpClient: ReadAsStringAsync().Result; В конце 2014-го года не использовать async/await как-то странно.
          2) Протухшие токены там вряд ли будут, ведь мы отправляем пуш сразу после получения токена, а при следующем вызове этого метода будет получен новый токен.
          Приложение с устройства удалили, код доступа невалиден. Не вижу обработки это ситуации.
            0
            1) По какой-то причине с async/await у нас пуши не отправлялись.
            2) Опять же, это внешняя логика: если от пользователя приходит новый DeviceID (уникальный и постоянный для каждого устройства) и новый токен push-канала, то старый мы удаляем из базы.
              0
              2) Опять же, это внешняя логика: если от пользователя приходит новый DeviceID (уникальный и постоянный для каждого устройства) и новый токен push-канала, то старый мы удаляем из базы.
              1) Вы вообще читаете? Приложение удалено. Новый никогда не придёт.
              2) У вас пользователь только на одно устройство может получать пуши? Что за бред.
                0
                1) Да, действительно старые токены мы не удаляем. Видимо, нужно читать ответ от сервера и по нему определять, устаревший токен или нет.
                2) Нет, у каждого пользователя может быть много устройств, но каждому устройству (DeviceID) может соответствовать только один токен push-канала и один пользователь (то есть если другой пользователь зайдёт с этого же устройства, то в БД к записи этого устройства будет приписан новый пользователь, тем самым старому пуши приходить на это устройство уже не будут).

      Only users with full accounts can post comments. Log in, please.