为 iOS 实现设备中继

Project Rome 的设备中继功能由一组 API 组成,这些 API 支持两个主要功能:远程启动(也称为命令)和应用服务。

远程启动 API 支持从本地设备启动远程设备上的进程的方案。 例如,用户可能在车里用手机听收音机,但回到家后,他们用手机将播放切换到这台连接到家庭立体声音响的 Xbox 上。

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

Project Rome 的功能受名为“已连接设备平台”的基础平台支持。 本指南提供了开始使用连接设备平台的必要步骤,然后说明如何使用平台实现设备中继相关功能。

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

设置连接设备平台和通知

注册应用

Project Rome SDK 的几乎所有功能都需要Microsoft帐户(MSA)或 Azure Active Directory (AAD) 身份验证(异常是附近的共享 API)。 如果还没有 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 注册应用程序以获取 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 推送通知服务将通知发送到 iOS 应用终结点。

转到开发人员中心仪表板,从左侧导航窗格中导航到跨设备体验,然后选择配置新的跨设备应用。 开发人员中心仪表板 - 跨设备体验

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

  • 选择支持的平台 – 选择您的应用将覆盖并支持跨设备体验的平台。 对于 Graph 通知集成,可以根据所使用的平台从 Windows、Android 和/或 iOS 中进行选择。 跨设备体验 - 支持的平台

  • 提供应用 ID - 为所使用的每个平台提供应用 ID。 对于 iOS 应用,这是创建项目时分配给应用的包名称。 请注意,可以为每个平台添加不同的 ID(最多 10 个),在这种情况下,你可能有多个版本的同一应用甚至不同的应用,他们希望能够接收针对同一用户的应用服务器发送的相同通知。 跨设备体验 - 应用 ID

  • 提供或选择在上述 MSA/AAD 应用注册步骤中获取的 MSA 和/或 AAD 应用注册中的应用 ID。 跨设备体验 - MSA 和 AAD 应用注册

  • 提供与应用相关的本机通知平台(即适用于 Windows 的 WNS、适用于 Android 的 FCM 和/或适用于 iOS 的 APNS)的凭据,以便在发布面向用户的通知时从应用服务器传递通知。 跨设备体验功能 - 推送凭据

  • 最后,验证跨设备应用域,确保应用拥有该域的所有权,并将其用作应用的跨设备标识。 跨设备体验 - 域验证

使用平台

创建平台的实例

要开始使用,只需实例化该平台即可。

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 都可用于构造 MCDRemoteSystemConnectionRequest,这是启动 URI 所需的内容。

示例中的以下代码显示了通过连接请求远程启动 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 的情况,或者多个应用可以处理它。 MCDRemoteLauncher 类和 MCDRemoteLauncherOptions 类描述如何执行此作。

B) 远程应用服务

iOS 应用可以使用连接设备门户与其他设备上的应用服务进行交互。 这提供了许多方式来与其他设备通信,所有这些方法无需将应用引入主机设备的前台。

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

本指南将使用 适用于 Windows 的 Roman 测试应用 作为其目标应用服务。 因此,下面的代码将导致 iOS 应用在给定远程系统上查找该特定应用服务。 如果要测试此方案,请在 Windows 设备上下载这个 Roman 测试应用,并确保使用上述初步步骤中使用的相同 Microsoft 帐户 (MSA) 登录。

有关如何编写自己的 UWP 应用服务的说明,请参阅创建和使用应用服务(UWP)。 需要进行一些更改才能使服务与连接的设备兼容。 有关如何执行此作的说明,请参阅 远程应用服务的 UWP 指南

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

iOS 应用必须获取对远程设备或应用程序的引用。 与启动部分一样,此场景需要使用 MCDRemoteSystemConnectionRequest,可以通过 MCDRemoteSystemMCDRemoteSystemApp 来构造,这两个对象分别代表系统上可用的应用程序。

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

// 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 将转换为 ValueSet 对象(即 .NET Framework),该对象随后可由应用服务解释。 在其他方向传递的信息将进行反向转换。

以下方法可制作一条消息,该消息可由 Windows 上的罗马测试应用的应用服务进行解释。

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

重要

在远程应用服务方案中的应用和服务之间传递的 NSDictionary 对象必须遵循以下格式:键必须是 NSStrings,值可以是: NSString、装箱数值类型(整数或浮点)、装箱布尔值、 NSDateNSUUID、任一类型的同质数组,或者满足此规范的其他 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 App 案例中,响应包含创建的日期,因此在此非常简单的用例中,我们可以比较日期以获取消息响应的总传输时间。

这结束了与远程应用服务的单个消息交换。

完成应用服务通信

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

- (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]; });
}