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 }