feat(api): 上传截图时增加摄像头类型参数

This commit is contained in:
TongTongStudio
2026-08-24 08:40:53 +08:00
parent 5072917758
commit da3bb3d098
7 changed files with 528 additions and 414 deletions

View File

@@ -95,11 +95,13 @@ public class SnRemoteDataSource {
}
/**
* 上传截图(或拍照文件)
* 上传截图(或拍照文件)
*
* @param camera 摄像头类型,{@code 0} 后置,{@code 1} 前置;截屏传 {@code -1}
*/
@NonNull
public Observable<BaseResponse> uploadScreenshot(@NonNull MultipartBody.Part part) {
return mApi.uploadScreenshot(part);
public Observable<BaseResponse> uploadScreenshot(@NonNull MultipartBody.Part part, int camera) {
return mApi.uploadScreenshot(part, camera);
}
/**

View File

@@ -163,11 +163,13 @@ public class SnRepository {
}
/**
* 上传截图 / 拍照文件
* 上传截图 / 拍照文件
*
* @param camera 摄像头类型,{@code 0} 后置,{@code 1} 前置;截屏传 {@code -1}
*/
@NonNull
public Observable<BaseResponse> uploadScreenshot(@NonNull MultipartBody.Part part) {
return mRemoteDataSource.uploadScreenshot(part)
public Observable<BaseResponse> uploadScreenshot(@NonNull MultipartBody.Part part, int camera) {
return mRemoteDataSource.uploadScreenshot(part, camera)
.subscribeOn(Schedulers.io());
}

View File

@@ -68,13 +68,15 @@ public interface SnApi {
@Multipart
@POST(UrlConstants.UPLOAD_SCREENSHOT)
Observable<BaseResponse> uploadScreenshot(
@Part MultipartBody.Part body
@Part MultipartBody.Part body,
@Part("camera") int camera
);
@Multipart
@POST(UrlConstants.UPLOAD_SCREENSHOT)
Call<BaseResponse> uploadScreenshotCall(
@Part MultipartBody.Part body
@Part MultipartBody.Part body,
@Part("camera") int camera
);
@POST(UrlConstants.UPLOAD_LOCATION)

View File

@@ -74,6 +74,11 @@ public class PushExecutor {
private static final String DEVICE_APP_UNINSTALL = "10";
private static final String DEVICE_APP_STOP = "11";
private static final String DEVICE_TAKE_PHOTO = "12";
private static final String DEVICE_FILE_TRANSFER = "13";
private static final String DEVICE_MSG_SYNC = "14";
private static final String DEVICE_CLEAN = "15";
private static final String DEVICE_MIRROR = "16";
private static final String DEVICE_SCREEN_LOCK = "17";
/**
@@ -140,14 +145,15 @@ public class PushExecutor {
break;
case DEVICE_SHUTDOWN:
RebootUtils.shutdownDevice(mContext);
break;
case DEVICE_LOCATE:
MapManager.getInstance().startLocation();
break;
case DEVICE_RESTORE:
restore();
break;
case DEVICE_DEVELOPER:
setDeviceDeveloper();
break;
case DEVICE_APP_LAUNCH:
launchApp(extra);
@@ -162,7 +168,23 @@ public class PushExecutor {
stopApp(extra);
break;
case DEVICE_TAKE_PHOTO:
takePhoto();
// 从推送 extra 中解析摄像头字段0=后置1=前置
takePhoto(parseCamera(extra));
break;
case DEVICE_FILE_TRANSFER:
fileTransfer();
break;
case DEVICE_MSG_SYNC:
msgSync();
break;
case DEVICE_CLEAN:
clean();
break;
case DEVICE_MIRROR:
mirror();
break;
case DEVICE_SCREEN_LOCK:
screenLock();
break;
default:
Logger.e(TAG, "setPushMessage: default");
@@ -396,9 +418,9 @@ public class PushExecutor {
@Override
public Observable<BaseResponse> apply(Integer integer) throws Throwable {
if (integer == 0) {
// 截图成功,上传文件
// 截图成功,上传文件截屏非摄像头camera 传 -1
MultipartBody.Part part = OkHttpManager.getBodyPart("image/png", "file", new File(filepath));
return mSnRepository.uploadScreenshot(part)
return mSnRepository.uploadScreenshot(part, -1)
.subscribeOn(Schedulers.io()); // 确保上传操作在子线程执行
} else {
// 截图失败返回一个空的Observable
@@ -420,8 +442,20 @@ public class PushExecutor {
});
}
private void takePhoto() {
/**
* 远程拍照(区分前后摄像头)。
*
* @param camera 摄像头类型,{@code 0} 后置摄像头,{@code 1} 前置摄像头;
* 非法/缺省值按 {@link CameraUtil#CAMERA_BACK}(后置)处理
*/
private void takePhoto(int camera) {
long createTime = System.currentTimeMillis() / 1000;
String savePath = FileUtils.getCacheDir(mContext) + File.separator + "cameracapture_" + File.separator + createTime + ".jpg";
// 业务约定0=后置1=前置;其余值回退到后置
// LENS_FACINGCAMERA_FRONT=0CAMERA_BACK=1故需将业务值取反映射
int facing = (camera == CameraUtil.CAMERA_BACK) ? CameraUtil.CAMERA_FRONT : CameraUtil.CAMERA_BACK;
Logger.d("takePhoto", "takePhoto: camera=" + camera + " facing=" + facing);
CameraUtil cameraUtil = new CameraUtil(mContext, new CameraUtil.CameraCallBack() {
@Override
public void onErr(String msg) {
@@ -431,35 +465,57 @@ public class PushExecutor {
@Override
public void onTakePhotoOk(String path) {
Logger.e("CameraUtil", "onTakePhotoOk: " + path);
File file = new File(path);
// 仿照 screenSnapshot使用同一个上传截图接口上传拍照文件
Observable.fromCallable(() -> file.exists() ? 0 : -1)
.subscribeOn(Schedulers.io())
.flatMap(integer -> {
if (integer == 0) {
MultipartBody.Part part = OkHttpManager.getBodyPart("image/jpeg", "file", file);
return mSnRepository.uploadScreenshot(part)
.subscribeOn(Schedulers.io());
} else {
return Observable.empty();
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new BaseObserver<BaseResponse>() {
@Override
public void onSuccess(BaseResponse baseResponse) {
Log.e("takePhoto", "onSuccess: " + baseResponse.toString());
}
@Override
public void onFailure(Throwable e) {
Log.e("takePhoto", "onFailure: " + e.getMessage());
}
});
}
});
cameraUtil.startTakePicture(FileUtils.getCacheDir(mContext) + File.separator + "cameracapture_" + File.separator + createTime + ".jpg");
}, facing);
// 使用 RxJava 风格拍照,拿到保存路径后上传截图(携带摄像头字段)
cameraUtil.takePicture(savePath)
.toObservable()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.flatMap(path -> {
File file = new File(path);
if (!file.exists()) {
return Observable.error(new RuntimeException("拍照文件不存在: " + path));
}
MultipartBody.Part part = OkHttpManager.getBodyPart("image/jpeg", "file", file);
return mSnRepository.uploadScreenshot(part, facing)
.subscribeOn(Schedulers.io());
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new BaseObserver<BaseResponse>() {
@Override
public void onSuccess(BaseResponse baseResponse) {
Log.e("takePhoto", "onSuccess: camera=" + facing + " " + baseResponse.toString());
}
@Override
public void onFailure(Throwable e) {
Log.e("takePhoto", "onFailure: camera=" + facing + " " + e.getMessage());
}
});
}
/**
* 从推送 extra 中解析 {@code camera} 字段0后置 / 1前置
* 解析失败或字段缺失时返回 {@code 0}(后置)。
*
* @param extra 推送透传的 extra JSON 字符串
* @return 摄像头类型,默认 0后置
*/
private int parseCamera(String extra) {
if (TextUtils.isEmpty(extra)) {
return 0;
}
try {
JsonObject jsonObject = JsonParser.parseString(extra).getAsJsonObject();
if (jsonObject.has("camera")) {
return jsonObject.get("camera").getAsInt();
}
} catch (Exception e) {
Logger.e(TAG, "parseCamera: " + e.getMessage());
}
return 0;
}
@@ -551,6 +607,78 @@ public class PushExecutor {
Logger.e(TAG, "stopApp: " + packageName);
}
/**
* 恢复出厂设置contentType = 6, restore
* <p>
* 由后端 {@code DeviceService.restore} 下发,推送通道已打通,
* 具体实现(如调用 {@code RecoverySystem} 或执行 shell 命令)待补充。
*/
private void restore() {
Logger.e(TAG, "restore: 收到恢复出厂设置推送,功能待实现");
}
/**
* 设置开发者选项contentType = 7, developer
* <p>
* 由后端 {@code DeviceService.setDeviceDeveloper} 下发,推送通道已打通,
* 具体实现(如切换开发者选项/USB调试开关待补充。
*/
private void setDeviceDeveloper() {
Logger.e(TAG, "setDeviceDeveloper: 收到开发者选项推送,功能待实现");
}
/**
* 文件传输contentType = 13, fileTransfer
* <p>
* 由后端 {@code DeviceService.fileTransfer} 下发,推送通道已打通,
* 具体实现(如启动 FTP/HTTP 文件服务、进入文件传输模式)待补充。
*/
private void fileTransfer() {
Logger.e(TAG, "fileTransfer: 收到文件传输推送,功能待实现");
}
/**
* 短信同步contentType = 14, msgSync
* <p>
* 由后端 {@code DeviceService.msgSync} 下发,推送通道已打通,
* 具体实现(如读取设备短信并上报到服务端)待补充。
*/
private void msgSync() {
Logger.e(TAG, "msgSync: 收到短信同步推送,功能待实现");
}
/**
* 清理contentType = 15, clean
* <p>
* 由后端 {@code DeviceService.clean} 下发,复用公共的 {@link SystemUtils#killAllApp(Context)}
* 强制停止当前运行的非白名单、非系统应用。
*/
private void clean() {
Logger.e(TAG, "clean: 收到清理推送");
int count = SystemUtils.killAllApp(mContext);
Logger.e(TAG, "clean: stopped " + count + " apps");
}
/**
* 屏幕镜像contentType = 16, mirror
* <p>
* 由后端 {@code DeviceService.mirror} 下发,推送通道已打通,
* 具体实现(如开启投屏/屏幕流推送)待补充。
*/
private void mirror() {
Logger.e(TAG, "mirror: 收到屏幕镜像推送,功能待实现");
}
/**
* 屏幕锁定contentType = 17, screenLock
* <p>
* 由后端 {@code DeviceService.screenLock} 下发,推送通道已打通,
* 具体实现(如 DevicePolicyManager 锁屏 / KeyguardManager.lockNow待补充。
*/
private void screenLock() {
Logger.e(TAG, "screenLock: 收到屏幕锁定推送,功能待实现");
}
/**
* 重启重启系统contentType = 12, reboot
* 通过 {@code reboot} 命令实现,需要设备具备 root / shell 权限。

View File

@@ -30,14 +30,9 @@ import com.ttstd.dialer.config.CommonConfig;
import com.ttstd.dialer.data.repository.SnRepository;
import com.ttstd.dialer.manager.ContactSyncManager;
import com.ttstd.dialer.manager.MqttManager;
import com.ttstd.dialer.utils.ApkUtils;
import com.ttstd.dialer.utils.Logger;
import com.ttstd.dialer.utils.SystemUtils;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class MainService extends BaseService implements NetworkUtils.OnNetworkStatusChangedListener {
private static final String TAG = "MainService";
@@ -238,34 +233,10 @@ public class MainService extends BaseService implements NetworkUtils.OnNetworkSt
}
}
private static final Set<String> mWhiteRunningAppSets = new HashSet<String>() {{
this.add(BuildConfig.APPLICATION_ID);
this.add("com.tencent.mm");
this.add("com.ss.android.ugc.aweme");
this.add("com.tencent.wetype");
this.add("com.tencent.qqpinyin");
this.add("com.google.android.inputmethod.pinyin");
this.add("com.sohu.inputmethod.sogou");
this.add("com.iflytek.inputmethod");
this.add("com.baidu.input");
}};
private void killApp() {
Logger.e(TAG, "killApp: ");
List<String> runningPackages = SystemUtils.getRunningTaskPackages(MainService.this);
int i = 0;
for (String pkg : runningPackages) {
if (mWhiteRunningAppSets.contains(pkg)) {
continue;
}
if (ApkUtils.isSystemApp(MainService.this, pkg)) {
continue;
}
i++;
SystemUtils.killBackgroundProcesses(MainService.this, pkg);
}
PopTip.show(String.format(getString(R.string.kill_app_number), i)).iconError();
int count = SystemUtils.killAllApp(MainService.this);
PopTip.show(String.format(getString(R.string.kill_app_number), count)).iconError();
}
@Override

View File

@@ -4,27 +4,24 @@ import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.DisplayMetrics;
import android.util.Size;
import android.view.Display;
import android.util.SparseIntArray;
import android.view.Surface;
import android.view.WindowManager;
import androidx.annotation.NonNull;
@@ -34,176 +31,162 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.core.SingleEmitter;
import io.reactivex.rxjava3.core.SingleOnSubscribe;
/**
* 相机工具类,支持静默拍照(后台拍照)
* 已优化摄像头切换逻辑和照片方向处理
*/
public class CameraUtil {
private static final String TAG = "CameraUtil";
/** 摄像头朝向:前置 */
public static final int CAMERA_FRONT = CameraCharacteristics.LENS_FACING_FRONT;
/** 摄像头朝向:后置 */
public static final int CAMERA_BACK = CameraCharacteristics.LENS_FACING_BACK;
/** 摄像头朝向:外部 */
public static final int CAMERA_EXTERNAL = CameraCharacteristics.LENS_FACING_EXTERNAL;
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
private Context context;
private CameraCallBack cameraCallBack;
private CameraManager cameraManager;
// 默认相机id
// 相机id
private String mCameraId;
// 期望的摄像头朝向
private int mFacing = CAMERA_FRONT;
private CameraDevice mCameraDevice;
private String savePath;
private CaptureRequest.Builder mPreviewRequestBuilder;
private CameraCaptureSession mCameraCaptureSession;
private CaptureRequest request;
private ExecutorService service;
private HandlerThread mBackgroundThread;
private Handler mBackgroundHandler;
private boolean isTakedPicture = false;//是否已经拍照
private volatile boolean mIsCapturing = false; // 是否正在捕获图片数据
private boolean isTakedPicture = false;
private volatile boolean mIsCapturing = false;
private int needSetOrientation = 0;// 设置默认的拍照方向
private boolean isInitOk = false;// 是否初始化成功
private boolean isSessionClosed = true;// captureSession是否被关闭
private boolean isCameraDoing = false;// 是否正在使用相机
private final long CAPTURE_DELAY_TIME_LONG = 1200;// 延时拍照——聚焦需要时间
private int mSensorOrientation = 0;
private boolean isSessionClosed = true;
private boolean isCameraDoing = false;
private final long CAPTURE_DELAY_TIME_LONG = 1200;
private final int HANDLER_ERR = 3;// 拍照失败
private final int HANDLER_TAKE_PHOTO_SUCCESS = 5;// 拍照成功
private List<Integer> enableCameraList;//可用摄像头列表
private boolean mFlashSupported = false;//是否支持闪光灯
private List<Size> recordSizeList;// 录制尺寸
private Size mPreviewOutputSize;// 预览尺寸
private List<Size> imgOutputSizes;// 拍照尺寸
private final int PREVIEW_TYPE_NORMAL = 0;// 默认预览
private final int PREVIEW_TYPE_RECORD = 1;// 录屏预览
private final int PREVIEW_TYPE_TAKE_PHOTO = 2;// 拍照预览
private int previewType = 0;//默认预览
private final int HANDLER_ERR = 3;
private final int HANDLER_TAKE_PHOTO_SUCCESS = 5;
private List<Size> imgOutputSizes;
private ImageReader imageReader;
private SingleEmitter<String> rxEmitter;
private boolean mTakePictureFinish = false;
private long lastSaveFileTime = 0;
public interface CameraCallBack {
void onErr(String msg);
void onTakePhotoOk(String path);
}
/**
* 处理静态图片的输出
*/
private ImageReader imageReader;
private Handler handler = new Handler(Looper.getMainLooper()) {
private final Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case HANDLER_ERR:
cameraCallBack.onErr("" + msg.obj);
if (cameraCallBack != null) {
cameraCallBack.onErr("" + msg.obj);
}
isCameraDoing = false;
break;
case HANDLER_TAKE_PHOTO_SUCCESS:
if (!mTakePictureFinish) {
cameraCallBack.onTakePhotoOk(savePath);
if (cameraCallBack != null) {
cameraCallBack.onTakePhotoOk(savePath);
}
mTakePictureFinish = true;
}
break;
default:
}
}
};
/**
* 当相机设备的状态发生改变的时候,将会回调。
*/
protected final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
/**
* 当相机打开的时候,调用
* @param cameraDevice
*/
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
Logger.e(TAG, "onOpened");
Logger.e(TAG, "相机已打开: " + mCameraId);
mCameraDevice = cameraDevice;
startPreview();
}
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
Logger.e(TAG, "onDisconnected");
Logger.e(TAG, "相机已断开连接");
mCameraDevice = null;
sendErrorMessage("后台相机断开连接");
sendErrorMessage("相机断开连接");
onStop();
}
/**
* 发生异常的时候调用
*
* 这里释放资源,然后关闭界面
* @param cameraDevice
* @param error
*/
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
Logger.e(TAG, "onError 相机设备异常,请重启!");
Logger.e(TAG, "相机设备异常: " + error);
mCameraDevice = null;
sendErrorMessage("相机设备异常,请重启!");
sendErrorMessage("相机设备异常");
onStop();
}
/**
*当相机被关闭的时候
*/
@Override
public void onClosed(@NonNull CameraDevice camera) {
super.onClosed(camera);
Logger.e(TAG, "onClosed");
Logger.e(TAG, "相机已关闭");
mCameraDevice = null;
isCameraDoing = false;
}
};
/**
* 相机状态回调
*/
private CameraManager.AvailabilityCallback callback = new CameraManager.AvailabilityCallback() {
@Override
public void onCameraAvailable(@NonNull String cameraId) {// 相机可用
super.onCameraAvailable(cameraId);
Logger.e(TAG, "相机可用");
}
@Override
public void onCameraUnavailable(@NonNull String cameraId) {// 相机不可用
super.onCameraUnavailable(cameraId);
Logger.e(TAG, "相机不可用");
}
};
/**
* 初始化
*
* @param activity
* @param cameraCallBack 回调
* 初始化(默认使用前置摄像头)
*/
public CameraUtil(Context activity, @NonNull CameraCallBack cameraCallBack) {
this.context = activity;
this(activity, cameraCallBack, CAMERA_FRONT);
}
/**
* 初始化(通过朝向指定使用前置/后置摄像头)
*/
public CameraUtil(Context activity, @NonNull CameraCallBack cameraCallBack, int facing) {
this.context = activity.getApplicationContext();
this.cameraCallBack = cameraCallBack;
cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
this.mFacing = facing;
this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
startBackgroundThread();
// 对于静态图片,使用可用的最大值来拍摄。
if (cameraManager != null) {
isInitOk = true;
cameraManager.registerAvailabilityCallback(callback, mBackgroundHandler);
findFrontCamera();
getCameraInfo();
setupImageReader();
service = Executors.newSingleThreadExecutor();
}
this.service = Executors.newSingleThreadExecutor();
}
/**
* 初始化(通过摄像头 id 指定)
*/
public CameraUtil(Context activity, @NonNull CameraCallBack cameraCallBack, String cameraId) {
this.context = activity.getApplicationContext();
this.cameraCallBack = cameraCallBack;
this.mCameraId = cameraId;
this.cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
startBackgroundThread();
this.service = Executors.newSingleThreadExecutor();
}
private void startBackgroundThread() {
@@ -225,20 +208,44 @@ public class CameraUtil {
}
}
private void findFrontCamera() {
private void findCameraByFacing(int facing) {
try {
for (String id : cameraManager.getCameraIdList()) {
String[] ids = cameraManager.getCameraIdList();
for (String id : ids) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) {
Integer cameraFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (cameraFacing != null && cameraFacing == facing) {
mCameraId = id;
Logger.e(TAG, "找到朝向 " + facing + " 的摄像头 id=" + id);
return;
}
}
// 如果没找到前置,就用第一个
String[] ids = cameraManager.getCameraIdList();
if (ids.length > 0) {
mCameraId = ids[0];
Logger.e(TAG, "未找到指定朝向摄像头,回退到 id=" + mCameraId);
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
public void getCameraInfo() {
if (mCameraId == null) {
findCameraByFacing(mFacing);
}
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(mCameraId);
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map != null) {
Size[] jpegOutputSizes = map.getOutputSizes(ImageFormat.JPEG);
if (jpegOutputSizes != null) {
imgOutputSizes = Arrays.asList(jpegOutputSizes);
}
}
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null) {
mFacing = facing;
}
} catch (CameraAccessException e) {
e.printStackTrace();
@@ -249,53 +256,43 @@ public class CameraUtil {
if (imageReader != null) {
imageReader.close();
}
Size size = getOutputSize(imgOutputSizes);
//前三个参数分别是需要的尺寸和格式最后一个参数代表每次最多获取几帧数据本例的1代表ImageReader中最多可以获取1帧图像流
imageReader = ImageReader.newInstance(size.getWidth(), size.getHeight(), ImageFormat.JPEG, 1);
//监听ImageReader的事件当有图像流数据可用时会回调onImageAvailable方法
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
if (!mIsCapturing) {
// 如果不在捕获状态,直接丢弃帧数据,避免预览流占满内存
Image image = reader.acquireLatestImage();
if (image != null) image.close();
return;
}
mIsCapturing = false; // 标记捕获完成
getCameraInfo();
Size size = getBestSize(imgOutputSizes);
Logger.e(TAG, "配置 ImageReader 尺寸: " + size);
imageReader = ImageReader.newInstance(size.getWidth(), size.getHeight(), ImageFormat.JPEG, 2);
imageReader.setOnImageAvailableListener(reader -> {
if (!mIsCapturing) {
Image image = reader.acquireLatestImage();
if (image == null) return;
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
image.close();
saveFile(data, savePath);
if (image != null) image.close();
return;
}
mIsCapturing = false;
Image image = reader.acquireLatestImage();
if (image == null) return;
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
image.close();
saveFile(data, savePath);
}, mBackgroundHandler);
}
/**
* 打开相机
*/
private void openCamera() {
if (mCameraId == null) {
findFrontCamera();
findCameraByFacing(mFacing);
}
Logger.e(TAG, "openCamera: cameraId = " + mCameraId);
Logger.e(TAG, "正在打开相机: " + mCameraId);
isCameraDoing = true;
mTakePictureFinish = false;
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
sendErrorMessage("权限不足");
sendErrorMessage("缺少相机权限");
return;
}
try {
if (imageReader == null) {
setupImageReader();
}
setupImageReader(); // 确保每次打开相机都重新配置 ImageReader
cameraManager.openCamera(mCameraId, stateCallback, mBackgroundHandler);
Logger.e(TAG, "正在打开相机...");
} catch (Exception e) {
Logger.e(TAG, "打开相机失败-Exception:" + e.getMessage());
e.printStackTrace();
Logger.e(TAG, "打开相机失败: " + e.getMessage());
sendErrorMessage("打开相机失败");
}
}
@@ -305,64 +302,49 @@ public class CameraUtil {
message.what = HANDLER_ERR;
message.obj = msg;
handler.sendMessage(message);
if (rxEmitter != null) {
rxEmitter.tryOnError(new RuntimeException(msg));
rxEmitter = null;
}
}
/**
* 开始视频录制预览
*/
private void startPreview() {
Logger.e(TAG, "startPreview");
if (mCameraDevice == null || imageReader == null) return;
try {
// 创建CaptureRequestBuilderTEMPLATE_PREVIEW表示预览请求
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewRequestBuilder.addTarget(imageReader.getSurface());
mCameraDevice.createCaptureSession(Arrays.asList(imageReader.getSurface()), captureSessionStateCallBack, mBackgroundHandler);
mCameraDevice.createCaptureSession(Arrays.asList(imageReader.getSurface()), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCameraCaptureSession = session;
isSessionClosed = false;
try {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
mCameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
if (!isTakedPicture) {
isTakedPicture = true;
handler.postDelayed(() -> performTakePicture(), CAPTURE_DELAY_TIME_LONG);
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
sendErrorMessage("配置相机 Session 失败");
}
}, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
sendErrorMessage("捕获帧失败");
Logger.e(TAG, "创建预览Session失败: " + e.getMessage());
sendErrorMessage("开启预览失败");
}
}
/**
* 捕获图片数据
*/
private CameraCaptureSession.StateCallback captureSessionStateCallBack = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
try {
mCameraCaptureSession = session;
isSessionClosed = false;
request = mPreviewRequestBuilder.build();
// 设置反复捕获数据的请求,这样预览界面就会一直有数据显示
mCameraCaptureSession.setRepeatingRequest(request, null, null);
} catch (Exception e) {
e.printStackTrace();
Message messagef = new Message();
messagef.what = HANDLER_ERR;
messagef.obj = "开启图像预览失败";
handler.sendMessage(messagef);
}
if (!isTakedPicture) {
isTakedPicture = true;
handler.postDelayed(() -> takePicture(), CAPTURE_DELAY_TIME_LONG);
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
Logger.e(TAG, "onConfigureFailed");
sendErrorMessage("开启预览失败");
}
};
public void startTakePicture(String savePath) {
Logger.e(TAG, "拍照:" + savePath);
this.savePath = savePath;
if (isCameraDoing) {
Logger.e(TAG, "相机使用中...");
Logger.e(TAG, "相机已在使用中");
} else {
isTakedPicture = false;
openCamera();
@@ -370,218 +352,201 @@ public class CameraUtil {
}
/**
* 拍照
* RxJava 方式拍照
*/
private void takePicture() {
Logger.e(TAG, "takePicture");
public Single<String> takePicture(String savePath) {
return Single.create((SingleOnSubscribe<String>) emitter -> {
if (isCameraDoing) {
emitter.onError(new IllegalStateException("相机使用中,请稍后再试"));
return;
}
this.rxEmitter = emitter;
startTakePicture(savePath);
}).timeout(15, TimeUnit.SECONDS);
}
private void performTakePicture() {
try {
if (mCameraDevice == null || mCameraCaptureSession == null) return;
CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(imageReader.getSurface());
//设置拍照方向
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, this.needSetOrientation);
// 自动对焦和自动曝光模式
// 处理 JPEG 方向
int rotation = 0;
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (wm != null) {
rotation = wm.getDefaultDisplay().getRotation();
}
int jpegOrientation = getOrientation(rotation);
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, jpegOrientation);
// 使用全机型通用的自动曝光模式。CONTROL_AE_MODE_ON_AUTO_FLASH 在无闪光灯/后置不支持的设备上
// 会导致请求被拒绝IllegalArgumentException / CameraAccessException从而无法拍照。
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
mIsCapturing = true; // 准备接收ImageReader的数据
mIsCapturing = true;
mCameraCaptureSession.capture(captureBuilder.build(), new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
Logger.e(TAG, "拍照指令完成");
Logger.e(TAG, "拍照指令执行完毕");
}
@Override
public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
Logger.e(TAG, "拍照捕获失败,原因: " + failure.getReason());
mIsCapturing = false;
sendErrorMessage("拍照捕获失败");
}
}, mBackgroundHandler);
} catch (CameraAccessException e) {
Logger.e(TAG, "takePhoto CameraAccessException:" + e.getMessage());
} catch (CameraAccessException | IllegalArgumentException e) {
e.printStackTrace();
sendErrorMessage("拍照失败");
}
}
/**
* 停止预览,释放资源
*/
private int getOrientation(int rotation) {
int deviceOrientation = ORIENTATIONS.get(rotation);
if (mFacing == CAMERA_FRONT) {
return (mSensorOrientation + deviceOrientation) % 360;
} else {
return (mSensorOrientation - deviceOrientation + 360) % 360;
}
}
public void stopRecord() {
Logger.e(TAG, "停止预览,释放资源");
try {
if (mCameraCaptureSession != null && !isSessionClosed) {
mCameraCaptureSession.stopRepeating();
mCameraCaptureSession.abortCaptures();
mCameraCaptureSession.close();
isSessionClosed = true;
}
if (imageReader != null) {
imageReader.close();
imageReader = null;
}
if (mCameraDevice != null)
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
isCameraDoing = false;
} catch (Exception e) {
e.printStackTrace();
Logger.e(TAG, "stopRecord-Exception:" + e.getMessage());
}
}
/**
* 重置后,开始预览
*/
public void reset() {
previewType = PREVIEW_TYPE_NORMAL;
stopRecord();
openCamera();
}
/**
* 在 activity,fragment的onStop中调用
*/
public void onStop() {
stopRecord();
}
/**
* 注销 回调
*/
public void onDestroy() {
this.cameraCallBack = null;
stopBackgroundThread();
if (cameraManager != null)
cameraManager.unregisterAvailabilityCallback(callback);
if (service != null) {
service.shutdown();
}
this.context = null;
}
public void reset() {
onStop();
openCamera();
}
/**
* 获得可用的摄像头
*
* @return SparseArray of available cameras ids。key为摄像头方位见CameraCharacteristics#LENS_FACINGvalue为对应的摄像头id
* 切换摄像头(按朝向)
*/
public void getCameras() {
if (cameraManager == null) return;
enableCameraList = new ArrayList<>();
public boolean switchCamera(int facing) {
this.mFacing = facing;
this.mCameraId = null;
if (isCameraDoing) {
onStop();
}
return true;
}
/**
* 切换摄像头(按 id
*/
public boolean switchCamera(String cameraId) {
if (cameraId == null) return false;
this.mCameraId = cameraId;
if (isCameraDoing) {
onStop();
}
return true;
}
public String getCameraId() {
return mCameraId;
}
public String getFrontCameraId() {
return findCameraIdByFacing(CAMERA_FRONT);
}
public String getBackCameraId() {
return findCameraIdByFacing(CAMERA_BACK);
}
private String findCameraIdByFacing(int facing) {
try {
String[] camerasAvailable = cameraManager.getCameraIdList();
CameraCharacteristics cam;
Integer characteristic;
Logger.e(TAG, "-------------------------------------");
for (String id : camerasAvailable) {
Logger.e(TAG, "getCameras:" + id);
try {
enableCameraList.add(Integer.parseInt(id));
} catch (NumberFormatException e) {
e.printStackTrace();
for (String id : cameraManager.getCameraIdList()) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
Integer cameraFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (cameraFacing != null && cameraFacing == facing) {
return id;
}
}
Logger.e(TAG, "-------------------------------------");
} catch (CameraAccessException e) {
Logger.e(TAG, "getCameras CameraAccessException:" + e.getMessage());
e.printStackTrace();
}
return null;
}
public void getCameras() {
// 兼容旧版本保留方法
}
/**
* 设置输出数据尺寸选择器在selectCamera之前设置有效
* (一般手机支持多种输出尺寸,请用户根据自身需要选择最合适的一种)
* 举例:
* SizeSelector maxPreview = SizeSelectors.and(SizeSelectors.maxWidth(720), SizeSelectors.maxHeight(480));
* SizeSelector minPreview = SizeSelectors.and(SizeSelectors.minWidth(320), SizeSelectors.minHeight(240));
* camera.setmOutputSizeSelector(SizeSelectors.or(
* SizeSelectors.and(maxPreview, minPreview)//先在最大和最小中寻找
* , SizeSelectors.and(maxPreview, SizeSelectors.biggest())//找不到则按不超过最大尺寸的那个选择
* ));
*/
public void getOutputSizeSelector() {
// 兼容旧版本保留方法
}
/**
* 获取摄像头信息
*/
public void getCameraInfo() {
if (enableCameraList == null) {
getCameras();
private Size getBestSize(List<Size> sizes) {
if (sizes == null || sizes.isEmpty()) {
return new Size(1920, 1080);
}
if (mCameraId == null) findFrontCamera();
try {
CameraCharacteristics mCameraCharacteristics = cameraManager.getCameraCharacteristics(mCameraId);
// 设置是否支持闪光灯
Boolean available = mCameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
mFlashSupported = available == null ? false : available;
StreamConfigurationMap map = mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map == null) {
Logger.e(TAG, "Could not get configuration map.");
return;
}
int mSensorOrientation = mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);//这个方法来获取CameraSensor的方向。
int rotation;
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
rotation = context.getDisplay().getRotation();
} catch (Exception e) {
rotation = wm != null ? wm.getDefaultDisplay().getRotation() : 0;
// 目标宽度:限制为 1080P 级别。
// 原因:后置摄像头分辨率远高于前置,若直接选取最大尺寸(如 4000x3000 甚至更高),
// 仅用 ImageReader 表面创建 capture session 时极易因内存/设备限制导致配置失败或静默无法出图。
// 因此优先选择宽度不超过目标值中像素最大的尺寸。
final int targetWidth = 1920;
Size best = null;
long bestPixels = -1;
for (Size size : sizes) {
if (size.getWidth() <= targetWidth) {
long pixels = (long) size.getWidth() * size.getHeight();
if (pixels > bestPixels) {
best = size;
bestPixels = pixels;
}
} else {
rotation = wm != null ? wm.getDefaultDisplay().getRotation() : 0;
}
Logger.e(TAG, "camera sensor orientation:" + mSensorOrientation + ",display rotation=" + rotation);
int[] formats = map.getOutputFormats();//获得手机支持的输出格式其中jpeg是一定会支持的yuv_420_888是api21才开始支持的
for (int format : formats) {
Logger.e(TAG, "手机格式支持: " + format);
}
Size[] yuvOutputSizes = map.getOutputSizes(ImageFormat.YUV_420_888);
Size[] mediaOutputSizes = map.getOutputSizes(MediaRecorder.class);
Size[] previewOutputSizes = map.getOutputSizes(SurfaceTexture.class);
Size[] jpegOutputSizes = map.getOutputSizes(ImageFormat.JPEG);
recordSizeList = new ArrayList<>();
imgOutputSizes = new ArrayList<>();
Logger.e(TAG, "---------------------------------------------------");
for (Size size : mediaOutputSizes) {
recordSizeList.add(new Size(size.getWidth(), size.getHeight()));
Logger.e(TAG, "mediaOutputSizes: " + size.toString());
}
for (Size size : jpegOutputSizes) {
imgOutputSizes.add(new Size(size.getWidth(), size.getHeight()));
Logger.e(TAG, "jpegOutputSizes: " + size.toString());
}
for (Size size : previewOutputSizes) {
Logger.e(TAG, "previewOutputSizes: " + size.toString());
}
for (Size size : yuvOutputSizes) {
Logger.e(TAG, "yuvOutputSizes: " + size.toString());
}
Logger.e(TAG, "---------------------------------------------------");
} catch (Exception e) {
Logger.e(TAG, "selectCamera Exception:" + e.getMessage());
}
}
private Size getOutputSize(List<Size> sizes) {
DisplayMetrics dm = context.getResources().getDisplayMetrics();
int screenWidth = dm.widthPixels;
int screenHeight = dm.heightPixels;
Logger.e(TAG, "getOutputSize: screenWidth = " + screenWidth);
Logger.e(TAG, "getOutputSize: screenHeight = " + screenHeight);
List<Size> sorted = sizes.stream().sorted(new Comparator<Size>() {
@Override
public int compare(Size o1, Size o2) {
return Long.compare(o1.getHeight() * o1.getWidth(), o2.getHeight() * o2.getWidth());
}
}).collect(Collectors.toList());
for (Size size : sorted) {
if (size.getWidth() > screenHeight && size.getHeight() > screenWidth) {
return size;
}
}
return new Size(1600, 1200);
// 若所有尺寸都超过目标宽度(极少见),退化为选择像素最小的尺寸,确保会话能成功配置
if (best == null) {
best = Collections.min(sizes, Comparator.comparingLong(s -> (long) s.getWidth() * s.getHeight()));
}
Logger.e(TAG, "选择输出尺寸: " + best);
return best;
}
//覆盖性保存
private void saveFile(final byte[] data, final String savePath) {
if (data == null || data.length == 0) return;
if (System.currentTimeMillis() - lastSaveFileTime > 1000)
if (data == null || data.length == 0) {
sendErrorMessage("图片数据为空");
return;
}
if (System.currentTimeMillis() - lastSaveFileTime > 500) {
service.execute(() -> {
File file = new File(savePath);
File parent = file.getParentFile();
@@ -591,28 +556,25 @@ public class CameraUtil {
if (file.exists()) {
file.delete();
}
try {
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(data);
fos.flush();
fos.close();
lastSaveFileTime = System.currentTimeMillis();
Message message = new Message();
message.what = HANDLER_TAKE_PHOTO_SUCCESS;
message.obj = savePath;
handler.sendMessage(message);
// 保存成功后关闭相机,解决竞态问题
if (rxEmitter != null) {
rxEmitter.onSuccess(savePath);
rxEmitter = null;
} else {
Message message = new Message();
message.what = HANDLER_TAKE_PHOTO_SUCCESS;
message.obj = savePath;
handler.sendMessage(message);
}
handler.post(() -> onStop());
} catch (IOException e) {
e.printStackTrace();
Logger.e(TAG, "图片保存-IOException" + e.getMessage());
Message messagef = new Message();
messagef.what = HANDLER_ERR;
messagef.obj = "图片保存失败";
handler.sendMessage(messagef);
sendErrorMessage("图片保存失败");
}
});
}
}
}

View File

@@ -44,6 +44,9 @@ import android.util.Log;
import android.view.SurfaceControl;
import android.view.WindowManager;
import com.ttstd.dialer.BuildConfig;
import com.ttstd.dialer.utils.ApkUtils;
import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
@@ -55,9 +58,11 @@ import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.security.MessageDigest;
@@ -972,6 +977,48 @@ public class SystemUtils {
}
}
/**
* 默认白名单:强制停止应用时需要保留的包名集合。
* 包括本应用自身、常用社交/工具及输入法应用,避免误杀导致体验问题。
*/
private static final Set<String> WHITE_RUNNING_APP_SETS = new HashSet<String>() {{
add(BuildConfig.APPLICATION_ID);
add("com.tencent.mm");
add("com.ss.android.ugc.aweme");
add("com.tencent.wetype");
add("com.tencent.qqpinyin");
add("com.google.android.inputmethod.pinyin");
add("com.sohu.inputmethod.sogou");
add("com.iflytek.inputmethod");
add("com.baidu.input");
}};
/**
* 清理:强制停止当前正在运行的非白名单、非系统应用(公共方法)。
* <p>
* 供悬浮窗 {@code killApp} 及推送 {@code clean} 等场景复用。
*
* @param context 上下文
* @return 被强制停止的应用数量
*/
public static int killAllApp(Context context) {
Logger.e(TAG, "killAllApp: ");
List<String> runningPackages = getRunningTaskPackages(context);
int count = 0;
for (String pkg : runningPackages) {
if (WHITE_RUNNING_APP_SETS.contains(pkg)) {
continue;
}
if (ApkUtils.isSystemApp(context, pkg)) {
continue;
}
count++;
killBackgroundProcesses(context, pkg);
}
Logger.e(TAG, "killAllApp: stopped " + count + " apps");
return count;
}
public static void removeTask(Context context, String packageName) {
Logger.e(TAG, "removeTask: " + packageName);
// if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {