feat(api): 上传截图时增加摄像头类型参数
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_FACING:CAMERA_FRONT=0,CAMERA_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 权限。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
// 创建CaptureRequestBuilder,TEMPLATE_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_FACING,value为对应的摄像头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("图片保存失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user