[WinRT] Mettre à jour le LockScreen avec une XamlRenderingBackgroundTask

Depuis quelque temps maintenant, le SDK WinRT permet de mettre à jour le LockScreen directement depuis une application, mais aussi depuis une Background Task. Plus récemment encore, une nouvelle API faisait son entrée : XamlRenderingBackgroundTask. Cette dernière permet notamment d’exécuter du code XAML et d’en effectuer le rendu. Ce que l’on entend par exécuter du code XAML est en fait la possibilité d’instancier des contrôles graphiques sans pour autant se trouver dans le contexte d’exécution d’une application WinRT. Cela peut donc être réalisé depuis une tâche en arrière plan, tâche qui pourra être déclenchée par le code de l’application ou encore en fonction d’évènements systèmes prédéfinis sans que l’application ne soit lancée.

Préparation de la solution

Pour l’exemple, nous considérerons une application UWP développée avec C#/Xaml. En ce qui concerne le code de la BackgroundTask, celui-ci se trouvera généralement dans un projet de type Windows Runtime Component. Le langage à privilégier pour ce dernier est C++. En effet, les contraintes d’exécution imposées à une tâche de fond comprennent entre autre une quantité limitée de mémoire (fonction de la mémoire totale du système). Comparée à un langage managé comme C#, C++ permet généralement un contrôle plus fin de la quantité de mémoire nécessaire à l’exécution du code, de part notamment son caractère déterministe et l’absence de garbage collector. L’avantage pour le développeur C# est qu’il peut manipuler un langage C++ évolué dans le cadre d’une application WinRT : C++/CX. Il retrouve ainsi la plupart des APIs dont il a l’habitude (celles du namespace Windows notamment), des mots clefs équivalents à async et await dans les dernières versions du compilateur ou la possibilité d’utiliser des tasks de la même façon qu’il manipulerait des promises.

Voici donc la structure retenue de la solution :

Capture1

Création de la classe BackgroundTask

Il faut ensuite créer une classe dans le projet BackgroundTask que l’on nommera ici LockScreenUpdater (clic droit > add > Class…), correspondant au point d’entrée de la tâche de fond. Le header de la classe doit implémenter l’interface IBackgroundTask et par conséquent contenir la déclaration de la méthode OnRun. Dans le cas actuel, on souhaite pouvoir exécuter du code XAML et en faire un rendu bitmap. Pour cela, il faut faire hériter la tâche de XamlRenderingBackgroundTask. Voici le code du header :

#pragma once

namespace BackgroundTask
{
    public ref class LockScreenUpdater sealed : Windows::UI::Xaml::Media::Imaging::XamlRenderingBackgroundTask
    {
    private:
    protected:
        void OnRun(Windows::ApplicationModel::Background::IBackgroundTaskInstance^ taskInstance) override;
    public:
        LockScreenUpdater();
    };
}

Le code du header s’accompagne des implémentations des méthodes déclarées, dans le fichier cpp :

#include "pch.h"
#include "LockScreenUpdater.h"
#include <string>
#include <sstream>
#include <robuffer.h>

using namespace BackgroundTask;
using namespace concurrency;
using namespace Platform;
using namespace Windows::ApplicationModel::Background;
using namespace Windows::ApplicationModel;
using namespace Windows::Foundation;
using namespace Windows::Graphics::Imaging;
using namespace Windows::Storage;
using namespace Windows::Storage::Streams;
using namespace Windows::System::UserProfile;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::UI::Xaml::Markup;
using namespace Windows::UI::Xaml::Media::Imaging;

LockScreenUpdater::LockScreenUpdater()
{
}

void LockScreenUpdater::OnRun(IBackgroundTaskInstance^ taskInstance)
{
}

Les différents includes et using effectué ici le sont pour les besoins du code à venir. Ils ne sont donc pas forcément nécessaires dans un autre scénario.

Mise à jour du manifeste

Dans le projet d’application initiale, il est nécessaire de déclarer la tâche de fond dans le manifeste. L’éditeur graphique permet de réaliser cette opération via l’onglet Declarations, en ajoutant une entrée de type Background Tasks. Il faut ensuite définir le type de tâche que l’on souhaite : dans notre cas le type System Event est très pratique car il permet de déclencher la t^che de fond assez simplement. Toutefois, le choix final dépendra évidemment du contexte. Le point d’entrée de la tâche de fond est ici le nom complet de la classe précédemment créée (BackgroundTask.LockScreenUpdater) :

Capture2

Enregistrement de la tâche

Les étapes précédentes effectuées, il faut ensuite enregistrer la tâche de fond et définir de quelle façon elle doit être déclenchée. Cela peut être fait par l’application directement, ou encore en fonction d’évènements systèmes par exemple. C’est ce dernier cas que nous retiendrons pour l’exemple. En fonction du contexte, il peut être intéressant de supprimer les enregistrements précédents de sorte à purger les versions obsolètes :

foreach (var task in BackgroundTaskRegistration.AllTasks)
{
    task.Value.Unregister(true);
}

Il peut être nécessaire de demander à l’utilisateur son consentement pour exécuter des tâches en arrière-plan :

var result = await BackgroundExecutionManager.RequestAccessAsync();
if (result == BackgroundAccessStatus.Denied)
{
    return;
}

Il faut ensuite créer le nouvel enregistrement :

BackgroundTaskBuilder builder = new BackgroundTaskBuilder();
builder.Name = "LockScreenUpdater";
builder.TaskEntryPoint = $"BackgroundTask.LockScreenUpdater";

builder.SetTrigger(new SystemTrigger(SystemTriggerType.NetworkStateChange, false));
var registration = builder.Register();

Dans ce cas, nous avons choisi de déclencher la tâche à chaque modification d’état du réseau. Cela permet de déclencher la tâche en switchant sur le mode avion par exemple. Cette méthode est un peu rustique, et visual studio permet normalement de lancer manuellement une tâche de fond, mais il permet aussi de lancer la tâche de fond sans que l’application ou le debugger ne soit lancés.

Code de la tâche

Il faut donc revenir à la méthode OnRun de la BackgroundTask, dans laquelle on pourra définir le code à exécuter lorsque la tâche sera lancée. Aujourd’hui, la plupart des APIs manipulées le sont de façon asynchrone, cela nécessite donc un mécanisme pour prévenir l’agent que le code de la tâche est terminé. De plus, le but premier de l’exemple est de mettre à jour le LockScreen, il faut donc vérifier si cela est possible et stopper la tâche si ça ne l’est pas :

void LockScreenUpdater::OnRun(IBackgroundTaskInstance^ taskInstance)
{
    Agile<BackgroundTaskDeferral^> deferral = Agile<BackgroundTaskDeferral^>(taskInstance->GetDeferral());

    if (UserProfilePersonalizationSettings::Current->IsSupported) {
        // ...
    } else {
        deferral->Complete();
    }
}

La prochaine étape consiste à récupérer le XAML à transformer en bitmap, XAML que nous récupérerons à partir d’un fichier XML stocké dans les assets de l’application. Voici le code XAML en question :

<Grid xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
      xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
      xmlns:mc='http://schemas.openxmlformats.org/markup-compatibility/2006'>
  <Grid.RowDefinitions>
    <RowDefinition Height="*"/>
    <RowDefinition Height="*"/>
    <RowDefinition Height="*"/>
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="*"/>
    <ColumnDefinition Width="*"/>
  </Grid.ColumnDefinitions>
  <Image Grid.Row="0" Grid.Column="0" x:Name="Img0" Stretch="UniformToFill" />
  <Image Grid.Row="0" Grid.Column="1" x:Name="Img1" Stretch="UniformToFill" />
  <Image Grid.Row="1" Grid.Column="0" x:Name="Img2" Stretch="UniformToFill" />
  <Image Grid.Row="1" Grid.Column="1" x:Name="Img3" Stretch="UniformToFill" />
  <TextBlock Grid.Row="0" Grid.Column="0" x:Name="Tb0" Foreground="White" FontFamily="Segoe WP Black" FontSize="30" Margin="5" VerticalAlignment="Bottom" HorizontalAlignment="Right" />
  <TextBlock Grid.Row="0" Grid.Column="1" x:Name="Tb1" Foreground="White" FontFamily="Segoe WP Black" FontSize="30" Margin="5" VerticalAlignment="Bottom" HorizontalAlignment="Right" />
  <TextBlock Grid.Row="1" Grid.Column="0" x:Name="Tb2" Foreground="White" FontFamily="Segoe WP Black" FontSize="30" Margin="5" VerticalAlignment="Bottom" HorizontalAlignment="Right" />
  <TextBlock Grid.Row="1" Grid.Column="1" x:Name="Tb3" Foreground="White" FontFamily="Segoe WP Black" FontSize="30" Margin="5" VerticalAlignment="Bottom" HorizontalAlignment="Right" />
</Grid>

Cette récupération se fait donc en lisant un fichier. Toutes les opérations IO de WinRT étant asynchrone, il convient de les encapsuler dans une task afin d’en orchestrer l’exécution. A la fin de celle-ci, nous pourrons compléter  la tâche de fond puisque  nous définirons auparavant le reste du code à exécuter :

create_task(StorageFile::GetFileFromApplicationUriAsync(ref new Uri("ms-appx:///Assets/lockScreen.xml"))).then([](StorageFile^ xamlFile) {
    return FileIO::ReadTextAsync(xamlFile);
}).then([](String^ content) {
    //...
}).then([deferral](bool ok) {
    deferral->Complete();
});

Il faut ensuite charger le XAML et l’interpréter. Si certaines valeurs sont dynamiques, il convient aussi de parcourir les contrôles instanciers et d’en modifier les propriétés comme suit :

auto size = Window::Current->Bounds;

Grid^ root = (Grid^)XamlReader::Load(content);
root->Width = size.Width;
root->Height = size.Height;
for (auto ix = 0; ix < 4; ++ix) {

    std::wstringstream imgName;
    imgName << L"Img" << ix;
    Image^ img = (Image^)root->FindName(ref new String(imgName.str().c_str()));

    std::wstringstream path;
    path << L"ms-appx:///Assets/" << ix << L".jpg";
    img->Source = ref new BitmapImage(ref new Uri(ref new String(path.str().c_str())));

    std::wstringstream tbName;
    tbName << L"Tb" << ix;
    TextBlock^ tb = (TextBlock^)root->FindName(ref new String(tbName.str().c_str()));

    std::wstringstream tbText;
    tbText << "text content" << ix;
    tb->Text = ref new String(tbText.str().c_str());;
}

//...

La classe RenderTargetBitmap permet ensuite d’effectuer le rendu des contrôles instanciés dans un bitmap. Il faut ensuite obtenir l’image brute générée pour l’encoder ensuite. L’encodage effectué, on peut ensuite définir le LockScreen à l’aide de la méthode TrySetLockScreen :

RenderTargetBitmap^ rtb = ref new RenderTargetBitmap();
return create_task(rtb->RenderAsync(root, size.Width, size.Height)).then([rtb]() {
    return rtb->GetPixelsAsync();
}).then([rtb](IBuffer^ buffer) {
    return create_task(ApplicationData::Current->LocalFolder->CreateFileAsync("lock-screen.jpg", CreationCollisionOption::ReplaceExisting)).then([buffer, rtb](StorageFile^ contentFile) {
        return create_task(contentFile->OpenAsync(FileAccessMode::ReadWrite)).then([](IRandomAccessStream^ output) {
            return BitmapEncoder::CreateAsync(BitmapEncoder::PngEncoderId, output);
        }).then([buffer, rtb](BitmapEncoder^ encoder) {

            //...

        }).then([contentFile]() {
            return Windows::System::UserProfile::UserProfilePersonalizationSettings::Current->TrySetLockScreenImageAsync(contentFile);
        });
    });
});

Ici, le choix a été fait de définir la sortie de l’encodeur dans le flux d’un fichier, ce qui pourrait être fait en mémoire aussi, mais ce serait sans tenir compte des limitations mémoires imposées à une BackgroundTask. Pour terminer l’encodage, il est possible d’obtenir un pointeur sur les données du buffer retourné par le RenderTargetBitmap, ce pointeur permet donc d’accéder directement à la mémoire et de la communiquer à l’encodeur sans recopie :

IUnknown* pUnk = reinterpret_cast<IUnknown*>(buffer);
IBufferByteAccess* pBufferByteAccess = nullptr;
pUnk->QueryInterface(IID_PPV_ARGS(&pBufferByteAccess));
byte *pixels = nullptr;
pBufferByteAccess->Buffer(&pixels);

Array<unsigned char>^ data = ref new Array<unsigned char>(pixels, rtb->PixelWidth * rtb->PixelHeight * 4);
encoder->SetPixelData(BitmapPixelFormat::Bgra8, BitmapAlphaMode::Premultiplied, rtb->PixelWidth, rtb->PixelHeight, 96, 96, data);

return encoder->FlushAsync();

 

Et voilà ! Une super application qui définit l’image du LockScreen à partir d’un fichier XAML ! Pour récupérer le code, c’est par ici.

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus