Cortana의 백그라운드 앱에서 포그라운드 앱으로의 딥 링크

Warning

이 기능은 Windows 10 2020년 5월 업데이트(버전 2004, 코드명 “20H1”)부터 더 이상 지원되지 않습니다.

백그라운드 앱의 딥 링크를 Cortana에 제공하여 특정 상태 또는 컨텍스트에서 앱을 포그라운드로 시작합니다.

참고 항목

포그라운드 앱이 시작되면 Cortana 및 백그라운드 앱 서비스가 모두 종료됩니다.

딥 링크는 여기("AdventureWorks로 이동")에 표시된 대로 기본적으로 Cortana 완료 화면에 표시되지만 다른 여러 화면에 딥 링크를 표시할 수 있습니다.

예정된 여정에 대한 Cortana 백그라운드 앱 완성 스크린샷

개요

사용자는 다음과 같은 방법으로 Cortana를 통해 앱에 액세스할 수 있습니다.

여기서는 딥 링크에 대해 설명합니다.

딥 링크는 Cortana와 앱 서비스가 전체 기능을 갖춘 앱에 대한 게이트웨이 역할을 하는 경우(사용자가 시작 메뉴를 통해 앱을 시작하도록 요구하지 않음) 또는 Cortana를 통해서는 사용할 수 없고 앱 내에서 제공되는 더 풍부한 세부 정보 및 기능에 대한 액세스를 제공하는 경우에 유용합니다. 딥 링크는 유용성을 높이고 앱을 홍보하기 위한 또 다른 방법입니다.

딥 링크를 제공하는 방법에는 세 가지가 있습니다.

  • 다양한 Cortana 화면의 "<앱>으로 이동" 링크.
  • 다양한 Cortana 화면의 콘텐츠 타일에 포함된 링크.
  • 백그라운드 앱 서비스에서 프로그래밍 방식으로 포그라운드 앱 시작.

Cortana는 대부분의 화면에서 콘텐츠 카드 아래에 "<앱>으로 이동" 딥 링크를 표시합니다.

백그라운드 앱 완성 화면의 Cortana '앱으로 이동' 딥 링크 스크린샷.

앱 서비스와 유사한 컨텍스트에서 앱을 여는 이 링크에 대한 시작 인수를 제공할 수 있습니다. 시작 인수를 제공하지 않으면 앱이 주 화면으로 시작됩니다.

AdventureWorks 샘플의 AdventureWorksVoiceCommandService.cs에 대한 이 예제에서는 지정된 대상(destination) 문자열을 SendCompletionMessageForDestination 메서드에 전달합니다. 이 메서드는 일치하는 모든 여행을 검색하고 앱에 대한 딥 링크를 제공합니다.

먼저 Cortana가 말하고 Cortana 캔버스에 표시되는 VoiceCommandUserMessage(userMessage)를 만듭니다. 그런 다음, 캔버스에 결과 카드의 컬렉션을 표시하기 위해 VoiceCommandContentTile 목록 개체가 생성됩니다.

그런 다음, 이 두 개체가 VoiceCommandResponse 개체(response)의 CreateResponse 메서드에 전달됩니다. 그런 다음 AppLaunchArgument 속성 값을 이 함수에 전달된 destination 값으로 설정합니다. 사용자가 Cortana 캔버스에서 콘텐츠 타일을 탭하면 매개 변수 값이 응답 개체를 통해 앱에 전달됩니다.

마지막으로 VoiceCommandServiceConnectionReportSuccessAsync 메서드를 호출합니다.

/// <summary>
/// Show details for a single trip, if the trip can be found. 
/// This demonstrates a simple response flow in Cortana.
/// </summary>
/// <param name="destination">The destination specified in the voice command.</param>
private async Task SendCompletionMessageForDestination(string destination)
{
...
    IEnumerable<Model.Trip> trips = store.Trips.Where(p => p.Destination == destination);

    var userMessage = new VoiceCommandUserMessage();
    var destinationsContentTiles = new List<VoiceCommandContentTile>();
...
    var response = VoiceCommandResponse.CreateResponse(userMessage, destinationsContentTiles);

    if (trips.Count() > 0)
    {
        response.AppLaunchArgument = destination;
    }

    await voiceServiceConnection.ReportSuccessAsync(response);
}

다양한 Cortana 화면에서 콘텐츠 카드에 딥 링크를 추가할 수 있습니다.

핸드오프와 함께 AdventureWorks 예정된 여행을 사용하는 엔드투엔드 Cortana 백그라운드 앱 흐름에 대한 Cortana 캔버스의 스크린샷핸드오프 화면이 있는 AdventureWorks "향후 여정"

"<앱>으로 이동" 링크와 마찬가지로 시작 인수를 제공하여 앱 서비스와 비슷한 컨텍스트를 사용하여 앱을 열 수 있습니다. 시작 인수를 제공하지 않으면 콘텐츠 타일이 앱에 연결되지 않습니다.

AdventureWorks 샘플의 AdventureWorksVoiceCommandService.cs에 대한 이 예제에서는 지정된 대상을 SendCompletionMessageForDestination 메서드에 전달합니다. 이 메서드는 일치하는 모든 여행을 검색하고 콘텐츠 카드에 앱에 대한 딥 링크를 제공합니다.

먼저 Cortana가 말하고 Cortana 캔버스에 표시되는 VoiceCommandUserMessage(userMessage)를 만듭니다. 그런 다음, 캔버스에 결과 카드의 컬렉션을 표시하기 위해 VoiceCommandContentTile 목록 개체가 생성됩니다.

그런 다음, 이 두 개체가 VoiceCommandResponse 개체(response)의 CreateResponse 메서드에 전달됩니다. 그런 다음, AppLaunchArgument 속성 값을 음성 명령의 대상 값으로 설정합니다.

마지막으로 VoiceCommandServiceConnectionReportSuccessAsync 메서드를 호출합니다. 여기서는 다른 AppLaunchArgument 매개 변수 값을 가진 두 개의 콘텐츠 타일을 VoiceCommandServiceConnection 개체의 ReportSuccessAsync 호출에 사용되는 VoiceCommandContentTile 목록에 추가합니다.

/// <summary>
/// Show details for a single trip, if the trip can be found. 
/// This demonstrates a simple response flow in Cortana.
/// </summary>
/// <param name="destination">The destination specified in the voice command.</param>
private async Task SendCompletionMessageForDestination(string destination)
{
    // If this operation is expected to take longer than 0.5 seconds, the task must
    // supply a progress response to Cortana before starting the operation, and
    // updates must be provided at least every 5 seconds.
    string loadingTripToDestination = string.Format(
               cortanaResourceMap.GetValue("LoadingTripToDestination", cortanaContext).ValueAsString,
               destination);
    await ShowProgressScreen(loadingTripToDestination);
    Model.TripStore store = new Model.TripStore();
    await store.LoadTrips();

    // Query for the specified trip. 
    // The destination should be in the phrase list. However, there might be  
    // multiple trips to the destination. We pick the first.
    IEnumerable<Model.Trip> trips = store.Trips.Where(p => p.Destination == destination);

    var userMessage = new VoiceCommandUserMessage();
    var destinationsContentTiles = new List<VoiceCommandContentTile>();
    if (trips.Count() == 0)
    {
        string foundNoTripToDestination = string.Format(
               cortanaResourceMap.GetValue("FoundNoTripToDestination", cortanaContext).ValueAsString,
               destination);
        userMessage.DisplayMessage = foundNoTripToDestination;
        userMessage.SpokenMessage = foundNoTripToDestination;
    }
    else
    {
        // Set plural or singular title.
        string message = "";
        if (trips.Count() > 1)
        {
            message = cortanaResourceMap.GetValue("PluralUpcomingTrips", cortanaContext).ValueAsString;
        }
        else
        {
            message = cortanaResourceMap.GetValue("SingularUpcomingTrip", cortanaContext).ValueAsString;
        }
        userMessage.DisplayMessage = message;
        userMessage.SpokenMessage = message;

        // Define a tile for each destination.
        foreach (Model.Trip trip in trips)
        {
            int i = 1;
            
            var destinationTile = new VoiceCommandContentTile();

            destinationTile.ContentTileType = VoiceCommandContentTileType.TitleWith68x68IconAndText;
            destinationTile.Image = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///AdventureWorks.VoiceCommands/Images/GreyTile.png"));

            destinationTile.AppLaunchArgument = trip.Destination;
            destinationTile.Title = trip.Destination;
            if (trip.StartDate != null)
            {
                destinationTile.TextLine1 = trip.StartDate.Value.ToString(dateFormatInfo.LongDatePattern);
            }
            else
            {
                destinationTile.TextLine1 = trip.Destination + " " + i;
            }

            destinationsContentTiles.Add(destinationTile);
            i++;
        }
    }

    var response = VoiceCommandResponse.CreateResponse(userMessage, destinationsContentTiles);

    if (trips.Count() > 0)
    {
        response.AppLaunchArgument = destination;
    }

    await voiceServiceConnection.ReportSuccessAsync(response);
}

시작 인수를 사용하여 프로그래밍 방식으로 앱을 시작하여 앱 서비스와 비슷한 컨텍스트로 앱을 열 수도 있습니다. 시작 인수를 제공하지 않으면 앱이 주 화면으로 시작됩니다.

여기서는 VoiceCommandServiceConnection 개체의 RequestAppLaunchAsync 호출에 사용되는 VoiceCommandResponse 개체에 값이 "Las Vegas"인 AppLaunchArgument 매개 변수를 추가합니다.

var userMessage = new VoiceCommandUserMessage();
userMessage.DisplayMessage = "Here are your trips.";
userMessage.SpokenMessage = 
  "You have one trip to Vegas coming up.";

response = VoiceCommandResponse.CreateResponse(userMessage);
response.AppLaunchArgument = "Las Vegas";
await  VoiceCommandServiceConnection.RequestAppLaunchAsync(response);

앱 매니페스트

앱에 대한 딥 링크를 사용하도록 설정하려면 앱 프로젝트의 Package.appxmanifest 파일에서 windows.personalAssistantLaunch 확장을 선언해야 합니다.

여기서는 Adventure Works 앱에 대한 windows.personalAssistantLaunch 확장을 선언합니다.

<Extensions>
  <uap:Extension Category="windows.appService" 
    EntryPoint="AdventureWorks.VoiceCommands.AdventureWorksVoiceCommandService">
    <uap:AppService Name="AdventureWorksVoiceCommandService"/>
  </uap:Extension>
  <uap:Extension Category="windows.personalAssistantLaunch"/> 
</Extensions>

프로토콜 계약

앱은 프로토콜 계약을 사용하여 URI(Uniform Resource Identifier) 활성화를 통해 포그라운드로 시작됩니다. 앱은 앱의 OnActivated 이벤트를 재정의하고 프로토콜ActivationKind를 확인해야 합니다. 자세한 내용은 URI 활성화 처리를 참조하세요.

여기서는 ProtocolActivatedEventArgs에서 제공하는 URI를 디코딩하여 시작 인수에 액세스합니다. 이 예제에서 Uri는 "windows.personalassistantlaunch:?LaunchContext=Las Vegas"로 설정됩니다.

if (args.Kind == ActivationKind.Protocol)
  {
    var commandArgs = args as ProtocolActivatedEventArgs;
    Windows.Foundation.WwwFormUrlDecoder decoder = 
      new Windows.Foundation.WwwFormUrlDecoder(commandArgs.Uri.Query);
    var destination = decoder.GetFirstValueByName("LaunchContext");

    navigationCommand = new ViewModel.TripVoiceCommand(
      "protocolLaunch",
      "text",
      "destination",
      destination);

    navigationToPageType = typeof(View.TripDetails);

    rootFrame.Navigate(navigationToPageType, navigationCommand);

    // Ensure the current window is active.
    Window.Current.Activate();
  }