网络和电话

本指南中的功能介绍了您可以在设备政策控制器 (DPC) 应用中实现的网络和电话管理功能。本文档包含代码示例,您还可以将 Test DPC 应用用作 Android 企业功能示例代码的来源。

DPC 应用可以在个人设备上以资料所有者模式运行,在全代管式设备上以设备所有者模式运行。下表列出了 DPC 在资料所有者模式或设备所有者模式下运行时可以使用哪些功能:

功能 资料所有者 设备所有者
跨资料访问工作联系人
确保工作流量的安全网络连接
跨区域设置单个无线网络 ID
为工作资料指定单独的拨号器

跨资料访问工作联系人

EMM 可以允许用户的个人资料访问其工作联系人,以便通过本地搜索和远程目录查找来访问用户的个人和工作联系人。在个人设备上,个人资料中的单个拨号器即可拨打和接听个人电话和工作电话。此外,工作通讯录已很好地集成到系统界面中。如果工作资料已加密,个人资料就无法访问其数据。

与系统界面集成

系统界面使用公文包图标指示传入的工作调用。callLog 还会显示用于指定工作来电和去电的图标。个人拨号器和通讯录应用可以使用远程目录查找显示工作联系人的来电显示信息,因此无需联系人已在本地设备上同步。该短信应用可以执行本地来电显示和搜索。

Android 兼容性定义文档 (CDD) 包括对在默认拨号器中显示工作联系人的要求,以及对联系人和即时通讯应用进行标记以表明其来自工作资料的要求。

工作联系人可供访问和搜索

用户可以通过其个人资料(显示在拨号器应用的搜索屏幕)访问和呼叫工作联系人。用户可以使用自动补全功能搜索在本地同步到设备并通过远程目录查找列出的工作联系人。

控制主要资料中的工作联系人

DPC 控制搜索工作联系人的权限。在资料所有者模式下运行,DPC 可管理个人资料中工作联系人的可见性。如需了解详情,请参阅构建设备政策控制器

默认情况下,系统会启用通过个人资料搜索工作联系人的功能。

确保工作流量的安全网络连接

在设备所有者模式或资料所有者模式下运行,设备政策控制器可以使用始终开启的虚拟专用网 (VPN) 连接,强制应用通过无法绕过的指定 VPN 应用传输流量。使用始终开启的 VPN 连接,DPC 可以确保来自工作资料或受管理设备的网络流量通过 VPN 服务,而无需用户干预。此过程会为工作资料中的持续流量创建安全的网络连接。

关于始终开启的 VPN 连接

作为系统框架的一部分,系统会自动管理 VPN 路由,因此用户无法绕过 VPN 服务。如果 VPN 服务在锁定模式下断开连接,则流量不能泄露到开放的互联网。对于实现 VpnService 的应用,始终开启的 VPN 提供了一个框架,用于通过可信服务器管理安全 VPN 连接并使其保持连接。无论通过 Wi-Fi 还是移动网络连接,VPN 服务都会在应用更新后自动重启连接。如果设备重新启动,框架也会重启 VPN 连接。

与 VPN 服务的连接对用户是透明的。对于公司自有设备,对于处于始终开启模式的 VPN,用户无需确认意见征求对话框。用户的 VPN 网络设置允许手动启用始终开启的连接。

如果 DISALLOW_CONFIG_VPNtrue,则阻止用户配置 VPN。启用 DISALLOW_DEBUGGING_FEATURES 可限制用户使用 adb 调试命令替换始终开启的 VPN。如需阻止用户卸载 VPN,请调用 DevicePolicyManager.setUninstallBlocked

设置 VPN 服务

使用企业 Android 解决方案的组织设置 VPN。

  1. 安装实现 VpnService 的 VPN 应用。您可以使用与操作 VpnService.SERVICE_INTERFACE 匹配的 intent 过滤器查找处于活跃状态的 VPN 服务。
  2. 在应用清单中声明一个受 BIND_VPN_SERVICE 权限保护的 VpnService
  3. 配置 VpnService,使其由系统启动。避免通过监听系统启动并控制其自身的生命周期,将 VPN 应用设置为自行启动。
  4. 为 VPN 应用设置托管配置(请参阅下面的示例)。

启用始终开启的 VPN 连接

DPC 可通过调用 DevicePolicyManager.setAlwaysOnVpnPackage() 通过特定应用配置始终开启的 VPN 连接。

系统会自动授予此连接,并在重新启动后保留此连接。如果 lockdownEnabled 为 false,则从手机重新启动且 VPN 连接起,网络流量可能会不安全。如果您不想在 VPN 发生故障时停止网络连接,或者 VPN 不是必需的,则此选项非常有用。

验证始终开启的 VPN 连接

DPC 可以使用 DevicePolicyManager.getAlwaysOnVpnPackage(). 读取管理当前用户始终开启的 VPN 连接的软件包的名称

如果没有此类软件包,或者 VPN 是在系统“设置”应用中创建的,则返回 null

示例

TestDPC 应用中,AlwaysOnVpnFragment.java 使用这些 API 启用始终开启的 VPN 连接的设置。

在以下示例中:

  • VPN 服务的托管配置DevicePolicyManager 使用其 setApplicationRestrictions() 方法设置。
  • 托管配置使用任意键值对,此示例应用在其他地方使用这些键值对来配置 VPN 的网络设置(请参阅检查托管配置)。
  • 该示例将 Android 软件包安装程序添加到了拒绝名单,因此它不会通过 VPN 更新系统软件包。工作资料或设备中的所有用户网络流量都会通过该 VPN 应用(软件包安装程序除外)传输;应用的更新使用开放的互联网。
  • 然后,DevicePolicyManager 会使用 setAlwaysOnVpnPackage() 为 VPN 软件包启用始终开启的 VPN 连接,并启用锁定模式。

Kotlin

// Set VPN's managed configurations
val config = Bundle().apply {
  putString(Extras.VpnApp.ADDRESS, "192.0.2.0")
  putString(Extras.VpnApp.IDENTITY, "vpn.account1")
  putString(Extras.VpnApp.CERTIFICATE, "keystore://auth_certificate")
  putStringArray(Extras.VpnApp.DENYLIST,
        arrayOf("com.android.packageinstaller"))
}

val dpm = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager

val admin = myDeviceAdminReceiver.getComponentName(this)

// Name of package to update managed configurations
val vpnPackageName = "com.example.vpnservice"

// Associate managed configurations with DeviceAdminReceiver
dpm.setApplicationRestrictions(admin, vpnPackageName, config)

// Enable always-on VPN connection through VPN package
try {
  val lockdownEnabled = true
  dpm.setAlwaysOnVpnPackage(admin, vpnPackageName, lockdownEnabled)
} catch (ex: Exception) {
  throw PolicyException()
}

Java

// Set VPN's managed configurations
final Bundle config = new Bundle();
config.putString(Extras.VpnApp.ADDRESS, "192.0.2.0");
config.putString(Extras.VpnApp.IDENTITY, "vpn.account1");
config.putString(Extras.VpnApp.CERTIFICATE, "keystore://auth_certificate");
config.putStringArray(Extras.VpnApp.DENYLIST,
                      new String[]{"com.android.packageinstaller"});

DevicePolicyManager dpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);

ComponentName admin = myDeviceAdminReceiver.getComponentName(this);

// Name of package to update managed configurations
final String vpnPackageName = "com.example.vpnservice";

// Associate managed configurations with DeviceAdminReceiver
dpm.setApplicationRestrictions(admin, vpnPackageName, config);

// Enable always-on VPN connection through VPN package
try {
  boolean lockdownEnabled = true;
  dpm.setAlwaysOnVpnPackage(admin, vpnPackageName, lockdownEnabled));
} catch (Exception ex) {
  throw new PolicyException(...);
}

跨区域设置单个无线网络 ID

在设备所有者模式或资料所有者模式下运行,设备政策控制器 (DPC) 可以将多个证书授权机构 (CA) 证书与单个无线网络配置相关联。通过这种配置,设备可以连接到具有相同网络名称或服务集标识符 (SSID) 但配置了不同 CA 证书的无线接入点。如果组织的无线网络位于多个地理区域,并且每个区域都需要不同的证书授权机构,则此功能非常有用。例如,合法签名可以要求需要区域 CA 的本地机构。

注意:从 API 18 (Jelly Bean) 开始,Android 一直支持 setCaCertificate (Jelly Bean),但 IT 管理员必须与每个 CA 分别配置其网络,以确保设备在每个接入点都能进行无缝身份验证(无论位于哪个地区)。

指定用于标识服务器的 CA 证书

如需指定使用同一 SSID 标识服务器的 X.509 证书列表,请使用 WifiEnterpriseConfig.setCaCertificates() 在无线配置中包含所有相关 CA。

如果服务器 CA 与给定证书之一匹配,则服务器的证书有效。系统会自动为证书分配默认名称,并在配置中使用默认名称。WifiManager 会安装证书,并在启用网络时自动保存配置,并在配置被删除时移除证书。

如需获取与无线配置关联的所有 CA 证书,请使用 WifiEnterpriseConfig.getCaCertificates() 返回 X509Certificate 对象的列表。

使用多个 CA 证书添加无线配置

  1. 验证服务器的身份:
    1. 加载 X.509 CA 证书。
    2. 加载客户端的私钥和证书。如需查看有关如何读取证书文件的示例,请参阅通过 HTTPS 和 SSL 确保安全
  2. 创建一个新的 WifiConfiguration,并设置其 SSID 和密钥管理。
  3. 在此 WifiConfiguration 上设置 WifiEnterpriseConfig 实例。
    1. 使用 setCaCertificates() 通过 X509Certificate 对象列表识别服务器。
    2. 设置客户端凭据、身份和密码。
    3. 在建立连接的过程中,设置可扩展的身份验证协议 (EAP) 和第 2 阶段方法。
  4. 使用 WifiManager 添加网络。
  5. 启用网络。WifiManager 在设置过程中会自动保存配置。

以下示例将这些步骤绑定在一起:

Kotlin

// Verify the server's identity
val caCert0 = getCaCert("cert0.crt")
val caCert1 = getCaCert("cert1.crt")
val clientKey = getClientKey()
val clientCert = getClientCert()

// Create Wi-Fi configuration
val wifiConfig = WifiConfiguration().apply {
  SSID = "mynetwork"
  allowedKeyManagement.set(KeyMgmt.WPA_EAP)
  allowedKeyManagement.set(KeyMgmt.IEEE8021X)

  // Set up Wi-Fi enterprise configuration
  enterpriseConfig.setCaCertificates(arrayOf<X509Certificate>(caCert0, caCert1))
  enterpriseConfig.setClientKeyEntry(clientKey, clientCert)
  enterpriseConfig.setIdentity("myusername")
  enterpriseConfig.setEapMethod(Eap.TLS)
  enterpriseConfig.setPhase2Method(Phase2.NONE)
}


// Add network
val wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
val netId = wifiManager.addNetwork(wifiConfig)

// Enable network
if (netId < 0) {
  // Error creating new network
} else {
  wifiManager.enableNetwork(netId, true)
}

Java

// Verify the server's identity
X509Certificate caCert0 = getCaCert("cert0.crt");
X509Certificate caCert1 = getCaCert("cert1.crt");
PrivateKey clientKey = getClientKey();
X509Certificate clientCert = getClientCert();

// Create Wi-Fi configuration
WifiConfiguration wifiConfig = new WifiConfiguration();
wifiConfig.SSID = "mynetwork";
wifiConfig.allowedKeyManagement.set(KeyMgmt.WPA_EAP);
wifiConfig.allowedKeyManagement.set(KeyMgmt.IEEE8021X);

// Set up Wi-Fi enterprise configuration
wifiConfig.enterpriseConfig.setCaCertificates(new X509Certificate[] {caCert0, caCert1});
wifiConfig.enterpriseConfig.setClientKeyEntry(clientKey, clientCert);
wifiConfig.enterpriseConfig.setIdentity("myusername");
wifiConfig.enterpriseConfig.setEapMethod(Eap.TLS);
wifiConfig.enterpriseConfig.setPhase2Method(Phase2.NONE);

// Add network
WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
int netId = wifiManager.addNetwork(wifiConfig);

// Enable network
if (netId < 0) {
  // Error creating new network
} else {
  wifiManager.enableNetwork(netId, true);
}

为工作资料指定单独的拨号器

您可以将要在工作资料中使用的单独拨号器应用列入许可名单。这可以是拨号器本身,也可以是为通话后端实现 ConnectionService API 的 IP 语音通话 (VoIP) 应用。这可以为工作资料中的 VoIP 应用提供相同的集成式系统界面拨号体验,从而有效地使工作拨号器成为核心功能。打给工作通话帐号的来电与打给个人通话帐号的来电不同。

用户可以选择通过电话帐号已列入许可名单的工作拨号器拨打和接听电话。通过该拨号器拨打的所有电话或拨打工作电话帐号的来电,都会记录在工作资料的 CallLog 提供程序中。工作拨号器会维护仅限工作用的通话记录,并且只能访问工作联系人。线路切换来电由主拨号器处理,并存储在个人通话记录中。如果工作资料被删除,则与该工作资料相关联的通话记录以及所有工作资料数据也会被删除。

第三方应用必须实现 ConnectionService

需要拨打电话并将这些通话集成到内置电话应用中的第三方 VoIP 应用可以实现 ConnectionService API。所有用于工作通话的 VoIP 服务都必须执行此操作。这些应用非常好用,可以将其通话视为传统移动网络通话。例如,它们会显示在内置系统拨号器和通话记录中。如果实现 ConnectionService 的应用安装在工作资料中,则只有也安装在该工作资料中的拨号器才能访问该应用。

开发者实现 ConnectionService 后,应将其添加到应用的清单文件中,并向 TelecomManager 注册 PhoneAccount。 电话帐号代表用于拨打或接听电话的不同方法,并且每个 ConnectionService 可以有多个 PhoneAccounts。注册电话帐号后,用户可以通过拨号器设置启用它。

系统界面集成和通知

对于使用 ConnectionService API 作为后端拨打电话的第三方应用,系统界面为用户提供一致的集成式拨号体验。如果在工作资料中使用应用,来电和状态栏中会显示一个公文包图标。实现工作资料中安装的 ConnectionService 的应用可以使用系统拨号器,也可以构建单独的工作拨号器。这些应用可以是单个应用,也可以是单独的应用。

拨号器应用通过检查标记 android.telecom.Call.Details.PROPERTY_ENTERPRISE_CALL 来确定其是否正在拨打或接收工作调用。如果调用是工作调用,拨号器会通过添加工作标记(公文包图标)向用户指明这一点:

Kotlin

// Call placed through a work phone account. getCurrentCall() is defined by the
// dialer.
val call = getCurrentCall()
if (call.hasProperty(android.telecom.Call.Details.PROPERTY_ENTERPRISE_CALL)) {
  // Set briefcase icon
}

Java

// Call placed through a work phone account. getCurrentCall() is defined by the
// dialer.
Call call = getCurrentCall();
if (call.hasProperty(android.telecom.Call.Details.PROPERTY_ENTERPRISE_CALL)) {
  // Set briefcase icon
}