diff --git a/.DS_Store b/.DS_Store index c94ca09..929b448 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/client/.DS_Store b/client/.DS_Store index fb9bb09..74cf6a7 100644 Binary files a/client/.DS_Store and b/client/.DS_Store differ diff --git a/client/app.go b/client/app.go index a642218..33623e7 100644 --- a/client/app.go +++ b/client/app.go @@ -25,6 +25,7 @@ import ( "time" "client/internal/blocker" + "client/internal/camera" "client/internal/guard" "client/internal/screen" "client/internal/winapi" @@ -124,6 +125,8 @@ type App struct { lastSyncTime time.Time isStreamingSc bool streamScStop chan struct{} + isStreamingCam bool + streamCamStop chan struct{} statusMsg string expectingLogin bool needsClearPortalStorage bool @@ -212,6 +215,13 @@ func (a *App) startup(ctx context.Context) { // Request Location Access (macOS) winapi.RequestLocationAccess() + winapi.RequestCameraAndMicAccess() + winapi.RequestScreenCaptureAccess() + + // Log initial permission statuses + log.Printf("[PERMISSIONS] Camera Status: %d (0=NotDetermined, 1=Restricted, 2=Denied, 3=Authorized)", winapi.GetCameraPermission()) + log.Printf("[PERMISSIONS] Microphone Status: %d (0=NotDetermined, 1=Restricted, 2=Denied, 3=Authorized)", winapi.GetMicrophonePermission()) + log.Printf("[PERMISSIONS] Screen Capture Status: %d (0=NoAccess, 1=Authorized, -1=NotSupported)", winapi.GetScreenCapturePermission()) // Register blocker callbacks blocker.Instance.OnBlocked = func(procName string, title string) { @@ -261,7 +271,7 @@ func (a *App) tearDownBeforeQuit() { blocker.Instance.Stop() a.stopScreenshotStream() - runtime.EventsEmit(a.ctx, "stop_webcam_stream") + a.stopWebcamStream() a.disconnectWS() guard.Stop() } @@ -1046,6 +1056,7 @@ func (a *App) fetchAllowedApps(classId int64) { } a.mu.Unlock() + log.Printf("[CLIENT] Allowed apps fetched from server: %s", res.Keywords) blocker.Instance.SetKeywords(res.Keywords) blocker.Instance.Start() } @@ -1141,9 +1152,9 @@ func (a *App) connectWS() { case "stop_screenshot_stream": a.stopScreenshotStream() case "start_webcam_stream": - runtime.EventsEmit(a.ctx, "start_webcam_stream") + a.startWebcamStream() case "stop_webcam_stream": - runtime.EventsEmit(a.ctx, "stop_webcam_stream") + a.stopWebcamStream() case "chat:message": senderRole, _ := msg.Data["senderRole"].(string) if senderRole == "staff" { @@ -1244,6 +1255,69 @@ func (a *App) stopScreenshotStream() { log.Println("[WS] Screenshot screen-streaming stopped.") } +func (a *App) startWebcamStream() { + a.mu.Lock() + if a.isStreamingCam { + a.mu.Unlock() + return + } + a.isStreamingCam = true + a.streamCamStop = make(chan struct{}) + a.mu.Unlock() + + if err := camera.StartCapture(); err != nil { + log.Printf("[WS] Failed to start native camera: %v", err) + a.mu.Lock() + a.isStreamingCam = false + a.mu.Unlock() + return + } + + go func() { + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + frame := camera.GetFrame() + if frame == "" { + continue + } + + a.mu.Lock() + conn := a.wsConn + a.mu.Unlock() + + if conn != nil { + _ = conn.WriteJSON(map[string]any{ + "event": "webcam_stream_frame", + "data": map[string]any{ + "imageBuffer": frame, + }, + }) + } + case <-a.streamCamStop: + return + } + } + }() + log.Println("[WS] Webcam native streaming started.") +} + +func (a *App) stopWebcamStream() { + a.mu.Lock() + defer a.mu.Unlock() + + if !a.isStreamingCam { + return + } + a.isStreamingCam = false + close(a.streamCamStop) + camera.StopCapture() + log.Println("[WS] Webcam native streaming stopped.") +} + func (a *App) alertChatIncoming(from, preview string) { if preview == "" { preview = "Bạn có tin nhắn mới" diff --git a/client/build/.DS_Store b/client/build/.DS_Store index 7b16691..ce26469 100644 Binary files a/client/build/.DS_Store and b/client/build/.DS_Store differ diff --git a/client/build/darwin/Info.dev.plist b/client/build/darwin/Info.dev.plist index 8496abb..abfbab4 100644 --- a/client/build/darwin/Info.dev.plist +++ b/client/build/darwin/Info.dev.plist @@ -66,5 +66,11 @@ NSLocationWhenInUseUsageDescription Ứng dụng cần quyền vị trí để xác thực mạng Wi-Fi phòng thi. + NSCameraUsageDescription + Ứng dụng cần quyền truy cập camera để giám sát thi và xác thực khuôn mặt sinh viên. + NSMicrophoneUsageDescription + Ứng dụng cần quyền truy cập microphone để giám sát âm thanh phòng thi. + NSScreenCaptureUsageDescription + Ứng dụng cần quyền chia sẻ màn hình để giám sát và ghi lại quá trình thi. diff --git a/client/build/darwin/Info.plist b/client/build/darwin/Info.plist index a4698d6..206712a 100644 --- a/client/build/darwin/Info.plist +++ b/client/build/darwin/Info.plist @@ -61,5 +61,12 @@ {{end}} NSLocationWhenInUseUsageDescription Ứng dụng cần quyền vị trí để xác thực mạng Wi-Fi phòng thi. + NSCameraUsageDescription + Ứng dụng cần quyền truy cập camera để giám sát thi và xác thực khuôn mặt sinh viên. + NSMicrophoneUsageDescription + Ứng dụng cần quyền truy cập microphone để giám sát âm thanh phòng thi. + NSScreenCaptureUsageDescription + Ứng dụng cần quyền chia sẻ màn hình để giám sát và ghi lại quá trình thi. + diff --git a/client/internal/blocker/blocker_darwin.go b/client/internal/blocker/blocker_darwin.go index 6d64af8..754c841 100644 --- a/client/internal/blocker/blocker_darwin.go +++ b/client/internal/blocker/blocker_darwin.go @@ -40,13 +40,27 @@ var systemAllowed = map[string]bool{ "simple_care_v1.0": true, } -func getVisibleProcesses() (map[uint32]string, error) { +type ProcessInfo struct { + Name string + BundleID string +} + +func getVisibleProcesses() (map[uint32]ProcessInfo, error) { script := `tell application "System Events" - set nameList to name of every process whose visible is true - set pidList to unix id of every process whose visible is true set out to "" - repeat with i from 1 to count of nameList - set out to out & item i of nameList & ":" & item i of pidList & "\n" + set procList to every process whose visible is true + repeat with p in procList + try + set nameStr to name of p + set pidVal to unix id of p + set bid to bundle identifier of p + if bid is missing value then + set bid to "" + end if + set out to out & nameStr & "|" & pidVal & "|" & bid & "\n" + on error + -- ignore + end try end repeat return out end tell` @@ -55,22 +69,29 @@ end tell` if err != nil { return nil, err } - procs := make(map[uint32]string) + procs := make(map[uint32]ProcessInfo) lines := strings.Split(string(out), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } - idx := strings.LastIndex(line, ":") - if idx == -1 { + parts := strings.Split(line, "|") + if len(parts) < 2 { continue } - pName := line[:idx] - pIdStr := line[idx+1:] + pName := parts[0] + pIdStr := parts[1] + bundleID := "" + if len(parts) >= 3 { + bundleID = parts[2] + } var pid uint32 if _, err := fmt.Sscanf(pIdStr, "%d", &pid); err == nil { - procs[pid] = pName + procs[pid] = ProcessInfo{ + Name: pName, + BundleID: bundleID, + } } } return procs, nil @@ -98,11 +119,13 @@ func (b *Blocker) checkAndKill() { } myPid := uint32(os.Getpid()) - for pid, name := range procs { - pNameLower := strings.ToLower(name) + for pid, info := range procs { + pNameLower := strings.ToLower(info.Name) + bundleIDLower := strings.ToLower(info.BundleID) - // 1. Always allow our app, system/critical developer tools, or agent helpers - if pid == myPid || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] || strings.Contains(pNameLower, "antigravity") { + // 1. Always allow our app, system/critical developer tools, or agent helpers (and Antigravity IDE) + isAntigravity := strings.Contains(pNameLower, "antigravity") || strings.Contains(bundleIDLower, "antigravity") + if pid == myPid || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] || isAntigravity { continue } @@ -118,14 +141,14 @@ func (b *Blocker) checkAndKill() { // 3. If not allowed, kill the application if !allowed { if b.OnBlocked != nil { - b.OnBlocked(name, name) + b.OnBlocked(info.Name, info.Name) } - log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d)", name, pid) + log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d)", info.Name, pid) proc, err := os.FindProcess(int(pid)) if err == nil { errKill := proc.Kill() if errKill == nil && b.OnKill != nil { - b.OnKill(name, name) + b.OnKill(info.Name, info.Name) } } } diff --git a/client/internal/camera/camera_darwin.go b/client/internal/camera/camera_darwin.go new file mode 100644 index 0000000..b4fc4b7 --- /dev/null +++ b/client/internal/camera/camera_darwin.go @@ -0,0 +1,46 @@ +//go:build darwin + +package camera + +/* +#cgo LDFLAGS: -framework AVFoundation -framework Foundation -framework CoreImage -framework CoreMedia -framework CoreVideo -framework AppKit +#include + +int StartNativeCamera(void); +void StopNativeCamera(void); +char* GetLatestCameraFrame(void); +*/ +import "C" +import ( + "fmt" + "log" + "unsafe" +) + +// StartCapture starts native camera capture via AVCaptureSession +func StartCapture() error { + ret := C.StartNativeCamera() + if ret != 0 { + log.Println("[CAMERA] Failed to start native camera capture") + return fmt.Errorf("failed to start camera (code %d)", int(ret)) + } + log.Println("[CAMERA] Native camera capture started successfully") + return nil +} + +// StopCapture stops native camera capture +func StopCapture() { + C.StopNativeCamera() + log.Println("[CAMERA] Native camera capture stopped") +} + +// GetFrame returns the latest camera frame as a data:image/jpeg;base64,... string +// Returns empty string if no frame is available +func GetFrame() string { + cstr := C.GetLatestCameraFrame() + if cstr == nil { + return "" + } + defer C.free(unsafe.Pointer(cstr)) + return C.GoString(cstr) +} diff --git a/client/internal/camera/camera_darwin.m b/client/internal/camera/camera_darwin.m new file mode 100644 index 0000000..dc9c0de --- /dev/null +++ b/client/internal/camera/camera_darwin.m @@ -0,0 +1,166 @@ +#import +#import +#import +#import +#include +#include + +// ── Shared state ────────────────────────────────────────────── +static AVCaptureSession *captureSession = nil; +static AVCaptureVideoDataOutput *videoOutput = nil; +static dispatch_queue_t captureQueue = nil; + +// Latest frame stored as JPEG base64 +static NSString *latestFrameBase64 = nil; +static NSLock *frameLock = nil; + +// ── Delegate that receives sample buffers ───────────────────── +@interface CameraFrameDelegate : NSObject +@end + +@implementation CameraFrameDelegate + +- (void)captureOutput:(AVCaptureOutput *)output + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection { + + CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + if (!imageBuffer) return; + + CIImage *ciImage = [CIImage imageWithCVPixelBuffer:imageBuffer]; + CIContext *context = [CIContext contextWithOptions:nil]; + CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent]; + if (!cgImage) return; + + // Convert to JPEG NSData (quality ≈ 0.4) + NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage]; + CGImageRelease(cgImage); + + NSDictionary *props = @{NSImageCompressionFactor: @(0.4)}; + NSData *jpegData = [rep representationUsingType:NSBitmapImageFileTypeJPEG properties:props]; + if (!jpegData) return; + + NSString *b64 = [jpegData base64EncodedStringWithOptions:0]; + NSString *dataUrl = [NSString stringWithFormat:@"data:image/jpeg;base64,%@", b64]; + + [frameLock lock]; + latestFrameBase64 = dataUrl; + [frameLock unlock]; +} + +@end + +static CameraFrameDelegate *frameDelegate = nil; + +// ── C-exported functions ────────────────────────────────────── + +// StartNativeCamera: returns 0 on success, -1 on failure +int StartNativeCamera(void) { + if (captureSession && captureSession.isRunning) { + return 0; // already running + } + + if (!frameLock) { + frameLock = [[NSLock alloc] init]; + } + + // Check camera permission + if (@available(macOS 10.14, *)) { + AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + if (status == AVAuthorizationStatusDenied || status == AVAuthorizationStatusRestricted) { + NSLog(@"[CAMERA] Camera permission denied (status=%ld)", (long)status); + return -1; + } + if (status == AVAuthorizationStatusNotDetermined) { + // Request permission synchronously via semaphore + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + __block BOOL granted = NO; + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL g) { + granted = g; + dispatch_semaphore_signal(sem); + }]; + dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); + if (!granted) { + NSLog(@"[CAMERA] Camera permission not granted"); + return -1; + } + } + } + + captureSession = [[AVCaptureSession alloc] init]; + captureSession.sessionPreset = AVCaptureSessionPresetLow; // 320×240-ish + + // Find default video device + AVCaptureDevice *camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; + if (!camera) { + NSLog(@"[CAMERA] No camera device found"); + captureSession = nil; + return -1; + } + + NSError *error = nil; + AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&error]; + if (error || !input) { + NSLog(@"[CAMERA] Cannot create camera input: %@", error); + captureSession = nil; + return -1; + } + + if ([captureSession canAddInput:input]) { + [captureSession addInput:input]; + } else { + NSLog(@"[CAMERA] Cannot add camera input to session"); + captureSession = nil; + return -1; + } + + // Video data output + videoOutput = [[AVCaptureVideoDataOutput alloc] init]; + videoOutput.videoSettings = @{ + (NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA) + }; + videoOutput.alwaysDiscardsLateVideoFrames = YES; + + captureQueue = dispatch_queue_create("com.simplecare.camera", DISPATCH_QUEUE_SERIAL); + frameDelegate = [[CameraFrameDelegate alloc] init]; + [videoOutput setSampleBufferDelegate:frameDelegate queue:captureQueue]; + + if ([captureSession canAddOutput:videoOutput]) { + [captureSession addOutput:videoOutput]; + } else { + NSLog(@"[CAMERA] Cannot add video output to session"); + captureSession = nil; + return -1; + } + + [captureSession startRunning]; + NSLog(@"[CAMERA] Native camera capture started"); + return 0; +} + +void StopNativeCamera(void) { + if (captureSession && captureSession.isRunning) { + [captureSession stopRunning]; + NSLog(@"[CAMERA] Native camera capture stopped"); + } + captureSession = nil; + videoOutput = nil; + frameDelegate = nil; + + [frameLock lock]; + latestFrameBase64 = nil; + [frameLock unlock]; +} + +// GetLatestCameraFrame: returns a C-string (caller must free) or NULL +char* GetLatestCameraFrame(void) { + [frameLock lock]; + NSString *frame = latestFrameBase64; + latestFrameBase64 = nil; // consume + [frameLock unlock]; + + if (!frame || frame.length == 0) { + return NULL; + } + return strdup([frame UTF8String]); +} diff --git a/client/internal/camera/camera_other.go b/client/internal/camera/camera_other.go new file mode 100644 index 0000000..104b975 --- /dev/null +++ b/client/internal/camera/camera_other.go @@ -0,0 +1,20 @@ +//go:build !darwin + +package camera + +import "log" + +// StartCapture is a no-op on non-darwin platforms (Windows uses different webcam API) +func StartCapture() error { + log.Println("[CAMERA] Native camera capture not supported on this platform") + return nil +} + +// StopCapture is a no-op on non-darwin platforms +func StopCapture() { +} + +// GetFrame always returns empty on non-darwin platforms +func GetFrame() string { + return "" +} diff --git a/client/internal/screen/screen_darwin.go b/client/internal/screen/screen_darwin.go new file mode 100644 index 0000000..fb064d4 --- /dev/null +++ b/client/internal/screen/screen_darwin.go @@ -0,0 +1,105 @@ +//go:build darwin + +package screen + +/* +#cgo CFLAGS: -x objective-c -Wno-deprecated-declarations -Wno-unguarded-availability-new +#cgo LDFLAGS: -framework CoreGraphics -framework Foundation -framework AppKit + +// Disable availability checks - we handle this at runtime +#define __API_UNAVAILABLE(...) +#define API_UNAVAILABLE(...) + +#import +#import +#include +#include +#include + +// Use dlsym to call CGWindowListCreateImage dynamically to bypass macOS 15 availability check +typedef CGImageRef (*CGWindowListCreateImageFunc)(CGRect, CGWindowListOption, CGWindowID, CGWindowImageOption); + +static unsigned char* CaptureFullDesktop(int* outLen, int quality) { + // Dynamically load CGWindowListCreateImage to bypass compile-time availability check + static CGWindowListCreateImageFunc createImageFunc = NULL; + if (!createImageFunc) { + void *handle = dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", RTLD_LAZY); + if (handle) { + createImageFunc = (CGWindowListCreateImageFunc)dlsym(handle, "CGWindowListCreateImage"); + } + } + + if (!createImageFunc) { + *outLen = 0; + return NULL; + } + + CGImageRef image = createImageFunc( + CGRectInfinite, + kCGWindowListOptionOnScreenOnly, + kCGNullWindowID, + kCGWindowImageDefault + ); + + if (!image) { + *outLen = 0; + return NULL; + } + + @autoreleasepool { + NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:image]; + CGImageRelease(image); + + if (!rep) { + *outLen = 0; + return NULL; + } + + float q = (float)quality / 100.0f; + if (q < 0.1f) q = 0.1f; + if (q > 1.0f) q = 1.0f; + + NSDictionary *props = @{NSImageCompressionFactor: @(q)}; + NSData *jpegData = [rep representationUsingType:NSBitmapImageFileTypeJPEG properties:props]; + + if (!jpegData || jpegData.length == 0) { + *outLen = 0; + return NULL; + } + + *outLen = (int)jpegData.length; + unsigned char *buf = (unsigned char*)malloc(jpegData.length); + memcpy(buf, jpegData.bytes, jpegData.length); + return buf; + } +} +*/ +import "C" +import ( + "encoding/base64" + "fmt" + "log" + "unsafe" +) + +// CaptureScreen captures the full desktop on macOS using CGWindowListCreateImage (via dlsym) +// Returns a data:image/jpeg;base64,... string +func CaptureScreen() (string, error) { + var outLen C.int + quality := C.int(50) // JPEG quality 50% + + buf := C.CaptureFullDesktop(&outLen, quality) + if buf == nil || int(outLen) == 0 { + return "", fmt.Errorf("failed to capture screen: CGWindowListCreateImage returned nil") + } + defer C.free(unsafe.Pointer(buf)) + + jpegBytes := C.GoBytes(unsafe.Pointer(buf), outLen) + + if len(jpegBytes) < 100 { + log.Printf("[SCREEN] Warning: captured image is very small (%d bytes), may indicate permission issue", len(jpegBytes)) + } + + encoded := base64.StdEncoding.EncodeToString(jpegBytes) + return "data:image/jpeg;base64," + encoded, nil +} diff --git a/client/internal/screen/screen.go b/client/internal/screen/screen_other.go similarity index 93% rename from client/internal/screen/screen.go rename to client/internal/screen/screen_other.go index c98f9ca..a0f52bb 100644 --- a/client/internal/screen/screen.go +++ b/client/internal/screen/screen_other.go @@ -1,3 +1,5 @@ +//go:build !darwin + package screen import ( @@ -10,6 +12,7 @@ import ( ) // CaptureScreen chụp màn hình chính và trả về chuỗi Base64 dạng "data:image/jpeg;base64,..." +// Non-darwin: dùng kbinani/screenshot func CaptureScreen() (string, error) { n := screenshot.NumActiveDisplays() if n <= 0 { diff --git a/client/internal/winapi/wifi_darwin.go b/client/internal/winapi/wifi_darwin.go index 21cea90..3db68a6 100644 --- a/client/internal/winapi/wifi_darwin.go +++ b/client/internal/winapi/wifi_darwin.go @@ -3,7 +3,7 @@ package winapi /* -#cgo LDFLAGS: -framework CoreWLAN -framework CoreLocation -framework Foundation +#cgo LDFLAGS: -framework CoreWLAN -framework CoreLocation -framework Foundation -framework AVFoundation -framework CoreGraphics #include typedef struct { @@ -12,7 +12,12 @@ typedef struct { } CWifiInfo; void RequestLocationPermission(); +void RequestCameraAndMicPermission(); +void RequestScreenCapturePermission(); CWifiInfo GetCurrentWifiInfo(); +int GetCameraPermissionStatus(); +int GetMicrophonePermissionStatus(); +int GetScreenCapturePermissionStatus(); */ import "C" import ( @@ -24,6 +29,31 @@ func RequestLocationAccess() { C.RequestLocationPermission() } +// RequestCameraAndMicAccess requests Camera and Microphone permissions on macOS +func RequestCameraAndMicAccess() { + C.RequestCameraAndMicPermission() +} + +// RequestScreenCaptureAccess requests Screen Capture permission on macOS +func RequestScreenCaptureAccess() { + C.RequestScreenCapturePermission() +} + +// GetCameraPermission status on macOS +func GetCameraPermission() int { + return int(C.GetCameraPermissionStatus()) +} + +// GetMicrophonePermission status on macOS +func GetMicrophonePermission() int { + return int(C.GetMicrophonePermissionStatus()) +} + +// GetScreenCapturePermission status on macOS +func GetScreenCapturePermission() int { + return int(C.GetScreenCapturePermissionStatus()) +} + // GetWifiConnection đọc SSID và BSSID từ CoreWLAN (macOS) func GetWifiConnection() WifiConnection { info := C.GetCurrentWifiInfo() diff --git a/client/internal/winapi/wifi_darwin.m b/client/internal/winapi/wifi_darwin.m index e9f8901..0bea050 100644 --- a/client/internal/winapi/wifi_darwin.m +++ b/client/internal/winapi/wifi_darwin.m @@ -1,5 +1,7 @@ #import #import +#import +#import #import static CLLocationManager *locationManager = nil; @@ -15,6 +17,60 @@ void RequestLocationPermission() { }); } +void RequestCameraAndMicPermission() { + dispatch_async(dispatch_get_main_queue(), ^{ + if (@available(macOS 10.14, *)) { + // Request Camera + AVAuthorizationStatus cameraStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + if (cameraStatus == AVAuthorizationStatusNotDetermined) { + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { + // Camera permission requested + }]; + } + + // Request Microphone + AVAuthorizationStatus micStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; + if (micStatus == AVAuthorizationStatusNotDetermined) { + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { + // Mic permission requested + }]; + } + } + }); +} + +void RequestScreenCapturePermission() { + dispatch_async(dispatch_get_main_queue(), ^{ + if (@available(macOS 11.0, *)) { + BOOL hasAccess = CGPreflightScreenCaptureAccess(); + if (!hasAccess) { + CGRequestScreenCaptureAccess(); + } + } + }); +} + +int GetCameraPermissionStatus() { + if (@available(macOS 10.14, *)) { + return (int)[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + } + return -1; +} + +int GetMicrophonePermissionStatus() { + if (@available(macOS 10.14, *)) { + return (int)[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; + } + return -1; +} + +int GetScreenCapturePermissionStatus() { + if (@available(macOS 11.0, *)) { + return CGPreflightScreenCaptureAccess() ? 1 : 0; + } + return -1; +} + typedef struct { char* ssid; char* bssid; diff --git a/client/internal/winapi/wifi_other.go b/client/internal/winapi/wifi_other.go index f823f62..61dde62 100644 --- a/client/internal/winapi/wifi_other.go +++ b/client/internal/winapi/wifi_other.go @@ -9,3 +9,18 @@ func GetWifiConnection() WifiConnection { // RequestLocationAccess requests Location permission (stub for other systems) func RequestLocationAccess() {} + +// RequestCameraAndMicAccess stub +func RequestCameraAndMicAccess() {} + +// RequestScreenCaptureAccess stub +func RequestScreenCaptureAccess() {} + +// GetCameraPermission stub +func GetCameraPermission() int { return -1 } + +// GetMicrophonePermission stub +func GetMicrophonePermission() int { return -1 } + +// GetScreenCapturePermission stub +func GetScreenCapturePermission() int { return -1 } diff --git a/client/internal/winapi/wifi_windows.go b/client/internal/winapi/wifi_windows.go index 4400976..6f738f9 100644 --- a/client/internal/winapi/wifi_windows.go +++ b/client/internal/winapi/wifi_windows.go @@ -41,3 +41,18 @@ func GetWifiConnection() WifiConnection { // RequestLocationAccess requests Location permission (stub for Windows) func RequestLocationAccess() {} + +// RequestCameraAndMicAccess stub +func RequestCameraAndMicAccess() {} + +// RequestScreenCaptureAccess stub +func RequestScreenCaptureAccess() {} + +// GetCameraPermission stub +func GetCameraPermission() int { return -1 } + +// GetMicrophonePermission stub +func GetMicrophonePermission() int { return -1 } + +// GetScreenCapturePermission stub +func GetScreenCapturePermission() int { return -1 }