diff --git a/packages/internet_detector.vm/internet_detector.vm.nuspec b/packages/internet_detector.vm/internet_detector.vm.nuspec new file mode 100644 index 000000000..6eaa8461f --- /dev/null +++ b/packages/internet_detector.vm/internet_detector.vm.nuspec @@ -0,0 +1,13 @@ + + + + internet_detector.vm + 1.0.0 + Elliot Chernofsky and Ana Martinez Gomez + Tool that changes the background and a taskbar icon if it detects internet connectivity + + + + + + diff --git a/packages/internet_detector.vm/tools/chocolateyinstall.ps1 b/packages/internet_detector.vm/tools/chocolateyinstall.ps1 new file mode 100644 index 000000000..2669c5175 --- /dev/null +++ b/packages/internet_detector.vm/tools/chocolateyinstall.ps1 @@ -0,0 +1,30 @@ +$ErrorActionPreference = 'Stop' +Import-Module vm.common -Force -DisableNameChecking + +$toolName = 'internet_detector' +$category = 'Utilities' + +# Install dependency for windows api +VM-Pip-Install "pywin32" + +# Create tool directory +$toolDir = Join-Path ${Env:RAW_TOOLS_DIR} $toolName +New-Item -Path $toolDir -ItemType Directory -Force -ea 0 +VM-Assert-Path $toolDir + +$packageToolDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" + +# Move images to %VM_COMMON_DIR% directory +$imagesPath = Join-Path $packageToolDir "Images" +Move-Item "$imagesPath\*" ${Env:VM_COMMON_DIR} -Force + +# Get path to pythonw for executing Python without terminal popup +$executablePath = (Get-Command pythonw).Source +$filePath = Join-Path $packageToolDir "$toolName.pyw" + +VM-Install-Shortcut $toolName $category $executablePath -arguments $filePath + +# Create scheduled task for tool to run every 2 minutes. +$action = New-ScheduledTaskAction -Execute $executablePath -Argument "$filePath" +$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 2) +Register-ScheduledTask -Action $action -Trigger $trigger -TaskName 'Internet Detector' -Force diff --git a/packages/internet_detector.vm/tools/chocolateyuninstall.ps1 b/packages/internet_detector.vm/tools/chocolateyuninstall.ps1 new file mode 100644 index 000000000..94da3c136 --- /dev/null +++ b/packages/internet_detector.vm/tools/chocolateyuninstall.ps1 @@ -0,0 +1,8 @@ +$ErrorActionPreference = 'Continue' +Import-Module vm.common -Force -DisableNameChecking + +$toolName = 'internet_detector' +$category = 'Utilities' + +VM-Uninstall $toolName $category +Unregister-ScheduledTask -TaskName 'Internet Detector' -Confirm:$false diff --git a/packages/internet_detector.vm/tools/images/background-internet.png b/packages/internet_detector.vm/tools/images/background-internet.png new file mode 100644 index 000000000..b380cb483 Binary files /dev/null and b/packages/internet_detector.vm/tools/images/background-internet.png differ diff --git a/packages/internet_detector.vm/tools/images/indicator_off.ico b/packages/internet_detector.vm/tools/images/indicator_off.ico new file mode 100644 index 000000000..c58bc6871 Binary files /dev/null and b/packages/internet_detector.vm/tools/images/indicator_off.ico differ diff --git a/packages/internet_detector.vm/tools/images/indicator_on.ico b/packages/internet_detector.vm/tools/images/indicator_on.ico new file mode 100644 index 000000000..abf03f0e4 Binary files /dev/null and b/packages/internet_detector.vm/tools/images/indicator_on.ico differ diff --git a/packages/internet_detector.vm/tools/internet_detector.pyw b/packages/internet_detector.vm/tools/internet_detector.pyw new file mode 100644 index 000000000..569ffcd05 --- /dev/null +++ b/packages/internet_detector.vm/tools/internet_detector.pyw @@ -0,0 +1,394 @@ +# This tool is intended to check if internet connectivity exists by reaching out to specific websites and checking if they return expected values and +# display the current state via changes to the background, theme, and icon in the taskbar. +# * It will work even with a tool like FakeNet running (provided it is running in default configuration) +# If internet is detected, it will: +# 1. Change the background to a new background called "background-dynamic.png". +# 2. Change the the taskbar to be transparent and Rose colored and the theme to dark mode. +# 3. Show an icon called "indicator_on.ico" in the taskbar. +# If internet is not detected, it will: +# 1. Change the background to a new background called "background.png". +# 2. Change the taskbar to be transparent and keep the current theme/color (dark or light). +# 3. Show an icon called "indicator_off.ico" in the taskbar. +import threading +import requests +import win32api +import win32gui +import win32con +import urllib3 +import winreg +import signal +import ctypes +import time +import os +import re + + +# Define constants +CHECK_INTERVAL = 2 # Seconds +CONNECT_TEST_URL_AND_RESPONSES = { + "https://www.msftconnecttest.com/connecttest.txt": "Microsoft Connect Test", # HTTPS Test #1 + "http://www.google.com": "Google", # HTTP Test + "https://www.wikipedia.com": "Wikipedia", # HTTPS Test #2 + "https://www.youtube.com": "YouTube", # HTTPS Test #3 +} +SPI_SETDESKWALLPAPER = 20 +SPIF_UPDATEINIFILE = 0x01 +SPIF_SENDWININICHANGE = 0x02 +WM_DWMCOLORIZATIONCOLORCHANGED = 0x0320 +ROSE_COLOR = "#C30052" +ROSE_ACCENT_PALETTE = "FFABCE00FF7FB400F74A9200C30052008C003A0069002C004D002000567C7300" +ICON_INDICATOR_ON = os.path.join(os.environ.get("VM_COMMON_DIR"), "indicator_on.ico") +ICON_INDICATOR_OFF = os.path.join(os.environ.get("VM_COMMON_DIR"), "indicator_off.ico") +DEFAULT_BACKGROUND = os.path.join(os.environ.get("VM_COMMON_DIR"), "background.png") +INTERNET_BACKGROUND = os.path.join(os.environ.get("VM_COMMON_DIR"), "background-internet.png") + +# Global variables +tray_icon = None +stop_event = threading.Event() # To signal the background thread to stop +hwnd = None # We'll assign the window handle here later +check_thread = None +tray_icon_thread = None +default_transparency = None +default_color = None +default_palette = None +# Win32 API icon handles +hicon_indicator_off = None +hicon_indicator_on = None + + +def signal_handler(sig, frame): + global check_thread, tray_icon_thread, tray_icon + print("Ctrl+C detected. Exiting...") + stop_event.set() # Signal the background thread to stop + if check_thread: + check_thread.join() + if tray_icon_thread: + tray_icon_thread.join() + if tray_icon: + del tray_icon + exit(0) + + +def load_icon(icon_path): + try: + return win32gui.LoadImage(None, icon_path, win32con.IMAGE_ICON, 0, 0, win32con.LR_LOADFROMFILE) + except Exception as e: + print(f"Error loading indicator icon: {e}") + return None + + +class SysTrayIcon: + def __init__(self, hwnd, icon, tooltip): + self.hwnd = hwnd + self.icon = icon + self.tooltip = tooltip + # System tray icon data structure + self.nid = ( + self.hwnd, + 0, + win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, + win32con.WM_USER + 20, + self.icon, + self.tooltip, + ) + # Add the icon to the system tray + win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self.nid) + + def set_tooltip(self, new_tooltip): + self.tooltip = new_tooltip + self.nid = ( + self.hwnd, + 0, + win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, + win32con.WM_USER + 20, + self.icon, + self.tooltip, + ) + win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self.nid) + + def set_icon(self, icon): + self.icon = icon + self.nid = ( + self.hwnd, + 0, + win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, + win32con.WM_USER + 20, + self.icon, + self.tooltip, + ) + win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self.nid) + + def __del__(self): + # Remove the icon from the system tray when the object is destroyed + win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, self.nid) + + +def get_current_color_palette(): + """ + Reads color palette binary data from a registry key and returns it as a hex string. + + Returns: + The binary data as hex string, or None if the value doesn't exist or an error occurs. + """ + try: + # Open the registry key for accent palette settings + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Accent", 0, winreg.KEY_READ + ) + value, _ = winreg.QueryValueEx(key, "AccentPalette") + winreg.CloseKey(key) + # return the data as hex string + return value.hex() + + except WindowsError: + print("Error accessing registry.") + return None + + +def get_current_taskbar_color(): + """ + Attempts to get the current taskbar color based on user personalization settings. + + Returns: + The color in hex format (e.g., '#RRGGBB') if inferred successfully, or None otherwise. + """ + try: + # Open the registry key for personalization settings + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Accent", 0, winreg.KEY_READ + ) + accent_color, _ = winreg.QueryValueEx(key, "AccentColorMenu") + winreg.CloseKey(key) + + # Convert the accent color value (DWORD) to RGB + r = accent_color & 0xFF + g = (accent_color >> 8) & 0xFF + b = (accent_color >> 16) & 0xFF + # Convert RGB to hex + return f"#{r:02X}{g:02X}{b:02X}" + + except WindowsError: + print("Error accessing registry.") + return None + + +def set_taskbar_accent_color(hex_color, hex_color_palette): + """ + Sets the accent color location in the Windows registry. + + Args: + hex_color = the hex value of RGB color with starting "#" (e.g., '#RRGGBB') + hex_color_palette = the hex string of the corresponding color paletter for the provided hex_color + """ + try: + print("Setting taskbar color to:", hex_color) + # Convert hex color to RGB + r = int(hex_color[1:3], 16) + g = int(hex_color[3:5], 16) + b = int(hex_color[5:7], 16) + + # Convert RGB to DWORD + color_value = (b << 16) | (g << 8) | r + + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Accent", + 0, + winreg.KEY_ALL_ACCESS, + ) + + binary_data = bytes.fromhex(hex_color_palette) + winreg.SetValueEx(key, "AccentColorMenu", 0, winreg.REG_DWORD, color_value) + winreg.SetValueEx(key, "AccentPalette", 0, winreg.REG_BINARY, binary_data) + winreg.CloseKey(key) + + except WindowsError as e: + print(f"Error accessing or modifying registry: {e}") + + +def get_transparency_effects(): + try: + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + 0, + winreg.KEY_ALL_ACCESS, + ) + value, _ = winreg.QueryValueEx(key, "EnableTransparency") + winreg.CloseKey(key) + return value + + except WindowsError as e: + print(f"Error accessing or modifying registry: {e}") + + +def set_transparency_effects(value): + try: + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + 0, + winreg.KEY_ALL_ACCESS, + ) + + winreg.SetValueEx(key, "EnableTransparency", 0, winreg.REG_DWORD, value) + winreg.CloseKey(key) + except WindowsError as e: + print(f"Error accessing or modifying registry: {e}") + + +# Attempt to extract a known good value in response. +def extract_title(data): + match = re.search(r"(.*?)", data) + if match: + return match.group(1) + else: + return None + + +def check_internet(): + for url, expected_response in CONNECT_TEST_URL_AND_RESPONSES.items(): + try: + # Perform internet connectivity tests + response = requests.get(url, timeout=5, verify=False) + if expected_response in (extract_title(response.text) or response.text): + print(f"Internet connectivity detected via URL: {url}") + return True + except: + pass + return False + + +def check_internet_and_update_tray_icon(): + global tray_icon, hicon_indicator_off, hicon_indicator_on, default_color + if check_internet(): + tray_icon.set_icon(hicon_indicator_on) + tray_icon.set_tooltip("Internet Connection: Detected") + set_transparency_effects(1) + set_taskbar_accent_color(ROSE_COLOR, ROSE_ACCENT_PALETTE) # Rose Color = #ff5200c3 + # Set the background to internet connection background + if get_wallpaper_path() != INTERNET_BACKGROUND: # Checked so program isn't continuously setting the wallpaper + set_wallpaper(INTERNET_BACKGROUND) + else: + tray_icon.set_icon(hicon_indicator_off) + tray_icon.set_tooltip("Internet Connection: Not Detected") + set_transparency_effects(default_transparency) + set_taskbar_accent_color(default_color, default_palette) # change color back to what user had originally + # Reset background when internet is not detected + if get_wallpaper_path() != DEFAULT_BACKGROUND: # Checked so program isn't continuously setting the wallpaper + set_wallpaper(DEFAULT_BACKGROUND) + + +def check_internet_loop(): + while not stop_event.is_set(): + check_internet_and_update_tray_icon() + time.sleep(CHECK_INTERVAL) + + +def tray_icon_loop(): + global hwnd, tray_icon, hicon_indicator_off, hicon_indicator_on, stop_event + # Load icons + hicon_indicator_off = load_icon(ICON_INDICATOR_OFF) + hicon_indicator_on = load_icon(ICON_INDICATOR_ON) + + # Wait for hwnd to be initialized + while hwnd is None: + time.sleep(0.1) + + if hicon_indicator_off is None or hicon_indicator_on is None: + print("Error: Failed to load icons. Exiting TrayIconThread.") + return + + tray_icon = SysTrayIcon(hwnd, hicon_indicator_off, "Internet Detector") + + while not stop_event.is_set(): + msg = win32gui.PeekMessage(hwnd, 0, 0, 0) + if msg and len(msg) == 6: + win32gui.TranslateMessage(msg) + win32gui.DispatchMessage(msg) + time.sleep(0.1) + + +def get_wallpaper_path(): + """Attempts to retrieve the path to the current wallpaper image.""" + # Try to get the path from the registry (for wallpapers set through Windows settings) + try: + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Control Panel\Desktop", 0, winreg.KEY_READ) + value, _ = winreg.QueryValueEx(key, "Wallpaper") + winreg.CloseKey(key) + if value: + return value + except WindowsError: + pass + + # Check for cached wallpaper files (if the above fails) + cached_files_dir = os.path.join(os.getenv("APPDATA"), r"Microsoft\Windows\Themes\CachedFiles") + transcoded_wallpaper_path = os.path.join(os.getenv("APPDATA"), r"Microsoft\Windows\Themes\TranscodedWallpaper") + + for file in os.listdir(cached_files_dir): + if file.endswith((".jpg", ".jpeg", ".bmp", ".png")): + return os.path.join(cached_files_dir, file) + + if os.path.exists(transcoded_wallpaper_path): + return transcoded_wallpaper_path + + # If all else fails, return None + return None + + +def set_wallpaper(image_path): + """Sets the desktop wallpaper to the image at the specified path.""" + print(f"Setting wallpaper to: {image_path}") + result = ctypes.windll.user32.SystemParametersInfoW( + SPI_SETDESKWALLPAPER, 0, image_path, SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE + ) + if not result: + print("Error setting wallpaper. Make sure the image path is correct.") + + +def main_loop(): + global stop_event, check_thread, tray_icon_thread, tray_icon + # Create and start the threads + tray_icon_thread = threading.Thread(target=tray_icon_loop) + check_thread = threading.Thread(target=check_internet_loop) + + tray_icon_thread.start() + # Wait for the tray icon to finish initializing + while tray_icon is None: + time.sleep(0.1) + + check_thread.start() + + while not stop_event.is_set(): + time.sleep(1) + + +if __name__ == "__main__": + # print(get_current_taskbar_color()) + signal.signal(signal.SIGINT, signal_handler) + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + default_transparency = get_transparency_effects() + default_color = get_current_taskbar_color() + default_palette = get_current_color_palette() + + # Create a hidden window to receive messages (required for system tray icons) + def wndProc(hwnd, msg, wparam, lparam): + if lparam == win32con.WM_LBUTTONDBLCLK: + print("Left button double clicked") + elif msg == win32con.WM_COMMAND: + if wparam == 1023: # Example menu item ID + print("Exit selected") + win32gui.DestroyWindow(hwnd) + return win32gui.DefWindowProc(hwnd, msg, wparam, lparam) + + wc = win32gui.WNDCLASS() + hinst = wc.hInstance = win32api.GetModuleHandle(None) + wc.lpszClassName = "Internet Detector" + wc.lpfnWndProc = wndProc + classAtom = win32gui.RegisterClass(wc) + hwnd = win32gui.CreateWindow(classAtom, "Internet Detector", 0, 0, 0, 0, 0, 0, 0, hinst, None) + + print("Current wallpaper: {}".format(get_wallpaper_path())) + print("Current color: {}".format(get_current_taskbar_color())) + + main_loop()