Merge "Camera: Add support for client requests&results for advanced extensions" into tm-dev am: 50a87aa8b1
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/17113806 Change-Id: Iba400a5d9d957dd788677fb4469d9ce77720b93b Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -824,21 +824,31 @@ public final class CameraExtensionCharacteristics {
|
|||||||
if (!isExtensionSupported(mCameraId, extension, mChars)) {
|
if (!isExtensionSupported(mCameraId, extension, mChars)) {
|
||||||
throw new IllegalArgumentException("Unsupported extension");
|
throw new IllegalArgumentException("Unsupported extension");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CameraMetadataNative captureRequestMeta = null;
|
||||||
|
if (areAdvancedExtensionsSupported()) {
|
||||||
|
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
|
||||||
|
extender.init(mCameraId);
|
||||||
|
captureRequestMeta = extender.getAvailableCaptureRequestKeys(mCameraId);
|
||||||
|
} else {
|
||||||
Pair<IPreviewExtenderImpl, IImageCaptureExtenderImpl> extenders =
|
Pair<IPreviewExtenderImpl, IImageCaptureExtenderImpl> extenders =
|
||||||
initializeExtension(extension);
|
initializeExtension(extension);
|
||||||
extenders.second.onInit(mCameraId, mChars.getNativeMetadata());
|
extenders.second.onInit(mCameraId, mChars.getNativeMetadata());
|
||||||
extenders.second.init(mCameraId, mChars.getNativeMetadata());
|
extenders.second.init(mCameraId, mChars.getNativeMetadata());
|
||||||
CameraMetadataNative captureRequestMeta =
|
captureRequestMeta = extenders.second.getAvailableCaptureRequestKeys();
|
||||||
extenders.second.getAvailableCaptureRequestKeys();
|
extenders.second.onDeInit();
|
||||||
|
}
|
||||||
|
|
||||||
if (captureRequestMeta != null) {
|
if (captureRequestMeta != null) {
|
||||||
int[] requestKeys = captureRequestMeta.get(
|
int[] requestKeys = captureRequestMeta.get(
|
||||||
CameraCharacteristics.REQUEST_AVAILABLE_REQUEST_KEYS);
|
CameraCharacteristics.REQUEST_AVAILABLE_REQUEST_KEYS);
|
||||||
if (requestKeys == null) {
|
if (requestKeys == null) {
|
||||||
throw new AssertionError("android.request.availableRequestKeys must be non-null"
|
throw new AssertionError(
|
||||||
|
"android.request.availableRequestKeys must be non-null"
|
||||||
+ " in the characteristics");
|
+ " in the characteristics");
|
||||||
}
|
}
|
||||||
CameraCharacteristics requestChars = new CameraCharacteristics(captureRequestMeta);
|
CameraCharacteristics requestChars = new CameraCharacteristics(
|
||||||
|
captureRequestMeta);
|
||||||
|
|
||||||
Object crKey = CaptureRequest.Key.class;
|
Object crKey = CaptureRequest.Key.class;
|
||||||
Class<CaptureRequest.Key<?>> crKeyTyped = (Class<CaptureRequest.Key<?>>) crKey;
|
Class<CaptureRequest.Key<?>> crKeyTyped = (Class<CaptureRequest.Key<?>>) crKey;
|
||||||
@@ -854,7 +864,6 @@ public final class CameraExtensionCharacteristics {
|
|||||||
if (!ret.contains(CaptureRequest.JPEG_ORIENTATION)) {
|
if (!ret.contains(CaptureRequest.JPEG_ORIENTATION)) {
|
||||||
ret.add(CaptureRequest.JPEG_ORIENTATION);
|
ret.add(CaptureRequest.JPEG_ORIENTATION);
|
||||||
}
|
}
|
||||||
extenders.second.onDeInit();
|
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new IllegalStateException("Failed to query the available capture request keys!");
|
throw new IllegalStateException("Failed to query the available capture request keys!");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -894,12 +903,19 @@ public final class CameraExtensionCharacteristics {
|
|||||||
throw new IllegalArgumentException("Unsupported extension");
|
throw new IllegalArgumentException("Unsupported extension");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CameraMetadataNative captureResultMeta = null;
|
||||||
|
if (areAdvancedExtensionsSupported()) {
|
||||||
|
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
|
||||||
|
extender.init(mCameraId);
|
||||||
|
captureResultMeta = extender.getAvailableCaptureResultKeys(mCameraId);
|
||||||
|
} else {
|
||||||
Pair<IPreviewExtenderImpl, IImageCaptureExtenderImpl> extenders =
|
Pair<IPreviewExtenderImpl, IImageCaptureExtenderImpl> extenders =
|
||||||
initializeExtension(extension);
|
initializeExtension(extension);
|
||||||
extenders.second.onInit(mCameraId, mChars.getNativeMetadata());
|
extenders.second.onInit(mCameraId, mChars.getNativeMetadata());
|
||||||
extenders.second.init(mCameraId, mChars.getNativeMetadata());
|
extenders.second.init(mCameraId, mChars.getNativeMetadata());
|
||||||
CameraMetadataNative captureResultMeta =
|
captureResultMeta = extenders.second.getAvailableCaptureResultKeys();
|
||||||
extenders.second.getAvailableCaptureResultKeys();
|
extenders.second.onDeInit();
|
||||||
|
}
|
||||||
|
|
||||||
if (captureResultMeta != null) {
|
if (captureResultMeta != null) {
|
||||||
int[] resultKeys = captureResultMeta.get(
|
int[] resultKeys = captureResultMeta.get(
|
||||||
@@ -926,7 +942,6 @@ public final class CameraExtensionCharacteristics {
|
|||||||
ret.add(CaptureResult.SENSOR_TIMESTAMP);
|
ret.add(CaptureResult.SENSOR_TIMESTAMP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
extenders.second.onDeInit();
|
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new IllegalStateException("Failed to query the available capture result keys!");
|
throw new IllegalStateException("Failed to query the available capture result keys!");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -265,8 +265,8 @@ public abstract class CameraExtensionSession implements AutoCloseable {
|
|||||||
* from the camera device, to produce a single high-quality output result.
|
* from the camera device, to produce a single high-quality output result.
|
||||||
*
|
*
|
||||||
* <p>Note that single capture requests currently do not support
|
* <p>Note that single capture requests currently do not support
|
||||||
* client parameters except for {@link CaptureRequest#JPEG_ORIENTATION orientation} and
|
* client parameters except for controls advertised in
|
||||||
* {@link CaptureRequest#JPEG_QUALITY quality} in case of ImageFormat.JPEG output target.
|
* {@link CameraExtensionCharacteristics#getAvailableCaptureRequestKeys}.
|
||||||
* The rest of the settings included in the request will be entirely overridden by
|
* The rest of the settings included in the request will be entirely overridden by
|
||||||
* the device-specific extension. </p>
|
* the device-specific extension. </p>
|
||||||
*
|
*
|
||||||
@@ -275,6 +275,11 @@ public abstract class CameraExtensionSession implements AutoCloseable {
|
|||||||
* arguments that include further targets will cause
|
* arguments that include further targets will cause
|
||||||
* IllegalArgumentException to be thrown. </p>
|
* IllegalArgumentException to be thrown. </p>
|
||||||
*
|
*
|
||||||
|
* <p>Starting with Android {@link android.os.Build.VERSION_CODES#TIRAMISU} single capture
|
||||||
|
* requests will also support the preview {@link android.graphics.ImageFormat#PRIVATE} target
|
||||||
|
* surface. These can typically be used for enabling AF/AE triggers. Do note, that single
|
||||||
|
* capture requests referencing both output surfaces remain unsupported.</p>
|
||||||
|
*
|
||||||
* <p>Each request will produce one new frame for one target Surface, set
|
* <p>Each request will produce one new frame for one target Surface, set
|
||||||
* with the CaptureRequest builder's
|
* with the CaptureRequest builder's
|
||||||
* {@link CaptureRequest.Builder#addTarget} method.</p>
|
* {@link CaptureRequest.Builder#addTarget} method.</p>
|
||||||
@@ -319,8 +324,10 @@ public abstract class CameraExtensionSession implements AutoCloseable {
|
|||||||
* rate possible.</p>
|
* rate possible.</p>
|
||||||
*
|
*
|
||||||
* <p>Note that repeating capture requests currently do not support
|
* <p>Note that repeating capture requests currently do not support
|
||||||
* client parameters. Settings included in the request will
|
* client parameters except for controls advertised in
|
||||||
* be completely overridden by the device-specific extension.</p>
|
* {@link CameraExtensionCharacteristics#getAvailableCaptureRequestKeys}.
|
||||||
|
* The rest of the settings included in the request will be entirely overridden by
|
||||||
|
* the device-specific extension. </p>
|
||||||
*
|
*
|
||||||
* <p>The {@link CaptureRequest.Builder#addTarget} supports only one
|
* <p>The {@link CaptureRequest.Builder#addTarget} supports only one
|
||||||
* target surface. {@link CaptureRequest} arguments that include further
|
* target surface. {@link CaptureRequest} arguments that include further
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.hardware.camera2.extension.ISessionProcessorImpl;
|
|||||||
import android.hardware.camera2.extension.LatencyRange;
|
import android.hardware.camera2.extension.LatencyRange;
|
||||||
import android.hardware.camera2.extension.Size;
|
import android.hardware.camera2.extension.Size;
|
||||||
import android.hardware.camera2.extension.SizeList;
|
import android.hardware.camera2.extension.SizeList;
|
||||||
|
import android.hardware.camera2.impl.CameraMetadataNative;
|
||||||
|
|
||||||
/** @hide */
|
/** @hide */
|
||||||
interface IAdvancedExtenderImpl
|
interface IAdvancedExtenderImpl
|
||||||
@@ -30,4 +31,6 @@ interface IAdvancedExtenderImpl
|
|||||||
@nullable List<SizeList> getSupportedPreviewOutputResolutions(in String cameraId);
|
@nullable List<SizeList> getSupportedPreviewOutputResolutions(in String cameraId);
|
||||||
@nullable List<SizeList> getSupportedCaptureOutputResolutions(in String cameraId);
|
@nullable List<SizeList> getSupportedCaptureOutputResolutions(in String cameraId);
|
||||||
ISessionProcessorImpl getSessionProcessor();
|
ISessionProcessorImpl getSessionProcessor();
|
||||||
|
CameraMetadataNative getAvailableCaptureRequestKeys(in String cameraId);
|
||||||
|
CameraMetadataNative getAvailableCaptureResultKeys(in String cameraId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
package android.hardware.camera2.extension;
|
package android.hardware.camera2.extension;
|
||||||
|
|
||||||
import android.hardware.camera2.extension.Request;
|
import android.hardware.camera2.extension.Request;
|
||||||
|
import android.hardware.camera2.impl.CameraMetadataNative;
|
||||||
|
|
||||||
/** @hide */
|
/** @hide */
|
||||||
interface ICaptureCallback
|
interface ICaptureCallback
|
||||||
@@ -25,4 +26,5 @@ interface ICaptureCallback
|
|||||||
void onCaptureFailed(int captureSequenceId);
|
void onCaptureFailed(int captureSequenceId);
|
||||||
void onCaptureSequenceCompleted(int captureSequenceId);
|
void onCaptureSequenceCompleted(int captureSequenceId);
|
||||||
void onCaptureSequenceAborted(int captureSequenceId);
|
void onCaptureSequenceAborted(int captureSequenceId);
|
||||||
|
void onCaptureCompleted(long shutterTimestamp, int requestId, in CameraMetadataNative results);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package android.hardware.camera2.extension;
|
package android.hardware.camera2.extension;
|
||||||
|
|
||||||
|
import android.hardware.camera2.CaptureRequest;
|
||||||
import android.hardware.camera2.extension.CameraSessionConfig;
|
import android.hardware.camera2.extension.CameraSessionConfig;
|
||||||
import android.hardware.camera2.extension.ICaptureCallback;
|
import android.hardware.camera2.extension.ICaptureCallback;
|
||||||
import android.hardware.camera2.extension.IRequestProcessorImpl;
|
import android.hardware.camera2.extension.IRequestProcessorImpl;
|
||||||
@@ -30,5 +31,7 @@ interface ISessionProcessorImpl
|
|||||||
void onCaptureSessionEnd();
|
void onCaptureSessionEnd();
|
||||||
int startRepeating(in ICaptureCallback callback);
|
int startRepeating(in ICaptureCallback callback);
|
||||||
void stopRepeating();
|
void stopRepeating();
|
||||||
int startCapture(in ICaptureCallback callback, int jpegRotation, int jpegQuality);
|
int startCapture(in ICaptureCallback callback);
|
||||||
|
void setParameters(in CaptureRequest captureRequest);
|
||||||
|
int startTrigger(in CaptureRequest captureRequest, in ICaptureCallback callback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes
|
|||||||
// maps camera extension output ids to camera registered image readers
|
// maps camera extension output ids to camera registered image readers
|
||||||
private final HashMap<Integer, ImageReader> mReaderMap = new HashMap<>();
|
private final HashMap<Integer, ImageReader> mReaderMap = new HashMap<>();
|
||||||
private final RequestProcessor mRequestProcessor = new RequestProcessor();
|
private final RequestProcessor mRequestProcessor = new RequestProcessor();
|
||||||
|
private final int mSessionId;
|
||||||
|
|
||||||
private Surface mClientRepeatingRequestSurface;
|
private Surface mClientRepeatingRequestSurface;
|
||||||
private Surface mClientCaptureSurface;
|
private Surface mClientCaptureSurface;
|
||||||
@@ -175,7 +176,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes
|
|||||||
|
|
||||||
CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(clientId,
|
CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(clientId,
|
||||||
extender, cameraDevice, repeatingRequestSurface, burstCaptureSurface,
|
extender, cameraDevice, repeatingRequestSurface, burstCaptureSurface,
|
||||||
config.getStateCallback(), config.getExecutor());
|
config.getStateCallback(), config.getExecutor(), sessionId);
|
||||||
ret.initialize();
|
ret.initialize();
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@@ -184,7 +185,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes
|
|||||||
private CameraAdvancedExtensionSessionImpl(long extensionClientId,
|
private CameraAdvancedExtensionSessionImpl(long extensionClientId,
|
||||||
@NonNull IAdvancedExtenderImpl extender, @NonNull CameraDevice cameraDevice,
|
@NonNull IAdvancedExtenderImpl extender, @NonNull CameraDevice cameraDevice,
|
||||||
@Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface,
|
@Nullable Surface repeatingRequestSurface, @Nullable Surface burstCaptureSurface,
|
||||||
@NonNull CameraExtensionSession.StateCallback callback, @NonNull Executor executor) {
|
@NonNull CameraExtensionSession.StateCallback callback, @NonNull Executor executor,
|
||||||
|
int sessionId) {
|
||||||
mExtensionClientId = extensionClientId;
|
mExtensionClientId = extensionClientId;
|
||||||
mAdvancedExtender = extender;
|
mAdvancedExtender = extender;
|
||||||
mCameraDevice = cameraDevice;
|
mCameraDevice = cameraDevice;
|
||||||
@@ -197,6 +199,7 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes
|
|||||||
mHandler = new Handler(mHandlerThread.getLooper());
|
mHandler = new Handler(mHandlerThread.getLooper());
|
||||||
mInitialized = false;
|
mInitialized = false;
|
||||||
mInitializeHandler = new InitializeSessionHandler();
|
mInitializeHandler = new InitializeSessionHandler();
|
||||||
|
mSessionId = sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,6 +370,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
mSessionProcessor.setParameters(request);
|
||||||
|
|
||||||
seqId = mSessionProcessor.startRepeating(new RequestCallbackHandler(request,
|
seqId = mSessionProcessor.startRepeating(new RequestCallbackHandler(request,
|
||||||
executor, listener));
|
executor, listener));
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
@@ -388,34 +393,32 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes
|
|||||||
throw new IllegalStateException("Uninitialized component");
|
throw new IllegalStateException("Uninitialized component");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mClientCaptureSurface == null) {
|
if (request.getTargets().size() != 1) {
|
||||||
throw new IllegalArgumentException("No output surface registered for single"
|
throw new IllegalArgumentException("Single capture to both preview & still" +
|
||||||
+ " requests!");
|
" capture outputs target is not supported!");
|
||||||
}
|
|
||||||
|
|
||||||
if (!request.containsTarget(mClientCaptureSurface) ||
|
|
||||||
(request.getTargets().size() != 1)) {
|
|
||||||
throw new IllegalArgumentException("Invalid single capture output target!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((mClientCaptureSurface != null) && request.containsTarget(mClientCaptureSurface)) {
|
||||||
try {
|
try {
|
||||||
// This will override the extension capture stage jpeg parameters with the user set
|
mSessionProcessor.setParameters(request);
|
||||||
// jpeg quality and rotation. This will guarantee that client configured jpeg
|
|
||||||
// parameters always have highest priority.
|
|
||||||
Integer jpegRotation = request.get(CaptureRequest.JPEG_ORIENTATION);
|
|
||||||
if (jpegRotation == null) {
|
|
||||||
jpegRotation = CameraExtensionUtils.JPEG_DEFAULT_ROTATION;
|
|
||||||
}
|
|
||||||
Byte jpegQuality = request.get(CaptureRequest.JPEG_QUALITY);
|
|
||||||
if (jpegQuality == null) {
|
|
||||||
jpegQuality = CameraExtensionUtils.JPEG_DEFAULT_QUALITY;
|
|
||||||
}
|
|
||||||
|
|
||||||
seqId = mSessionProcessor.startCapture(new RequestCallbackHandler(request,
|
seqId = mSessionProcessor.startCapture(new RequestCallbackHandler(request,
|
||||||
executor, listener), jpegRotation, jpegQuality);
|
executor, listener));
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR,
|
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR, "Failed " +
|
||||||
"Failed to submit capture request, extension service failed to respond!");
|
" to submit capture request, extension service failed to respond!");
|
||||||
|
}
|
||||||
|
} else if ((mClientRepeatingRequestSurface != null) &&
|
||||||
|
request.containsTarget(mClientRepeatingRequestSurface)) {
|
||||||
|
try {
|
||||||
|
seqId = mSessionProcessor.startTrigger(request,
|
||||||
|
new RequestCallbackHandler(request, executor, listener));
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR, "Failed " +
|
||||||
|
" to submit trigger request, extension service failed to respond!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid single capture output target!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,6 +664,28 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes
|
|||||||
Binder.restoreCallingIdentity(ident);
|
Binder.restoreCallingIdentity(ident);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCaptureCompleted(long timestamp, int requestId, CameraMetadataNative result) {
|
||||||
|
if (result == null) {
|
||||||
|
Log.e(TAG,"Invalid capture result!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.set(CaptureResult.SENSOR_TIMESTAMP, timestamp);
|
||||||
|
TotalCaptureResult totalResult = new TotalCaptureResult(mCameraDevice.getId(), result,
|
||||||
|
mClientRequest, requestId, timestamp, new ArrayList<>(), mSessionId,
|
||||||
|
new PhysicalCaptureResultInfo[0]);
|
||||||
|
final long ident = Binder.clearCallingIdentity();
|
||||||
|
try {
|
||||||
|
mExecutor.execute(
|
||||||
|
() -> mClientCallbacks.onCaptureResultAvailable(
|
||||||
|
CameraAdvancedExtensionSessionImpl.this, mClientRequest,
|
||||||
|
totalResult));
|
||||||
|
} finally {
|
||||||
|
Binder.restoreCallingIdentity(ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class CaptureCallbackHandler extends CameraCaptureSession.CaptureCallback {
|
private final class CaptureCallbackHandler extends CameraCaptureSession.CaptureCallback {
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ public class CameraExtensionJpegProcessor implements ICaptureProcessorImpl {
|
|||||||
jpegBuffer, jpegCapacity, jpegParams.mQuality,
|
jpegBuffer, jpegCapacity, jpegParams.mQuality,
|
||||||
0, 0, yuvImage.getWidth(), yuvImage.getHeight(),
|
0, 0, yuvImage.getWidth(), yuvImage.getHeight(),
|
||||||
jpegParams.mRotation);
|
jpegParams.mRotation);
|
||||||
|
jpegImage.setTimestamp(yuvImage.getTimestamp());
|
||||||
yuvImage.close();
|
yuvImage.close();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -475,8 +475,8 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
mInternalRepeatingRequestEnabled = false;
|
mInternalRepeatingRequestEnabled = false;
|
||||||
try {
|
try {
|
||||||
return setRepeatingRequest(mPreviewExtender.getCaptureStage(),
|
return setRepeatingRequest(mPreviewExtender.getCaptureStage(),
|
||||||
new RepeatingRequestHandler(request, executor, listener,
|
new PreviewRequestHandler(request, executor, listener,
|
||||||
mRepeatingRequestImageCallback));
|
mRepeatingRequestImageCallback), request);
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
Log.e(TAG, "Failed to set repeating request! Extension service does not "
|
Log.e(TAG, "Failed to set repeating request! Extension service does not "
|
||||||
+ "respond");
|
+ "respond");
|
||||||
@@ -530,8 +530,10 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
CaptureRequest request = requestBuilder.build();
|
CaptureRequest request = requestBuilder.build();
|
||||||
CameraMetadataNative.update(request.getNativeMetadata(), captureStage.parameters);
|
CameraMetadataNative.update(request.getNativeMetadata(), captureStage.parameters);
|
||||||
ret.add(request);
|
ret.add(request);
|
||||||
|
if (captureMap != null) {
|
||||||
captureMap.put(request, captureStage.id);
|
captureMap.put(request, captureStage.id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -583,14 +585,13 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
throw new IllegalStateException("Uninitialized component");
|
throw new IllegalStateException("Uninitialized component");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mClientCaptureSurface == null) {
|
if (request.getTargets().size() != 1) {
|
||||||
throw new IllegalArgumentException("No output surface registered for single requests!");
|
throw new IllegalArgumentException("Single capture to both preview & still capture " +
|
||||||
}
|
"outputs target is not supported!");
|
||||||
|
|
||||||
if (!request.containsTarget(mClientCaptureSurface) || (request.getTargets().size() != 1)) {
|
|
||||||
throw new IllegalArgumentException("Invalid single capture output target!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int seqId = -1;
|
||||||
|
if ((mClientCaptureSurface != null) && request.containsTarget(mClientCaptureSurface)) {
|
||||||
HashMap<CaptureRequest, Integer> requestMap = new HashMap<>();
|
HashMap<CaptureRequest, Integer> requestMap = new HashMap<>();
|
||||||
List<CaptureRequest> burstRequest;
|
List<CaptureRequest> burstRequest;
|
||||||
try {
|
try {
|
||||||
@@ -603,13 +604,38 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
|
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
|
||||||
}
|
}
|
||||||
if (burstRequest == null) {
|
if (burstRequest == null) {
|
||||||
throw new UnsupportedOperationException("Failed to create still capture burst request");
|
throw new UnsupportedOperationException(
|
||||||
|
"Failed to create still capture burst request");
|
||||||
}
|
}
|
||||||
|
|
||||||
return mCaptureSession.captureBurstRequests(burstRequest,
|
seqId = mCaptureSession.captureBurstRequests(burstRequest,
|
||||||
new CameraExtensionUtils.HandlerExecutor(mHandler),
|
new CameraExtensionUtils.HandlerExecutor(mHandler),
|
||||||
new BurstRequestHandler(request, executor, listener, requestMap,
|
new BurstRequestHandler(request, executor, listener, requestMap,
|
||||||
mBurstCaptureImageCallback));
|
mBurstCaptureImageCallback));
|
||||||
|
} else if ((mClientRepeatingRequestSurface != null) &&
|
||||||
|
request.containsTarget(mClientRepeatingRequestSurface)) {
|
||||||
|
|
||||||
|
CaptureRequest captureRequest = null;
|
||||||
|
try {
|
||||||
|
ArrayList<CaptureStageImpl> captureStageList = new ArrayList<>();
|
||||||
|
captureStageList.add(mPreviewExtender.getCaptureStage());
|
||||||
|
|
||||||
|
captureRequest = createRequest(mCameraDevice, captureStageList,
|
||||||
|
mCameraRepeatingSurface, CameraDevice.TEMPLATE_PREVIEW, request);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(TAG, "Failed to initialize capture request! Extension service does"
|
||||||
|
+ " not respond!");
|
||||||
|
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
seqId = mCaptureSession.capture(captureRequest, new PreviewRequestHandler(request,
|
||||||
|
executor, listener, mRepeatingRequestImageCallback, true /*singleCapture*/),
|
||||||
|
mHandler);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Capture request to unknown output surface!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return seqId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -847,7 +873,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
setRepeatingRequest(mPreviewExtender.getCaptureStage(),
|
setRepeatingRequest(mPreviewExtender.getCaptureStage(),
|
||||||
new RepeatingRequestHandler(null, null, null,
|
new PreviewRequestHandler(null, null, null,
|
||||||
mRepeatingRequestImageCallback));
|
mRepeatingRequestImageCallback));
|
||||||
} catch (CameraAccessException | RemoteException e) {
|
} catch (CameraAccessException | RemoteException e) {
|
||||||
Log.e(TAG,
|
Log.e(TAG,
|
||||||
@@ -1010,7 +1036,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
if (timestamp != null) {
|
if (timestamp != null) {
|
||||||
if (mCaptureResultsSupported && (mCaptureResultHandler == null)) {
|
if (mCaptureResultsSupported && (mCaptureResultHandler == null)) {
|
||||||
mCaptureResultHandler = new CaptureResultHandler(mClientRequest, mExecutor,
|
mCaptureResultHandler = new CaptureResultHandler(mClientRequest, mExecutor,
|
||||||
mCallbacks, result.getSessionId());
|
mCallbacks, result.getSequenceId());
|
||||||
}
|
}
|
||||||
if (mImageProcessor != null) {
|
if (mImageProcessor != null) {
|
||||||
if (mCapturePendingMap.indexOfKey(timestamp) >= 0) {
|
if (mCapturePendingMap.indexOfKey(timestamp) >= 0) {
|
||||||
@@ -1179,7 +1205,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
setRepeatingRequest(mPreviewExtender.getCaptureStage(),
|
setRepeatingRequest(mPreviewExtender.getCaptureStage(),
|
||||||
new RepeatingRequestHandler(null, null, null,
|
new PreviewRequestHandler(null, null, null,
|
||||||
mImageCallback));
|
mImageCallback));
|
||||||
} catch (CameraAccessException | RemoteException e) {
|
} catch (CameraAccessException | RemoteException e) {
|
||||||
Log.e(TAG, "Failed to start the internal repeating request!");
|
Log.e(TAG, "Failed to start the internal repeating request!");
|
||||||
@@ -1320,17 +1346,20 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This handler can operate in two modes:
|
// This handler can operate in three modes:
|
||||||
// 1) Using valid client callbacks, which means camera buffers will be propagated the
|
// 1) Using valid client callbacks, which means camera buffers will be propagated the
|
||||||
// registered output surfaces and clients will be notified accordingly.
|
// registered output surfaces and clients will be notified accordingly.
|
||||||
// 2) Without any client callbacks where an internal repeating request is kept active
|
// 2) Without any client callbacks where an internal repeating request is kept active
|
||||||
// to satisfy the extensions continuous preview/(repeating request) requirement.
|
// to satisfy the extensions continuous preview/(repeating request) requirement.
|
||||||
private class RepeatingRequestHandler extends CameraCaptureSession.CaptureCallback {
|
// 3) Single capture mode, where internal repeating requests are ignored and the preview
|
||||||
|
// logic is only triggered for the image processor case.
|
||||||
|
private class PreviewRequestHandler extends CameraCaptureSession.CaptureCallback {
|
||||||
private final Executor mExecutor;
|
private final Executor mExecutor;
|
||||||
private final ExtensionCaptureCallback mCallbacks;
|
private final ExtensionCaptureCallback mCallbacks;
|
||||||
private final CaptureRequest mClientRequest;
|
private final CaptureRequest mClientRequest;
|
||||||
private final boolean mClientNotificationsEnabled;
|
private final boolean mClientNotificationsEnabled;
|
||||||
private final CameraOutputImageCallback mRepeatingImageCallback;
|
private final CameraOutputImageCallback mRepeatingImageCallback;
|
||||||
|
private final boolean mSingleCapture;
|
||||||
private OnImageAvailableListener mImageCallback = null;
|
private OnImageAvailableListener mImageCallback = null;
|
||||||
private LongSparseArray<Pair<Image, TotalCaptureResult>> mPendingResultMap =
|
private LongSparseArray<Pair<Image, TotalCaptureResult>> mPendingResultMap =
|
||||||
new LongSparseArray<>();
|
new LongSparseArray<>();
|
||||||
@@ -1338,15 +1367,22 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
|
|
||||||
private boolean mRequestUpdatedNeeded = false;
|
private boolean mRequestUpdatedNeeded = false;
|
||||||
|
|
||||||
public RepeatingRequestHandler(@Nullable CaptureRequest clientRequest,
|
public PreviewRequestHandler(@Nullable CaptureRequest clientRequest,
|
||||||
@Nullable Executor executor, @Nullable ExtensionCaptureCallback listener,
|
@Nullable Executor executor, @Nullable ExtensionCaptureCallback listener,
|
||||||
@NonNull CameraOutputImageCallback imageCallback) {
|
@NonNull CameraOutputImageCallback imageCallback) {
|
||||||
|
this(clientRequest, executor, listener, imageCallback, false /*singleCapture*/);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreviewRequestHandler(@Nullable CaptureRequest clientRequest,
|
||||||
|
@Nullable Executor executor, @Nullable ExtensionCaptureCallback listener,
|
||||||
|
@NonNull CameraOutputImageCallback imageCallback, boolean singleCapture) {
|
||||||
mClientRequest = clientRequest;
|
mClientRequest = clientRequest;
|
||||||
mExecutor = executor;
|
mExecutor = executor;
|
||||||
mCallbacks = listener;
|
mCallbacks = listener;
|
||||||
mClientNotificationsEnabled =
|
mClientNotificationsEnabled =
|
||||||
(mClientRequest != null) && (mExecutor != null) && (mCallbacks != null);
|
(mClientRequest != null) && (mExecutor != null) && (mCallbacks != null);
|
||||||
mRepeatingImageCallback = imageCallback;
|
mRepeatingImageCallback = imageCallback;
|
||||||
|
mSingleCapture = singleCapture;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1393,7 +1429,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
public void onCaptureSequenceAborted(@NonNull CameraCaptureSession session,
|
public void onCaptureSequenceAborted(@NonNull CameraCaptureSession session,
|
||||||
int sequenceId) {
|
int sequenceId) {
|
||||||
synchronized (mInterfaceLock) {
|
synchronized (mInterfaceLock) {
|
||||||
if (mInternalRepeatingRequestEnabled) {
|
if (mInternalRepeatingRequestEnabled && !mSingleCapture) {
|
||||||
resumeInternalRepeatingRequest(true);
|
resumeInternalRepeatingRequest(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1420,10 +1456,10 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
long frameNumber) {
|
long frameNumber) {
|
||||||
|
|
||||||
synchronized (mInterfaceLock) {
|
synchronized (mInterfaceLock) {
|
||||||
if (mRequestUpdatedNeeded) {
|
if (mRequestUpdatedNeeded && !mSingleCapture) {
|
||||||
mRequestUpdatedNeeded = false;
|
mRequestUpdatedNeeded = false;
|
||||||
resumeInternalRepeatingRequest(false);
|
resumeInternalRepeatingRequest(false);
|
||||||
} else if (mInternalRepeatingRequestEnabled) {
|
} else if (mInternalRepeatingRequestEnabled && !mSingleCapture) {
|
||||||
resumeInternalRepeatingRequest(true);
|
resumeInternalRepeatingRequest(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1471,10 +1507,10 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
if (mCaptureResultsSupported && mClientNotificationsEnabled &&
|
if (mCaptureResultsSupported && mClientNotificationsEnabled &&
|
||||||
(mCaptureResultHandler == null)) {
|
(mCaptureResultHandler == null)) {
|
||||||
mCaptureResultHandler = new CaptureResultHandler(mClientRequest, mExecutor,
|
mCaptureResultHandler = new CaptureResultHandler(mClientRequest, mExecutor,
|
||||||
mCallbacks, result.getSessionId());
|
mCallbacks, result.getSequenceId());
|
||||||
}
|
}
|
||||||
if (mPreviewProcessorType ==
|
if ((!mSingleCapture) && (mPreviewProcessorType ==
|
||||||
IPreviewExtenderImpl.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY) {
|
IPreviewExtenderImpl.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY)) {
|
||||||
CaptureStageImpl captureStage = null;
|
CaptureStageImpl captureStage = null;
|
||||||
try {
|
try {
|
||||||
captureStage = mPreviewRequestUpdateProcessor.process(
|
captureStage = mPreviewRequestUpdateProcessor.process(
|
||||||
@@ -1582,10 +1618,10 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession {
|
|||||||
try {
|
try {
|
||||||
if (internal) {
|
if (internal) {
|
||||||
setRepeatingRequest(mPreviewExtender.getCaptureStage(),
|
setRepeatingRequest(mPreviewExtender.getCaptureStage(),
|
||||||
new RepeatingRequestHandler(null, null, null,
|
new PreviewRequestHandler(null, null, null,
|
||||||
mRepeatingImageCallback));
|
mRepeatingImageCallback));
|
||||||
} else {
|
} else {
|
||||||
setRepeatingRequest(mPreviewExtender.getCaptureStage(), this);
|
setRepeatingRequest(mPreviewExtender.getCaptureStage(), this, mClientRequest);
|
||||||
}
|
}
|
||||||
} catch (RemoteException e) {
|
} catch (RemoteException e) {
|
||||||
Log.e(TAG, "Failed to resume internal repeating request, extension service"
|
Log.e(TAG, "Failed to resume internal repeating request, extension service"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import android.app.Service;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Camera;
|
||||||
import android.graphics.GraphicBuffer;
|
import android.graphics.GraphicBuffer;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.hardware.HardwareBuffer;
|
import android.hardware.HardwareBuffer;
|
||||||
@@ -752,13 +753,65 @@ public class CameraExtensionsProxyService extends Service {
|
|||||||
public ISessionProcessorImpl getSessionProcessor() {
|
public ISessionProcessorImpl getSessionProcessor() {
|
||||||
return new SessionProcessorImplStub(mAdvancedExtender.createSessionProcessor());
|
return new SessionProcessorImplStub(mAdvancedExtender.createSessionProcessor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CameraMetadataNative getAvailableCaptureRequestKeys(String cameraId) {
|
||||||
|
if (RESULT_API_SUPPORTED) {
|
||||||
|
List<CaptureRequest.Key> supportedCaptureKeys =
|
||||||
|
mAdvancedExtender.getAvailableCaptureRequestKeys();
|
||||||
|
|
||||||
|
if ((supportedCaptureKeys != null) && !supportedCaptureKeys.isEmpty()) {
|
||||||
|
CameraMetadataNative ret = new CameraMetadataNative();
|
||||||
|
long vendorId = mMetadataVendorIdMap.containsKey(cameraId) ?
|
||||||
|
mMetadataVendorIdMap.get(cameraId) : Long.MAX_VALUE;
|
||||||
|
ret.setVendorId(vendorId);
|
||||||
|
int requestKeyTags [] = new int[supportedCaptureKeys.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (CaptureRequest.Key key : supportedCaptureKeys) {
|
||||||
|
requestKeyTags[i++] = CameraMetadataNative.getTag(key.getName(), vendorId);
|
||||||
|
}
|
||||||
|
ret.set(CameraCharacteristics.REQUEST_AVAILABLE_REQUEST_KEYS, requestKeyTags);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CameraMetadataNative getAvailableCaptureResultKeys(String cameraId) {
|
||||||
|
if (RESULT_API_SUPPORTED) {
|
||||||
|
List<CaptureResult.Key> supportedResultKeys =
|
||||||
|
mAdvancedExtender.getAvailableCaptureResultKeys();
|
||||||
|
|
||||||
|
if ((supportedResultKeys != null) && !supportedResultKeys.isEmpty()) {
|
||||||
|
CameraMetadataNative ret = new CameraMetadataNative();
|
||||||
|
long vendorId = mMetadataVendorIdMap.containsKey(cameraId) ?
|
||||||
|
mMetadataVendorIdMap.get(cameraId) : Long.MAX_VALUE;
|
||||||
|
ret.setVendorId(vendorId);
|
||||||
|
int resultKeyTags [] = new int[supportedResultKeys.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (CaptureResult.Key key : supportedResultKeys) {
|
||||||
|
resultKeyTags[i++] = CameraMetadataNative.getTag(key.getName(), vendorId);
|
||||||
|
}
|
||||||
|
ret.set(CameraCharacteristics.REQUEST_AVAILABLE_RESULT_KEYS, resultKeyTags);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CaptureCallbackStub implements SessionProcessorImpl.CaptureCallback {
|
private class CaptureCallbackStub implements SessionProcessorImpl.CaptureCallback {
|
||||||
private final ICaptureCallback mCaptureCallback;
|
private final ICaptureCallback mCaptureCallback;
|
||||||
|
private final String mCameraId;
|
||||||
|
|
||||||
private CaptureCallbackStub(ICaptureCallback captureCallback) {
|
private CaptureCallbackStub(ICaptureCallback captureCallback, String cameraId) {
|
||||||
mCaptureCallback = captureCallback;
|
mCaptureCallback = captureCallback;
|
||||||
|
mCameraId = cameraId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -820,6 +873,29 @@ public class CameraExtensionsProxyService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCaptureCompleted(long timestamp, int requestId,
|
||||||
|
Map<CaptureResult.Key, Object> result) {
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
Log.e(TAG, "Invalid capture result received!");
|
||||||
|
}
|
||||||
|
|
||||||
|
CameraMetadataNative captureResults = new CameraMetadataNative();
|
||||||
|
if (mMetadataVendorIdMap.containsKey(mCameraId)) {
|
||||||
|
captureResults.setVendorId(mMetadataVendorIdMap.get(mCameraId));
|
||||||
|
}
|
||||||
|
for (Map.Entry<CaptureResult.Key, Object> entry : result.entrySet()) {
|
||||||
|
captureResults.set(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mCaptureCallback.onCaptureCompleted(timestamp, requestId, captureResults);
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.e(TAG, "Failed to notify capture complete due to remote exception!");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RequestCallbackStub extends IRequestCallback.Stub {
|
private class RequestCallbackStub extends IRequestCallback.Stub {
|
||||||
@@ -1124,7 +1200,7 @@ public class CameraExtensionsProxyService extends Service {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int startRepeating(ICaptureCallback callback) {
|
public int startRepeating(ICaptureCallback callback) {
|
||||||
return mSessionProcessor.startRepeating(new CaptureCallbackStub(callback));
|
return mSessionProcessor.startRepeating(new CaptureCallbackStub(callback, mCameraId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1133,12 +1209,29 @@ public class CameraExtensionsProxyService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int startCapture(ICaptureCallback callback, int jpegRotation, int jpegQuality) {
|
public void setParameters(CaptureRequest captureRequest) {
|
||||||
HashMap<CaptureRequest.Key<?>, Object> paramMap = new HashMap<>();
|
HashMap<CaptureRequest.Key<?>, Object> paramMap = new HashMap<>();
|
||||||
paramMap.put(CaptureRequest.JPEG_ORIENTATION, jpegRotation);
|
for (CaptureRequest.Key captureRequestKey : captureRequest.getKeys()) {
|
||||||
paramMap.put(CaptureRequest.JPEG_QUALITY, jpegQuality);
|
paramMap.put(captureRequestKey, captureRequest.get(captureRequestKey));
|
||||||
|
}
|
||||||
|
|
||||||
mSessionProcessor.setParameters(paramMap);
|
mSessionProcessor.setParameters(paramMap);
|
||||||
return mSessionProcessor.startCapture(new CaptureCallbackStub(callback));
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int startTrigger(CaptureRequest captureRequest, ICaptureCallback callback) {
|
||||||
|
HashMap<CaptureRequest.Key<?>, Object> triggerMap = new HashMap<>();
|
||||||
|
for (CaptureRequest.Key captureRequestKey : captureRequest.getKeys()) {
|
||||||
|
triggerMap.put(captureRequestKey, captureRequest.get(captureRequestKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mSessionProcessor.startTrigger(triggerMap,
|
||||||
|
new CaptureCallbackStub(callback, mCameraId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int startCapture(ICaptureCallback callback) {
|
||||||
|
return mSessionProcessor.startCapture(new CaptureCallbackStub(callback, mCameraId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user