День девятнадцатый. Уведомления (Push Notifications)

Вступление

Изучая возможности Windows Phone 7, нельзя не обратить внимания на механизм уведомлений Push Notification, которого не было в предыдущих версиях Windows Mobile. Этот механизм сам по себе очень удобный для разработчиков и пользователей и позволяет асинхронно уведомлять пользователя о происходящих событиях. Например, сервис из облака может сообщить пользователю о том, что произошло какое-то событие, на которое нужно как-то отреагировать. Давайте разберемся как устроен этот механизм.

Если вы еще не слышали о том, что такое Push Notifications, то ничего страшного. Все очень просто: вместо того, чтобы заставлять ваше приложение проверять сервер на наличие новых данных каждые несколько минут, ваш сервер сам сообщает устройству, что появились новые данные.

Зачем использовать Push Notifications?

Одна из главных причин - экономия заряда батареи. Постоянные запросы к серверу - это лишняя нагрузка на аппарат, что приводит к расходу энергии.

Вторая причина - используя механизм уведомлений, вы можете сообщить пользователю о появлении интересного события, даже если ваше приложение не запущено в данный момент. Вы можете предупредить пользователя, что нужно открыть ваше приложение, чтобы выполнить то или иное действие.

Как работает Push Notification

В структуре Push Notifications существует специальный сервис в облаке (размещенный за счет мощностей Microsoft), который делает возможность работы уведомлений. Когда устройство хочет получать уведомления от обращается к этому сервису и устанавливает с ним постоянное соединение. В ответ на это сервис выдает этому устройству уникальный URI, используя который можно отправить по протоколу HTTP некоторое сообщение. Это сообщение получает тот самый сервис в облаке и пересылает его соответствующему устройству. Логично, что для каждого устройства и приложения этот URI будет уникальный. Таким образом, работу уведомлений в Windows Phone 7 можно описать так:

  1. Устройство устанавливает соединение с сервером Microsoft и получает уникальный HTTP-адрес;
  2. Устройство передает этот HTTP-адрес тому, кто будет уведомлять устройство о чем-либо;
  3. Когда необходимо уведомить устройство о чем-либо, приложение, которому был передан HTTP-адрес отправляет специальное HTTP-сообщение на этот адрес;
  4. Сервис Microsoft получает это сообщение и по возможности доставляет его на устройство. Если доставить сообщение на устройство не удалось, то отправитель получит соответствующее сообщение.

Схематически этот алгоритм можно изобразить следующим образом.

Push Notification

Типы уведомлений

Существуют три разных вида уведомлений для Windows Phone 7:

  • Tile Notifications – если ваше приложение закреплено на начальном экране Start, вы можете обновить Tile (плитку) вашего приложения. Например, вы можете изменить фон картинки, используя диапазон чисел 0-99. При отправке уведомления указывается URL некоторого изображения, расположенного в сети. Когда устройство получает уведомление оно самостоятельно загружает изображение из сети и обновляет его на рабочем столе устройства.
  • Toast Notifications – при получении этого типа уведомления на экране устройства всплывает небольшое сообщение, содержащее текст уведомления. При этом приложение в данный момент может быть неактивным. При нажатии на это сообщение пользователь может быстро переключиться к соответствующему уведомлению.
  • Raw Notifications – при получении этого типа уведомления его содержимое передается непосредственно приложению. Приложение в праве обработать содержимое уведомление так, как считает нужным по своему собственному алгоритму. Это наиболее гибкий способ получения уведомлений от внешних сервисов.

Рассмотрим все описываемые случаи. Если вы хотите пошагово пройти пример, то рекомендую посмотреть пример в Windows Phone Developer Training Kit, где подробно описан пример работы с Push Notification.

Tile Notifications

Пожалуй, самый простой способ push-обновления - это смена плитки по расписанию. Для смены плитки используется класс Microsoft.Phone.Shell.ShellTileSchedule, который позволяет скачать новую картинку для плитки с вашего сайта. Вам необходимо подготовить картинку PNG, которое не будет превышать размер 80 кб, а время загрузки не должно превышать 60 секунд. Закачайте картинку на свой сайт и запомните его URI-адрес.

Далее нужно создать экземпляр класса ShellTileSchedule, который выполнит обновление плитки приложения. Откройте файл App.xaml.cs вашего проекта, найдите конструктор приложения App и вставьте сразу после него следующий фрагмент кода.


// To store the instance for the application lifetime
private ShellTileSchedule shellTileSchedule;

  /// 
  /// Create the application shell tile schedule instance
  /// 
  private void CreateShellTileSchedule()
  {
      shellTileSchedule = new ShellTileSchedule();
      shellTileSchedule.Recurrence = UpdateRecurrence.Interval;
      shellTileSchedule.Interval = UpdateInterval.EveryHour;
      shellTileSchedule.StartTime = DateTime.Now;
      shellTileSchedule.RemoteImageUri = new   Uri(@"http://developer.alexanderklimov.ru/images/tileimage.png");
      shellTileSchedule.Start();
  }

В данном коде вы можете изменить только свойство RemoteImageUri, в котором нужно прописать путь к вашему изображению на сайте. Свойство работает только с интернет-адресами, нельзя ссылаться на файл, расположенный на локальной машине или на устройстве (эмуляторе).

О других свойствах и методах:

  • Recurrent = Interval - задает периодическое обновление плитки. Другой возможный вариант: Recurrent = OneTime соответствует однократному обновлению.
  • Interval = EveryHour обновляет плитку каждые 60 минут. Час — это минимальное возможное значение интервала обновления. Это ограничение вводится для экономии ресурсов телефона.
  • Start запускает процесс обновления плитки. Обратите внимание, что первое обновление может задержаться на час. К сожалению, во время отладки нельзя повлиять на этот процесс, поэтому вам придется оставить эмулятор на час включенным, чтобы увидеть смену плитки.

Теперь перейдите в конструктор App() и добавьте в него вызов функции CreateShellTileSchedule:


// Constructor
public App()
{
    // Global handler for uncaught exceptions. 
    // Note that exceptions thrown by ApplicationBarItem.Click will not get caught here.
    UnhandledException += Application_UnhandledException;

    // Standard Silverlight initialization
    InitializeComponent();

    // Phone-specific initialization
    InitializePhoneApplication();

    // Create the shell tile schedule instance
    CreateShellTileSchedule();
}

Запустите приложение. На эмуляторе телефона нажмите кнопку перемещения назад, чтобы выйти из приложения. Далее закрепите вашу программу на стартовой странице. Теперь изображение карточки PushNotifications будет обновляться согласно настройкам ShellTileSchedule. Наберитесь терпения и приблизительно через час плитка вашего приложения автоматически изменится на изображение, которое будет скачано с указанного вами адреса в свойстве RemoteImageUri.

Получение Custom URI от службы Push Notification

Для работы нам понадобится сборка Microsoft.Phone.Notification. С другой стороны мы можем получить нужный URI от службы Push Notification (PNS), написав 10 строчек кода. Сначала мы создаем экземпляр HttpNotificationChannel, который автоматически связывается с PNS (в отдельном потоке). Нам остается создать событие для захвата ответа службы.


HttpNotificationChannel channel;

void GetPushChannel()
{
	channel = new HttpNotificationChannel("BLANKENSOFT_" + DateTime.Now.Ticks.ToString());
	channel.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(channel_ChannelUriUpdated);
	channel.Open();
}

void channel_ChannelUriUpdated(object sender, NotificationChannelUriEventArgs e)
{
	Dispatcher.BeginInvoke(delegate
	{
		URIBlock.Text = channel.ChannelUri.ToString();
	});
}

Вот таким был ответ в моем случае:


http://sn1.notify.live.net/throttledthirdparty/01.00/AAHsLicyiJgtTaiDbJoSgmFiAgAAAAADAwAAAAQUZm52OjlzOEQ2NDJDRkl5MEVFMEQ

Имея URI, вы можете сохранить его для вашего веб-сервиса. Ваш веб-сервис должен создать необходимое сообщение для телефона и отослать его тремя способами: Tile notification, Toast notification, или a Raw notification.

Работаем с Toast Notification

Получив наш Push URI, мы просто создаем HTTP-сообщение и посылаем его на наш URI. Вот как это выглядит в коде:


HttpWebRequest sendNotificationRequest = (HttpWebRequest)WebRequest.Create(channel.ChannelUri.ToString());
sendNotificationRequest.Method = "POST";
//Indicate that you'll send toast notifications!
sendNotificationRequest.ContentType = "text/xml";
sendNotificationRequest.Headers = new WebHeaderCollection();
sendNotificationRequest.Headers.Add("X-NotificationClass", "2");
if (string.IsNullOrEmpty(txtMessage.Text)) return;
		
//Create xml envelope
string data = "X-WindowsPhone-Target: toast\r\n\r\n" +
		"<?xml version='1.0' encoding='utf-8'?>" + 
		"<wp:Notification xmlns:wp='WPNotification'>" + 
		"<wp:Toast>" + 
		"<wp:Text1>{0}</wp:Text1>" +
		"</wp:Toast>" + 
		"</wp:Notification>"; 
		
//Wrap custom data into envelope
string message = string.Format(data, txtMessage.Text);
byte[] notificationMessage = Encoding.Default.GetBytes(message);
		
// Set Content Length
sendNotificationRequest.ContentLength = notificationMessage.Length;
		
//Push data to stream
using (Stream requestStream = sendNotificationRequest.GetRequestStream())
{
	requestStream.Write(notificationMessage, 0, notificationMessage.Length);
}
	
//Get reponse for message sending
HttpWebResponse response = (HttpWebResponse)sendNotificationRequest.GetResponse();
string notificationStatus = response.Headers["X-NotificationStatus"];
string notificationChannelStatus = response.Headers["X-SubscriptionStatus"];
string deviceConnectionStatus = response.Headers["X-DeviceConnectionStatus"];

Как видите, код достаточно длинный и запутанный. Поэтому еще раз рекомендую изучить пример из Windows Phone Developer Training Kit(Push Notification example). Там подробно описан весь процесс от начала и до конца, и показано, насколько мощным является механизм обновления вашего приложения перед глазами пользователя.

Также можете посмотреть пример Сергея Звездина (ссылка ниже)

Raw Notifications

Raw Notifications – это наиболее гибкий тип уведомлений. Обрабатывать данный тип уведомлений можно по своему усмотрению, используя нужный для приложения алгоритм. Если в случае с Toast Notifications и Tile Notifications мы должны соблюдать определенный формат и тип передаваемых данных (для Toast – строка, для Tile – адрес до изображения). то в случае с Raw Notifications мы можем передавать абсолютно любые данные, которые нам необходимы. Основная идея отправки уведомления остается той же самой – мобильное приложение подключается к сервису в облаке, получает уникальный URI и каким-либо образом передает его приложению, которое должно уведомлять наш сервис.


private HttpNotificationChannel notifications;
private const string notificationServiceName = "Some test";

private void InitializePushChannel()
{
    if (notifications != null)
    {
        ShowUri(notifications.ChannelUri);
    }
    else
    {
        try
        {
            notifications = new HttpNotificationChannel(Application.Current.ToString(), notificationServiceName);
            notifications.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(notifications_ChannelUriUpdated);
            notifications.Open();
            ShowUri(notifications.ChannelUri);

            notifications.BindToShellNotification();
        }
        catch (NotificationChannelExistsException)
        {
            notifications = HttpNotificationChannel.Find(Application.Current.ToString());
            notifications.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(notifications_ChannelUriUpdated);
            ShowUri(notifications.ChannelUri);
        }
    }
}

Как видно, для работы с механизмом уведомлений используется все тот же объект HttpNotificationChannel. Аналогичным образом следует обрабатывать ситуацию, когда сервис уже был зарегистрирован (в этом случае следует использовать метод Find). Также следует обрабатывать событие ChannelUriUpdated на случай, если сервис в облаке решит изменить адрес для отправки уведомлений. Для этого мы создадим специальный обработчик, который в случае изменения адреса будет отображать его пользователю.


private void ShowUri(Uri uri)
{
    if (uri != null)
    {
        Dispatcher.BeginInvoke(() => ChannelUri.Text = uri.ToString());
        Debug.WriteLine(uri.ToString());
    }
}

Для того, чтобы обработать данные, которые были получены с помощью Raw-уведомлений следует подписаться на событие HttpNotificationReceived. Это событие будет срабатывать всякий раз, когда поступает новое Raw-уведомление.


private HttpNotificationChannel notifications;
private const string notificationServiceName = "Some test";

private void InitializePushChannel()
{
    if (notifications != null)
    {
        ShowUri(notifications.ChannelUri);
    }
    else
    {
        try
        {
            notifications = new HttpNotificationChannel(Application.Current.ToString(), notificationServiceName);
            notifications.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(notifications_ChannelUriUpdated);
            notifications.HttpNotificationReceived += new EventHandler<HttpNotificationEventArgs>(notifications_HttpNotificationReceived);
            notifications.Open();
            ShowUri(notifications.ChannelUri);

            notifications.BindToShellNotification();
        }
        catch (NotificationChannelExistsException)
        {
            notifications = HttpNotificationChannel.Find(Application.Current.ToString());
            notifications.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(notifications_ChannelUriUpdated);
            notifications.HttpNotificationReceived += new EventHandler<HttpNotificationEventArgs>(notifications_HttpNotificationReceived);
            ShowUri(notifications.ChannelUri);
        }
    }
}

Теперь создадим обработчик, который будет срабатывать в момент получения уведомлений. Для этого мы уже подписались на событие HttpNotificationReceived. В параметрах обработчика события передается объект HttpNotificationEventArgs, позволяющий получить доступ к телу HTTP-запроса, который был получен от приложения, создавшего уведомление. В простейшем случае давайте отобразим это сообщение пользователю. Тело сообщения доступно как поток (Stream). Для его чтения мы будем использовать объект StreamReader.


void notifications_HttpNotificationReceived(object sender, HttpNotificationEventArgs e)
{
    using (var r = new StreamReader(e.Notification.Body))
    {
        var data = r.ReadToEnd();
        Debug.WriteLine(data);
        Dispatcher.BeginInvoke(() => ReceivedData.Text = data);
    }
}

В данном обработчике мы используем элемент управления ReceivedData. Для того, чтобы это стало возможным его необходимо добавить на форму. Сделаем это легким движением руки.


<phoneNavigation:PhoneApplicationPage 
    x:Class="ToastNotification.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phoneNavigation="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Navigation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="800"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}">

    <Grid x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.Background>
            <ImageBrush ImageSource="FormBackground.jpg" Stretch="UniformToFill"/>
        </Grid.Background>

        <!--TitleGrid is the name of the application and page title-->
        <Grid x:Name="TitleGrid" Grid.Row="0">
            <TextBlock Text="MY APPLICATION" x:Name="textBlockPageTitle" Style="{StaticResource PhoneTextPageTitle1Style}"/>
            <TextBlock Text="Notifications" x:Name="textBlockListTitle" Style="{StaticResource PhoneTextPageTitle2Style}"/>
        </Grid>

        <!--ContentGrid is empty. Place new content here-->
        <Grid x:Name="ContentGrid" Grid.Row="1">
            <Border VerticalAlignment="Top" HorizontalAlignment="Center" Margin="20" Padding="20" CornerRadius="5">
                <Border.Background>
                    <SolidColorBrush Color="Black" Opacity="0.2"/>
                </Border.Background>
                <TextBlock Name="ReceivedData"
                           TextWrapping="Wrap"
                           FontSize="30"/>
            </Border>

            <Border VerticalAlignment="Center" HorizontalAlignment="Center" Margin="20" Padding="20" CornerRadius="5">
                <Border.Background>
                    <SolidColorBrush Color="Black" Opacity="0.2"/>
                </Border.Background>
                <TextBlock Name="ChannelUri"
                           TextWrapping="Wrap"
                           FontSize="30"/>
            </Border>
            
        </Grid>
    </Grid>
    
</phoneNavigation:PhoneApplicationPage>

Мобильное приложение готово, теперь, используя полученный URI для отправки уведомлений отправить HTTP-запрос по этому адресу. Для этого как раньше создадим небольшое консольное приложение, способное выполнить эти несложные действия. Для этих целей мы все так же будем использовать объект WebClient. Однако, в отличие от Toast Notifications, при отправке Raw Notifications не нужно соблюдать какой-либо формат сообщения – можно просто записать в тело сообщения те данные, которые необходимо отправить мобильному устройству. Также стоит изменить заголовок “X-NotificationClass” на значение “3”, что будет означать наивысший приоритет отправки для Raw-уведомлений.

При отправке уведомления, в ответ мы также получим два заголовка “X-DeviceConnectionStatus” и “X-NotificationStatus”, которые позволяют определить состояние подключения устройства к сети и статус доставки этого сообщения. Таким образом, код нашего простого приложения будет выглядеть следующим образом.


class Program
{
    private const string Url = @"http://sn1.notify.live.net/throttledthirdparty/01.00/AAFTkavjFpAuT4i1fIKvueDiAgoOs1ADAgAAAAQOMDAwAAAAAAAAAAAAAAA";

    static void Main(string[] args)
    {
        Console.Write("Text: ");
        string text = Console.ReadLine();
        SendToast(Url, text);
        Console.ReadLine();
    }

    static void SendToast(string uri, string text)
    {
        var client = new WebClient();

        client.Headers.Add("Content-Type", "text/html");
        client.Headers.Add("X-NotificationClass", "3");

        var result = client.UploadString(uri, "POST", text);
        Console.WriteLine(result);
        Console.WriteLine("Device status is {0}", client.ResponseHeaders["X-DeviceConnectionStatus"]);
        Console.WriteLine("Notification status is {0}", client.ResponseHeaders["X-NotificationStatus"]);
    }
}

Теперь при запуске приложения и отправке текста, этот текст будет отображен на экране мобильного устройства.

Мы создали с вами тривиальное приложение, которое получая строку просто отображает её на экране. Однако, вся мощь Raw Notifications состоит в том, что мы можем определить совершенно любое поведение для обработки уведомлений. Например, давайте изменим приложение так, чтобы при получении названий некоторых цветов оно изменяло цвет букв, отображающих сообщение уведомления.


void notifications_HttpNotificationReceived(object sender, HttpNotificationEventArgs e)
{
    using (var r = new StreamReader(e.Notification.Body))
    {
        var data = r.ReadToEnd().Trim('\0');
        Debug.WriteLine(data);

        switch (data)
        {
            case "red":
                Dispatcher.BeginInvoke(() => ReceivedData.Foreground = new SolidColorBrush(Colors.Red));
                break;
            case "green":
                Dispatcher.BeginInvoke(() => ReceivedData.Foreground = new SolidColorBrush(Colors.Green));
                break;
            case "blue":
                Dispatcher.BeginInvoke(() => ReceivedData.Foreground = new SolidColorBrush(Colors.Blue));
                break;
            case "white":
                Dispatcher.BeginInvoke(() => ReceivedData.Foreground = new SolidColorBrush(Colors.White));
                break;
            case "black":
                Dispatcher.BeginInvoke(() => ReceivedData.Foreground = new SolidColorBrush(Colors.Black));
                break;
            case "yellow":
                Dispatcher.BeginInvoke(() => ReceivedData.Foreground = new SolidColorBrush(Colors.Yellow));
                break;
            case "orange":
                Dispatcher.BeginInvoke(() => ReceivedData.Foreground = new SolidColorBrush(Colors.Orange));
                break;
            case "gray":
                Dispatcher.BeginInvoke(() => ReceivedData.Foreground = new SolidColorBrush(Colors.Gray));
                break;
            default:
                Dispatcher.BeginInvoke(() => ReceivedData.Text = data);
                break;
        }
    }
}

Теперь, при отправке уведомлений, содержащих имена цветов, цвет текста в приложении будет изменяться.

Понятно, что это тоже очень простой алгоритм обработки. Однако, он хорошо демонстрирует, что здесь никто не ограничивает нас тем, каким образом построить обработку уведомлений. В общем случае мы можем создавать совершенно любые типы манипуляций и изменять поведение приложения.

Реклама

© 2017 А.Климов Google+ Рейтинг@Mail.ru