构建在工作资料上运行的应用

1. 准备工作

什么是工作资料?

工作资料是指公司允许员工使用其个人设备办公时,员工可在其个人设备上启用的辅助资料。

工作资料可以由 IT 管理员控制,它所提供的功能与用户的主要个人资料的功能相互独立。通过这种方式,单位可以控制在用户设备上运行公司专用应用和数据的环境,同时仍允许用户使用他们的个人应用和个人资料。

这对您的应用有何影响?任何应用都可以安装在工作资料下,这意味着应用可能会面临运行时限制和行为变更。如果出于工作目的使用应用,还需确保应用的安全性。即使应用在个人资料中运行,工作资料仍可能会影响应用的行为方式。

前提条件

此 Codelab 专为具备中级技能基础知识的 Android 开发者而设计。

此 Codelab 假定您之前构建过应用、使用过 Android Studio,并且在设备或模拟器上测试过应用。

实践内容

在此 Codelab 中,您将修改应用,以便在装有工作资料的设备上使用时提供最佳用户体验。您将学习如何让自己的应用:

  • 同时处理个人联系人和工作联系人。
  • 在应用内切换工作资料和个人资料。

caf809dbd1e16c75.png

所需条件

  • 不受管理的 Android 设备(不归单位所有或不受单位管理)。

2. 进行设置

设置测试设备

我们建议您在此 Codelab 中使用实体设备。但是,您仍可使用包含 Google Play 商店的映像在模拟器上执行下方相同的设置。

TestDPC

Google 构建了 TestDPC 应用,以帮助您在自己的设备上模拟和测试受管理环境。此应用可用于设置工作资料,并为您提供用于启用/停用设备上的某些功能,让您如 IT 管理员一样执行相关操作。

安装 TestDPC 应用

在您的设备上,打开 Google Play 商店并下载 TestDPC 应用

设置工作资料

安装 TestDPC 应用后,设备上会显示 2 个图标:一个设置图标和一个 TestDPC 应用图标。点按设置图标并按照相关步骤操作。

现在,您有两份单独的资料,分别针对个人应用和工作应用。您可以通过应用列表顶部的标签页在它们之间切换。

请注意,每个资料都有自己的 Play 商店应用。您可以通过启动器图标顶部的小型公文包图片识别工作应用。

46175af7ad32979d.gif

您可以像往常一样通过 Play 商店安装应用,根据您启动的 Play 商店(是个人资料的 Play 商店还是工作资料的 Play 商店),该应用将仅安装在相应的资料中。通过两个 Play 商店安装应用后,应用也可以同时存在于两个资料中。在这种情况下,应用的每个版本都将具有完全独立的存储空间和配置空间。

如何在特定资料中安装应用

在下面的段落中,我们将了解如何使用 CrossProfileApps 类在默认资料和工作资料之间切换。为了确认该行为,需要同时在这两个资料中安装应用

您可以使用下面的 adb 命令确认相应资料的 ID 号。

“adb shell pm list users”

您可以使用下面的 adb 命令将应用安装到指定的资料中。

“adb pm install –user [id number of profile] [path of apk file]”

您也可以在项目中配置“Run/Debug Configuration”,并选择“Install for all users”选项,效果是一样的。

7634e3dcb0a744ca.png

通过从 Android Studio 运行应用来更新应用时,该应用会安装在两个资料中。

3. 加载联系人

设置一些要在演示版应用中使用的测试联系人:

  1. 从个人资料中启动设备的“通讯录”应用。
  2. 添加一些您可确认为个人联系人的测试联系人。
  3. 从工作资料启动“通讯录”应用。(您不会看到自己刚添加的任何个人联系人)。
  4. 添加一些您可确认为工作联系人的测试联系人。

如果您对自己设置的联系人感到满意,请试用演示版应用的起始代码。

4. 获取起始代码

  1. 如需获取示例应用,请执行以下任一操作:
  • 从 GitHub 克隆代码库:
$ git clone https://github.com/android/enterprise-samples.git

$ cd e

nterprise-samples/Work-profile-codelab

  • 或者,您也可以通过下面的链接下载项目。

下载 ZIP 文件

  1. 在 Android Studio 中打开并运行应用。

以下是您首次启动该应用时的情形:

f9779ab476511718.png

试试看

在此 Codelab 结束时,您的应用在个人资料中运行时会同时显示工作联系人和个人联系人。您还可以通过在应用的其他资料中启动另一个应用实例来切换资料。

5. 同时显示工作联系人和个人联系人

使用 ContactsContract.Contacts.CONTENT_URI 加载联系人时,应用将根据其在哪个资料中运行来决定要显示哪些联系人。但在很多情况下,您可能希望应用同时加载两个联系人列表。例如,用户可能希望与同事共享个人内容,例如照片或文档。为此,您需要检索两个联系人列表。

打开 MainActivity.kt

onCreateLoader() 方法负责创建用于检索和加载联系人的光标加载器。目前,它仅返回使用默认 ContentURI 的 CursorLoader。您将调用此方法两次,一次用于个人联系人,另一次用于工作联系人。为区分这两个用例,在每个用例中,我们将向 onCreateLoader() 传递不同的 ID。您需要检查传递到方法中的 ID,以确定要使用哪个 ContentURI。

首先,根据传递给方法的 ID 的值更改 ContentURI 变量的值。对于 PERSONAL_CONTACTS_LOADER_ID,将其分配给默认的 ContactsContract.Contacts.CONTENT_URI,否则您将按照此处的说明构建 ENTERPRISE_CONTENT_FILTER_URI

ContactsContract.Contacts.ENTERPRISE_CONTENT_FILTER_URI
                    .buildUpon()
                    .appendPath(nameFilter)
                    .appendQueryParameter(
                        ContactsContract.DIRECTORY_PARAM_KEY,
                        ContactsContract.Directory.ENTERPRISE_DEFAULT.toString()
                    )
                    .build()

您会发现,由于这是一个内容过滤 URI,构建器需要在搜索/加载联系人时使用搜索过滤器(搜索词组)。

现在,将搜索词组硬编码为以字母“a”开头的任何名称。

val nameFilter = Uri.encode("a") // names that start with a

您还需要指定用于搜索的联系人目录。您将使用 ENTERPRISE_DEFAULT 目录来搜索存储在设备本地的联系人。

您的 onCreateLoader() 方法应如下所示:

override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
        val nameFilter = Uri.encode("a") // names that start with a
        val contentURI = when (id) {
            PERSONAL_CONTACTS_LOADER_ID -> ContactsContract.Contacts.CONTENT_URI
            else -> {
                ContactsContract.Contacts.ENTERPRISE_CONTENT_FILTER_URI
                    .buildUpon()
                    .appendPath(nameFilter)
                    .appendQueryParameter(
                        ContactsContract.DIRECTORY_PARAM_KEY,
                        ContactsContract.Directory.ENTERPRISE_DEFAULT.toString()
                    )
                    .build()
            }
        }
        return CursorLoader(
            this, contentURI, arrayOf(
                ContactsContract.Contacts.DISPLAY_NAME_PRIMARY
            ), null, null, null
        )
    }

现在,您需要使用新 ID 值初始化另一个 Loader 以触发上述方法。

首先,在 MainActivity 顶部为工作联系人创建新的常量 ID 值:

const val WORK_CONTACTS_LOADER_ID = 1

然后,在 initLoaders() 中,使用 LoaderManager 初始化包含上面创建的新 ID 的新 Loader

private fun initLoaders() {
        LoaderManager.getInstance(this).
            initLoader(PERSONAL_CONTACTS_LOADER_ID, null, this)
        LoaderManager.getInstance(this).
            initLoader(WORK_CONTACTS_LOADER_ID, null, this)
    }

其他所有方法的运行方式应该相同,因为来自两个加载器的数据光标具有相同的结构。

试用

在个人资料中运行应用,现在您可同时看到工作联系人和个人联系人了!

3b8f9c73feee88fb.png

如何处理工作资料?

如果您在工作资料中运行应用,您仍然只能看到工作联系人,而看不到任何个人联系人。这是因为工作资料的主要目标之一是保护用户的隐私,因此工作应用通常无法从个人资料中获取任何个人信息。

9312158a2dc03891.png

6. 在应用内切换资料

Android 添加了一些 API,用于在另一种资料中启动应用的另一个实例,以帮助用户实现帐号切换。例如,电子邮件应用可以提供一个界面,让用户可以在个人资料与工作资料之间切换,以便访问两个电子邮件帐号。

所有应用都可以调用这些 API 来启动同一应用的主 activity,前提是在另一资料中已安装该应用。

如需为您的应用添加跨资料帐号切换功能,首先需在我们的主 activity 布局中添加一个按钮,以便用户切换资料。

打开 activity_main.xml 并在 recycler 视图 widget 下添加一个按钮 widget:

<androidx.appcompat.widget.AppCompatButton
        android:id="@+id/button"
        app:layout_constraintTop_toBottomOf="@+id/contacts_rv"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

返回 MainActivity.kt,在 onCreate 方法中设置按钮的点击事件来切换资料。

为此,请先获取 CrossProfileApps 系统服务:

val crossProfileApps = getSystemService(CrossProfileApps::class.java)

此类提供了实现资料切换功能所需的所有 API。您可以通过调用 targetUserProfiles 来检索用户资料列表,该方法将返回已在其中安装此应用的所有其他资料。

val userHandles = crossProfileApps.targetUserProfiles

您现在可以使用返回的 userHandle 作为第一项,并在另一资料中启动应用。

crossProfileApps.startMainActivity(
                    componentName,
                    userHandles.first()
                )

您甚至可以获取提示用户切换资料的本地化文本,并使用该文本设置按钮的文本值。

val label = crossProfileApps.getProfileSwitchingLabel(userHandles.first())

现在,整合上述所有部分,就是您需要添加到 MainActivity.kt 中 onCreate 方法末尾的内容:

override fun onCreate(savedInstanceState: Bundle?) {

     ...

     val crossProfileApps = getSystemService(CrossProfileApps::class.java)
       val userHandles = crossProfileApps.targetUserProfiles
       val label = crossProfileApps.getProfileSwitchingLabel(userHandles.first())
        binding.button.apply {
            text = label
            setOnClickListener {
                crossProfileApps.startMainActivity(
                    componentName,
                    userHandles.first()
                )
            }
        }
}

您还需要添加一个属性,以便将切换按钮的实例保存在类作用域中。

class MainActivity : AppCompatActivity(), LoaderManager.LoaderCallbacks<Cursor> {
   ...
    private lateinit var button: AppCompatButton
   ...
}

试试看

如果您现在运行应用,在应用底部将能看到相应按钮,表示您可以在工作资料个人资料之间切换,切换方向取决于您从哪个位置启动应用。

点击相应按钮将在另一资料中启动该应用。

db741d4872052fbc.gif

7. 恭喜!

您已成功修改应用,使之既可在个人资料中运行,也可在工作资料中运行。该应用能感知已安装的工作资料,甚至在个人模式下运行时,也能检索工作联系人。

此外,您还实现了一种方法,让用户可在运行应用时在同一应用的工作资料和个人资料之间切换,而无需关闭应用并从相应的资料重新启动该应用。这是一种很好的做法,可帮助用户在不同的资料中以不同方式使用您的应用。

了解详情