アプリへの市販デモ (RDX) 機能の追加

販売フロアで PC やデバイスを試しているユーザーがすぐに開始できるように、Windows アプリに小売デモ モードを含めます。

お客様は、小売店で PC やデバイスのデモを試用できることを期待しています。 多くの場合、市販デモ エクスペリエンス (RDX) を通じて、かなりの時間をアプリの試用に費やします。

"通常" モードと "市販" モードで異なるエクスペリエンスを提供するようにアプリを設定することができます。 たとえば、アプリが設定 プロセスから始まる場合、市販モードではそのプロセスをスキップし、アプリにサンプル データと既定の設定をあらかじめ登録しておき、お客様がすぐに使用できるようにすることができます。

お客様の視点から見えるアプリは 1 つのみです。 お客様が 2 つのモードを区別できるように、アプリが市販モードの間は、タイトル バーや適切な場所に "市販" という単語を目立つように表示することをお勧めします。

RDX 対応アプリは、ご来店のお客様に常に肯定的なエクスペリエンスを提供できるように、アプリの Microsoft Store 要件に加え、RDX の設定、クリーンアップ、プロセスの更新に完全に対応している必要があります。

設計原則

  • 最大のメリットを提示する。 市販デモ モードは、アプリのメリットを伝えるために使ってください。 多くの場合、これはお客様がこのアプリに触れる最初の機会です。最も魅力的な部分をアピールしましょう。

  • スピーディな伝達。 お客様に見ていただける時間は限られています。アプリの真価がすぐに実感できるように構成してください。

  • シンプルなストーリー。 市販デモ モードは、ごく限られた時間でアプリの真価を伝えるチャンスです。

  • エクスペリエンスを重視。 お客様がコンテンツを理解する時間を設けましょう。 魅力的な部分をすばやく伝えることは重要ですが、適切な空白時間を設けることでエクスペリエンスがさらに向上します。

技術的な要件

RDX 対応アプリは、ご来店のお客様にアプリの真価をご理解いただくことを目的としているため、次の技術的要件を満たすと共に、すべての市販デモ エクスペリエンス アプリに関して Microsoft Store が定めるプライバシー規則に従う必要があります。

これをチェックリストとして利用して、検証プロセスの準備や、テスト プロセスの明確化に役立てることができます。 これらの要件は、検証プロセスだけでなく、アプリが市販デモ デバイスで実行される限り、市販デモ エクスペリエンス アプリのライフタイム全体にわたって満たす必要があります。

重要な要件

これらの必須要件を満たしていない RDX 対応アプリは、可能な限り速やかにすべての市販デモ デバイスから削除されます。

  • 個人を特定できる情報 (PII) を尋ねない。 これには、ログイン情報、Microsoft アカウント情報、連絡先の詳細が含まれます。

  • エラーのないエクスペリエンス。 アプリは、エラーなしで動作する必要があります。 また市販デモ デバイスを使うお客様に、エラー ポップアップやエラー通知を表示してはなりません。 エラーは、アプリ自体、ブランド、デバイスのブランド、デバイスの製造元のブランド、Microsoft のブランドに悪影響を及ぼします。

  • 有料アプリには試用モードを用意する。 アプリは無料であるか、試用モードを含める必要があります。 お客様は小売店での試用に料金を支払うことは想定していません。

優先度の高い要件

これらの優先度の高い要件を満たしていない RDX 対応アプリは、直ちに調査して修正する必要があります。 直ちに修正されない場合、このアプリをすべての市販デモ デバイスから削除することがあります。

  • 優れたオフライン エクスペリエンス。 小売拠点に展示されているデバイスの約 50% がオフラインであるため、アプリは優れたオフライン エクスペリエンスを提供する必要があります。 この要件は、お客様がオフラインでアプリを操作する場合でも、意味のある肯定的なエクスペリエンスを保証することを目的としています。

  • 更新済みのコンテンツ エクスペリエンス。 オンラインのときに、更新プログラムのプロンプトを表示しないでください。 更新が必要な場合は、プロンプトを表示せずに実行するようにします。

  • 匿名通信の禁止。 市販デモ デバイスを使うお客様は匿名ユーザーであるため、デバイスからのメッセージ送信やコンテンツの共有を抑制する必要があります。

  • クリーンアップ プロセスを使って一貫したエクスペリエンス提供する。 市販デモ デバイスは、使用開始にあたってすべてのお客様に同じエクスペリエンスを提供する必要があります。 クリーンアップ プロセスを使って、アプリの使用後は常に同じ既定の状態に戻るようにします。 前のお客様が残したものを次のお客様に見せないでください。 これには、スコアボード、達成度、ロック解除が含まれます。

  • 年齢に応じた適切なコンテンツ。 すべてのアプリのコンテンツは、ティーン以下の年齢区分向けでなければなりません。 詳細については、アプリの評価に関する IARC のページESRB 評価に関するページを参照してください。

中程度の優先度の要件

Windows リテール ストア チームは、これらの問題の修正方法について、直接開発者に連絡して話し合いの場を設けることがあります。

  • 多様なデバイスで正常に動作する能力。 アプリは、ローエンド仕様のデバイスを含む、すべてのデバイスで適切に動作する必要があります。 最小限の仕様を満たさないデバイスにアプリがインストールされた場合は、そのことをユーザーに明確に通知する必要があります。 アプリが常に高いパフォーマンスで動作できるように、最小のデバイス要件を明らかにする必要があります。

  • 小売店アプリのサイズ要件を満たす。 アプリのサイズは、800 MB 未満である必要があります。 RDX 対応アプリがこのサイズ要件を満たしていない場合は、Windows リテール ストア チームに直接お問い合わせください。

RetailInfo API: デモ モード用のコードの準備

IsDemoModeEnabled

RetailInfo ユーティリティ クラスの IsDemoModeEnabled プロパティは、Windows 10および Windows 11 SDK の Windows.System.Profile 名前空間の一部であり、ブール型インジケーターとして使用され、アプリが実行するコード パス (標準モードまたは小売モード) を指定します。

using Windows.Storage;

StorageFolder folder = ApplicationData.Current.LocalFolder;

if (Windows.System.Profile.RetailInfo.IsDemoModeEnabled) 
{
    // Use the demo specific directory
    folder = await folder.GetFolderAsync("demo");
}

StorageFile file = await folder.GetFileAsync("hello.txt");
// Now read from file
using namespace Windows::Storage;

StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;

if (Windows::System::Profile::RetailInfo::IsDemoModeEnabled) 
{
    // Use the demo specific directory
    create_task(localFolder->GetFolderAsync("demo").then([this](StorageFolder^ demoFolder)
    {
        return demoFolder->GetFileAsync("hello.txt");
    }).then([this](task<StorageFile^> fileTask)
    {
        StorageFile^ file = fileTask.get();
    });
    // Do something with file
}
else
{
    create_task(localFolder->GetFileAsync("hello.txt").then([this](StorageFile^ file)
    {
        // Do something with file
    });
}
if (Windows.System.Profile.retailInfo.isDemoModeEnabled) {
    console.log("Retail mode is enabled.");
} else {
    Console.log("Retail mode is not enabled.");
}

RetailInfo.Properties

IsDemoModeEnabled から true が返されたら、RetailInfo.Properties を使ってデバイスに関する一連のプロパティを照会することで、さらにカスタマイズした市販デモ エクスペリエンスを構築できます。 これらのプロパティには、ManufacturerNameScreensizeMemory などが含まれます。

using Windows.UI.Xaml.Controls;
using Windows.System.Profile

TextBlock priceText = new TextBlock();
priceText.Text = RetailInfo.Properties[KnownRetailInfo.Price];
// Assume infoPanel is a StackPanel declared in XAML
this.infoPanel.Children.Add(priceText);
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::System::Profile;

TextBlock ^manufacturerText = ref new TextBlock();
manufacturerText.set_Text(RetailInfo::Properties[KnownRetailInfoProperties::Price]);
// Assume infoPanel is a StackPanel declared in XAML
this->infoPanel->Children->Add(manufacturerText);
var pro = Windows.System.Profile;
console.log(pro.retailInfo.properties[pro.KnownRetailInfoProperties.price);

IDL

//  Copyright (c) Microsoft Corporation. All rights reserved.
//
//  WindowsRuntimeAPISet

import "oaidl.idl";
import "inspectable.idl";
import "Windows.Foundation.idl";
#include <sdkddkver.h>

namespace Windows.System.Profile
{
    runtimeclass RetailInfo;
    runtimeclass KnownRetailInfoProperties;

    [version(NTDDI_WINTHRESHOLD), uuid(0712C6B8-8B92-4F2A-8499-031F1798D6EF), exclusiveto(RetailInfo)]
    [version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
    interface IRetailInfoStatics : IInspectable
    {
        [propget] HRESULT IsDemoModeEnabled([out, retval] boolean *value);
        [propget] HRESULT Properties([out, retval, hasvariant] Windows.Foundation.Collections.IMapView<HSTRING, IInspectable *> **value);
    }

    [version(NTDDI_WINTHRESHOLD), uuid(50BA207B-33C4-4A5C-AD8A-CD39F0A9C2E9), exclusiveto(KnownRetailInfoProperties)]
    [version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
    interface IKnownRetailInfoPropertiesStatics : IInspectable
    {
        [propget] HRESULT RetailAccessCode([out, retval] HSTRING *value);
        [propget] HRESULT ManufacturerName([out, retval] HSTRING *value);
        [propget] HRESULT ModelName([out, retval] HSTRING *value);
        [propget] HRESULT DisplayModelName([out, retval] HSTRING *value);
        [propget] HRESULT Price([out, retval] HSTRING *value);
        [propget] HRESULT IsFeatured([out, retval] HSTRING *value);
        [propget] HRESULT FormFactor([out, retval] HSTRING *value);
        [propget] HRESULT ScreenSize([out, retval] HSTRING *value);
        [propget] HRESULT Weight([out, retval] HSTRING *value);
        [propget] HRESULT DisplayDescription([out, retval] HSTRING *value);
        [propget] HRESULT BatteryLifeDescription([out, retval] HSTRING *value);
        [propget] HRESULT ProcessorDescription([out, retval] HSTRING *value);
        [propget] HRESULT Memory([out, retval] HSTRING *value);
        [propget] HRESULT StorageDescription([out, retval] HSTRING *value);
        [propget] HRESULT GraphicsDescription([out, retval] HSTRING *value);
        [propget] HRESULT FrontCameraDescription([out, retval] HSTRING *value);
        [propget] HRESULT RearCameraDescription([out, retval] HSTRING *value);
        [propget] HRESULT HasNfc([out, retval] HSTRING *value);
        [propget] HRESULT HasSdSlot([out, retval] HSTRING *value);
        [propget] HRESULT HasOpticalDrive([out, retval] HSTRING *value);
        [propget] HRESULT IsOfficeInstalled([out, retval] HSTRING *value);
        [propget] HRESULT WindowsVersion([out, retval] HSTRING *value);
    }

    [version(NTDDI_WINTHRESHOLD), static(IRetailInfoStatics, NTDDI_WINTHRESHOLD)]
    [version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone), static(IRetailInfoStatics, NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
    [threading(both)]
    [marshaling_behavior(agile)]
    runtimeclass RetailInfo
    {
    }

    [version(NTDDI_WINTHRESHOLD), static(IKnownRetailInfoPropertiesStatics, NTDDI_WINTHRESHOLD)]
    [version(NTDDI_WINTHRESHOLD, Platform.WindowsPhone), static(IKnownRetailInfoPropertiesStatics, NTDDI_WINTHRESHOLD, Platform.WindowsPhone)]
    [threading(both)]
    [marshaling_behavior(agile)]
    runtimeclass KnownRetailInfoProperties
    {
    }
}

クリーンアップ プロセス

クリーンアップは、お客様がデバイスの操作を止めてから 2 分後に開始されます。 市販デモが再生され、Windows によって連絡先、写真、その他のアプリのサンプル データのリセットを開始されます。 デバイスによっては、すべてを完全にリセットして通常の状態に戻るのに 1 分から 5 分かかる場合があります。 この処理によって、小売店のすべてのお客様がデバイスのところに来て、同じようにデバイスを操作できるようになります。

手順 1: クリーンアップ

  • すべての Win32 アプリとストア アプリが終了します
  • ピクチャビデオミュージックドキュメント保存済みの写真カメラロールデスクトップダウンロードフォルダーなどのすべての既知のフォルダーが削除されます
  • 構造化されていないローミング状態と構造化されたローミング状態が削除されます
  • 構造化されたローカル状態が削除されます

手順 2: 設定

  • オフライン デバイスの場合: フォルダーは空のままです
  • オンライン デバイスの場合: Microsoft Store から市販デモ アセットがデバイスにプッシュされます

ユーザー セッションをまたいでデータを格納する

ユーザー セッションをまたいでデータを格納する場合は、情報を ApplicationData.Current.TemporaryFolder に格納します。このフォルダーのデータは、既定のクリーンアップ処理によって自動的に削除されません。 LocalState を使って保存した情報は、クリーンアップ処理中に削除されることに注意してください。

クリーンアップ プロセスをカスタマイズする

クリーンアップ プロセスをカスタマイズするには、アプリに Microsoft-RetailDemo-Cleanup アプリ サービスを実装します。

カスタムのクリーンアップ ロジックが必要なシナリオには、負荷の高い設定の実行、データのダウンロードとキャッシュ、LocalState データを削除したくない場合などがあります。

手順 1: アプリ マニフェストで Microsoft-RetailDemo-Cleanup サービスを宣言します。

  <Applications>
      <Extensions>
        <uap:Extension Category="windows.appService" EntryPoint="MyCompany.MyApp.RDXCustomCleanupTask">
          <uap:AppService Name="Microsoft-RetailDemo-Cleanup" />
        </uap:Extension>
      </Extensions>
   </Application>
  </Applications>

手順 2: 下のサンプル テンプレートを使って、AppdataCleanup ケース関数の下に、カスタムのクリーンアップ ロジックを実装します。

using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Collections;
using Windows.Storage;

namespace MyCompany.MyApp
{
    public sealed class RDXCustomCleanupTask : IBackgroundTask
    {
        BackgroundTaskCancellationReason _cancelReason = BackgroundTaskCancellationReason.Abort;
        BackgroundTaskDeferral _deferral = null;
        IBackgroundTaskInstance _taskInstance = null;
        AppServiceConnection _appServiceConnection = null;

        const string MessageCommand = "Command";

        public void Run(IBackgroundTaskInstance taskInstance)
        {
            // Get the deferral object from the task instance, and take a reference to the taskInstance;
            _deferral = taskInstance.GetDeferral();
            _taskInstance = taskInstance;
            _taskInstance.Canceled += new BackgroundTaskCanceledEventHandler(OnCanceled);

            AppServiceTriggerDetails appService = _taskInstance.TriggerDetails as AppServiceTriggerDetails;
            if ((appService != null) && (appService.Name == "Microsoft-RetailDemo-Cleanup"))
            {
                _appServiceConnection = appService.AppServiceConnection;
                _appServiceConnection.RequestReceived += _appServiceConnection_RequestReceived;
                _appServiceConnection.ServiceClosed += _appServiceConnection_ServiceClosed;
            }
            else
            {
                _deferral.Complete();
            }
        }

        void _appServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
        {
        }

        async void _appServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            //Get a deferral because we will be calling async code
            AppServiceDeferral requestDeferral = args.GetDeferral();
            string command = null;
            var returnData = new ValueSet();

            try
            {
                ValueSet message = args.Request.Message;
                if (message.ContainsKey(MessageCommand))
                {
                    command = message[MessageCommand] as string;
                }

                if (command != null)
                {
                    switch (command)
                    {
                        case "AppdataCleanup":
                            {
                                // Do custom clean up logic here
                                break;
                            }
                    }
                }
            }
            catch (Exception e)
            {
            }
            finally
            {
                requestDeferral.Complete();
                // Also release the task deferral since we only process one request per instance.
                _deferral.Complete();
            }
        }

        private void OnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
        {
            _cancelReason = reason;
        }
    }
}