Android DeviceAdminReceiver密码失败时拍照功能失效问题排查
问题分析与解决方案
一、核心问题排查
相机被设备管理策略禁用
你的device_admin.xml中添加了<disable-camera />策略,这会直接让系统禁用设备相机,导致无法调用相机拍照,这是最关键的问题。权限申请的错误强转
在onPasswordFailed方法中,你把Receiver的Context强转为AppCompatActivity来申请权限:ActivityCompat.requestPermissions((AppCompatActivity) context, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);但
DeviceAdminReceiver的上下文是Receiver类型,并非Activity,强转会直接抛出ClassCastException,导致权限申请失败。Camera2 API的回调与资源管理问题
ImageReader.setOnImageAvailableListener设置在onCaptureCompleted回调里,此时图像可能已经生成,会错过监听时机。- 调用
cameraManager.openCamera时传入null作为Handler,回调会在主线程执行,相机操作属于耗时操作,容易引发ANR或回调不执行。 - 未正确关闭
CameraCaptureSession,会导致相机资源泄漏。
存储权限与路径问题
Android 10(API 29)及以上版本,Environment.getExternalStoragePublicDirectory已被废弃,且Scoped Storage限制了直接写入公共目录的权限,导致图片无法保存。
二、修正后的解决方案
1. 移除相机禁用策略
修改device_admin.xml,删除<disable-camera />:
<device-admin xmlns:android="http://schemas.android.com/apk/res/android"> <uses-policies> <limit-password /> <watch-login /> </uses-policies> </device-admin>
2. 修复权限申请逻辑
Receiver无法直接申请权限,需启动透明Activity处理权限请求:
- 创建
PermissionRequestActivity(透明主题):
public class PermissionRequestActivity extends AppCompatActivity { private static final int REQUEST_CAMERA_STORAGE = 100; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 设置透明主题避免界面闪烁 getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); getWindow().setAlpha(0.0f); requestPermissions(); } private void requestPermissions() { List<String> permissions = new ArrayList<>(); if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { permissions.add(Manifest.permission.CAMERA); } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); } if (!permissions.isEmpty()) { ActivityCompat.requestPermissions(this, permissions.toArray(new String[0]), REQUEST_CAMERA_STORAGE); } else { MyDeviceAdminReceiver.captureImage(this); finish(); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CAMERA_STORAGE) { boolean allGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (allGranted) { MyDeviceAdminReceiver.captureImage(this); } finish(); } } }
- 在
AndroidManifest.xml中注册该Activity并设置透明主题:
<activity android:name=".PermissionRequestActivity" android:exported="false" android:theme="@style/TransparentTheme" />
- 添加透明主题到
styles.xml:
<style name="TransparentTheme" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsTranslucent">true</item> <item name="android:windowAnimationStyle">@null</item> </style>
- 修改
MyDeviceAdminReceiver的onPasswordFailed方法:
@Override public void onPasswordFailed(Context context, Intent intent) { super.onPasswordFailed(context, intent); boolean hasCameraPerm = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED; boolean hasStoragePerm = true; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { hasStoragePerm = ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } if (hasCameraPerm && hasStoragePerm) { captureImage(context); } else { Intent permIntent = new Intent(context, PermissionRequestActivity.class); permIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(permIntent); } }
3. 修复Camera2 API实现
调整监听时机、添加Handler线程、正确释放资源:
public static void captureImage(Context context) { CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); HandlerThread handlerThread = new HandlerThread("CameraCaptureThread"); handlerThread.start(); Handler handler = new Handler(handlerThread.getLooper()); try { String cameraId = getBackCameraId(cameraManager); if (cameraId == null) { Log.e(TAG, "No back camera found"); handlerThread.quitSafely(); return; } Size imageDimensions = getImageDimensions(cameraManager, cameraId); if (imageDimensions == null) { Log.e(TAG, "Failed to get image dimensions"); handlerThread.quitSafely(); return; } final ImageReader imageReader = ImageReader.newInstance( imageDimensions.getWidth(), imageDimensions.getHeight(), ImageFormat.JPEG, 1 ); // 提前设置图像监听 imageReader.setOnImageAvailableListener(reader -> { try (Image image = reader.acquireLatestImage()) { if (image == null) return; ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.capacity()]; buffer.get(bytes); saveImage(context, bytes); } finally { imageReader.close(); handlerThread.quitSafely(); } }, handler); List<Surface> outputSurfaces = Collections.singletonList(imageReader.getSurface()); CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice camera) { try { CaptureRequest.Builder captureRequestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureRequestBuilder.addTarget(imageReader.getSurface()); captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); camera.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { try { session.capture(captureRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); session.close(); camera.close(); } }, handler); } catch (CameraAccessException e) { Log.e(TAG, "Capture failed: " + e.getMessage()); session.close(); camera.close(); handlerThread.quitSafely(); } } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { Log.e(TAG, "Capture session configuration failed"); session.close(); camera.close(); handlerThread.quitSafely(); } }, handler); } catch (CameraAccessException e) { Log.e(TAG, "Camera open failed: " + e.getMessage()); camera.close(); handlerThread.quitSafely(); } } @Override public void onDisconnected(@NonNull CameraDevice camera) { camera.close(); handlerThread.quitSafely(); } @Override public void onError(@NonNull CameraDevice camera, int error) { Log.e(TAG, "Camera error: " + error); camera.close(); handlerThread.quitSafely(); } }; if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { cameraManager.openCamera(cameraId, stateCallback, handler); } else { Log.e(TAG, "Camera permission not granted"); handlerThread.quitSafely(); } } catch (CameraAccessException e) { Log.e(TAG, "Camera manager error: " + e.getMessage()); handlerThread.quitSafely(); } }
4. 适配Android 10+存储逻辑
使用MediaStore保存图片:
private static void saveImage(Context context, byte[] imageBytes) { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()); String fileName = "IMG_" + sdf.format(new Date()) + ".jpg"; ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName); values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/SecurityCapture"); values.put(MediaStore.Images.Media.IS_PENDING, 1); } ContentResolver resolver = context.getContentResolver(); Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (uri == null) { Log.e(TAG, "Failed to create media store entry"); return; } try (OutputStream outputStream = resolver.openOutputStream(uri)) { outputStream.write(imageBytes); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.clear(); values.put(MediaStore.Images.Media.IS_PENDING, 0); resolver.update(uri, values, null, null); } Log.d(TAG, "Image saved to: " + uri.toString()); } catch (IOException e) { Log.e(TAG, "Failed to save image: " + e.getMessage()); resolver.delete(uri, null, null); } }
三、额外注意事项
- 需要在
MainActivity中引导用户激活设备管理员权限,否则onPasswordFailed不会触发。 - 发送邮件功能可通过
Intent.ACTION_SEND启动系统邮件应用,若需后台发送,需使用JavaMail库并配合前台服务(Android 10+后台启动Activity受限)。
内容的提问来源于stack exchange,提问作者Lambda Developers
相关产品推荐
相关产品推荐

