mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-23 02:00:20 +07:00
fixed UI scanner iOS
This commit is contained in:
@@ -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 &) {}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
Reference in New Issue
Block a user