目录
背景
公司安全政策限制,上班期间不能拍照/录像。虽然已经差不多养成习惯,但老虎保不住也有打盹的时候,曾经有一次就差点违规。所以期望有一款app能代为管理,上班期间拍照/录像的时候会自动提示或者功能不可用,下班期间自动放开限制。
方案设计
可选方案1:接收NEW_PICTURE事件广播
首先自然而然地想到广播,相机拍照时系统会发出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是点击拍照按钮产生的广播事件,找了一圈没有找到打开相机产生的广播事件,这时拍照已成既定违规事实了,不能起到事前/事中提醒或禁止作用,不符合要求。
可选方案2:CameraManager的onCameraUnavailable回调
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在后台运行太浪费手机资源。而且这个提醒功能只能做到事中提醒,能不能直接把相机直接禁用掉,事前防范,让用户没有犯错的机会呢。
可选方案3(最终方案):Android Device Administration API禁用相机功能
Android Device Administration API 是Android 用来提供企业应用支持的,通过API可以在系统级别提供密码管理、停用相机等设备管理功能。通过调用API,打开相机时给予以下错误提示:
再配合闹钟(AlarmManager)实现定时开关,这恰恰就是我想要的,完美!
主要实现
1. 启用Device Administration API (MainActivity)
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); }
2. 根据当前是否在工作时间段,发送启用/禁用相机广播
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")); } } }
3. 根据广播启用/禁用相机 ,并设置下一次的闹钟 (AlarmReceiver)
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上面提了问题,还没人回复。