[TOC]
公司安全政策限制,上班期间不能拍照/录像。虽然已经差不多养成习惯,但老虎保不住也有打盹的时候,曾经有一次就差点违规。所以期望有一款 app 能代为管理,上班期间拍照/录像的时候会自动提示或者功能不可用,下班期间自动放开限制。
首先自然而然地想到广播,相机拍照时系统会发出 action 为 com.android.camera.NEW_PICTURE 的广播,可以创建一个接收器在接收到这个广播时提示不允许拍照。AndroidManifest.xml 文件中对广播接收器做静态注册,如下:
<!-- AndroidManifest.xml -->
<receiver
android:name=".CameraEventReceiver"
android:enabled="true">
<intent-filter>
<action android:name="com.android.camera.NEW_PICTURE" />
</intent-filter>
</receiver>
问题是,这里的 NEW_PICTURE 是点击拍照按钮产生的广播事件,找了一圈没有找到打开相机产生的广播事件,这时拍照已成既定违规事实了,不能起到事前/事中提醒或禁止作用,不符合要求。
CameraManager
是系统服务之一,专门用于 检测 、打开相机以及获取相机设备特性。可以使用 onCameraUnavailable 回调函数在相机被占用时(意味着打开了相机)进行提醒。核心代码如下:
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
manager.registerAvailabilityCallback(new CameraManager.AvailabilityCallback() {
@Override
public void onCameraUnavailable(String cameraId) {
super.onCameraUnavailable(cameraId);
Log.i(TAG, "camera unavailable");
// 提示相机不可用等弹框
}
功能上没问题,只是要保证服务一直在后台运行,才能在需要的时候提醒用户。其实安装这个 App 只是起到辅助提醒作用,真正需要提醒用户的概率很小很小,一直让 App 在后台运行太浪费手机资源。而且这个提醒功能只能做到事中提醒,能不能直接把相机直接禁用掉,事前防范,让用户没有犯错的机会呢。
Android Device Administration API 是 Android 用来提供企业应用支持的,通过 API 可以在系统级别提供密码管理、停用相机等设备管理功能。通过调用 API,打开相机时给予以下错误提示:
再配合闹钟(AlarmManager)实现定时开关,这恰恰就是我想要的,完美!
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
ComponentName deviceAdmin = new ComponentName(this, MyDeviceAdminReceiver.class);
if (!(devicePolicyManager.isAdminActive(deviceAdmin))) {
// 启用Device Admin API
startActivateDeviceAdminActivityForResult();
} else {
CameraUtil.blockOrUnblockCameraNow(this, amStartWorkTime, amStopWorkTime, pmStartWorkTime, pmStopWorkTime);
}
private void startActivateDeviceAdminActivityForResult() {
Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
intent.putExtra(
DevicePolicyManager.EXTRA_DEVICE_ADMIN,
deviceAdmin);
intent.putExtra(
DevicePolicyManager.EXTRA_ADD_EXPLANATION,
getString(R.string.admin_explanation));
startActivityForResult(intent, REQUEST_ENABLE);
}
public class CameraUtil {
public static void blockOrUnblockCameraNow(Context context, int amStartWorkTime, int amStopWorkTime, int pmStartWorkTime, int pmStopWorkTime) {
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
intent.setComponent(new ComponentName("com.aniu.cameramanager","com.aniu.cameramanager.AlarmReceiver"));
if (WorkTime.isNowWorkTime(amStartWorkTime, amStopWorkTime, pmStartWorkTime, pmStopWorkTime)) {
context.sendBroadcast(intent.setAction("ACTION_BLOCK_CAMERA"));
} else {
context.sendBroadcast(intent.setAction("ACTION_UNBLOCK_CAMERA"));
}
}
}
public void onReceive(Context context, Intent intent) {
if (action.equals("ACTION_BLOCK_CAMERA")) {
devicePolicyManager.setCameraDisabled(deviceAdmin, true);
Log.i(TAG, "禁用相机成功, action: " + action);
// 设置下一次的闹钟
setCameraAlarm(context, getNextCameraAlarm());
} else if (action.equals("ACTION_UNBLOCK_CAMERA")) {
devicePolicyManager.setCameraDisabled(deviceAdmin, false);
Log.i(TAG, "启用相机成功, action: " + action);
// 设置下一次的闹钟
setCameraAlarm(context, getNextCameraAlarm());
}
}
private void setCameraAlarm(Context context, Alarm alarm) {
Intent intent = new Intent(context, alarm.getAlarmReceiver());
intent.setAction(alarm.getAction());
intent.setFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
intent.setComponent(new ComponentName("com.aniu.cameramanager","com.aniu.cameramanager.AlarmReceiver"));
PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 999, intent, 0);
Calendar calendar = alarm.getCalendar();
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), alarmIntent);
Log.i(TAG, "设置闹钟成功: " + alarm.getCalendar().getTime() + " " + alarm.getAction());
}
原计划使用 AlarmManager 的循环闹钟,但目前(2021-6-8)情况下循环闹钟的事件都是不精确的,在华为畅享 9(我的主力测试机)上实测差几分钟/几十分钟的情况都存在,所以使用了单个闹钟AlarmManager.setExactAndAllowWhileIdle
方法,低功耗下也可以准确执行。方法的第一个参数使用AlarmManager.RTC_WAKEUP
,即以系统时间为参照。
广播定向发送
在很多手机上应用发送广播需要申请权限或者特殊处理,不然会接收不到广播,这里设置成了定向广播:
intent.setComponent(new ComponentName("com.aniu.cameramanager","com.aniu.cameramanager.AlarmReceiver"));
手机重启后需要让应用保持运行,需要在 AlarmReceiver 中同时监听开机事件。
if (action.equals("android.intent.action.BOOT_COMPLETED")) {
Log.i(TAG, "监听到重启完成事件, action: " + action);
// 重启后根据当前时间段禁用/启用相机
CameraUtil.blockOrUnblockCameraNow(context, amStartWorkTime, amStopWorkTime, pmStartWorkTime, pmStopWorkTime);
}
同时在 manifest 文件中 receiver 的 intent-filter 中增加 action android:name="android.intent.action.BOOT_COMPLETED" 。
如果不把应用设置成可后台运行,监听开机事件拉起 receiver 后很快应用进程就会被系统杀掉,需要提供入口或引导用户设置应用保持后台运行。
很多手机用户喜欢清理最近使用的应用,这里在 manifest 文件中设置android:excludeFromRecents="true"
属性来规避。
虽然程序中增加了当天是否为工作日的判断,节假日不会去停用相机,但如果用户休假,那在原工作时间段内相机还是会停用,导致用户没办法使用相机。需要在提示用户相机被停用的界面增加临时启用相机入口,不妨碍用户正常使用相机。
目前还不知道怎么修改这个界面,在 stackoverflow 上面提了问题,还没人回复。