Android 学习之“工作资料”的初步学习与开发

一、何为“工作资料”“工作配置”(work profile)

1. 接触:

第一次接触“工作资料”这个概念是在研究“如何使一个应用的图标不出现在桌面上”时了解到的。相关的博客中提到,在高版本系统的手机上如果要实现应用图标不显示在桌面上这一功能,有一种方法是将该应用设置为工作资料,设置完的应用可以隐藏图标。于是开始留意“工作资料”相关的内容,学习后整理出此文。

2. 介绍:

以下为谷歌官方对“工作资料”的介绍

Users often want to use their personal devices in an enterprise
setting. This situation can present organizations with a dilemma. If
the user can use their own device, the organization has to worry that
confidential information (like employee emails and contacts) are on a
device the organization does not control.

To address this situation, Android 5.0 (API level 21) allows
organizations to set up work profiles. If a device has a work profile,
the profile’s settings are under the control of the IT admin. The IT
admin can choose which apps are allowed for that profile, and can
control just what device features are available to the profile.

通俗的说工作资料是为了让用户可以使用同一台 Android 设备进行工作和娱乐,不至于二者的数据产生冲突或混淆而开发的功能或者说是一种配置。这一概念可以让公司对相关应用加以控制,保证企业数据的安全性和可控性也可以让用户不受工作数据干扰,发错数据或者误删除数据。

3. 概念说明:

  • 工作资料(工作配置)——见介绍。
  • 工作空间——当一个设备设置并开启了 work profile 功能,系统会开辟出一个数据空间用以存储相关应用、数据和配置,这个空间可以称之为工作空间。
  • 个人空间——和工作空间相区别的系统原生的数据空间,存放着普通的应用、数据和配置等。
  • IT admin——开启工作空间的应用,即工作资料的管理者,通常是一个应用,这个应用开启了工作空间,拥有对工作资料进行各种配置的权限。

4. 应用场景

  • 企业管理——work profile 本身就是为企业打造的功能,企业的应用作为工作资料的话,一些企业内部应用可以安装在其中,那么它们的数据可以得到管理和控制。
  • 双开与隐藏——同一个应用可以既存在于工作空间,也可以存在个人空间,而且它们的数据的分开存储的;IT admin 可以隐藏工作空间内部的特定应用,使它们的图标不出现在桌面上,从而达到隐藏应用的目的。

二、Demo 的学习与研究

1. 源码地址

https://github.com/android/enterprisesamples/tree/main/BasicManagedProfile#readme

2. Demo 项目结构展示Demo 项目结构

3. 图解

在这里插入图片描述

4. 代码解读

1.)首页入口与工作资料的设置:MainActivity与SetupProfileFragment

A. MainActivity
首页承载着两个 Fragment 一个是工作资料的设置开启页另一个是工作资料的管理页,通过 DevicePolicyManagerisProfileOwnerApp(…) 方法来判断是加载开启页还是设置页,如果“根据 userId 能获取 profileOwner 且包名与 profileOwner 的包名一致”则说明当前应用是 IT admin 处于工作空间且工作资料已经设置完毕,则加载工作资料管理页否则说明工作资料未设置,加载设置开启页。

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
        if (savedInstanceState == null) {
            DevicePolicyManager manager =
                    (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
            // hzf add 判断标准是根据 userId 能否获取 profileOwner 且包名与 profileOwner 的包名一致
            if (manager.isProfileOwnerApp(getApplicationContext().getPackageName())) {
                // 若符合条件,则加载工作资料管理页 BasicManagedProfileFragment
                showMainFragment();
            } else {
                // 否则加载设置开启页 SetupProfileFragment
                showSetupProfile(); 
            }
        }
    }
	...
}

B. SetupProfileFragment
工作资料的开启主要依靠特定的 intent 进行隐式启动

public class SetupProfileFragment extends Fragment {
	...
    // 设置开启按钮点击事件
    private void provisionManagedProfile() {
        ...
        Intent intent = new Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE);
        // 根据不同的系统版本,传入不同的 extra
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            intent.putExtra(
                    DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME,
                    activity.getApplicationContext().getPackageName()
            );
        } else {
            final ComponentName component = new ComponentName(activity,
                    BasicDeviceAdminReceiver.class.getName());
            intent.putExtra(
                    DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,
                    component
            );
        }
		// 先判断目标 intent 是否可以解析为一个 Activity,可以的话再继续设置工作资料
        if (intent.resolveActivity(activity.getPackageManager()) != null) {
            startActivityForResult(intent, REQUEST_PROVISION_MANAGED_PROFILE);
            activity.finish();
        } else {
            // ... toast 提示
        }
    }
	...
}
2.)获取工作资料设置完成消息的三种方式:startActivityForResult(…)、ProvisioningSuccessActivity与BasicDeviceAdminReceiver

A. startActivityForResult(…)
SetupProfileFragment 中我们依靠隐式启动来开启工作资料的设置,使用的是 startActivityForResult(…),可以在 onActivityResult(…) 中获取设置的情况。

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_PROVISION_MANAGED_PROFILE) {
        if (resultCode == Activity.RESULT_OK) {
            Toast.makeText(getActivity(), "Provisioning done.", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(getActivity(), "Provisioning failed.", Toast.LENGTH_SHORT).show();
        }
        return;
    }
    super.onActivityResult(requestCode, resultCode, data);
}

B. ProvisioningSuccessActivity
笔者一开始没有注意到该活动,后来通过日志看到这个活动是有被启动过的,翻看代码得知在 onCreate(…) 结束之前活动就被 finish() 掉了,这就是为啥页面没看见起来,但是日志分析是有启动的原因,

public class ProvisioningSuccessActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        finish(); // finish 掉了,这就是为啥页面没看见起来,但是日志分析是有启动的原因
    }
}

经查找在 AndroidManifest.xml源码中发现该活动如何启动的答案:
I)通过android.app.action.PROVISIONING_SUCCESSFUL 隐式启动。

...
    <activity
        android:name=".ProvisioningSuccessActivity"
        android:label="@string/app_name">
        <intent-filter>
            <action android:name="android.app.action.PROVISIONING_SUCCESSFUL" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
...

II)以下是 Android 9 源码中关于这个 action 的定义何解释,大概意思是说在工作资料设置完成后会发送带有这个 action 的 intent 来隐式启动配有该 action 的 Activity。从解释中我们可以得知设置完成时,除了会隐式启动带有特定 action 的一个活动,还会发送一个广播(C 中会提到)来告知用户资料配置完成,而且就发送接收速度来说 intent 的方式更快

/**
* Activity action: This activity action is sent to indicate that provisioning of a managed
* profile or managed device has completed successfully. It'll be sent at the same time as
* {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE} broadcast but this will be
* delivered faster as it's an activity intent.
*
* <p>The intent is only sent to the new device or profile owner.
*
* @see #ACTION_PROVISION_MANAGED_PROFILE
* @see #ACTION_PROVISION_MANAGED_DEVICE
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_PROVISIONING_SUCCESSFUL = "android.app.action.PROVISIONING_SUCCESSFUL";

C. BasicDeviceAdminReceiver
广播接受者,继承自 DeviceAdminReceiver,在工作资料设置后会接收到一条广播,它的 onProfileProvisioningComplete(…) 将会被调用。

...
    <receiver
        android:name=".BasicDeviceAdminReceiver"
        android:description="@string/app_name"
        android:label="@string/app_name"
        android:permission="android.permission.BIND_DEVICE_ADMIN">
        <meta-data
            android:name="android.app.device_admin"
            android:resource="@xml/basic_device_admin_receiver" />
        <intent-filter>
            <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
        </intent-filter>
    </receiver>
...
public class BasicDeviceAdminReceiver extends DeviceAdminReceiver {
    @Override
    public void onProfileProvisioningComplete(@NonNull Context context, @NonNull Intent intent) {
        // 这个方法会在工作资料设置后被调用
    }

    /**
     * 提供一个全局的 IT admin 的 ComponentName
     */
    public static ComponentName getComponentName(Context context) {
        return new ComponentName(context.getApplicationContext(), BasicDeviceAdminReceiver.class);
    }
}
3.)使能工作资料与工作资料的设置:EnableProfileActivity、PostProvisioningHelper与BasicManagedProfileFragment

A.)EnableProfileActivity
Demo 中,当接收到工作资料设置完成后的广播,在回调函数中以新的任务栈启动该活动进行使能,在工作资料设置后,但还未完成使能启动时,还是不完全可见的,此时桌面上是没有什么变化的,工作资料相关的应用图标也不会出现,使能启动后,才算完成真正完整的设置。

public class EnableProfileActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final PostProvisioningHelper helper = new PostProvisioningHelper(this);
        if (!helper.isDone()) {
            // Important: After the profile has been created, the MDM must enable it for corporate
            // apps to become visible in the launcher.
            helper.completeProvisioning();
        }
		...
    }
}

B.)PostProvisioningHelper
这是一个工作资料启用的帮助类,用于完成最后的工作资料使能开启任务。

class PostProvisioningHelper {
	...
    public void completeProvisioning() {
        ...
        ComponentName componentName = BasicDeviceAdminReceiver.getComponentName(mContext);
        // 设置工作资料的名称
        mDevicePolicyManager.setProfileName(
                componentName,
                mContext.getString(R.string.profile_name)
        );
        // 使能工作资料
        mDevicePolicyManager.setProfileEnabled(componentName);
    }
	...
}

C.)BasicManagedProfileFragment
这个管理页面提供了一些控制工作资料设置的方法,包括:启用/禁用其他应用程序,设置应用的一些限制条件,启用/禁用部分 intent 传输和清除工作资料的所有数据。

/**
 * Provides several functions that are available in a managed profile. This includes
 * enabling/disabling other apps, setting app restrictions, enabling/disabling intent forwarding,
 * and wiping out all the data in the profile.
 */
public class BasicManagedProfileFragment extends Fragment
        implements View.OnClickListener,
        CompoundButton.OnCheckedChangeListener {
        ...
}

I)判断一个应用在这个工作空间中是否被启用

  • 先判断应用是否安装:0 == (applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED)
  • 再判断是否启用:DevicePolicyManagerisApplicationHidden(…) 方法
private boolean isApplicationEnabled(String packageName) {
    // 先判断应用是否安装
    Activity activity = getActivity();
    PackageManager packageManager = activity.getPackageManager();
    try {
        int packageFlags;
        if(Build.VERSION.SDK_INT < 24){
            //noinspection deprecation
            packageFlags = PackageManager.GET_UNINSTALLED_PACKAGES;
        }else{
            packageFlags = PackageManager.MATCH_UNINSTALLED_PACKAGES;
        }
        ApplicationInfo applicationInfo = packageManager.getApplicationInfo(
                    packageName, packageFlags);
        // Return false if the app is not installed in this profile
        if (0 == (applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED)) {
            return false;
        }
        // 再判断是否启用
        DevicePolicyManager devicePolicyManager =
                    (DevicePolicyManager) activity.getSystemService(Activity.DEVICE_POLICY_SERVICE);
        return !devicePolicyManager.isApplicationHidden(
                BasicDeviceAdminReceiver.getComponentName(activity), packageName);
    } catch (PackageManager.NameNotFoundException e) {
        return false;
    }
}

II)对应用在这个工作空间中运行时设置一些限制条件,以及清除已经设置的限制条件,以 Chrome 为例:

  • DevicePolicyManagersetApplicationRestrictions(…) 方法
  • 最后一个参数为 Bundle 类型的键值对,即限制条件
  • 限制条件为清除限制条件
/**
 * 对 Chrome App 设置一些限制,不同应用可设置的信息和信息的格式不同
 */
private void setChromeRestrictions() {
	....
    final DevicePolicyManager manager =(DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
    final Bundle settings = new Bundle();
    // 禁止编辑书签
    settings.putString("EditBookmarksEnabled", "false");
    ...
    // 访问黑名单
    settings.putString("URLBlacklist", "[\"example.com\", \"example.org\"]");
    StringBuilder message = new StringBuilder("Setting Chrome restrictions:");
    ... // 构造弹窗样式和要展示的信息,即 Bundle 对象中保存的键值对
    // 构造弹窗并弹出
    new AlertDialog.Builder(activity)
    	...
        .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                // This is how you can set restrictions to an app.
                // The format for settings in Bundle differs from app to app.
                manager.setApplicationRestrictions(BasicDeviceAdminReceiver.getComponentName(activity),PACKAGE_NAME_CHROME, settings);
				Toast.makeText(activity, R.string.restrictions_set,
                Toast.LENGTH_SHORT).show();
            }
        })
        .show();
}
/**
 * 清除对 Chrome 设置的限制条件
 */
private void clearChromeRestrictions() {
	...
    final DevicePolicyManager manager =(DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
    // restriction Bundle 传入 null
    manager.setApplicationRestrictions(BasicDeviceAdminReceiver.getComponentName(activity),PACKAGE_NAME_CHROME, null);
    Toast.makeText(activity,R.string.cleared,Toast.LENGTH_SHORT).show();
}

III)启用/禁用个人空间与工作空间之间的 intent 传递

  • 将 IntentFilter 注册为允许的 intent 转发模式:addCrossProfileIntentFilter(…)
  • 禁止转发所有的 intent:clearCrossProfileIntentFilters(…)
/**
 * 运行指定的个人空间与工作空间之间传递的 intent
 */
private void enableForwarding() {
	...
    DevicePolicyManager manager = (DevicePolicyManager)activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
    try {
		IntentFilter filter = new IntentFilter(Intent.ACTION_SEND);
        filter.addDataType("text/plain");
        filter.addDataType("image/jpeg");
        // 将 IntentFilter 注册为允许的 intent 转发模式
		manager.addCrossProfileIntentFilter(BasicDeviceAdminReceiver.getComponentName(activity),filter, FLAG_MANAGED_CAN_ACCESS_PARENT | FLAG_PARENT_CAN_ACCESS_MANAGED);
    } catch (IntentFilter.MalformedMimeTypeException e) {
        e.printStackTrace();
    }
}
/**
 * 禁止转发所有的 intent
 */
private void disableForwarding() {
	...
    DevicePolicyManager manager = (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
    manager.clearCrossProfileIntentFilters(BasicDeviceAdminReceiver.getComponentName(activity));
}

IV)清空工作资料的数据并移除它:

  • DevicePolicyManager 的 wipeData(0)
/**
 * 清除与此工作资料相关的所有数据。
 */
private void removeProfile() {
	...
    DevicePolicyManager manager = (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
    manager.wipeData(0);
    // 清除数据后,当前活动将会退出,桌面上关于工作资料的图标也会消失
}
4.)注意点:

Demo 中工作资料创建后,出现的几个活动都是在另一个进程中启动的,它们的 userId 也不同,是属于工作空间里运行的 IT admin 的活动,这一点可以从打印日志,查看相关进程信息以及最近任务栏可以看的出来。

三、心得体会

多学习,多编码,多思考。学习新的开源库或者系统功能时多参照 Demo 既可以快速上手也可以学习他人编码风格中可以借鉴的地方。与君共勉,一同进步!

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值