# Copyright 2021 Erfan Abdi # SPDX-License-Identifier: GPL-3.0-or-later import logging import threading import tools.config import tools.helpers.net from pathlib import Path from contextlib import suppress from tools.interfaces import IUserMonitor from tools.interfaces import IPlatform from gi.repository import GLib stopping = False def start(args, session, unlocked_cb=None): apps_dir = Path(session["xdg_data_home"]) / "applications" apps_dir.mkdir(0o700, exist_ok=True) waydroid_user_state_dir = Path(session["waydroid_user_state"]) waydroid_data_icons_dir = Path(session["waydroid_data"]) / "icons" system_apps = [ "com.android.calculator2", "com.android.camera2", "com.android.contacts", "com.android.deskclock", "com.android.documentsui", "com.android.email", "com.android.gallery3d", "com.android.inputmethod.latin", "com.android.settings", "com.google.android.gms", "org.lineageos.aperture", "org.lineageos.eleven", "org.lineageos.etar", "org.lineageos.jelly", "org.lineageos.recorder" ] def prepend_list(old_list, new_list): for s in reversed(new_list): if s not in old_list: old_list.insert(0, s) def glib_key_file_get_string_list(key_file, group, key): try: return key_file.get_string_list(group, key) except GLib.GError: return [] def glib_key_file_prepend_string_list(key_file, group, key, new_list): lst = glib_key_file_get_string_list(key_file, group, key) prepend_list(lst, new_list) key_file.set_string_list(group, key, lst) def glib_key_file_has_value(key_file, group, key): try: key_file.get_value(group, key) return True except GLib.GError: return False # Migrate waydroid user configs after upgrade def user_migration(): if not any(apps_dir.glob('waydroid.*.desktop')): # first ever run, no need to migrate return migrated_main_path = waydroid_user_state_dir / ".migrated-main-desktop-file" if not migrated_main_path.exists(): main_app_path = apps_dir / "Waydroid.desktop" main_app_path.unlink(missing_ok=True) migrated_main_path.touch() migrated_apps_path = waydroid_user_state_dir / ".migrated-app-settings-desktop-action" if not migrated_apps_path.exists(): for app in apps_dir.glob("waydroid.*.desktop"): with suppress(GLib.GError): desktop_file = GLib.KeyFile() flags = GLib.KeyFileFlags.KEEP_COMMENTS | GLib.KeyFileFlags.KEEP_TRANSLATIONS desktop_file.load_from_file(str(app), flags) with suppress(GLib.GError): desktop_file.remove_group("Desktop Action app_settings") with suppress(GLib.GError, ValueError): actions = glib_key_file_get_string_list(desktop_file, "Desktop Entry", "Actions") actions.remove("app_settings") desktop_file.set_string_list("Desktop Entry", "Actions", actions) desktop_file.save_to_file(str(app)) migrated_apps_path.touch() # Creates, deletes, or updates desktop file def updateDesktopFile(appInfo): if appInfo is None: return packageName = appInfo["packageName"] desktop_file_path = apps_dir / f"waydroid.{packageName}.desktop" showApp = False for cat in appInfo["categories"]: if cat.strip() == "android.intent.category.LAUNCHER": showApp = True break if not showApp: # Remove the desktop file if it exists and do not create a new one. desktop_file_path.unlink(missing_ok=True) return desktop_file = GLib.KeyFile() with suppress(GLib.GError): flags = GLib.KeyFileFlags.KEEP_COMMENTS | GLib.KeyFileFlags.KEEP_TRANSLATIONS desktop_file.load_from_file(str(desktop_file_path), flags) desktop_file.set_string("Desktop Entry", "Type", "Application") desktop_file.set_string("Desktop Entry", "Name", appInfo["name"]) desktop_file.set_string("Desktop Entry", "Exec", f"waydroid app launch {packageName}") desktop_file.set_string("Desktop Entry", "Icon", str(waydroid_data_icons_dir / f"{packageName}.png")) glib_key_file_prepend_string_list(desktop_file, "Desktop Entry", "Categories", ["X-WayDroid-App"]) desktop_file.set_string_list("Desktop Entry", "X-Purism-FormFactor", ["Workstation", "Mobile"]) glib_key_file_prepend_string_list(desktop_file, "Desktop Entry", "Actions", ["app-settings"]) if packageName in system_apps and not glib_key_file_has_value(desktop_file, "Desktop Entry", "NoDisplay"): desktop_file.set_boolean("Desktop Entry", "NoDisplay", True) desktop_file.set_string("Desktop Action app-settings", "Name", "App Settings") desktop_file.set_string("Desktop Action app-settings", "Exec", f"waydroid app intent android.settings.APPLICATION_DETAILS_SETTINGS package:{packageName}") desktop_file.set_string("Desktop Action app-settings", "Icon", str(waydroid_data_icons_dir / "com.android.settings.png")) desktop_file.save_to_file(str(desktop_file_path)) def userUnlocked(uid): cfg = tools.config.load(args) logging.info("Android with user {} is ready".format(uid)) user_migration() if cfg["waydroid"]["auto_adb"] == "True": with suppress(RuntimeError): tools.helpers.net.adb_connect(args) platformService = IPlatform.get_service(args) if platformService: appsList = platformService.getAppsInfo() for app in appsList: updateDesktopFile(app) for existing in apps_dir.glob("waydroid.*.desktop"): if existing.name not in map(lambda appInfo: f"waydroid.{appInfo['packageName']}.desktop", appsList): existing.unlink() if unlocked_cb: unlocked_cb() def packageStateChanged(mode, packageName, uid): platformService = IPlatform.get_service(args) if platformService: desktop_file_path = apps_dir / f"waydroid.{packageName}.desktop" if mode == IUserMonitor.PACKAGE_REMOVED: desktop_file_path.unlink(missing_ok=True) else: appInfo = platformService.getAppInfo(packageName) updateDesktopFile(appInfo) def service_thread(): while not stopping: IUserMonitor.add_service(args, userUnlocked, packageStateChanged) global stopping stopping = False args.user_manager = threading.Thread(target=service_thread) args.user_manager.start() def stop(args): global stopping stopping = True try: if args.userMonitorLoop: args.userMonitorLoop.quit() except AttributeError: logging.debug("UserMonitor service is not even started")