实现适用于 iOS 的设备中继

Project Rome 的设备中继功能包括一组支持以下两项主要功能的 API:远程启动(也称为“指挥”)和应用服务。

远程启动 API 支持从本地设备启动远程设备上的进程的方案。 例如,在车上用户可能会收听手机上的收音机,但回到家中后,他们可能想要使用手机将播放内容转接到与家庭立体声系统相连的 Xbox。

应用程序可以使用应用服务 API 在两个设备之间建立永久的管道,通过该管道可以发送包含任意内容的消息。 例如,在移动设备上运行的照片共享应用可与用户的电脑建立连接以检索照片。

底层平台支持的 Project Rome 功能称为互联设备平台。 本指南提供开始使用互联设备平台所要完成的步骤,然后介绍如何使用该平台实现设备中继相关的功能。

以下步骤将会参考 GitHub 上提供的 Project Rome iOS 示例应用中的代码。

设置互联设备平台和通知

注册应用

Project Rome SDK 的几乎所有功能(就近共享 API 除外)都需要 Microsoft 帐户 (MSA) 或 Azure Active Directory (AAD) 身份验证。 如果你没有 MSA 但想要使用 MSA,请在 account.microsoft.com 上注册。

注意

设备中继 API 不支持 Azure Active Directory (AAD) 帐户。

使用所选的身份验证方法时,必须遵照有关应用程序注册门户的说明,将应用注册到 Microsoft。 如果没有 Microsoft 开发人员帐户,需要创建一个。

使用 MSA 注册应用时,应会收到一个客户端 ID 字符串。 请保存此字符串,供稍后使用。 这样,应用便可以访问 Microsoft 的互联设备平台资源。 如果使用 AAD,请参阅 Azure Active Directory 身份验证库了解有关获取客户端 ID 字符串的说明。

添加 SDK

将互联设备平台添加到 iOS 应用的最简单方法是使用 CocoaPods 依赖项管理器。 转到 iOS 项目的 Podfile 并插入以下条目:

platform :ios, "10.0"
workspace 'iOSSample'

target 'iOSSample' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!

	pod 'ProjectRomeSdk'

  # Pods for iOSSample

注意

若要使用 CocoaPod,必须使用项目中的 .xcworkspace 文件。

设置身份验证和帐户管理

互联设备平台需要一个可在注册过程中使用的有效 OAuth 令牌。 你可以使用偏好的方法来生成和管理 OAuth 令牌。 但是,为了帮助开发人员开始使用该平台,我们在 iOS 示例应用中包含了一个身份验证提供程序,可用于在应用中生成和管理刷新令牌。

如果不使用提供的代码,则需要自行实现 MCDConnectedDevicesAccountManager 接口。

如果使用 MSA,请在登录请求中包含以下范围:"wl.offline_access""ccs.ReadWrite""dds.read""dds.register""wns.connect""asimovrome.telemetry""https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp"

注意

设备中继 API 不支持 Azure Active Directory (AAD) 帐户。

如果使用 AAD 帐户,需要请求以下受众:"https://cdpcs.access.microsoft.com""https://cs.dds.microsoft.com""https://wns.windows.com/""https://activity.microsoft.com"

不管是否使用提供的 MCDConnectedDevicesAccountManager 实现,只要使用 AAD,就需要在 Azure 门户上的应用注册(portal.azure.com >“Azure Active Directory”>“应用注册”)中指定以下权限:

  • Microsoft 活动源服务
    • 传送和修改此应用的用户通知
    • 读取应用活动以及将其写入用户的活动源
  • Windows 通知服务
    • 将设备连接到 Windows 通知服务
  • Microsoft 设备目录服务
    • 查看设备列表
    • 添加到设备和应用列表
  • Microsoft 命令服务
    • 与用户设备通信
    • 读取用户设备

为推送通知注册应用程序

将应用程序注册到 Apple 以获得推送通知支持。 请务必记下收到的发送方 ID 和服务器密钥,因为稍后需要用到。

注册后,必须在应用中将推送通知功能关联到互联设备平台。

self.notificationRegistration = [[MCDConnectedDevicesNotificationRegistration alloc] init];
    if ([[UIApplication sharedApplication] isRegisteredForRemoteNotifications])
    {
        self.notificationRegistration.type = MCDNotificationTypeAPN;
    }
    else
    {
        self.notificationRegistration.type = MCDNotificationTypePolling;
    }
    self.notificationRegistration.appId = [[NSBundle mainBundle] bundleIdentifier];
    self.notificationRegistration.appDisplayName = (NSString*)[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"];
    self.notificationRegistration.token = deviceToken;
    self.isRegisteredWithToken = YES;

在 Microsoft Windows 开发人员中心注册应用以获得跨设备体验

警告

仅当你要使用 Project Rome 功能访问非 Windows 设备中的数据或对这些设备发出请求时,才需要执行此步骤。 如果只是针对 Windows 设备,则不需要完成此步骤。

注册应用以获得 Microsoft 开发人员仪表板的跨设备体验功能。 此过程不同于前面所述的 MSA 和 AAD 应用注册。 此过程的主要目的是将平台特定的应用标识映射到互联设备平台识别的跨平台应用标识。 此步骤还会使用与应用所用的移动平台对应的本机推送通知服务来启用通知发送。 对于 iOS,它会启用通过 APNS(Apple Push Notification 服务)向 iOS 应用终结点发送通知。

转到开发人员中心仪表板,在左侧导航窗格中导航到“跨设备体验”,并选择配置新的跨设备应用。 Dev Center Dashboard – Cross-Device Experiences

开发人员中心加入过程需要执行以下步骤:

  • 选择支持的平台 – 选择运行你的应用的、要为跨设备体验启用的平台。 对于 Graph 通知集成,可以根据所用的平台,选择“Windows”、“Android”和/或“iOS”。 Cross-Device Experiences – Supported Platforms

  • 提供应用 ID – 为所用的每个平台提供应用 ID。 对于 iOS 应用,这是创建项目时分配给应用的包名称。 请注意,如果相同的应用有多个版本,或者存在不同的应用,而这些应用希望能够收到应用服务器发送的面向同一用户的相同通知,则你可以为每个平台添加不同的 ID(最多 10 个)。 Cross-Device Experiences – App IDs

  • 提供应用 ID,或者选择在前面的 MSA 和/或 AAD 应用注册步骤中获取的应用 ID。 Cross-Device Experiences – MSA and AAD App Registrations

  • 提供与应用相关的本机通知平台(即,Windows 的 WNS、Android 的 FCM 和/或 iOS 的 APNS)的凭据,以便在发布面向用户的通知时,可以传送应用服务器发出的通知。 Cross-Device Experiences – Push Credentials

  • 最后,验证跨设备应用域,以确保应用拥有该域,并可将其用作应用的跨设备标识。 Cross-Device Experiences – Domain Verification

使用平台

创建平台的实例

若要开始,只需实例化平台即可。

MCDConnectedDevicesPlatform* platform = [MCDConnectedDevicesPlatform new];

订阅 MCDConnectedDevicesAccountManager

必须以经过身份验证的用户身份访问平台。 需要订阅 MCDConnectedDevicesAccountManager 事件,以确保使用有效帐户。

[MCDConnectedDevicesPlatform* platform.accountManager.accessTokenRequested
     subscribe:^(MCDConnectedDevicesAccountManager* _Nonnull manager __unused,
                 MCDConnectedDevicesAccessTokenRequestedEventArgs* _Nonnull request __unused) {

                    // Get access token

                 }
[MCDConnectedDevicesPlatform* platform.platform.accountManager.accessTokenInvalidated
     subscribe:^(MCDConnectedDevicesAccountManager* _Nonnull manager __unused,
                 MCDConnectedDevicesAccessTokenInvalidatedEventArgs* _Nonnull request) {

                      // Refresh and renew existing access token

                 }

订阅 MCDConnectedDevicesNotificationRegistrationManager

同样,平台将使用通知在设备之间传送命令。 因此,必须订阅 MCDConnectedDevicesNotificationRegistrationManager 事件,以确保云注册状态对于所用的帐户有效。 使用 MCDConnectedDevicesNotificationRegistrationState 验证状态

[MCDConnectedDevicesPlatform* platform.notificationRegistrationManager.notificationRegistrationStateChanged
     subscribe:^(MCDConnectedDevicesNotificationRegistrationManager* manager __unused,
                 MCDConnectedDevicesNotificationRegistrationStateChangedEventArgs* args __unused) {

                     // Check state using MCDConnectedDevicesNotificationRegistrationState enum

                 }

启动平台

初始化平台并准备好事件处理程序后,可以开始发现远程系统设备。

[MCDConnectedDevicesPlatform* platform start];

检索应用已知的用户帐户

必须确保设备已知的用户帐户列表已与 MCDConnectedDevicesAccountManager 正确同步。

使用 MCDConnectedDevicesAccountManager.addAccountAsync 添加新用户帐户。

[MCDConnectedDevicesPlatform* platform.accountManager
     addAccountAsync:self.mcdAccount
     callback:^(MCDConnectedDevicesAddAccountResult* _Nonnull result, NSError* _Nullable error) {

     // Check state using **MCDConnectedDevicesAccountAddedStatus** enum

     }

若要删除无效帐户,可以使用 MCDConnectedDevicesAccountManager.removeAccountAsync

 [MCDConnectedDevicesPlatform* platform.accountManager
     removeAccountAsync:existingAccount
     callback:^(MCDConnectedDevicesRemoveAccountResult* _Nonnull result __unused, NSError* _Nullable error) {

                    // Remove invalid user account

     }

发现远程设备和应用

MCDRemoteSystemWatcher 实例将会处理本部分所述的核心功能。 请在用于发现远程系统的类中声明该实例。

MCDRemoteSystemWatcher* _watcher;

在创建观察程序并开始发现设备之前,你可能想要添加发现筛选器,以确定应用所针对的设备类型。 可按用户的输入确定这些类型,或将其硬编码到应用中,具体取决于用例。

示例应用中的以下代码演示如何创建并启动一个观察程序实例,使应用能够分析已发现的设备并与之交互。

// Start watcher with filter for transport types, form factors
- (void)startWatcherWithFilter:(NSMutableArray<NSObject<MCDRemoteSystemFilter>*>*)remoteSystemFilter
{
    _discoveredSystems = [[NSMutableArray alloc] init];
    _devicesAdded = 0;
    _devicesUpdated = 0;
    _devicesRemoved = 0;

    // add filters (not defined here)
    _watcher = (remoteSystemFilter.count > 0) ? [[MCDRemoteSystemWatcher alloc] initWithFilters:remoteSystemFilter] :
        [[MCDRemoteSystemWatcher alloc] init];

    // add event handlers
    RemoteSystemViewController* __weak weakSelf = self;
    [_watcher addRemoteSystemAddedListener:^(
        __unused MCDRemoteSystemWatcher* watcher, MCDRemoteSystem* system) { [weakSelf _onRemoteSystemAdded:system]; }];

    [_watcher addRemoteSystemUpdatedListener:^(
        __unused MCDRemoteSystemWatcher* watcher, MCDRemoteSystem* system) { [weakSelf _onRemoteSystemUpdated:system]; }];

    [_watcher addRemoteSystemRemovedListener:^(
        __unused MCDRemoteSystemWatcher* watcher, MCDRemoteSystem* system) { [weakSelf _onRemoteSystemRemoved:system]; }];

    // start watcher
    [_watcher start];
}

事件处理程序方法在此处定义。

// Handle when RemoteSystems are added
- (void)_onRemoteSystemAdded:(MCDRemoteSystem*)system
{
    @synchronized(self)
    {
        _devicesAdded++;
        [_discoveredSystems addObject:system];
        [_delegate remoteSystemsDidUpdate];
    }
}

// Handle when RemoteSystems are updated
- (void)_onRemoteSystemUpdated:(MCDRemoteSystem*)system
{
    @synchronized(self)
    {
        _devicesUpdated++;

        for (unsigned i = 0; i < _discoveredSystems.count; i++)
        {
            MCDRemoteSystem* cachedSystem = [_discoveredSystems objectAtIndex:i];
            if ([cachedSystem.displayName isEqualToString:system.displayName])
            {
                [_discoveredSystems replaceObjectAtIndex:i withObject:system];
                break;
            }
        }
    }
}

// Handle when RemoteSystems are removed
- (void)_onRemoteSystemRemoved:(MCDRemoteSystem*)system
{
    @synchronized(self)
    {
        _devicesRemoved++;

        for (unsigned i = 0; i < _discoveredSystems.count; i++)
        {
            MCDRemoteSystem* cachedSystem = [_discoveredSystems objectAtIndex:i];
            if ([cachedSystem.displayName isEqualToString:system.displayName])
            {
                [_discoveredSystems removeObjectAtIndex:i];
                break;
            }
        }
    }
}

我们建议让应用保留一组发现的设备(由 MCDRemoteSystem 实例表示),并在 UI 中显示有关可用设备及其应用的信息(例如显示名称和设备类型)。

调用 [_watcher start] 后,它会开始观察远程系统活动,当互联设备被发现、更新或者从检测到的设备集中删除时,它会引发事件。 它会持续在后台扫描,因此,建议在不再需要观察程序时将其停止(使用 [_watcher stop]),以免发生不必要的网络通信和电池消耗。

示例用例:实现远程启动和远程应用服务

此时,代码中应该包含一个引用可用设备的有效 MCDRemoteSystem 对象列表。 在这些设备上执行的操作取决于应用的功能。 主要交互类型是远程启动和远程应用服务。 后续部分将会介绍这些操作。

A) 远程启动

以下代码演示如何选择其中一个 MCDRemoteSystem 对象(最好是通过 UI 控件来选择),然后使用 MCDRemoteLauncher 通过传递一个与应用兼容的 URI 在该设备上启动应用。

必须注意,远程启动可以针对远程设备(在此情况下,主机设备将会根据该 URI 方案,使用其默认应用启动给定的 URI),或该设备上的特定远程应用程序。

如前一部分所示,发现首先在设备级别发生(MCDRemoteSystem 表示设备),但你可以针对 MCDRemoteSystem 实例调用 getApplications 方法以获取 MCDRemoteSystemApp 对象的数组,这些对象表示远程设备上已注册为使用互联设备平台的应用(就如同在前面的初步步骤中注册你自己的应用一样)。 MCDRemoteSystemMCDRemoteSystemApp 都可用于构造启动 URI 所需的 RemoteSystemConnectionRequest

示例中的以下代码演示如何通过一个连接请求来远程启动 URI。

// Send a remote launch of a uri to RemoteSystemApplication
- (IBAction)launchUriButton:(id)sender
{
    NSString* uri = self.uriField.text;
    MCDRemoteLauncher* remoteLauncher = [[MCDRemoteLauncher alloc] init];
    MCDRemoteSystemConnectionRequest* connectionRequest =
        [MCDRemoteSystemConnectionRequest requestWithRemoteSystemApplication:self.selectedApplication];
    [remoteLauncher launchUriAsync:uri
        withConnectionRequest:connectionRequest
            completion:^(MCDRemoteLaunchUriStatus result, NSError* _Nullable error) {
                if (error)
                {
                    NSLog(@"LaunchURI [%@]: ERROR: %@", uri, error);
                    return;
                }

                if (result == MCDRemoteLaunchUriStatusSuccess)
                {
                    NSLog(@"LaunchURI [%@]: Success!", uri);
                }
                else
                {
                    NSLog(@"LaunchURI [%@]: Failed with code %d", uri, (int)result);
                }
            }];
}

根据发送的 URI,可以在远程设备上以特定的状态或配置启动应用。 这样,便可以在其他设备上继续执行用户任务(例如看电影),而不会出现中断。

根据具体的用法,可能需要考虑到目标系统上的没有任何应用可以处理 URI,或者多个应用可以处理 URI 的情况。 MCDRemoteLauncher 类和 MCDRemoteLauncherOptions 类描述如何实现此目的。

B) 远程应用服务

iOS 应用可以使用互联设备门户来与其他设备上的应用服务交互。 这样,就可通过多种方式来与其他设备通信 - 根本不需要将应用放到主机设备的前台。

在目标设备上设置应用服务

本指南将使用 Roman Test App for Windows 作为其目标应用服务。 因此,以下代码会导致 iOS 应用在给定的远程系统上查找该特定的应用服务。 若要测试此方案,请在 Windows 设备上下载 Roman Test App,并确保使用前面初步步骤中所用的相同 MSA 登录。

有关如何编写自己的 UWP 应用服务的说明,请参阅创建和使用应用服务 (UWP)。 需要做出几项更改才能使该服务与互联设备兼容。 有关如何执行此操作的说明,请参阅远程应用服务的 UWP 指南

在客户端设备上打开应用服务连接

iOS 应用必须获取对远程设备或应用程序的引用。 与“启动”部分中一样,此方案要求使用 MCDRemoteSystemConnectionRequest,可以从表示系统上的可用应用的 MCDRemoteSystemMCDRemoteSystemApp 构造该对象。

此外,应用需要使用两个字符串来识别其目标应用服务:应用服务名称和包标识符。 可以在应用服务提供商的源代码中找到这些字符串(有关详细信息,请参阅创建和使用应用服务 (UWP))。 这些字符串共同构造了馈送到 MCDAppServiceConnection 实例的 MCDAppServiceDescription

// Step #1:  Establish an app service connection
- (IBAction)connectAppServiceButton:(id)sender
{
    MCDAppServiceConnection* connection = nil;
    @synchronized(self)
    {
        connection = _appServiceConnection;
        if (!connection)
        {
            connection = _appServiceConnection = [MCDAppServiceConnection new];
            connection.appServiceDescription =
                [MCDAppServiceDescription descriptionWithName:g_appServiceName packageId:g_packageIdentifier];
            _serviceClosedRegistration = [connection addServiceClosedListener:^(__unused MCDAppServiceConnection* connection,
                MCDAppServiceClosedStatus status) { [self appServiceConnection:connection closedWithStatus:status]; }];
        }
    }

    @try
    {
        MCDRemoteSystemConnectionRequest* connectionRequest =
            [MCDRemoteSystemConnectionRequest requestWithRemoteSystemApplication:self.selectedApplication];
        [connection openRemoteAsync:connectionRequest
            completion:^(MCDAppServiceConnectionStatus status, NSError* error) {
                if (error)
                {
                    NSLog(@"ConnectAppService: ERROR: %@", error);
                    return;
                }
                if (status != MCDAppServiceConnectionStatusSuccess)
                {
                    NSLog(@"ConnectAppService: Failed with code %d", (int)status);
                    return;
                }
                NSLog(@"Successfully connected!");
                dispatch_async(
                    dispatch_get_main_queue(), ^{ self.appServiceStatusLabel.text = @"App service connected! no ping sent"; });
            }];
    }
    @catch (NSException* ex)
    {
        NSLog(@"ConnectAppService: EXCEPTION! %@", ex);
    }
}

创建要发送到应用服务的消息

声明一个用于存储要发送的消息的变量。 在 iOS 上,要发送到远程应用服务的消息的类型为 NSDictionary

注意

当应用与在其他平台上的应用服务通信时,互联设备平台会将 NSDictionary 转换为接收方平台上的等效构造。 例如,从此应用发送到 Windows 应用服务的 NSDictionary 将转换为(.NET Framework 的)ValueSet 对象,然后应用服务会解释该对象。 朝另一个方向传递的信息将进行反向转换。

以下方法将编写一条可由适用于 Windows 的 Roman Test App 应用服务解释的消息。

// Create a message to send
- (NSDictionary*)_createPingMessage
{
    return @{
        @"Type" : @"ping",
        @"CreationDate" : [_dateFormatter stringFromDate:[NSDate date]],
        @"TargetId" : _selectedApplication.applicationId
    };
}

重要

在远程应用服务方案中的应用和服务之间传递的 NSDictionary 对象必须遵循以下格式:键必须是 NSString,而值可以是:NSString、装箱数值类型(整数或浮点)、装箱布尔值、NSDate、NSUUID、任何这些类型的同类数组,或者符合此规范的其他 NSDictionary 对象。

将消息发送到应用服务

建立应用服务连接并创建消息后,将消息发送到应用服务的过程很简单,可以在应用中引用连接实例和该消息的任何位置完成此操作。

示例中的以下代码演示如何将消息发送到应用服务和处理响应。

//  Send a message using the app service connection
- (IBAction)sendAppServiceButton:(id)sender
{
    if (!_appServiceConnection)
    {
        return;
    }

    // Send the message and get a response
    @try
    {
        [_appServiceConnection sendMessageAsync:[self _createPingMessage]
            completion:^(MCDAppServiceResponse* response, NSError* error) {
                if (error)
                {
                    NSLog(@"SendPing: ERROR: %@", error);
                    return;
                }

                if (response.status != MCDAppServiceResponseStatusSuccess)
                {
                    NSLog(@"SendPing: Response received with bad status code %d", (int)response.status);
                    return;
                }

                NSString* creationDateString = response.message[@"CreationDate"];
                if (creationDateString)
                {
                    NSDate* date = [_dateFormatter dateFromString:creationDateString];
                    if (date)
                    {
                        NSTimeInterval diff = [[NSDate date] timeIntervalSinceDate:date];
                        dispatch_async(dispatch_get_main_queue(),
                            ^{ self.appServiceStatusLabel.text = [NSString stringWithFormat:@"%g", diff]; });
                    }
                }
            }];
    }
    @catch (NSException* ex)
    {
        NSLog(@"SendPing: EXCEPTION! %@", ex);
    }
}

在 Roman 应用用例中,响应包含消息的创建日期,因此,在这个非常简单的用例中,我们可以比较日期以获取消息响应的总传输时间。

使用远程应用服务交换单个消息的介绍到此结束。

完成应用服务通信

在应用与目标设备的应用服务交互完成后,请关闭两个设备之间的连接。

- (void)appServiceConnection:(__unused MCDAppServiceConnection*)connection closedWithStatus:(MCDAppServiceClosedStatus)status
{
    NSLog(@"AppService closed with status %d", (int)status);
    dispatch_async(
        dispatch_get_main_queue(), ^{ self.appServiceStatusLabel.text = [NSString stringWithFormat:@"disconnected (%d)", (int)status]; });
}