fixed UI scanner iOS

This commit is contained in:
dranik
2026-05-08 21:35:08 +03:00
parent bb56008c3d
commit b7e2847393
18 changed files with 1542 additions and 306 deletions
@@ -12,4 +12,5 @@ QRect QRCodeReader::cameraSize() {
void QRCodeReader::startReading() {}
void QRCodeReader::stopReading() {}
void QRCodeReader::setCameraSize(QRect) {}
void QRCodeReader::setTorchEnabled(bool) {}
void QRCodeReader::notifyCodeRead(const QString &) {}
+1
View File
@@ -16,6 +16,7 @@ public slots:
void startReading();
void stopReading();
void setCameraSize(QRect value);
void setTorchEnabled(bool on);
/// Called from AVFoundation delegate on the main queue; emits codeReaded.
void notifyCodeRead(const QString &code);
+276 -42
View File
@@ -1,19 +1,109 @@
#if !MACOS_NE
#include "QRCodeReaderBase.h"
#include "platforms/ios/iosPairingCameraAccess.h"
#include <QByteArray>
#include <QDebug>
#include <QThread>
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
static NSString *amneziaQrThreadTag(void)
{
if ([NSThread isMainThread]) {
return @"main";
}
return [NSString stringWithFormat:@"bg:%p", (void *)[NSThread currentThread]];
}
static void amneziaQrLogDeviceAuth(void)
{
AVAuthorizationStatus st = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
NSString *stName = @"unknown";
switch (st) {
case AVAuthorizationStatusNotDetermined:
stName = @"notDetermined";
break;
case AVAuthorizationStatusRestricted:
stName = @"restricted";
break;
case AVAuthorizationStatusDenied:
stName = @"denied";
break;
case AVAuthorizationStatusAuthorized:
stName = @"authorized";
break;
default:
break;
}
NSLog(@"[QRCodeReader] camera auth status=%@ (%ld)", stName, (long)st);
}
static UIWindow *amneziaKeyWindowForQrCamera(void)
{
UIApplication *app = [UIApplication sharedApplication];
NSMutableArray<NSString *> *trace = [NSMutableArray array];
if (@available(iOS 13.0, *)) {
NSInteger sceneCount = app.connectedScenes.count;
[trace addObject:[NSString stringWithFormat:@"connectedScenes=%ld", (long)sceneCount]];
for (UIScene *scene in app.connectedScenes) {
if (scene.activationState != UISceneActivationStateForegroundActive) {
continue;
}
if (![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
UIWindowScene *windowScene = (UIWindowScene *)scene;
NSInteger winN = windowScene.windows.count;
[trace addObject:[NSString stringWithFormat:@"foreground UIWindowScene windows=%ld", (long)winN]];
for (UIWindow *window in windowScene.windows) {
if (window.isKeyWindow) {
NSLog(@"[QRCodeReader] keyWindow pick: scene keyWindow=%@ bounds=%@", window, NSStringFromCGRect(window.bounds));
return window;
}
}
for (UIWindow *window in windowScene.windows) {
if (!window.isHidden) {
NSLog(@"[QRCodeReader] keyWindow pick: scene nonHidden=%@ bounds=%@", window, NSStringFromCGRect(window.bounds));
return window;
}
}
}
}
if (app.keyWindow) {
[trace addObject:@"app.keyWindow"];
NSLog(@"[QRCodeReader] keyWindow pick: application.keyWindow=%@ bounds=%@",
app.keyWindow, NSStringFromCGRect(app.keyWindow.bounds));
return app.keyWindow;
}
for (UIWindow *window in app.windows) {
if (window.isKeyWindow) {
NSLog(@"[QRCodeReader] keyWindow pick: windows scan key=%@ bounds=%@", window, NSStringFromCGRect(window.bounds));
return window;
}
}
UIWindow *first = app.windows.firstObject;
if (first) {
NSLog(@"[QRCodeReader] keyWindow pick: firstObject=%@ bounds=%@ trace=[%@]",
first, NSStringFromCGRect(first.bounds), [trace componentsJoinedByString:@", "]);
return first;
}
NSLog(@"[QRCodeReader] keyWindow pick: NONE trace=[%@]", [trace componentsJoinedByString:@", "]);
return nil;
}
@interface QRCodeReaderImpl : UIViewController
@end
@interface QRCodeReaderImpl () <AVCaptureMetadataOutputObjectsDelegate>
@property (nonatomic) QRCodeReader* qrCodeReader;
@property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
@property (nonatomic, assign) QRCodeReader *qrCodeReader;
@property (nonatomic, retain) AVCaptureSession *captureSession;
@property (nonatomic, retain) AVCaptureVideoPreviewLayer *videoPreviewPlayer;
@property (nonatomic, retain) AVCaptureDevice *activeCaptureDevice;
@property (nonatomic) dispatch_queue_t sessionQueue;
@end
@@ -23,88 +113,223 @@
- (void)viewDidLoad {
[super viewDidLoad];
_captureSession = nil;
self.captureSession = nil;
if (!_sessionQueue) {
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
}
}
- (void)setQrCodeReader: (QRCodeReader*)value {
- (void)setQrCodeReader:(QRCodeReader *)value {
_qrCodeReader = value;
}
- (BOOL)startReading {
[self stopReading];
- (AVCaptureDevice *)resolvedCaptureDevice {
if (self.activeCaptureDevice) {
return self.activeCaptureDevice;
}
AVCaptureSession *session = self.captureSession;
if (!session) {
return nil;
}
for (AVCaptureInput *input in session.inputs) {
if ([input isKindOfClass:[AVCaptureDeviceInput class]]) {
AVCaptureDevice *d = ((AVCaptureDeviceInput *)input).device;
if (d) {
NSLog(@"[QRCodeReader] resolvedCaptureDevice from session input device=%p", d);
return d;
}
}
}
return nil;
}
- (void)applyTorchOnMainThread:(BOOL)on {
AVCaptureDevice *device = [self resolvedCaptureDevice];
if (!device) {
if (on) {
NSLog(@"[QRCodeReader] torch ON failed: no device (active=%p session=%p inputs=%lu)",
self.activeCaptureDevice,
self.captureSession,
(unsigned long)(self.captureSession ? self.captureSession.inputs.count : 0));
}
return;
}
if (![device hasTorch]) {
NSLog(@"[QRCodeReader] torch: device %p has no torch", device);
return;
}
AVCaptureSession *session = self.captureSession;
if (on && session && ![session isRunning]) {
NSLog(@"[QRCodeReader] torch: session not running yet; retry in 0.25s (session=%p)", session);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (on) {
[self applyTorchOnMainThread:YES];
}
});
return;
}
NSError *err = nil;
if (![device lockForConfiguration:&err]) {
NSLog(@"[QRCodeReader] torch lock failed: %@", err.localizedDescription);
return;
}
if (on) {
err = nil;
if (![device setTorchModeOnWithLevel:AVCaptureMaxAvailableTorchLevel error:&err]) {
NSLog(@"[QRCodeReader] setTorchModeOnWithLevel failed: %@ — trying torchMode", err.localizedDescription);
if ([device isTorchModeSupported:AVCaptureTorchModeOn]) {
device.torchMode = AVCaptureTorchModeOn;
}
} else {
NSLog(@"[QRCodeReader] torch ON ok level=maxAvailable");
}
} else {
device.torchMode = AVCaptureTorchModeOff;
}
[device unlockForConfiguration];
}
- (void)applyTorch:(BOOL)on {
NSLog(@"[QRCodeReader] applyTorch requested on=%d thread=%@", (int)on, amneziaQrThreadTag());
if ([NSThread isMainThread]) {
[self applyTorchOnMainThread:on];
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self applyTorchOnMainThread:on];
});
}
}
- (BOOL)startReadingOnMainThread {
NSLog(@"[QRCodeReader] startReadingOnMainThread begin thread=%@", amneziaQrThreadTag());
amneziaQrLogDeviceAuth();
[self stopReadingOnMainThread];
NSError *error = nil;
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice: captureDevice error: &error];
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if (!captureDevice) {
NSLog(@"[QRCodeReader] defaultDeviceWithMediaType:Video is nil");
return NO;
}
NSLog(@"[QRCodeReader] capture device=%p localizedName=%@", captureDevice, captureDevice.localizedName);
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
if (!deviceInput) {
NSLog(@"[QRCodeReader] deviceInput failed: %@", error.localizedDescription);
return NO;
}
_captureSession = [[AVCaptureSession alloc]init];
[_captureSession addInput:deviceInput];
self.activeCaptureDevice = captureDevice;
NSLog(@"[QRCodeReader] activeCaptureDevice set to %p", self.activeCaptureDevice);
AVCaptureSession *session = [[AVCaptureSession alloc] init];
[session addInput:deviceInput];
AVCaptureMetadataOutput *capturedMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
[_captureSession addOutput:capturedMetadataOutput];
[session addOutput:capturedMetadataOutput];
if (!_sessionQueue) {
_sessionQueue = dispatch_queue_create("org.amnezia.qr.session", DISPATCH_QUEUE_SERIAL);
}
[capturedMetadataOutput setMetadataObjectsDelegate: self queue: _sessionQueue];
[capturedMetadataOutput setMetadataObjectTypes: [NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
[capturedMetadataOutput setMetadataObjectsDelegate:self queue:_sessionQueue];
[capturedMetadataOutput setMetadataObjectTypes:[NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];
_videoPreviewPlayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession: _captureSession];
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
self.captureSession = session;
[session release];
QRect cameraRect = _qrCodeReader->cameraSize();
CGRect cameraCGRect = CGRectMake(cameraRect.x(),
cameraRect.y() + statusBarHeight,
cameraRect.width(),
cameraRect.height());
AVCaptureVideoPreviewLayer *preview = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
[preview setVideoGravity:AVLayerVideoGravityResizeAspectFill];
self.videoPreviewPlayer = preview;
[preview release];
[_videoPreviewPlayer setVideoGravity: AVLayerVideoGravityResizeAspectFill];
[_videoPreviewPlayer setFrame: cameraCGRect];
UIWindow *keyWindow = amneziaKeyWindowForQrCamera();
if (!keyWindow) {
NSLog(@"[QRCodeReader] startReading: no keyWindow (UIKit must run on main)");
[self stopReadingOnMainThread];
return NO;
}
CALayer* layer = [UIApplication sharedApplication].keyWindow.layer;
[layer addSublayer: _videoPreviewPlayer];
CGRect bounds = keyWindow.bounds;
[self.videoPreviewPlayer setFrame:bounds];
self.videoPreviewPlayer.zPosition = -1000.f;
[keyWindow.layer insertSublayer:self.videoPreviewPlayer atIndex:0];
amneziaIosPairingRelayoutChromeIfNeeded();
NSLog(@"[QRCodeReader] previewLayer inserted window=%@ layer.sublayers.count=%lu bounds=%@",
keyWindow, (unsigned long)keyWindow.layer.sublayers.count, NSStringFromCGRect(bounds));
AVCaptureSession *session = _captureSession;
AVCaptureSession *runningSession = self.captureSession;
dispatch_async(_sessionQueue, ^{
[session startRunning];
NSLog(@"[QRCodeReader] session startRunning on session queue session=%p", runningSession);
[runningSession startRunning];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"[QRCodeReader] after startRunning isRunning=%d", (int)runningSession.isRunning);
});
});
NSLog(@"[QRCodeReader] startReading OK frame=(%.1f,%.1f,%.1f,%.1f) statusBar=%.1f",
cameraCGRect.origin.x, cameraCGRect.origin.y, cameraCGRect.size.width, cameraCGRect.size.height, statusBarHeight);
NSLog(@"[QRCodeReader] startReading OK activeDevice=%p window bounds=%@",
self.activeCaptureDevice, NSStringFromCGRect(bounds));
return YES;
}
- (void)stopReading {
if (_captureSession) {
AVCaptureSession *session = _captureSession;
- (BOOL)startReading {
NSLog(@"[QRCodeReader] startReading entry thread=%@ qt=%p", amneziaQrThreadTag(), (void *)QThread::currentThread());
if ([NSThread isMainThread]) {
return [self startReadingOnMainThread];
}
__block BOOL ok = NO;
dispatch_sync(dispatch_get_main_queue(), ^{
ok = [self startReadingOnMainThread];
});
NSLog(@"[QRCodeReader] startReading exit ok=%d (dispatched to main)", (int)ok);
return ok;
}
- (void)stopReadingOnMainThread {
NSLog(@"[QRCodeReader] stopReadingOnMainThread thread=%@", amneziaQrThreadTag());
[self applyTorchOnMainThread:NO];
self.activeCaptureDevice = nil;
if (self.captureSession) {
AVCaptureSession *session = self.captureSession;
dispatch_async(_sessionQueue, ^{
NSLog(@"[QRCodeReader] session stopRunning session=%p", session);
[session stopRunning];
});
_captureSession = nil;
self.captureSession = nil;
}
if (_videoPreviewPlayer) {
[_videoPreviewPlayer removeFromSuperlayer];
_videoPreviewPlayer = nil;
if (self.videoPreviewPlayer) {
NSLog(@"[QRCodeReader] remove preview from superlayer");
[self.videoPreviewPlayer removeFromSuperlayer];
self.videoPreviewPlayer = nil;
}
}
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
- (void)stopReading {
NSLog(@"[QRCodeReader] stopReading entry thread=%@ qt=%p", amneziaQrThreadTag(), (void *)QThread::currentThread());
if ([NSThread isMainThread]) {
[self stopReadingOnMainThread];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self stopReadingOnMainThread];
});
}
NSLog(@"[QRCodeReader] stopReading exit");
}
- (void)captureOutput:(AVCaptureOutput *)output
didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects
fromConnection:(AVCaptureConnection *)connection {
if (metadataObjects != nil && metadataObjects.count > 0) {
AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0];
if ([[metadataObject type] isEqualToString: AVMetadataObjectTypeQRCode]) {
if ([[metadataObject type] isEqualToString:AVMetadataObjectTypeQRCode]) {
NSString *value = [metadataObject stringValue];
if (value.length == 0) {
return;
@@ -123,7 +348,7 @@
QRCodeReader::QRCodeReader() {
m_qrCodeReader = [[QRCodeReaderImpl alloc] init];
[m_qrCodeReader setQrCodeReader: this];
[m_qrCodeReader setQrCodeReader:this];
}
QRect QRCodeReader::cameraSize() {
@@ -136,20 +361,28 @@ void QRCodeReader::setCameraSize(QRect value) {
}
void QRCodeReader::startReading() {
qInfo() << "[QRCodeReader] C++ startReading thread" << QThread::currentThread();
const BOOL ok = [m_qrCodeReader startReading];
if (!ok) {
qWarning() << "[QRCodeReader] startReading failed (see NSLogs)";
qWarning() << "[QRCodeReader] C++ startReading failed (see NSLogs)";
} else {
qInfo() << "[QRCodeReader] C++ startReading ok";
}
}
void QRCodeReader::stopReading() {
qInfo() << "[QRCodeReader] C++ stopReading thread" << QThread::currentThread();
[m_qrCodeReader stopReading];
qInfo() << "[QRCodeReader] stopReading";
}
void QRCodeReader::notifyCodeRead(const QString &code) {
emit codeReaded(code);
}
void QRCodeReader::setTorchEnabled(bool on) {
qInfo() << "[QRCodeReader] C++ setTorchEnabled" << on << "thread" << QThread::currentThread();
[(QRCodeReaderImpl *)m_qrCodeReader applyTorch:on ? YES : NO];
}
#else
#include "QRCodeReaderBase.h"
@@ -165,5 +398,6 @@ QRect QRCodeReader::cameraSize() {
void QRCodeReader::startReading() {}
void QRCodeReader::stopReading() {}
void QRCodeReader::setCameraSize(QRect) {}
void QRCodeReader::setTorchEnabled(bool) {}
void QRCodeReader::notifyCodeRead(const QString &) {}
#endif
@@ -7,4 +7,16 @@ bool amneziaIosPairingCameraAccessGranted();
void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDone);
void amneziaIosOpenApplicationSettings();
/** When true, makes Qt's root UIView non-opaque so window-layer camera preview shows through transparent QML. */
void amneziaIosApplyEmbeddedCameraUnderlayToQtView(bool enable);
/**
* Extra height (points) added to the bottom native dim strip above UIKit safe-area bottom, when the Qt root view
* fills the host (tab bar lives in QML). Typically set from QML scanDimBleedBottom. Pass 0 to clear.
*/
void amneziaIosSetPairingEmbeddedCameraNativeBottomExtraPt(int extraPt);
/** Call after AVCaptureVideoPreviewLayer is inserted on UIWindow so the window-layer mask stacks above it. */
void amneziaIosPairingRelayoutChromeIfNeeded(void);
#endif
@@ -3,6 +3,266 @@
#import <AVFoundation/AVFoundation.h>
#import <UIKit/UIKit.h>
static UIViewController *amneziaKeyWindowViewController(void)
{
UIApplication *app = [UIApplication sharedApplication];
if (@available(iOS 13.0, *)) {
for (UIScene *scene in app.connectedScenes) {
if (scene.activationState != UISceneActivationStateForegroundActive) {
continue;
}
if (![scene isKindOfClass:[UIWindowScene class]]) {
continue;
}
UIWindowScene *windowScene = (UIWindowScene *)scene;
for (UIWindow *window in windowScene.windows) {
if (window.isKeyWindow && window.rootViewController) {
return window.rootViewController;
}
}
for (UIWindow *window in windowScene.windows) {
if (!window.isHidden && window.rootViewController) {
return window.rootViewController;
}
}
}
}
for (UIWindow *window in app.windows) {
if (window.isKeyWindow && window.rootViewController) {
return window.rootViewController;
}
}
for (UIWindow *window in app.windows) {
if (window.rootViewController) {
return window.rootViewController;
}
}
return nil;
}
/** QML window is shorter than UIKit UIWindow (e.g. 759 vs 852); camera preview covers full window — dim strips in safe areas. */
static UIView *s_pairingSafeTopDim = nil;
static UIView *s_pairingSafeBottomDim = nil;
static id s_pairingSafeDimOrientationToken = nil;
/** Qt root for layout: bottom strip fills from maxY to host bottom when Qt view is shorter than host. */
static __unsafe_unretained UIView *s_pairingDimQtRoot = nil;
/** QML-driven tab band (e.g. scanDimBleedBottom) added to safe bottom when Qt root is full height. */
static int s_pairingNativeBottomExtraPt = 0;
/** Opaque strip on UIWindow.layer above AVCaptureVideoPreviewLayer — preview can composite above UIDropShadowView subviews. */
static CALayer *s_pairingWindowBottomMaskLayer = nil;
static UIColor *amneziaPairingBottomChromeOpaqueColor(void)
{
return [UIColor colorWithRed:(CGFloat)(28.0 / 255.0) green:(CGFloat)(29.0 / 255.0) blue:(CGFloat)(33.0 / 255.0) alpha:1.0f];
}
static CALayer *amneziaFindVideoPreviewLayerInWindow(UIWindow *window)
{
if (!window) {
return nil;
}
for (CALayer *ly in window.layer.sublayers) {
if ([ly isKindOfClass:[AVCaptureVideoPreviewLayer class]]) {
return ly;
}
}
return nil;
}
static void amneziaRemovePairingWindowBottomMaskLayer(void)
{
[s_pairingWindowBottomMaskLayer removeFromSuperlayer];
s_pairingWindowBottomMaskLayer = nil;
}
/** Maps bottom strip from host (UIDropShadowView) into window coords and stacks above camera preview on window.layer. */
static void amneziaSyncPairingWindowBottomMaskLayer(UIWindow *window, UIView *host, CGFloat bottomY, CGFloat bottomH, CGFloat width)
{
if (!window || !host || bottomH < 0.5f) {
amneziaRemovePairingWindowBottomMaskLayer();
return;
}
CGRect hostRect = CGRectMake(0, bottomY, width, bottomH);
CGRect winRect = [window convertRect:hostRect fromView:host];
if (!s_pairingWindowBottomMaskLayer) {
s_pairingWindowBottomMaskLayer = [[CALayer alloc] init];
s_pairingWindowBottomMaskLayer.backgroundColor = amneziaPairingBottomChromeOpaqueColor().CGColor;
}
s_pairingWindowBottomMaskLayer.frame = winRect;
s_pairingWindowBottomMaskLayer.zPosition = -500.f;
CALayer *preview = amneziaFindVideoPreviewLayerInWindow(window);
if (preview) {
[window.layer insertSublayer:s_pairingWindowBottomMaskLayer above:preview];
} else if (s_pairingWindowBottomMaskLayer.superlayer) {
[s_pairingWindowBottomMaskLayer removeFromSuperlayer];
}
}
static void amneziaRemovePairingSafeAreaDimStrips(void)
{
if (s_pairingSafeDimOrientationToken) {
[[NSNotificationCenter defaultCenter] removeObserver:s_pairingSafeDimOrientationToken];
s_pairingSafeDimOrientationToken = nil;
}
[s_pairingSafeTopDim removeFromSuperview];
[s_pairingSafeBottomDim removeFromSuperview];
s_pairingSafeTopDim = nil;
s_pairingSafeBottomDim = nil;
s_pairingDimQtRoot = nil;
amneziaRemovePairingWindowBottomMaskLayer();
}
static void amneziaLayoutPairingSafeAreaDimStrips(void)
{
if (!s_pairingSafeTopDim || !s_pairingSafeTopDim.superview) {
return;
}
UIWindow *w = s_pairingSafeTopDim.window;
UIView *host = s_pairingSafeTopDim.superview;
if (!w || !host) {
return;
}
UIEdgeInsets insets = w.safeAreaInsets;
CGRect hb = host.bounds;
s_pairingSafeTopDim.frame = CGRectMake(0, 0, hb.size.width, insets.top);
CGFloat bottomY = hb.size.height - insets.bottom;
CGFloat bottomH = insets.bottom;
UIView *qt = s_pairingDimQtRoot;
CGFloat qMaxYForLog = hb.size.height;
if (qt && qt.superview) {
CGRect qInHost = [host convertRect:qt.bounds fromView:qt];
const CGFloat qMaxY = CGRectGetMaxY(qInHost);
qMaxYForLog = qMaxY;
if (qMaxY < hb.size.height - 0.5f) {
bottomY = qMaxY;
bottomH = hb.size.height - qMaxY;
} else if (s_pairingNativeBottomExtraPt > 0) {
bottomH = insets.bottom + (CGFloat)s_pairingNativeBottomExtraPt;
bottomY = hb.size.height - bottomH;
}
}
if (bottomH < 0.5f) {
bottomH = 0.f;
bottomY = hb.size.height;
} else {
/** Pull strip slightly past layout bounds — QML vs UIKit gap can leave a hairline of preview at the physical bottom. */
const CGFloat kBottomOverscanPt = 12.f;
bottomH += kBottomOverscanPt;
bottomY = hb.size.height - bottomH;
if (bottomY < insets.top + 2.f) {
bottomY = insets.top + 2.f;
bottomH = hb.size.height - bottomY;
}
/** Stack↔tab hairline: preview on UIWindow.layer can leak slightly above computed strip top vs Qt chrome. */
const CGFloat kHairlineCoverUpPt = 4.f;
bottomY -= kHairlineCoverUpPt;
bottomH += kHairlineCoverUpPt;
if (bottomY < insets.top + 2.f) {
bottomH -= (insets.top + 2.f) - bottomY;
bottomY = insets.top + 2.f;
}
}
s_pairingSafeBottomDim.frame = CGRectMake(0, bottomY, hb.size.width, bottomH);
amneziaSyncPairingWindowBottomMaskLayer(w, host, bottomY, bottomH, hb.size.width);
CGRect winStrip = [w convertRect:s_pairingSafeBottomDim.frame fromView:host];
CALayer *previewLy = amneziaFindVideoPreviewLayerInWindow(w);
CGRect previewWin = CGRectZero;
if (previewLy) {
previewWin = [w.layer convertRect:previewLy.bounds fromLayer:previewLy];
}
CGRect maskWin = s_pairingWindowBottomMaskLayer ? s_pairingWindowBottomMaskLayer.frame : CGRectZero;
CGRect qtWin = CGRectZero;
if (qt) {
qtWin = [w convertRect:qt.bounds fromView:qt];
}
NSLog(@"[PairingCamera] safeAreaDim bottom strip y=%.1f h=%.1f qtMaxY=%.1f hostH=%.1f safeBottom=%.1f extraPt=%d winMask=%d",
bottomY, bottomH, qMaxYForLog, hb.size.height, insets.bottom, s_pairingNativeBottomExtraPt,
(int)(s_pairingWindowBottomMaskLayer.superlayer != nil));
NSLog(@"[PairingCamera] geom winStrip={{%.1f,%.1f},{%.1f,%.1f}} qtWin={{%.1f,%.1f},{%.1f,%.1f}} previewWin={{%.1f,%.1f},{%.1f,%.1f}} "
@"maskWin={{%.1f,%.1f},{%.1f,%.1f}} dyStripTopMinusPreviewTop=%.1f dyStripTopMinusQtMaxY=%.1f",
winStrip.origin.x, winStrip.origin.y, winStrip.size.width, winStrip.size.height, qtWin.origin.x, qtWin.origin.y,
qtWin.size.width, qtWin.size.height, previewWin.origin.x, previewWin.origin.y, previewWin.size.width,
previewWin.size.height, maskWin.origin.x, maskWin.origin.y, maskWin.size.width, maskWin.size.height,
CGRectGetMinY(winStrip) - CGRectGetMinY(previewWin), CGRectGetMinY(winStrip) - CGRectGetMaxY(qtWin));
}
static void amneziaInstallPairingSafeAreaDimStrips(UIWindow *window, UIView *qtRootView)
{
amneziaRemovePairingSafeAreaDimStrips();
if (!window || !qtRootView) {
return;
}
UIView *host = qtRootView.superview ?: window;
if (!host) {
return;
}
UIEdgeInsets insets = window.safeAreaInsets;
CGRect hb = host.bounds;
UIColor *dim = [UIColor colorWithWhite:0 alpha:0.55f];
UIColor *bottomChromeOpaque = amneziaPairingBottomChromeOpaqueColor();
s_pairingSafeTopDim = [[UIView alloc] initWithFrame:CGRectMake(0, 0, hb.size.width, insets.top)];
s_pairingSafeTopDim.backgroundColor = dim;
s_pairingSafeTopDim.opaque = NO;
s_pairingSafeTopDim.layer.opaque = NO;
s_pairingSafeTopDim.userInteractionEnabled = NO;
s_pairingSafeTopDim.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin;
s_pairingSafeBottomDim = [[UIView alloc] initWithFrame:CGRectMake(0, hb.size.height - insets.bottom, hb.size.width, insets.bottom)];
s_pairingSafeBottomDim.backgroundColor = bottomChromeOpaque;
s_pairingSafeBottomDim.opaque = YES;
s_pairingSafeBottomDim.layer.opaque = YES;
s_pairingSafeBottomDim.userInteractionEnabled = NO;
s_pairingSafeBottomDim.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
s_pairingDimQtRoot = qtRootView;
[host insertSubview:s_pairingSafeTopDim belowSubview:qtRootView];
[host insertSubview:s_pairingSafeBottomDim belowSubview:qtRootView];
NSLog(@"[PairingCamera] safeAreaDim host=%@ top=%.1f bottomInset=%.1f qtFrame=%@ hostBounds=%@ window=%@",
NSStringFromClass(host.class), insets.top, insets.bottom, NSStringFromCGRect(qtRootView.frame), NSStringFromCGRect(hb),
NSStringFromCGRect(window.bounds));
s_pairingSafeDimOrientationToken = [[NSNotificationCenter defaultCenter]
addObserverForName:UIDeviceOrientationDidChangeNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(__unused NSNotification *note) {
dispatch_async(dispatch_get_main_queue(), ^{
amneziaLayoutPairingSafeAreaDimStrips();
});
}];
}
static void amneziaApplyUnderlayTransparencyToView(UIView *view, BOOL transparent, NSUInteger depth, NSUInteger maxDepth)
{
if (!view || depth > maxDepth) {
return;
}
if (transparent) {
view.opaque = NO;
view.backgroundColor = [UIColor clearColor];
view.layer.opaque = NO;
} else {
view.opaque = YES;
view.backgroundColor = [UIColor blackColor];
view.layer.opaque = YES;
}
if (depth < maxDepth) {
for (UIView *child in view.subviews) {
amneziaApplyUnderlayTransparencyToView(child, transparent, depth + 1, maxDepth);
}
}
}
bool amneziaIosPairingCameraAccessGranted()
{
const AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
@@ -35,3 +295,64 @@ void amneziaIosOpenApplicationSettings()
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
}
void amneziaIosPairingRelayoutChromeIfNeeded(void)
{
if (!s_pairingSafeBottomDim || !s_pairingSafeBottomDim.superview) {
return;
}
amneziaLayoutPairingSafeAreaDimStrips();
}
void amneziaIosSetPairingEmbeddedCameraNativeBottomExtraPt(int extraPt)
{
const int v = extraPt < 0 ? 0 : extraPt;
if (s_pairingNativeBottomExtraPt == v) {
return;
}
s_pairingNativeBottomExtraPt = v;
void (^relayout)(void) = ^{
amneziaLayoutPairingSafeAreaDimStrips();
};
if ([NSThread isMainThread]) {
relayout();
} else {
dispatch_async(dispatch_get_main_queue(), relayout);
}
}
void amneziaIosApplyEmbeddedCameraUnderlayToQtView(bool enable)
{
void (^work)(void) = ^{
UIViewController *vc = amneziaKeyWindowViewController();
if (!vc || !vc.view) {
NSLog(@"[PairingCamera] amneziaIosApplyEmbeddedCameraUnderlayToQtView: no root VC (enable=%d)", (int)enable);
return;
}
UIView *root = vc.view;
UIWindow *win = root.window;
if (!enable) {
s_pairingNativeBottomExtraPt = 0;
amneziaRemovePairingSafeAreaDimStrips();
}
if (enable) {
vc.edgesForExtendedLayout = UIRectEdgeAll;
if (@available(iOS 11.0, *)) {
vc.extendedLayoutIncludesOpaqueBars = YES;
}
}
const NSUInteger kMaxDepth = 6;
amneziaApplyUnderlayTransparencyToView(root, enable ? YES : NO, 0, kMaxDepth);
if (enable && win) {
amneziaInstallPairingSafeAreaDimStrips(win, root);
amneziaLayoutPairingSafeAreaDimStrips();
}
NSLog(@"[PairingCamera] Qt view underlay transparency %@ subviews=%lu",
enable ? @"ON" : @"OFF", (unsigned long)root.subviews.count);
};
if ([NSThread isMainThread]) {
work();
} else {
dispatch_sync(dispatch_get_main_queue(), work);
}
}
@@ -11,3 +11,9 @@ void amneziaIosRequestPairingCameraAccess(const std::function<void(bool)> &onDon
}
void amneziaIosOpenApplicationSettings() {}
void amneziaIosApplyEmbeddedCameraUnderlayToQtView(bool) {}
void amneziaIosSetPairingEmbeddedCameraNativeBottomExtraPt(int) {}
void amneziaIosPairingRelayoutChromeIfNeeded(void) {}