diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ad7a2288..b883c23b7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,7 +24,6 @@ plugins { alias(libs.plugins.nav.safeargs) alias(libs.plugins.autoresconfig) alias(libs.plugins.materialthemebuilder) - alias(libs.plugins.lsplugin.resopt) alias(libs.plugins.lsplugin.apksign) } diff --git a/app/src/main/java/org/lsposed/manager/ConfigManager.java b/app/src/main/java/org/lsposed/manager/ConfigManager.java index 9be97ba5e..afb74e0aa 100644 --- a/app/src/main/java/org/lsposed/manager/ConfigManager.java +++ b/app/src/main/java/org/lsposed/manager/ConfigManager.java @@ -178,25 +178,6 @@ public static boolean setVerboseLogEnabled(boolean enabled) { } } - public static boolean isLogWatchdogEnabled() { - try { - return LSPManagerServiceHolder.getService().isLogWatchdogEnabled(); - } catch (RemoteException e) { - Log.e(App.TAG, Log.getStackTraceString(e)); - return false; - } - } - - public static boolean setLogWatchdog(boolean enabled) { - try { - LSPManagerServiceHolder.getService().setLogWatchdog(enabled); - return true; - } catch (RemoteException e) { - Log.e(App.TAG, Log.getStackTraceString(e)); - return false; - } - } - public static ParcelFileDescriptor getLog(boolean verbose) { try { return verbose ? LSPManagerServiceHolder.getService().getVerboseLog() : LSPManagerServiceHolder.getService().getModulesLog(); @@ -336,15 +317,6 @@ public static boolean setHiddenIcon(boolean hide) { } } - public static String getApi() { - try { - return LSPManagerServiceHolder.getService().getApi(); - } catch (RemoteException e) { - Log.e(App.TAG, Log.getStackTraceString(e)); - return e.toString(); - } - } - public static List getDenyListPackages() { List list = new ArrayList<>(); try { @@ -363,25 +335,6 @@ public static void flashZip(String zipPath, ParcelFileDescriptor outputStream) { } } - public static boolean isDexObfuscateEnabled() { - try { - return LSPManagerServiceHolder.getService().getDexObfuscate(); - } catch (RemoteException e) { - Log.e(App.TAG, Log.getStackTraceString(e)); - return false; - } - } - - public static boolean setDexObfuscateEnabled(boolean enabled) { - try { - LSPManagerServiceHolder.getService().setDexObfuscate(enabled); - return true; - } catch (RemoteException e) { - Log.e(App.TAG, Log.getStackTraceString(e)); - return false; - } - } - public static int getDex2OatWrapperCompatibility() { try { return LSPManagerServiceHolder.getService().getDex2OatWrapperCompatibility(); diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java index ac9b9d450..99978e0ac 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java @@ -150,8 +150,8 @@ private void updateStates(Activity activity, boolean binderAlive, boolean needUp binding.statusTitle.setText(R.string.activated); binding.statusIcon.setImageResource(R.drawable.ic_round_check_circle_24); } - binding.statusSummary.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", - ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi())); + binding.statusSummary.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", + ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode())); binding.developerWarningCard.setVisibility(isDeveloper() ? View.VISIBLE : View.GONE); } else { boolean isMagiskInstalled = ConfigManager.isMagiskInstalled(); @@ -177,7 +177,6 @@ private void updateStates(Activity activity, boolean binderAlive, boolean needUp if (ConfigManager.isBinderAlive()) { binding.apiVersion.setText(String.valueOf(ConfigManager.getXposedApiVersion())); - binding.api.setText(ConfigManager.isDexObfuscateEnabled() ? R.string.enabled : R.string.not_enabled); binding.frameworkVersion.setText(String.format(LocaleDelegate.getDefaultLocale(), "%1$s (%2$d)", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode())); binding.managerPackageName.setText(activity.getPackageName()); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { @@ -196,7 +195,6 @@ private void updateStates(Activity activity, boolean binderAlive, boolean needUp } } else { binding.apiVersion.setText(R.string.not_installed); - binding.api.setText(R.string.not_installed); binding.frameworkVersion.setText(R.string.not_installed); binding.managerPackageName.setText(activity.getPackageName()); } @@ -213,10 +211,6 @@ private void updateStates(Activity activity, boolean binderAlive, boolean needUp "\n" + binding.apiVersion.getText() + "\n\n" + - activity.getString(R.string.settings_xposed_api_call_protection) + - "\n" + - binding.api.getText() + - "\n\n" + activity.getString(R.string.info_dex2oat_wrapper) + "\n" + binding.dex2oatWrapper.getText() + @@ -242,7 +236,6 @@ private void updateStates(Activity activity, boolean binderAlive, boolean needUp binding.systemAbi.getText(); var map = new HashMap(); map.put("apiVersion", binding.apiVersion.getText().toString()); - map.put("api", binding.api.getText().toString()); map.put("frameworkVersion", binding.frameworkVersion.getText().toString()); map.put("systemAbi", Arrays.toString(Build.SUPPORTED_ABIS)); binding.copyInfo.setOnClickListener(v -> { diff --git a/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java b/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java index ef8c5f728..501bcef0a 100644 --- a/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java @@ -80,7 +80,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c getChildFragmentManager().beginTransaction().add(R.id.setting_container, new PreferenceFragment()).commitNow(); } if (ConfigManager.isBinderAlive()) { - binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode(), ConfigManager.getApi())); + binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", ConfigManager.getXposedVersionName(), ConfigManager.getXposedVersionCode())); } else { binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getString(R.string.not_installed))); } @@ -134,18 +134,6 @@ public void onDetach() { parentFragment = null; } - private boolean setNotificationPreferenceEnabled(MaterialSwitchPreference notificationPreference, boolean preferenceEnabled) { - var notificationEnabled = ConfigManager.enableStatusNotification(); - if (notificationPreference != null) { - notificationPreference.setEnabled(!notificationEnabled || preferenceEnabled); - notificationPreference.setSummaryOn(preferenceEnabled ? - notificationPreference.getContext().getString(R.string.settings_enable_status_notification_summary) : - notificationPreference.getContext().getString(R.string.settings_enable_status_notification_summary) + "\n" + - notificationPreference.getContext().getString(R.string.disable_status_notification_error)); - } - return notificationEnabled; - } - @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { final String SYSTEM = "SYSTEM"; @@ -161,37 +149,17 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { prefVerboseLogs.setOnPreferenceChangeListener((preference, newValue) -> ConfigManager.setVerboseLogEnabled(!(boolean) newValue)); } - MaterialSwitchPreference prefLogWatchDog = findPreference("enable_log_watchdog"); - if (prefLogWatchDog != null) { - prefLogWatchDog.setEnabled(!BuildConfig.DEBUG && installed); - if (BuildConfig.DEBUG) ConfigManager.setLogWatchdog(true); - prefLogWatchDog.setChecked(!installed || ConfigManager.isLogWatchdogEnabled()); - prefLogWatchDog.setOnPreferenceChangeListener((preference, newValue) -> ConfigManager.setLogWatchdog((boolean) newValue)); - } - - MaterialSwitchPreference prefDexObfuscate = findPreference("enable_dex_obfuscate"); - if (prefDexObfuscate != null) { - prefDexObfuscate.setEnabled(installed); - prefDexObfuscate.setChecked(!installed || ConfigManager.isDexObfuscateEnabled()); - prefDexObfuscate.setOnPreferenceChangeListener((preference, newValue) -> { - parentFragment.showHint(R.string.reboot_required, true, R.string.reboot, v -> ConfigManager.reboot()); - return ConfigManager.setDexObfuscateEnabled((boolean) newValue); - }); - } - MaterialSwitchPreference notificationPreference = findPreference("enable_status_notification"); if (notificationPreference != null) { notificationPreference.setVisible(installed); if (installed) { - notificationPreference.setChecked(setNotificationPreferenceEnabled(notificationPreference, !App.isParasitic || ShortcutUtil.isLaunchShortcutPinned())); + notificationPreference.setChecked(ConfigManager.enableStatusNotification()); + notificationPreference.setSummaryOn(R.string.settings_enable_status_notification_summary); + notificationPreference.setEnabled(true); } - notificationPreference.setOnPreferenceChangeListener((p, v) -> { - var succeeded = ConfigManager.setEnableStatusNotification((boolean) v); - if ((boolean) v && App.isParasitic && !ShortcutUtil.isLaunchShortcutPinned()) { - setNotificationPreferenceEnabled(notificationPreference, false); - } - return succeeded; - }); + notificationPreference.setOnPreferenceChangeListener((p, v) -> + ConfigManager.setEnableStatusNotification((boolean) v) + ); } Preference shortcut = findPreference("add_shortcut"); @@ -203,7 +171,6 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { } shortcut.setOnPreferenceClickListener(preference -> { if (!ShortcutUtil.requestPinLaunchShortcut(() -> { - setNotificationPreferenceEnabled(notificationPreference, true); App.getPreferences().edit().putBoolean("never_show_welcome", true).apply(); parentFragment.showHint(R.string.settings_shortcut_pinned_hint, false); })) { diff --git a/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java b/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java index 8e7285419..525149815 100644 --- a/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java +++ b/app/src/main/java/org/lsposed/manager/util/UpdateUtil.java @@ -55,13 +55,12 @@ public void onResponse(@NonNull Call call, @NonNull Response response) { if (!response.isSuccessful()) return; var body = response.body(); if (body == null) return; - String api = ConfigManager.isBinderAlive() ? ConfigManager.getApi() : "riru"; try { var info = JsonParser.parseReader(body.charStream()).getAsJsonObject(); var notes = info.get("body").getAsString(); var assetsArray = info.getAsJsonArray("assets"); for (var assets : assetsArray) { - checkAssets(assets.getAsJsonObject(), notes, api.toLowerCase(Locale.ROOT)); + checkAssets(assets.getAsJsonObject(), notes); } } catch (Throwable t) { Log.e(App.TAG, t.getMessage(), t); @@ -79,11 +78,10 @@ public void onFailure(@NonNull Call call, @NonNull IOException e) { App.getOkHttpClient().newCall(request).enqueue(callback); } - private static void checkAssets(JsonObject assets, String releaseNotes, String api) { + private static void checkAssets(JsonObject assets, String releaseNotes) { var pref = App.getPreferences(); var name = assets.get("name").getAsString(); var splitName = name.split("-"); - if (!splitName[3].equals(api)) return; pref.edit() .putInt("latest_version", Integer.parseInt(splitName[2])) .putLong("latest_check", Instant.now().getEpochSecond()) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 09fe189b9..f1f2f5b98 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -272,19 +272,6 @@ android:layout_height="wrap_content" android:text="@string/not_installed" /> - - - - Die huidige versteklanseerder ondersteun nie penkortpaaie nie Statuskennisgewing Wys \'n kennisgewing wat parasitiese bestuurder kan oopmaak - Geen kortpad nie, kan nie kennisgewing deaktiveer nie Dateer kanaal op Stabiel Beta Nag bou - Xposed API-oproepbeskerming - Blokkeer dinamies gelaaide modulekode om Xposed API te gebruik, dit kan sommige modules breek, maar bevoordeel sekuriteit Lees my Vrystellings diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ea591014c..8db9e9e20 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -176,8 +176,6 @@ إطار العمل تعطيل السجلات المفصّلة الإبلاغ عن مشاكل طلب لتضمين السجلات المفصولة - تمكين مراقبة السجل - يقوم مراقب السجل الخاص بـ LSPosed بتعديل خصائص النظام، والتي يمكن استغلالها للكشف عن LSPosed السمة السوداء المظلمة استخدام السمة السوداء الخالصة إذا تم تمكين السمة المظلمة السمة @@ -206,13 +204,10 @@ المشغل الافتراضي الحالي لا يدعم اختصارات الدبوس إشعارات الحالة إظهار إشعار يمكنه فتح مدير الطفيليات - لا يوجد اختصار، لا يمكن تعطيل الإشعار قناة التحديث مستقر تجريبي البناء الليلي - حماية استدعاء Xposed API - حظر رمز الوحدة الذي يتم تحميله ديناميكيًا لاستخدام Xposed API ، قد يؤدي ذلك إلى كسر بعض الوحدات ولكنه يفيد الأمان اقرأني إصدارات diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index cbd463750..f0770d699 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -194,13 +194,10 @@ Текущият стартер по подразбиране не поддържа преки пътища Известие за състоянието Показване на известие, което може да отвори паразитен мениджър - Няма пряк път, не мога да деактивирам известието Актуализиране на канала Стабилен Бета Нощно изграждане - Защита на повикванията на Xposed API - Блокиране на динамично зареждания код на модула, за да се използва Xposed API, това може да наруши някои модули, но е от полза за сигурността Readme Освобождава diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index cd2521ca9..a0033cde2 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -194,13 +194,10 @@ বর্তমান ডিফল্ট লঞ্চার পিন শর্টকাট সমর্থন করে না স্থিতি বিজ্ঞপ্তি পরজীবী ম্যানেজার খুলতে পারে এমন একটি বিজ্ঞপ্তি দেখান - কোন শর্টকাট, বিজ্ঞপ্তি নিষ্ক্রিয় করতে পারবেন না চ্যানেল আপডেট করুন স্থিতিশীল বেটা রাতারাতি নির্মাণ - Xposed API কল সুরক্ষা - এক্সপোজড এপিআই ব্যবহার করতে গতিশীলভাবে লোড করা মডিউল কোড ব্লক করুন, এটি কিছু মডিউল ভেঙে ফেলতে পারে তবে নিরাপত্তার সুবিধা পাবে রিডমি মুক্তি দেয় diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 20bd621e2..979e60012 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -194,13 +194,10 @@ El llançador predeterminat actual no admet dreceres de pin Notificació d\'estat Mostra una notificació que pugui obrir el gestor de paràsits - Sense drecera, no es pot desactivar la notificació Actualitza el canal Estable Beta Construcció nocturna - Protecció de trucades de l\'API Xposed - Bloqueja el codi del mòdul carregat dinàmicament per utilitzar l\'API Xposed, això pot trencar alguns mòduls però beneficiar la seguretat Llegiu-me Alliberaments diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 598917cc5..b711e7f0e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -198,13 +198,10 @@ Současný výchozí spouštěč nepodporuje připnuté zkratky Oznámení o stavu Zobrazení oznámení, které může otevřít parazitního správce - Žádný zástupce, nelze zakázat oznámení Kanál aktualizace Stabilní Beta Noční sestavení - Ochrana volání rozhraní Xposed API - Blokování dynamicky načítaného kódu modulu pro použití rozhraní Xposed API, což může narušit některé moduly, ale prospěje bezpečnosti Přečti si mě Vydání diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 78ccfa9e9..2c34dc02c 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -194,13 +194,10 @@ Den nuværende standardstarter understøtter ikke pin-genveje Meddelelse om status Vis en meddelelse, der kan åbne parasitic manager - Ingen genvej, kan ikke deaktivere meddelelse Opdater kanal Stabil Beta Nightly build - Beskyttelse af Xposed API-opkald - Bloker dynamisk indlæst modulkode for at bruge Xposed API, hvilket kan ødelægge nogle moduler, men gavner sikkerheden Læs Udgivelser diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 10d050e07..839e72243 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -173,8 +173,6 @@ JJ108 Framework Ausführliche Protokolle deaktivieren Ausführliche Protokolle in Problemberichtsmeldungen einschließen - Aktiviere Log Watchdog - Der Log-Watchdog von LSPosed verändert Systemeigenschaften, die zum Aufspüren von LSPosed ausgenutzt werden können Dunkelschwarzes Thema Schwarzes Thema verwenden, wenn dunkles Thema aktiviert ist Design @@ -203,13 +201,10 @@ JJ108 Der aktuelle Standard-Launcher unterstützt keine Pin-Verknüpfungen Status-Benachrichtigung Eine Benachrichtigung anzeigen, die den parasitären Manager öffnen kann - Keine Verknüpfung, Benachrichtigung kann nicht deaktiviert werden Update-Kanal Stabil Beta Nightly Build - Xposed API-Aufrufschutz - Dynamisch geladenen Modulcode blockieren, um Xposed API zu verwenden. Einige Module werden nicht mehr funktionieren, aber die Sicherheit profitiert davon Liesmich Veröffentlichungen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index f8fd39ef9..cb41a7870 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -194,13 +194,10 @@ Ο τρέχων προεπιλεγμένος εκτοξευτής δεν υποστηρίζει συντομεύσεις καρφίτσας Ειδοποίηση καταστάσεως Εμφάνιση μιας ειδοποίησης που μπορεί να ανοίξει τον διαχειριστή παρασιτικής - Δεν υπάρχει συντόμευση, δεν είναι δυνατή η απενεργοποίηση της ειδοποίησης Ενημέρωση καναλιού Σταθερό Βήτα Νυχτερινή κατασκευή - Προστασία κλήσεων Xposed API - Αποκλείστε τον δυναμικά φορτωμένο κώδικα μονάδας για να χρησιμοποιήσετε το Xposed API, αυτό μπορεί να σπάσει ορισμένες μονάδες αλλά να ωφελήσει την ασφάλεια Έτοιμο Εκδόσεις diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5698d96ec..1380e195b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -172,8 +172,6 @@ Framework Desactivar registros detallados Solicitud de inclusión de registros detallados en los informes de incidencias - Habilitar el watchdog de registro - El watchdog de registro de LSPosed modifica propiedades del sistema, lo que podría usarse para detectar LSPosed Tema negro oscuro Usar el tema negro puro si el tema oscuro está activado Tema @@ -202,13 +200,10 @@ El actual lanzador por defecto no admite accesos directos a pines Notificación de estado Mostrar una notificación que puede abrir el gestor de parásitos - No hay acceso directo, no se puede desactivar la notificación Actualizar canal Estable Beta Construcción nocturna - Protección de llamadas a la API Xposed - Bloquear el código del módulo cargado dinámicamente para utilizar Xposed API, esto puede romper algunos módulos, pero beneficiar a la seguridad Léeme Versiones diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index afcb2d8ef..da959db7e 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -194,13 +194,10 @@ Praegune vaikekäivitusprogramm ei toeta nööpnõelte otseteid Staatuse teatamine Kuva teatis, mis võib avada parasiitide halduri - Otsetee puudub, teavitust ei saa keelata Uuenduskanal Stabiilne Beeta Nightly build - Xposed API call protection - Blokeerige dünaamiliselt laaditud mooduli kood, et kasutada Xposed API-t, see võib mõne mooduli rikkuda, kuid toob kasu turvalisusele Readme Väljaanded diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index fcebf6b06..d3f112ae8 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -172,8 +172,6 @@ چارچوب غیرفعال کردن لاگ مفصل لاگ مفصل برای گزارش مشکل لازم است - فعال کردن نظارت لاگ - نظارت لاگ LSPosed ویژگی های سیستم را تغییر می دهد و ممکن است برای شناسایی LSPosed استفاده شود تم سیاه کامل اگر تم تاریک فعال است از تم کاملا سیاه استفاده کن تم @@ -202,13 +200,10 @@ لانچر پیش فرض فعلی شورتکات های پین شده را پشتیبانی نمی کند نمایش اعلان وضعیت نمایش اعلانی که مدیر Parasitic را باز کند - شورتکات نیست، نمی توان اعلان را غیرفعال کرد کانال بروزرسانی پایدار بتا نسخه شبانه - حفاظت از تماس API Xposed - جلوگیری از استفاده کد ماژول های بارگذاری شده به صورت داینامیک از API های Xposed، ممکن است بعضی ماژول ها کار نکنند ولی امنیت بهتر می شود راهنما نسخه ها diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index f0ce33e30..ac6727987 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -194,13 +194,10 @@ Nykyinen oletuskäynnistin ei tue pin-pikakuvakkeita. Tilailmoitus Näytä ilmoitus, joka voi avata loishallintaohjelman - Ei pikakuvaketta, ilmoitusta ei voi poistaa käytöstä Päivitä kanava Vakaa Beeta Yöllinen rakentaminen - Xposedin API-kutsujen suojaus - Estetään dynaamisesti ladatun moduulin koodin käyttö Xposed API:n avulla, mikä saattaa rikkoa joitakin moduuleja mutta hyödyttää turvallisuutta. Luennot Julkaisut diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 77ca0c4fa..9a804e250 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -174,8 +174,6 @@ JingMatrix Sous-système Désactiver les journaux détaillés Les journaux détaillés sont requis pour signaler des problèmes - Activer le chien de garde de journal - Le chien de garde du journal LSPosed modifie les propriétés du système, qui peuvent être exploitées pour détecter LSPosed Thème noir et sombre Utiliser le thème noir pur si le thème noir est activé Thème @@ -204,13 +202,10 @@ JingMatrix Le lanceur par défaut actuel ne supporte pas les raccourcis épinglés Notification d\'état Afficher une notification qui peut ouvrir le gestionnaire de parasites - Pas de raccourcis, la notification ne peut pas être désactivée Canal de mise à jour Stable Bêta Alpha - Protection des appels API Xposed - Bloquer dynamiquement le code de module chargé pour utiliser l\'API Xposed, celà peut casser certains modules mais être bénéfique à la sécurité Lisez-moi Versions diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 7121a47db..5af17db59 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -194,13 +194,10 @@ वर्तमान डिफ़ॉल्ट लांचर पिन शॉर्टकट का समर्थन नहीं करता स्थिति अधिसूचना एक अधिसूचना दिखाएं जो परजीवी प्रबंधक खोल सकती है - कोई शॉर्टकट नहीं, अधिसूचना अक्षम नहीं कर सकता चैनल अपडेट करें स्थिर बीटा सॉफ़्टवेयर की स्थिरता - Xposed API कॉल सुरक्षा - Xposed API का उपयोग करने के लिए गतिशील रूप से लोड किए गए मॉड्यूल कोड को ब्लॉक करें, इससे कुछ मॉड्यूल टूट सकते हैं लेकिन सुरक्षा को लाभ होगा रीडमी विज्ञप्ति diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index f89868636..e7b667af3 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -196,13 +196,10 @@ Trenutačni zadani pokretač ne podržava prečace pribadače Obavijest o statusu Prikaži obavijest koja može otvoriti parazitski upravitelj - Nema prečaca, nije moguće onemogućiti obavijest Ažurirajte kanal Stabilan Beta Noćna izgradnja - Xposed API zaštita poziva - Blokirajte dinamički učitani kod modula za korištenje Xposed API-ja, to može pokvariti neke module, ali doprinosi sigurnosti Pročitaj me Izdanja diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 06e98ce33..1d1117e96 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -195,13 +195,10 @@ A jelenlegi alapértelmezett indítóprogram nem támogatja a pin parancsikonokat Állapot értesítés Értesítés megjelenítése, amely megnyithatja a parazita-kezelőt - Nincs parancsikon, nem lehet letiltani az értesítést Frissítési csatorna Stabil Béta Nightly - Xposed API hívásvédelem - A dinamikusan betöltött modul kódjának blokkolása az Xposed API használatához, ez néhány modult tönkretehet, de a biztonság javára válik Olvass el Kiadások diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index abaed2c0c..f2b672e16 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -166,8 +166,6 @@ Kerangka kerja Nonaktifkan log verbose Permintaan laporan masalah dengan menyertakan log-log verbose - Aktifkan watchdog - Catatan watchdog LSPosed memodifikasi properti sistem, yang dapat diekploitasi untuk mendeteksi LSPosed Tema hitam gelap Gunakan tema hitam murni jika tema gelap diaktifkan Tema @@ -196,13 +194,10 @@ Peluncur default saat ini tidak mendukung pintasan pin Notifikasi Status Tampilkan notifikasi yang dapat membuka manajer parasit - Tidak ada pintasan, tidak dapat mematikan notifikasi Perbarui saluran Stabil Beta Rilis harian - Proteksi panggilan API Xposed - Blokir kode modul yang dimuat secara dinamis untuk menggunakan Xposed API, ini mungkin mengganggu beberapa modul namun lebih aman Baca aku Rilis diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 576bacc17..d7caca8df 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -168,8 +168,6 @@ Framework Disabilita il log verboso La segnalazione di problemi richiede l\'inclusione di log dettagliati - Abilita log watchdog - Il watchdog log di LSPosed modifica le proprietà del sistema, che potrebbero essere sfruttate per rilevare LSPosed Tema nero scuro Usa il tema nero puro quando è abilitato il tema scuro Tema @@ -198,13 +196,10 @@ L\'attuale launcher predefinito non supporta le scorciatoie con i pin Notifica di stato Mostra una notifica che può aprire il manager parassitario - Nessuna scorciatoia, impossibile disattivare la notifica Canale di aggiornamento Stabile Beta Nightly - API di protezione delle chiamate di Xposed - Blocca dinamicamente il codice del modulo caricato per utilizzare l\'API di Xposed, questo potrebbe interrompere alcuni moduli ma favorire la sicurezza Leggimi Versioni diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 9f9aeda99..0340da0a0 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -198,13 +198,10 @@ מפעיל ברירת המחדל הנוכחי אינו תומך בקיצורי דרך הודעת סטטוס הצג הודעה שיכולה לפתוח מנהל טפילי - אין קיצור דרך, לא ניתן להשבית הודעה ערוץ עדכון יציב בטא בניה לילית - הגנה מפני קריאות Xposed API - חסום קוד מודול שנטען באופן דינמי לשימוש ב-Xposed API, זה עשוי לשבור מודולים מסוימים אך להועיל לאבטחה קרא אותי גרסאות diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1d4f348f0..4c3fae1a6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -170,8 +170,6 @@ フレームワーク 詳細ログの無効化 問題を報告する際は、詳細なログを含めるようにしてください - ログウォッチドッグを有効化 - LSPosed のログウォッチドッグはシステムプロパティを変更し、LSPosed を検出するために悪用される可能性があります 黒のダークテーマ ダークテーマが有効になっている場合は、ピュアブラックテーマを使用します テーマ @@ -200,13 +198,10 @@ 現在のデフォルトランチャーはショートカットのピン留めをサポートしていません ステータス通知 パラサイトマネージャーを開くことができる通知を表示します - ショートカットがないため、通知を無効にできません 更新チャンネル 安定版 ベータ版 ナイトリービルド - Xposed API の呼び出し保護 - 動的にロードされたモジュールコードが Xposed API を使用するのをブロックします。これにより一部のモジュールが動作しなくなる可能性がありますが、セキュリティが向上します。 Readme リリース diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 59be53f2f..555432967 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -194,13 +194,10 @@ 현재 기본 런쳐는 핀 바로가기를 지원하지 않습니다 상태 알림 기생 관리자를 열 수 있는 알림 표시 - 바로 가기 없음, 알림을 비활성화할 수 없음 업데이트 채널 안정 베타 야간 빌드 - Xposed API 호출 보호 - Xposed API를 사용하기 위해 로드된 모듈 코드를 동적으로 차단하면 일부 모듈이 손상될 수 있지만 보안에 도움이 됩니다. 읽어보기 릴리스 diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index dcf58d61d..ffee6599c 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -194,13 +194,10 @@ Destpêkera xwerû ya heyî kurtebirên pin piştgirî nake Notification Status Agahdariyek nîşan bide ku dikare rêveberê parazît veke - Ne kurtebir, nekare ragihandinê neçalak bike Kanalê nûve bikin Stewr Beta Avakirina şevê - Parastina banga Xposed API - Koda modulê ya bi dînamîk barkirî asteng bike ku Xposed API bikar bîne, ev dibe ku hin modulan bişkîne lê ji ewlehiyê sûd werdigire Readme Releases diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index b6c82f79c..d5f71663a 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -198,13 +198,10 @@ Dabartinė numatytoji paleidimo programa nepalaiko prisegamų nuorodų Pranešimas apie būseną Rodyti pranešimą, kad galima atidaryti parazitinį tvarkytuvą - Nėra sparčiųjų klavišų, negalima išjungti pranešimo Atnaujinti kanalą Stabilus Beta Naktinis kūrimas - \"Xposed API\" skambučių apsauga - Blokuoti dinamiškai įkeltą modulio kodą, kad būtų galima naudoti Xposed API, tai gali pažeisti kai kuriuos modulius, bet naudinga saugumui Readme Leidiniai diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 13550f3ff..72bb2caf2 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -194,13 +194,10 @@ De huidige standaard launcher ondersteunt geen pin-snelkoppelingen Statusmelding Toon een melding die parasitaire manager kan openen - Geen snelkoppeling, kan melding niet uitschakelen Kanaal bijwerken Stabiel Bèta Nachtelijke bouw - Xposed API-oproepbeveiliging - Blokkeer dynamisch geladen modulecode om Xposed API te gebruiken, dit kan sommige modules breken maar komt de veiligheid ten goede Leesmij Publicaties diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index e3477001a..eb6b595df 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -194,13 +194,10 @@ Den gjeldende standardstarteren støtter ikke pin-snarveier Statusvarsel Vis et varsel som kan åpne parasitic manager - Ingen snarvei, kan ikke deaktivere varsling Oppdater kanal Stabil beta Nattlig bygg - Xposed API-anropsbeskyttelse - Blokker dynamisk lastet modulkode for å bruke Xposed API, dette kan ødelegge noen moduler, men gi fordeler for sikkerheten Lesmeg Utgivelser diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 81b347d54..2e3008c89 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -176,8 +176,6 @@ Framework Wyłącz logi Zgłaszanie problemów wymaga dołączenia pełnych dzienników - Włącz nadzór dziennika - Nadzór dziennika LSPosed modyfikuje właściwości systemowe, które mogą być wykorzystywane do wykrywania LSPosed Czarny ciemny motyw Użyj czarnego motywu, jeśli ciemny motyw jest włączony Motyw @@ -206,13 +204,10 @@ Bieżący domyślny launcher nie wspiera skrótów pinezkowych Status powiadomień Pokazuj powiadomienie, za pomocą którego otworzysz menadżera pasożytów - Brak skrótu, nie można wyłączyć powiadomienia Kanał aktualizacji Stabilny Beta Wersja nocna - Ochrona wywołania Xposed API - Blokuj dynamicznie ładowany kod modułu do używania API Xposed, wzmocni to ochronę kosztem możliwego uszkodzenia niektórych modułów Readme Wersje diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 9c987ad5b..4c4375bf7 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -172,8 +172,6 @@ Framework Desativar registros detalhados Relate o problema para incluir os registros detalhados - Ativar monitoramento de registro - O monitoramento de registro do LSPosed modifica as propriedades do sistema, que podem ser exploradas para detectar o LSPosed. Tema preto puro Use o tema preto puro se o tema escuro estiver ativo Tema @@ -202,13 +200,10 @@ A launcher padrão atual não suporta atalhos fixados Notificação de estado Mostre uma notificação que possa abrir o gerenciador parasita - Nenhum atalho, não é possível desativar a notificação. Canal de atualizações Estável Beta Nightly - Proteção da API do Xposed - Bloqueie módulos carregados dinamicamente de usarem a API do Xposed. Isso pode quebrar alguns módulos, mas melhora a segurança. Leia-me Versões diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index feae37676..76cd20662 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -194,13 +194,10 @@ O lançador padrão actual não suporta atalhos de pinos Notificação de estado Mostrar uma notificação que possa abrir o gerenciador parasita - Nenhum atalho, não é possível desativar a notificação Canal de Atualizações Estável Beta Nightly - Proteção da API do Xposed - Bloquear módulos carregados dinamicamente de usarem a API do Xposed, isso pode travar alguns módulos, mas melhora a segurança Leia-me Versões diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 6cee3e32a..04a5b0203 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -174,8 +174,6 @@ Comportament Dezactivare jurnale detaliate Rapoartele de depanare necesită includerea de jurnale detaliate - Proces de supervizare cu jurnal - Monitorizează LSPosed. Activarea acestuia modifică proprietățile de sistem, care pot fi exploatate de către alte aplicații pentru detectarea drepturilor de superutilizator Temă întunecată Utilizați negru pur pentru tema întunecată Temă @@ -204,13 +202,10 @@ Lansatorul implicit nu acceptă scurtături Notificare de stare Afișează o notificare pentru deschiderea managerului parazitar - Nu se poate dezactiva notificarea: nu există scurtătură Canal de actualizare Stabil Beta Nightly - Protecția apelurilor Xposed API - Interzice modulelor utilizarea codului încărcat dinamic pentru a apela Xposed. Acest lucru poate cauza probleme unor module, dar crește securitatea A se citi mai întâi Versiuni diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 78d7d0359..fb16caed9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -176,8 +176,6 @@ Framework Выключить подробный журнал Отчёт об ошибке требует включение подробного журнала - Включить отслеживание журнала - Отслеживание журнала LSPosed (log watchdog) изменяет системные настройки, которыми можно обнаружить LSPosed Чёрная тема Использовать чёрную тему, когда включена тёмная тема Тема @@ -206,13 +204,10 @@ Текущий лаунчер по умолчанию не поддерживает закрепление ярлыков Уведомление о состоянии Показывать уведомление, через которое можно открыть parasitic-менеджер (т. е. LSPosed в скрытом режиме) - Отсутствует ярлык, невозможно отключить уведомление Канал обновлений Стабильные версии Бета-версии Еженочные версии - Защита запросов Xposed API - Блокировка динамически загружаемого кода модуля для использования Xposed API, это может нарушить работу некоторых модулей, но повысит безопасность ПрочтиМеня Версии diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index df16254e2..2e4c7ca3e 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -194,13 +194,10 @@ වත්මන් පෙරනිමි දියත් කිරීම pin කෙටිමං සඳහා සහය නොදක්වයි තත්ත්ව දැනුම්දීම පරපෝෂිත කළමනාකරු විවෘත කළ හැකි දැනුම්දීමක් පෙන්වන්න - කෙටි මගක් නැත, දැනුම්දීම අක්‍රිය කළ නොහැක නාලිකාව යාවත්කාලීන කරන්න ස්ථාවර බීටා රාත්රී ගොඩනැගීම - Xposed API ඇමතුම් ආරක්ෂාව - Xposed API භාවිතා කිරීමට ගතිකව පටවන ලද මොඩියුල කේතය අවහිර කරන්න, මෙය සමහර මොඩියුල බිඳ දැමිය හැකි නමුත් ආරක්ෂාවට ප්‍රතිලාභ ලබා දෙයි කියවන්න නිකුත් කරයි diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 2e4ee835a..1554c2629 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -198,13 +198,10 @@ Súčasný predvolený spúšťač nepodporuje pripnuté skratky Oznámenie o stave Zobrazenie oznámenia, ktoré môže otvoriť parazitický správca - Žiadna skratka, nie je možné vypnúť upozornenie Aktualizačný kanál Stabilné Beta Nočné zostavenie - Ochrana volaní Xposed API - Blokovať dynamicky načítaný kód modulu, aby používal Xposed API, čo môže rozbiť niektoré moduly, ale prospeje bezpečnosti Readme Uvoľňuje diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 4619c19fc..016761736 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -172,8 +172,6 @@ Framework Inaktivera utförliga loggar Begäran om att inkludera verbose-loggar i rapporten - Aktivera loggövervakning - Loggövervakning av LSPosed ändrar systemegenskaper, vilket kan utnyttjas för att upptäcka LSPosed Svart mörkt tema Använd det rena svarta temat om mörkt tema är aktiverat Tema @@ -202,13 +200,10 @@ Den nuvarande standardstartaren har inte stöd för genvägar med stift Meddelande om status Visa en anmälan som kan öppna parasitansvarig - Ingen genväg, kan inte inaktivera anmälan Uppdatera kanal Stabil Beta Nattligt bygg - Skydd av Xposed API-anrop - Blockera dynamiskt laddad modulkod för att använda Xposed API, vilket kan förstöra vissa moduler men gynnar säkerheten. Läsa Släpp diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 3d72b6851..d09c02723 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -193,13 +193,10 @@ Launcher เริ่มต้นปัจจุบันไม่รองรับทางลัด PIN การแจ้งเตือนสถานะ แสดงการแจ้งเตือนที่สามารถเปิดตัวจัดการปรสิต - ไม่มีทางลัด ไม่สามารถปิดการแจ้งเตือนได้ อัพเดทช่อง มั่นคง เบต้า สร้างทุกคืน - การป้องกันการโทร Xposed API - บล็อกโค้ดโมดูลที่โหลดแบบไดนามิกเพื่อใช้ Xposed API ซึ่งอาจทำให้บางโมดูลเสียหายแต่ได้ประโยชน์ด้านความปลอดภัย ต้องอ่าน เผยแพร่ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 8fdb44ce8..a920227d1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -172,8 +172,6 @@ Çerçeve Ayrıntılı günlükleri devre dışı bırak Ayrıntılı günlükleri dahil etme isteğini raporlayın - Watchdog log\'u etkinleştir - LSPosed\'in watchdog log\'u, LSPosed\'i tespit etmek için kullanılabilecek sistem özelliklerini değiştirir Siyah koyu tema Koyu tema etkinse saf siyah temayı kullanın Tema @@ -202,13 +200,10 @@ Geçerli varsayılan başlatıcı, pin kısayollarını desteklemiyor Durum Bildirimi Parazit yöneticiyi açabilmek için bir bildirim göster - Kısayol yok, bildirim devre dışı bırakılamaz Güncelleme kanalı Stabil Beta Gecelik - Xposed API çağrı koruması - Xposed API kullanmak için dinamik olarak yüklenen modül kodunu engelleyin, bu bazı modülleri bozabilir ancak güvenliğe faydalı da olabilir Beni oku Sürümler diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 10a998efe..8e32620e6 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -176,8 +176,6 @@ Framework Вимкнути детальні журнали Вимоги до звітів про проблеми, що виникають, включати розгорнуті журнали - Увімкнути відстеження журналу - Відстеження журналу LSPosed (log watchdog) змінює системні налаштування, якими можна виявити LSPosed Чорна тема Використовувати повністю чорну тему, коли темну тему увімкнено Тема @@ -206,13 +204,10 @@ Поточна панель запуску за замовчуванням не підтримує шорткати пін-кодів Повідомлення про статус Показати повідомлення, яке може відкрити паразитний менеджер - Немає ярлика, не можна відключити сповіщення Канал оновлень Стабільний Бета-версія Нічна збірка - Захист викликів Xposed API - Блокування динамічно завантаженого коду модуля для використання Xposed API, це може призвести до поломки деяких модулів, але покращить безпеку Опис Релізи diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index f5eb06938..e15aa9c6e 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -194,13 +194,10 @@ موجودہ ڈیفالٹ لانچر پن شارٹ کٹس کو سپورٹ نہیں کرتا ہے۔ اسٹیٹس کی اطلاع ایک اطلاع دکھائیں جو پرجیوی مینیجر کو کھول سکے۔ - کوئی شارٹ کٹ نہیں، اطلاع کو غیر فعال نہیں کر سکتا چینل کو اپ ڈیٹ کریں۔ مستحکم بیٹا رات کی تعمیر - Xposed API کال تحفظ - ایکسپوزڈ API استعمال کرنے کے لیے متحرک طور پر بھرے ہوئے ماڈیول کوڈ کو بلاک کریں، اس سے کچھ ماڈیول ٹوٹ سکتے ہیں لیکن سیکیورٹی کو فائدہ پہنچ سکتا ہے ریڈمی جاری کرتا ہے۔ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b7101142f..0ec5de33b 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -167,8 +167,6 @@ Nên gỡ cài đặt ứng dụng hiện tại. Khung hệ thống Vô hiệu hoá nhật ký chi tiết Báo cáo sự cố yêu cầu bao gồm nhật ký chi tiết - Bật chức năng giám sát nhật ký - Log watchdog sửa đổi các thuộc tính hệ thống của LSPosed, có thể được khai thác để phát hiện LSPosed Chủ đề Đen - Tối Sử dụng chủ đề đen nếu chủ đề tối được bật Chủ đề @@ -197,14 +195,10 @@ Nên gỡ cài đặt ứng dụng hiện tại. Trình khởi chạy hiện tại không hỗ trợ tạo lối tắt Thông báo trạng thái Hiện thông báo để mở trình quản lý phụ thuộc - Không có lối tắt nên không thể tắt thông báo Kênh cập nhật Ổn định Thử nghiệm Bản dựng hàng đêm - Bảo vệ kết nối - Chặn mã tiện ích hoạt động được sử dụng. -Có thể làm hỏng một số tiện ích bổ sung nhưng có lợi cho bảo mật Đọc Bản phát hành diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index dd8cc0592..4d89c6a34 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -171,8 +171,6 @@ JingMatrix 框架 禁用详细日志 报告问题要求包含详细日志 - 启用日志监控 - LSPosed 的日志监视修改了系统属性,可以被利用来检测 LSPosed 纯黑主题 当深色主题启用时使用纯黑主题 主题 @@ -201,13 +199,10 @@ JingMatrix 当前默认桌面不支持固定快捷方式 状态通知 显示一个通知以打开寄生管理器 - 没有快捷方式,无法禁用通知 模块更新通道 稳定版 测试版 每夜版 - Xposed API 调用保护 - 阻止模块动态加载的代码使用 Xposed API,这会使某些模块失效,但有利于安全性 自述文件 版本 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index dff9e4f4a..dd3625ac4 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -170,8 +170,6 @@ 框架 禁用詳細紀錄檔 回報問題要求包含詳細記錄檔 - 啟用日誌監控 - LSPosed 的日誌監控會修改系統屬性,可能被利用來偵測 LSPosed 使用純黑深色主題 使用純黑色背景當深色模式已啟用 主題 @@ -200,13 +198,10 @@ 目前的預設啟動器不支援釘選捷徑 狀態通知 顯示通知以便開啟寄生管理員 - 沒有捷徑,無法停用通知 更新頻道 穩定版 測試版 每夜構建 - Xposed API 呼叫保護 - 封鎖動態載入的模組代碼以使用 Xposed API,這可能會損毀某些模組但有利於安全性 自述文件 版本 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 856c1723a..dfc1ef661 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -166,8 +166,6 @@ 框架 停用詳細日誌 回報問題要求包含詳細日誌 - 啟用日誌看門狗 - LSPosed 的日誌看門狗會修改系統屬性,可被利用來偵測 LSPosed 黑色主題 當深色主題啟用時使用純黑色主題 主題 @@ -196,13 +194,10 @@ 目前的預設啟動器不支援釘選捷徑 狀態通知 顯示通知以便開啟寄生管理員 - 沒有捷徑,無法停用通知 更新通道 穩定版 測試版 每夜構建 - Xposed API 呼叫保護 - 封鎖動態載入的模組代碼以使用 Xposed API,這可能會損毀某些模組但有益於安全性 自述檔案 版本 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6b4b2694e..d13e49e3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -172,8 +172,6 @@ Framework Disable verbose logs Verbose logs are required to report issues - Enable log watchdog - Log watchdog of LSPosed modifies system properties, which could be exploited to detect LSPosed Black dark theme Use the pure black theme if dark theme is enabled Theme @@ -202,13 +200,10 @@ The current default launcher does not support pin shortcuts Status Notification Show a notification that can open parasitic manager - No shortcut, cannot disable notification Update channel Stable Beta Nightly build - Xposed API call protection - Block dynamically loaded module code to use Xposed API, this may break some modules but benefit security Readme Releases diff --git a/app/src/main/res/xml/prefs.xml b/app/src/main/res/xml/prefs.xml index 6b9d8691c..35511c6b3 100644 --- a/app/src/main/res/xml/prefs.xml +++ b/app/src/main/res/xml/prefs.xml @@ -91,22 +91,6 @@ android:summary="@string/settings_disable_verbose_log_summary" android:title="@string/settings_disable_verbose_log" /> - - - - ("format") { "hiddenapi/*/build.gradle.kts", "services/*-service/build.gradle.kts", ) + dependsOn(":daemon:ktfmtFormat") dependsOn(":xposed:ktfmtFormat") dependsOn(":zygisk:ktfmtFormat") } diff --git a/daemon/README.md b/daemon/README.md new file mode 100644 index 000000000..8e5572ff7 --- /dev/null +++ b/daemon/README.md @@ -0,0 +1,87 @@ +# Vector Daemon Subsystem + +The Vector daemon is a standalone, root-privileged Dalvik executable bootstrapped via `app_process`. Operating entirely outside the standard Android application sandbox, it serves as the central coordinator, state manager, and inter-process communication (IPC) asset server for the Vector framework. + +Target processes operating under strict Android sandbox and SELinux constraints cannot safely access external configuration files or SQLite databases. The daemon offloads these operations, providing an IPC backend that serves memory-mapped resources, configuration states, and native file descriptors to target applications securely and efficiently. + +## Directory Structure + +The daemon is organized into discrete packages handling IPC, state management, OS interfacing, and native environments. + +```text +src/main/ +├── jni/ # Native C++ implementations (dex2oat wrapper, logcat parser) +└── kotlin/org/matrix/vector/daemon/ + ├── data/ # SQLite schema, immutable state cache, and file operations + ├── env/ # UNIX domain socket servers and native process monitors + ├── ipc/ # AIDL endpoints (Application, Manager, Module, SystemServer) + ├── system/ # System binder delegates and Notification UI + ├── utils/ # Context forgery, signature verification, and JNI bridges + ├── Cli.kt # Command-line interface definitions + ├── VectorDaemon.kt # Main entry point and looper initialization + └── VectorService.kt # Primary IDaemonService implementation +``` + +## Concurrency and State Management + +To handle concurrent IPC requests without starving Android Binder thread pools, the daemon separates background I/O operations from state reads. + +* Immutable State Container: The `DaemonState` data class holds a frozen snapshot of all enabled modules and process scopes. IPC threads read from this object without acquiring locks. +* Atomic Swaps: When the underlying SQLite database changes, the daemon triggers a conflated channel request. A background coroutine queries the database, computes the new module topology, instantiates a new `DaemonState`, and atomically swaps the reference in `ConfigCache`. +* Preference Isolation: High-frequency module preference reads and writes are decoupled from the core state. Managed by `PreferenceStore`, preferences are serialized as binary blobs and pushed as differential updates to modules, preventing unnecessary cache rebuilds. + +## IPC Architecture + +The daemon implements a multi-layered IPC design utilizing Android's Binder mechanism and UNIX domain sockets. It avoids registering standard AIDL services with `ServiceManager`, relying instead on intercepting Binder transactions via the Zygisk module and actively pushing Binder references to target processes. + +### 1. System Server Bootstrapping +During device boot, the daemon establishes a communication channel with the native Vector Zygisk module residing in `system_server`. + +* The daemon registers an `IServiceCallback` to listen for the registration of a hardware proxy service (typically the `serial` service). Once intercepted, the daemon replaces the proxy service with its own binder. +* The Zygisk module queries this proxy service to retrieve the framework loader DEX via `SharedMemory` and the class obfuscation map. +* Concurrently, the daemon sends a raw `ACTION_SEND_BINDER` transaction to the `activity` service. The Zygisk module's JNI hook intercepts this transaction before it reaches the Activity Manager, extracting and storing the daemon's primary `VectorService` binder for future use. + +### 2. Target Application Rendezvous +When a standard user application spawns, it requests framework access from the daemon. + +* The target application queries the `activity` service. The Zygisk module inside `system_server` intercepts this query. +* The `system_server` forwards the application's UID, PID, process name, and a newly created heartbeat `BBinder` to the daemon using the previously stored `VectorService` reference. +* The daemon verifies the request against its `ConfigCache` to determine if the application is within the scope of any enabled modules. +* If approved, the daemon returns an `ApplicationService` binder, which the `system_server` passes back to the target application. +* The daemon links a `DeathRecipient` to the heartbeat binder to automatically clean up internal tracking maps when the application process dies. +* The target application uses the `ApplicationService` binder to fetch its specific module list, framework DEX, and obfuscation map. + +### 3. Libxposed Module Injection +Unlike target applications which request access, the daemon actively pushes its API binder to module processes. This mechanism is strictly limited to modules utilizing the modern libxposed API. + +* The daemon registers an `IUidObserver` with the Activity Manager to monitor process lifecycles. +* When a UID becomes active, `ModuleService` checks if the UID belongs to an enabled libxposed module. +* The daemon retrieves an `IXposedService` binder. To deliver it, the daemon calls `IActivityManager.getContentProviderExternal`, targeting a synthetic authority constructed from the module's package name. +* The daemon executes `IContentProvider.call` with the action `SEND_BINDER` and a `Bundle` containing the binder. This injects the binder into the module's process space before `Application.onCreate` executes, providing access to API verification, scope requests, and remote preferences. + +### 4. Native Socket IPC +For native components that operate outside the Java Binder context, the daemon provisions two distinct types of UNIX domain sockets. + +* Command-Line Interface: The `CliSocketServer` exposes a filesystem-based socket at `/data/adb/lspd/.cli_sock`. The CLI client authenticates using a compiled-in UUID token and communicates using structured JSON. For live log streaming, the daemon attaches the log file's raw `FileDescriptor` to the socket reply payload, allowing the client to read directly from the OS-level stream buffer. +* Dex2Oat Wrapper: The `Dex2OatServer` listens on an abstract UNIX domain socket. To prevent conflicts and detection, the exact name of this abstract socket is randomized during module installation. The C++ `dex2oat` wrapper connects to this socket to receive necessary file descriptors via `SCM_RIGHTS`. + +## Native Environment Subsystems + +The daemon relies on native C++ subsystems to intercept Android's compilation pipeline and parse system log buffers directly, avoiding the overhead and limitations of standard shell utilities. + +### AOT Compilation Hijacking + +Android's ART compiler aggressively inlines methods, which permanently prevents those methods from being hooked at runtime. To enforce the `--inline-max-code-units=0` flag system-wide, Vector utilizes a C++ binary wrapper mounted over the system's `dex2oat` and `dex2oat64` binaries. + +The daemon manages this interception entirely through its native JNI layer. To ensure the replaced compiler binaries are globally visible to all newly spawned application processes, the daemon forks a privileged child process and uses `setns` with `CLONE_NEWNS` to enter the `init` (`PID 1`) mount namespace via `/proc/1/ns/mnt`. It then performs read-only bind mounts (`MS_BIND | MS_REMOUNT | MS_RDONLY`) over the target compiler binaries located in the `/apex` mount points. + +When the wrapper executes, it connects to the daemon's abstract UNIX domain socket to retrieve the original compiler binary and the hooking library (`liboat_hook.so`) via `SCM_RIGHTS`. To guarantee the wrapper can connect without SELinux denials, the daemon dynamically writes to `/proc/self/task/[tid]/attr/sockcreate` before binding the socket. This instructs the kernel to label the abstract socket with a specific context, such as `u:r:dex2oat:s0` or `u:r:installd:s0`, matching the strict domains under which the compiler operates. + +If the wrapper is disabled or incompatible, the daemon unmounts the binaries and utilizes `resetprop` to inject the inline flag directly into the `dalvik.vm.dex2oat-flags` system property as a fallback. The Kotlin daemon continuously monitors SELinux states via a `FileObserver` on `/sys/fs/selinux/enforce` and its policy files. It dynamically remounts the wrappers if the system drops to permissive mode or alters policy, ensuring the interception persists across state changes. + +### Native Logcat Telemetry +Instead of relying on standard logcat shell execution, the daemon runs a native C++ process that interfaces directly with Android's `liblog` buffers (`LOG_ID_MAIN` and `LOG_ID_CRASH`). + +The native parser performs zero-copy processing of log events, strictly filtering output by predefined exact tags (e.g., Magisk, KernelSU) and prefix tags (e.g., dex2oat, Vector, LSPosed). It writes the filtered output into two rotating log files: one for module frameworks and one for verbose system debugging, rotating them automatically when they reach 4MB. + +To control this isolated native loop, the Kotlin daemon injects specific string triggers (such as `!!refresh_modules!!` or `!!start_verbose!!`) directly into the Android log stream. The C++ parser intercepts these specific messages originating from its own parent PID and dynamically rotates its file descriptors or alters its verbosity state without requiring additional IPC overhead. diff --git a/daemon/build.gradle.kts b/daemon/build.gradle.kts index cee0e4d42..40b7e7b61 100644 --- a/daemon/build.gradle.kts +++ b/daemon/build.gradle.kts @@ -1,6 +1,7 @@ import com.android.build.api.dsl.ApplicationExtension import com.android.ide.common.signing.KeystoreHelper import java.io.PrintStream +import java.util.UUID val defaultManagerPackageName: String by rootProject.extra val injectedPackageName: String by rootProject.extra @@ -9,98 +10,102 @@ val versionCodeProvider: Provider by rootProject.extra val versionNameProvider: Provider by rootProject.extra plugins { - alias(libs.plugins.agp.app) - alias(libs.plugins.lsplugin.resopt) + alias(libs.plugins.agp.app) + alias(libs.plugins.kotlin) + alias(libs.plugins.ktfmt) } android { - buildFeatures { - prefab = true - buildConfig = true - } + defaultConfig { + buildConfigField( + "String", + "DEFAULT_MANAGER_PACKAGE_NAME", + """"$defaultManagerPackageName"""", + ) + buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") + buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") + buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") + buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") + buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) - defaultConfig { - applicationId = "org.lsposed.daemon" + val cliToken = UUID.randomUUID() + // Inject the MSB and LSB as Long constants + buildConfigField("Long", "CLI_TOKEN_MSB", "${cliToken.mostSignificantBits}L") + buildConfigField("Long", "CLI_TOKEN_LSB", "${cliToken.leastSignificantBits}L") + } - buildConfigField( - "String", - "DEFAULT_MANAGER_PACKAGE_NAME", - """"$defaultManagerPackageName"""", - ) - buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") - buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") - buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") - buildConfigField("String", "VERSION_NAME", """"${versionNameProvider.get()}"""") - buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) + buildTypes { + all { externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } } } + release { + isMinifyEnabled = true + proguardFiles("proguard-rules.pro") } + } - buildTypes { - all { - externalNativeBuild { cmake { arguments += "-DANDROID_ALLOW_UNDEFINED_SYMBOLS=true" } } - } - release { - isMinifyEnabled = true - isShrinkResources = true - proguardFiles("proguard-rules.pro") - } - } + externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } } - externalNativeBuild { cmake { path("src/main/jni/CMakeLists.txt") } } - - namespace = "org.lsposed.daemon" + namespace = "org.matrix.vector.daemon" } android.applicationVariants.all { - val variantCapped = name.replaceFirstChar { it.uppercase() } - val variantLowered = name.lowercase() + val variantCapped = name.replaceFirstChar { it.uppercase() } + val variantLowered = name.lowercase() + + val outSrcDir = layout.buildDirectory.dir("generated/source/signInfo/${variantLowered}").get() + val signInfoTask = + tasks.register("generate${variantCapped}SignInfo") { + dependsOn(":app:validateSigning${variantCapped}") + val sign = + rootProject + .project(":app") + .extensions + .getByType(ApplicationExtension::class.java) + .buildTypes + .named(variantLowered) + .get() + .signingConfig + val outSrc = file("$outSrcDir/org/matrix/vector/daemon/utils/SignInfo.kt") + outputs.file(outSrc) + doLast { + outSrc.parentFile.mkdirs() + val certificateInfo = + KeystoreHelper.getCertificateInfo( + sign?.storeType, + sign?.storeFile, + sign?.storePassword, + sign?.keyPassword, + sign?.keyAlias, + ) - val outSrcDir = layout.buildDirectory.dir("generated/source/signInfo/${variantLowered}").get() - val signInfoTask = - tasks.register("generate${variantCapped}SignInfo") { - dependsOn(":app:validateSigning${variantCapped}") - val sign = - rootProject - .project(":app") - .extensions - .getByType(ApplicationExtension::class.java) - .buildTypes - .named(variantLowered) - .get() - .signingConfig - val outSrc = file("$outSrcDir/org/lsposed/lspd/util/SignInfo.java") - outputs.file(outSrc) - doLast { - outSrc.parentFile.mkdirs() - val certificateInfo = - KeystoreHelper.getCertificateInfo( - sign?.storeType, - sign?.storeFile, - sign?.storePassword, - sign?.keyPassword, - sign?.keyAlias, - ) - PrintStream(outSrc) - .print( - """ - |package org.lsposed.lspd.util; - |public final class SignInfo { - | public static final byte[] CERTIFICATE = {${ + PrintStream(outSrc) + .print( + """ + |package org.matrix.vector.daemon.utils + | + |object SignInfo { + | @JvmField + | val CERTIFICATE = byteArrayOf(${ certificateInfo.certificate.encoded.joinToString(",") - }}; + }) |}""" - .trimMargin() - ) - } + .trimMargin()) } - registerJavaGeneratingTask(signInfoTask, outSrcDir.asFile) + } + // registeoJavaGeneratingTask(signInfoTask, outSrcDir.asFile) + + kotlin.sourceSets.getByName(variantLowered) { kotlin.srcDir(signInfoTask.map { outSrcDir }) } } dependencies { - implementation(libs.agp.apksig) - implementation(projects.external.apache) - implementation(projects.hiddenapi.bridge) - implementation(projects.services.daemonService) - implementation(projects.services.managerService) - compileOnly(libs.androidx.annotation) - compileOnly(projects.hiddenapi.stubs) + implementation(libs.agp.apksig) + implementation(libs.gson) + implementation(libs.picocli) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.kotlinx.coroutines.core) + implementation(projects.external.apache) + implementation(projects.hiddenapi.bridge) + implementation(projects.services.daemonService) + implementation(projects.services.managerService) + compileOnly(libs.androidx.annotation) + compileOnly(projects.hiddenapi.stubs) } diff --git a/daemon/proguard-rules.pro b/daemon/proguard-rules.pro index 74c73f81e..4f1bd0b1a 100644 --- a/daemon/proguard-rules.pro +++ b/daemon/proguard-rules.pro @@ -1,17 +1,13 @@ -keepclasseswithmembers,includedescriptorclasses class * { native ; } --keepclasseswithmembers class org.lsposed.lspd.Main { +-keepclasseswithmembers class org.matrix.vector.daemon.VectorDaemon { public static void main(java.lang.String[]); } --keepclasseswithmembers class org.lsposed.lspd.service.Dex2OatService { - private java.lang.String devTmpDir; - private java.lang.String magiskPath; - private java.lang.String fakeBin32; - private java.lang.String fakeBin64; - private java.lang.String[] dex2oatBinaries; +-keepclasseswithmembers class org.matrix.vector.daemon.Cli { + public static void main(java.lang.String[]); } --keepclasseswithmembers class org.lsposed.lspd.service.LogcatService { +-keepclasseswithmembers class org.matrix.vector.daemon.env.LogcatMonitor { private int refreshFd(boolean); } -keepclassmembers class ** implements android.content.ContextWrapper { diff --git a/daemon/src/main/java/org/lsposed/lspd/Main.java b/daemon/src/main/java/org/lsposed/lspd/Main.java deleted file mode 100644 index 3263091f1..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/Main.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.lsposed.lspd; - -import org.lsposed.lspd.service.ServiceManager; - -public class Main { - - public static void main(String[] args) { - ServiceManager.start(args); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java deleted file mode 100644 index 04fbe6a4a..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.app.ContentProviderHolder; -import android.app.IActivityManager; -import android.app.IApplicationThread; -import android.app.IServiceConnection; -import android.app.IUidObserver; -import android.app.ProfilerInfo; -import android.content.Context; -import android.content.IContentProvider; -import android.content.IIntentReceiver; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.UserInfo; -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -public class ActivityManagerService { - private static IActivityManager am = null; - private static IBinder binder = null; - private static IApplicationThread appThread = null; - private static IBinder token = null; - - private static final IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "am is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - am = null; - appThread = null; - token = null; - } - }; - - public static IActivityManager getActivityManager() { - if (binder == null || am == null) { - binder = ServiceManager.getService(Context.ACTIVITY_SERVICE); - if (binder == null) return null; - try { - binder.linkToDeath(deathRecipient, 0); - am = IActivityManager.Stub.asInterface(binder); - // For oddo Android 9 we cannot set activity controller here... - // am.setActivityController(null, false); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - return am; - } - - public static int broadcastIntentWithFeature(String callingFeatureId, - Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, - String resultData, Bundle map, String[] requiredPermissions, - int appOp, Bundle options, boolean serialized, boolean sticky, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return -1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - try { - return am.broadcastIntentWithFeature(appThread, callingFeatureId, intent, resolvedType, resultTo, - resultCode, resultData, null, requiredPermissions, null, null, appOp, null, - serialized, sticky, userId); - } catch (NoSuchMethodError ignored) { - return am.broadcastIntentWithFeature(appThread, callingFeatureId, intent, resolvedType, resultTo, - resultCode, resultData, null, requiredPermissions, null, appOp, null, - serialized, sticky, userId); - } - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - return am.broadcastIntentWithFeature(appThread, callingFeatureId, intent, resolvedType, resultTo, resultCode, resultData, map, requiredPermissions, appOp, options, serialized, sticky, userId); - } else { - return am.broadcastIntent(appThread, intent, resolvedType, resultTo, resultCode, resultData, map, requiredPermissions, appOp, options, serialized, sticky, userId); - } - } - - public static void forceStopPackage(String packageName, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return; - am.forceStopPackage(packageName, userId); - } - - public static boolean startUserInBackground(int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return false; - return am.startUserInBackground(userId); - } - - public static Intent registerReceiver(String callerPackage, - String callingFeatureId, IIntentReceiver receiver, IntentFilter filter, - String requiredPermission, int userId, int flags) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return null; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - return am.registerReceiverWithFeature(appThread, callerPackage, callingFeatureId, "null", receiver, filter, requiredPermission, userId, flags); - else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - return am.registerReceiverWithFeature(appThread, callerPackage, callingFeatureId, receiver, filter, requiredPermission, userId, flags); - } else { - return am.registerReceiver(appThread, callerPackage, receiver, filter, requiredPermission, userId, flags); - } - } - - public static void finishReceiver(IBinder intentReceiver, IBinder applicationThread, int resultCode, - String resultData, Bundle resultExtras, boolean resultAbort, - int flags) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - am.finishReceiver(applicationThread, resultCode, resultData, resultExtras, resultAbort, flags); - } else { - am.finishReceiver(intentReceiver, resultCode, resultData, resultExtras, resultAbort, flags); - } - } - - public static int bindService(Intent service, - String resolvedType, IServiceConnection connection, int flags, - String callingPackage, int userId) throws RemoteException { - - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return -1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - return am.bindService(appThread, token, service, resolvedType, connection, (long) flags, callingPackage, userId); - else - return am.bindService(appThread, token, service, resolvedType, connection, flags, callingPackage, userId); - } - - public static boolean unbindService(IServiceConnection connection) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return false; - return am.unbindService(connection); - } - - public static int startActivityAsUserWithFeature(String callingPackage, - String callingFeatureId, Intent intent, String resolvedType, - IBinder resultTo, String resultWho, int requestCode, int flags, - ProfilerInfo profilerInfo, Bundle options, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null || appThread == null) return -1; - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - return am.startActivityAsUserWithFeature(appThread, callingPackage, callingFeatureId, intent, resolvedType, resultTo, resultWho, requestCode, flags, profilerInfo, options, userId); - } else { - return am.startActivityAsUser(appThread, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, flags, profilerInfo, options, userId); - } - } - - public static void onSystemServerContext(IApplicationThread thread, IBinder token) { - ActivityManagerService.appThread = thread; - ActivityManagerService.token = token; - } - - public static boolean switchUser(int userid) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return false; - return am.switchUser(userid); - } - - public static UserInfo getCurrentUser() throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return null; - return am.getCurrentUser(); - } - - public static Configuration getConfiguration() throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return null; - return am.getConfiguration(); - } - - public static IContentProvider getContentProvider(String auth, int userId) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return null; - ContentProviderHolder holder; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - holder = am.getContentProviderExternal(auth, userId, token, null); - } else { - holder = am.getContentProviderExternal(auth, userId, token); - } - return holder != null ? holder.provider : null; - } - - public static void registerUidObserver(IUidObserver observer, int which, int cutpoint, String callingPackage) throws RemoteException { - IActivityManager am = getActivityManager(); - if (am == null) return; - am.registerUidObserver(observer, which, cutpoint, callingPackage); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java b/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java deleted file mode 100644 index 850cc4d08..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.app.ActivityManager; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Parcel; -import android.os.ServiceManager; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; - -import org.lsposed.daemon.BuildConfig; - -import java.lang.reflect.Field; -import java.util.Map; - -public class BridgeService { - - static final int TRANSACTION_CODE = ('_' << 24) | ('V' << 16) | ('E' << 8) | 'C'; - private static final String SERVICE_NAME = "activity"; - - enum ACTION { - ACTION_UNKNOWN, - ACTION_SEND_BINDER, - ACTION_GET_BINDER, - } - - public interface Listener { - void onSystemServerRestarted(); - - void onResponseFromBridgeService(boolean response); - - void onSystemServerDied(); - } - - private static IBinder serviceBinder = null; - - private static Listener listener; - private static IBinder bridgeService; - private static final IBinder.DeathRecipient bridgeRecipient = new IBinder.DeathRecipient() { - - @Override - public void binderDied() { - Log.i(TAG, "service " + SERVICE_NAME + " is dead. "); - - try { - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - Field field = ServiceManager.class.getDeclaredField("sServiceManager"); - field.setAccessible(true); - field.set(null, null); - - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - field = ServiceManager.class.getDeclaredField("sCache"); - field.setAccessible(true); - Object sCache = field.get(null); - if (sCache instanceof Map) { - //noinspection rawtypes - ((Map) sCache).clear(); - } - Log.i(TAG, "clear ServiceManager"); - - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - field = ActivityManager.class.getDeclaredField("IActivityManagerSingleton"); - field.setAccessible(true); - Object singleton = field.get(null); - if (singleton != null) { - //noinspection PrivateApi DiscouragedPrivateApi - field = Class.forName("android.util.Singleton").getDeclaredField("mInstance"); - field.setAccessible(true); - synchronized (singleton) { - field.set(singleton, null); - } - } - Log.i(TAG, "clear ActivityManager"); - } catch (Throwable e) { - Log.w(TAG, "clear ServiceManager: " + Log.getStackTraceString(e)); - } - - bridgeService.unlinkToDeath(this, 0); - bridgeService = null; - listener.onSystemServerDied(); - new Handler(Looper.getMainLooper()).post(() -> sendToBridge(serviceBinder, true)); - } - }; - - // For service - // This MUST run in main thread - private static synchronized void sendToBridge(IBinder binder, boolean isRestart) { - assert Looper.myLooper() == Looper.getMainLooper(); - try { - Os.seteuid(0); - } catch (ErrnoException e) { - Log.e(TAG, "seteuid 0", e); - } - try { - do { - bridgeService = ServiceManager.getService(SERVICE_NAME); - if (bridgeService != null && bridgeService.pingBinder()) { - break; - } - - Log.i(TAG, "service " + SERVICE_NAME + " is not started, wait 1s."); - - try { - //noinspection BusyWait - Thread.sleep(1000); - } catch (Throwable e) { - Log.w(TAG, "sleep" + Log.getStackTraceString(e)); - } - } while (true); - - if (isRestart && listener != null) { - listener.onSystemServerRestarted(); - } - - try { - bridgeService.linkToDeath(bridgeRecipient, 0); - } catch (Throwable e) { - Log.w(TAG, "linkToDeath " + Log.getStackTraceString(e)); - var snapshot = bridgeService; - sendToBridge(binder, snapshot == null || !snapshot.isBinderAlive()); - return; - } - - boolean res = false; - // try at most three times - for (int i = 0; i < 3; i++) { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - try { - data.writeInt(ACTION.ACTION_SEND_BINDER.ordinal()); - Log.v(TAG, "binder " + binder.toString()); - data.writeStrongBinder(binder); - if (bridgeService == null) break; - res = bridgeService.transact(TRANSACTION_CODE, data, reply, 0); - reply.readException(); - } catch (Throwable e) { - Log.e(TAG, "send binder " + Log.getStackTraceString(e)); - var snapshot = bridgeService; - sendToBridge(binder, snapshot == null || !snapshot.isBinderAlive()); - return; - } finally { - data.recycle(); - reply.recycle(); - } - - if (res) break; - - Log.w(TAG, "no response from bridge, retry in 1s"); - - try { - Thread.sleep(1000); - } catch (InterruptedException ignored) { - } - } - - if (listener != null) { - listener.onResponseFromBridgeService(res); - } - } finally { - try { - if (!BuildConfig.DEBUG) { - Os.seteuid(1000); - } - } catch (ErrnoException e) { - Log.e(TAG, "seteuid 1000", e); - } - } - } - - public static void send(LSPosedService service, Listener listener) { - BridgeService.listener = listener; - BridgeService.serviceBinder = service.asBinder(); - sendToBridge(serviceBinder, false); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java deleted file mode 100644 index 1f4d08e09..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java +++ /dev/null @@ -1,518 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.toGlobalNamespace; - -import android.content.res.AssetManager; -import android.content.res.Resources; -import android.os.Binder; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.SharedMemory; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.models.PreLoadedApk; -import org.lsposed.lspd.util.InstallerVerifier; -import org.lsposed.lspd.util.Utils; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.lang.reflect.Method; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.OpenOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFilePermissions; -import java.time.Instant; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.stream.Stream; -import java.util.zip.Deflater; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import hidden.HiddenApiBridge; - -public class ConfigFileManager { - static final Path basePath = Paths.get("/data/adb/lspd"); - static final Path modulePath = basePath.resolve("modules"); - static final Path daemonApkPath = Paths.get(System.getProperty("java.class.path", null)); - static final Path managerApkPath = daemonApkPath.getParent().resolve("manager.apk"); - static final File magiskDbPath = new File("/data/adb/magisk.db"); - private static final Path lockPath = basePath.resolve("lock"); - private static final Path configDirPath = basePath.resolve("config"); - static final File dbPath = configDirPath.resolve("modules_config.db").toFile(); - private static final Path logDirPath = basePath.resolve("log"); - private static final Path oldLogDirPath = basePath.resolve("log.old"); - private static final DateTimeFormatter formatter = - DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(Utils.getZoneId()); - @SuppressWarnings("FieldCanBeLocal") - private static FileLocker locker = null; - private static Resources res = null; - private static ParcelFileDescriptor fd = null; - private static SharedMemory preloadDex = null; - - static { - try { - Files.createDirectories(basePath); - SELinux.setFileContext(basePath.toString(), "u:object_r:system_file:s0"); - Files.createDirectories(configDirPath); - createLogDirPath(); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - public static void transfer(InputStream in, OutputStream out) throws IOException { - int size = 8192; - var buffer = new byte[size]; - int read; - while ((read = in.read(buffer, 0, size)) >= 0) { - out.write(buffer, 0, read); - } - } - - private static void createLogDirPath() throws IOException { - if (!Files.isDirectory(logDirPath, LinkOption.NOFOLLOW_LINKS)) { - Files.deleteIfExists(logDirPath); - } - Files.createDirectories(logDirPath); - } - - public static Resources getResources() { - loadRes(); - return res; - } - - private static void loadRes() { - if (res != null) return; - try { - var am = AssetManager.class.newInstance(); - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - Method addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); - addAssetPath.setAccessible(true); - //noinspection ConstantConditions - if ((int) addAssetPath.invoke(am, daemonApkPath.toString()) > 0) { - //noinspection deprecation - res = new Resources(am, null, null); - } - } catch (Throwable e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - static void reloadConfiguration() { - loadRes(); - try { - var conf = ActivityManagerService.getConfiguration(); - if (conf != null) - //noinspection deprecation - res.updateConfiguration(conf, res.getDisplayMetrics()); - } catch (Throwable e) { - Log.e(TAG, "reload configuration", e); - } - } - - static ParcelFileDescriptor getManagerApk() throws IOException { - if (fd != null) return fd.dup(); - InstallerVerifier.verifyInstallerSignature(managerApkPath.toString()); - - SELinux.setFileContext(managerApkPath.toString(), "u:object_r:system_file:s0"); - fd = ParcelFileDescriptor.open(managerApkPath.toFile(), ParcelFileDescriptor.MODE_READ_ONLY); - return fd.dup(); - } - - static void deleteFolderIfExists(Path target) throws IOException { - if (Files.notExists(target)) return; - Files.walkFileTree(target, new SimpleFileVisitor<>() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException e) - throws IOException { - if (e == null) { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } else { - throw e; - } - } - }); - } - - public static boolean chattr0(Path path) { - try { - var dir = Os.open(path.toString(), OsConstants.O_RDONLY, 0); - // Clear all special file attributes on the directory - HiddenApiBridge.Os_ioctlInt(dir, Process.is64Bit() ? 0x40086602 : 0x40046602, 0); - Os.close(dir); - return true; - } catch (ErrnoException e) { - // If the operation is not supported (ENOTSUP), it means the filesystem doesn't support attributes. - // We can assume the file is not immutable and proceed. - if (e.errno == OsConstants.ENOTSUP) { - return true; - } - Log.d(TAG, "chattr 0", e); - return false; - } catch (Throwable e) { - Log.d(TAG, "chattr 0", e); - return false; - } - } - - static void moveLogDir() { - try { - if (Files.exists(logDirPath)) { - if (chattr0(logDirPath)) { - deleteFolderIfExists(oldLogDirPath); - Files.move(logDirPath, oldLogDirPath); - } - } - Files.createDirectories(logDirPath); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - private static String getNewLogFileName(String prefix) { - return prefix + "_" + formatter.format(Instant.now()) + ".log"; - } - - static File getNewVerboseLogPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve(getNewLogFileName("verbose")).toFile(); - } - - static File getNewModulesLogPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve(getNewLogFileName("modules")).toFile(); - } - - static File getPropsPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve("props.txt").toFile(); - } - - static File getKmsgPath() throws IOException { - createLogDirPath(); - return logDirPath.resolve("kmsg.log").toFile(); - } - - static void getLogs(ParcelFileDescriptor zipFd) throws IllegalStateException { - try (zipFd; var os = new ZipOutputStream(new FileOutputStream(zipFd.getFileDescriptor()))) { - var comment = String.format(Locale.ROOT, "LSPosed %s %s (%d)", - BuildConfig.BUILD_TYPE, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE); - os.setComment(comment); - os.setLevel(Deflater.BEST_COMPRESSION); - zipAddDir(os, logDirPath); - zipAddDir(os, oldLogDirPath); - zipAddDir(os, Paths.get("/data/tombstones")); - zipAddDir(os, Paths.get("/data/anr")); - var data = Paths.get("/data/data"); - var app1 = data.resolve(BuildConfig.MANAGER_INJECTED_PKG_NAME + "/cache/crash"); - var app2 = data.resolve(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME + "/cache/crash"); - zipAddDir(os, app1); - zipAddDir(os, app2); - zipAddProcOutput(os, "full.log", "logcat", "-b", "all", "-d"); - zipAddProcOutput(os, "dmesg.log", "dmesg"); - var magiskDataDir = Paths.get("/data/adb"); - try (var l = Files.list(magiskDataDir.resolve("modules"))) { - l.forEach(p -> { - zipAddFile(os, p, magiskDataDir); - zipAddFile(os, p.resolve("module.prop"), magiskDataDir); - zipAddFile(os, p.resolve("remove"), magiskDataDir); - zipAddFile(os, p.resolve("disable"), magiskDataDir); - zipAddFile(os, p.resolve("update"), magiskDataDir); - zipAddFile(os, p.resolve("sepolicy.rule"), magiskDataDir); - }); - } - var proc = Paths.get("/proc"); - for (var pid : new String[]{"self", String.valueOf(Binder.getCallingPid())}) { - var pidPath = proc.resolve(pid); - zipAddFile(os, pidPath.resolve("maps"), proc); - zipAddFile(os, pidPath.resolve("mountinfo"), proc); - zipAddFile(os, pidPath.resolve("status"), proc); - } - zipAddFile(os, dbPath.toPath(), configDirPath); - ConfigManager.getInstance().exportScopes(os); - } catch (Throwable e) { - Log.w(TAG, "get log", e); - throw new IllegalStateException(e); - } - } - - private static void zipAddProcOutput(ZipOutputStream os, String name, String... command) { - try (var is = new ProcessBuilder(command).start().getInputStream()) { - os.putNextEntry(new ZipEntry(name)); - transfer(is, os); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } - - private static void zipAddFile(ZipOutputStream os, Path path, Path base) { - var name = base.relativize(path).toString(); - if (Files.isDirectory(path)) { - try { - os.putNextEntry(new ZipEntry(name + "/")); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } else if (Files.exists(path)) { - try (var is = new FileInputStream(path.toFile())) { - os.putNextEntry(new ZipEntry(name)); - transfer(is, os); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } - } - - private static void zipAddDir(ZipOutputStream os, Path path) throws IOException { - if (!Files.isDirectory(path)) return; - Files.walkFileTree(path, new SimpleFileVisitor<>() { - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (Files.isRegularFile(file)) { - var name = path.getParent().relativize(file).toString(); - try (var is = new FileInputStream(file.toFile())) { - os.putNextEntry(new ZipEntry(name)); - transfer(is, os); - os.closeEntry(); - } catch (IOException e) { - Log.w(TAG, name, e); - } - } - return FileVisitResult.CONTINUE; - } - }); - } - - private static SharedMemory readDex(InputStream in, boolean obfuscate) throws IOException, ErrnoException { - var memory = SharedMemory.create(null, in.available()); - var byteBuffer = memory.mapReadWrite(); - Channels.newChannel(in).read(byteBuffer); - SharedMemory.unmap(byteBuffer); - if (obfuscate) { - var newMemory = ObfuscationManager.obfuscateDex(memory); - if (memory != newMemory) { - memory.close(); - memory = newMemory; - } - } - memory.setProtect(OsConstants.PROT_READ); - return memory; - } - - private static void readDexes(ZipFile apkFile, List preLoadedDexes, - boolean obfuscate) { - int secondary = 2; - for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null; - dexFile = apkFile.getEntry("classes" + secondary + ".dex"), secondary++) { - try (var is = apkFile.getInputStream(dexFile)) { - preLoadedDexes.add(readDex(is, obfuscate)); - } catch (IOException | ErrnoException e) { - Log.w(TAG, "Can not load " + dexFile + " in " + apkFile, e); - } - } - } - - private static void readName(ZipFile apkFile, String initName, List names) { - var initEntry = apkFile.getEntry(initName); - if (initEntry == null) return; - try (var in = apkFile.getInputStream(initEntry)) { - var reader = new BufferedReader(new InputStreamReader(in)); - String name; - while ((name = reader.readLine()) != null) { - name = name.trim(); - if (name.isEmpty() || name.startsWith("#")) continue; - names.add(name); - } - } catch (IOException | OutOfMemoryError e) { - Log.e(TAG, "Can not open " + initEntry, e); - } - } - - @Nullable - static PreLoadedApk loadModule(String path, boolean obfuscate) { - if (path == null) return null; - var file = new PreLoadedApk(); - var preLoadedDexes = new ArrayList(); - var moduleClassNames = new ArrayList(1); - var moduleLibraryNames = new ArrayList(1); - try (var apkFile = new ZipFile(toGlobalNamespace(path))) { - readDexes(apkFile, preLoadedDexes, obfuscate); - readName(apkFile, "META-INF/xposed/java_init.list", moduleClassNames); - if (moduleClassNames.isEmpty()) { - file.legacy = true; - readName(apkFile, "assets/xposed_init", moduleClassNames); - readName(apkFile, "assets/native_init", moduleLibraryNames); - } else { - file.legacy = false; - readName(apkFile, "META-INF/xposed/native_init.list", moduleLibraryNames); - } - } catch (IOException e) { - Log.e(TAG, "Can not open " + path, e); - return null; - } - if (preLoadedDexes.isEmpty()) return null; - if (moduleClassNames.isEmpty()) return null; - - if (obfuscate) { - var signatures = ObfuscationManager.getSignatures(); - for (int i = 0; i < moduleClassNames.size(); i++) { - var s = moduleClassNames.get(i); - for (var entry : signatures.entrySet()) { - if (s.startsWith(entry.getKey())) { - moduleClassNames.add(i, s.replace(entry.getKey(), entry.getValue())); - } - } - } - } - - file.preLoadedDexes = preLoadedDexes; - file.moduleClassNames = moduleClassNames; - file.moduleLibraryNames = moduleLibraryNames; - return file; - } - - static boolean tryLock() { - var openOptions = new HashSet(); - openOptions.add(StandardOpenOption.CREATE); - openOptions.add(StandardOpenOption.WRITE); - var p = PosixFilePermissions.fromString("rw-------"); - var permissions = PosixFilePermissions.asFileAttribute(p); - - try { - var lockChannel = FileChannel.open(lockPath, openOptions, permissions); - locker = new FileLocker(lockChannel); - return locker.isValid(); - } catch (Throwable e) { - return false; - } - } - - synchronized static SharedMemory getPreloadDex(boolean obfuscate) { - if (preloadDex == null) { - try (var is = new FileInputStream("framework/lspd.dex")) { - preloadDex = readDex(is, obfuscate); - } catch (Throwable e) { - Log.e(TAG, "preload dex", e); - } - } - return preloadDex; - } - - static void ensureModuleFilePath(String path) throws RemoteException { - if (path == null || path.indexOf(File.separatorChar) >= 0 || ".".equals(path) || "..".equals(path)) { - throw new RemoteException("Invalid path: " + path); - } - } - - static Path resolveModuleDir(String packageName, String dir, int userId, int uid) throws IOException { - var path = modulePath.resolve(String.valueOf(userId)).resolve(packageName).resolve(dir).normalize(); - // Ensure the directory and any necessary parent directories exist. - path.toFile().mkdirs(); - - if (SELinux.getFileContext(path.toString()) != "u:object_r:xposed_data:s0") { - // SELinux label could be reset after a reboot. - try { - setSelinuxContextRecursive(path, "u:object_r:xposed_data:s0"); - Os.chown(path.toString(), uid, uid); - Os.chmod(path.toString(), 0755); - } catch (ErrnoException e) { - throw new IOException(e); - } - } - return path; - } - - private static void setSelinuxContextRecursive(Path path, String context) throws IOException { - try { - SELinux.setFileContext(path.toString(), context); - - if (Files.isDirectory(path)) { - try (Stream stream = Files.list(path)) { - for (Path entry : (Iterable) stream::iterator) { - setSelinuxContextRecursive(entry, context); - } - } - } - } catch (Exception e) { - throw new IOException("Failed to recursively set SELinux context for " + path, e); - } - } - - private static class FileLocker { - private final FileChannel lockChannel; - private final FileLock locker; - - FileLocker(FileChannel lockChannel) throws IOException { - this.lockChannel = lockChannel; - this.locker = lockChannel.tryLock(); - } - - boolean isValid() { - return this.locker != null && this.locker.isValid(); - } - - @Override - protected void finalize() throws Throwable { - this.locker.release(); - this.lockChannel.close(); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java deleted file mode 100644 index 4cdc43903..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java +++ /dev/null @@ -1,1266 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.PackageService.MATCH_ALL_FLAGS; -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.existsInGlobalNamespace; -import static org.lsposed.lspd.service.ServiceManager.toGlobalNamespace; - -import android.annotation.SuppressLint; -import android.content.ContentValues; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageParser; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteStatement; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.SharedMemory; -import android.os.SystemClock; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.apache.commons.lang3.SerializationUtilsX; -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.models.Application; -import org.lsposed.lspd.models.Module; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Serializable; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; - -import hidden.HiddenApiBridge; - -public class ConfigManager { - private static ConfigManager instance = null; - - private final SQLiteDatabase db = openDb(); - - private boolean verboseLog = true; - private boolean logWatchdog = true; - private boolean dexObfuscate = true; - private boolean enableStatusNotification = true; - private Path miscPath = null; - - private int managerUid = -1; - - private final Handler cacheHandler; - - private long lastModuleCacheTime = 0; - private long requestModuleCacheTime = 0; - - private long lastScopeCacheTime = 0; - private long requestScopeCacheTime = 0; - - private String api = "(???)"; - - static class ProcessScope { - final String processName; - final int uid; - - ProcessScope(@NonNull String processName, int uid) { - this.processName = processName; - this.uid = uid; - } - - @Override - public boolean equals(@Nullable Object o) { - if (o instanceof ProcessScope) { - ProcessScope p = (ProcessScope) o; - return p.processName.equals(processName) && p.uid == uid; - } - return false; - } - - @Override - public int hashCode() { - return Objects.hashCode(processName) ^ uid; - } - } - - private static final String CREATE_MODULES_TABLE = "CREATE TABLE IF NOT EXISTS modules (" + - "mid integer PRIMARY KEY AUTOINCREMENT," + - "module_pkg_name text NOT NULL UNIQUE," + - "apk_path text NOT NULL, " + - "enabled BOOLEAN DEFAULT 0 " + - "CHECK (enabled IN (0, 1))" + - ");"; - - private static final String CREATE_SCOPE_TABLE = "CREATE TABLE IF NOT EXISTS scope (" + - "mid integer," + - "app_pkg_name text NOT NULL," + - "user_id integer NOT NULL," + - "PRIMARY KEY (mid, app_pkg_name, user_id)," + - "CONSTRAINT scope_module_constraint" + - " FOREIGN KEY (mid)" + - " REFERENCES modules (mid)" + - " ON DELETE CASCADE" + - ");"; - - private static final String CREATE_CONFIG_TABLE = "CREATE TABLE IF NOT EXISTS configs (" + - "module_pkg_name text NOT NULL," + - "user_id integer NOT NULL," + - "`group` text NOT NULL," + - "`key` text NOT NULL," + - "data blob NOT NULL," + - "PRIMARY KEY (module_pkg_name, user_id, `group`, `key`)," + - "CONSTRAINT config_module_constraint" + - " FOREIGN KEY (module_pkg_name)" + - " REFERENCES modules (module_pkg_name)" + - " ON DELETE CASCADE" + - ");"; - - private final Map> cachedScope = new ConcurrentHashMap<>(); - - // packageName, Module - private final Map cachedModule = new ConcurrentHashMap<>(); - - // packageName, userId, group, key, value - private final Map, Map>> cachedConfig = new ConcurrentHashMap<>(); - - private Set scopeRequestBlocked = new HashSet<>(); - - private static SQLiteDatabase openDb() { - var params = new SQLiteDatabase.OpenParams.Builder() - .addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) - .setErrorHandler(sqLiteDatabase -> Log.w(TAG, "database corrupted")); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - params.setSynchronousMode("NORMAL"); - } - return SQLiteDatabase.openDatabase(ConfigFileManager.dbPath.getAbsoluteFile(), params.build()); - } - - private void updateCaches(boolean sync) { - synchronized (cacheHandler) { - requestScopeCacheTime = requestModuleCacheTime = SystemClock.elapsedRealtime(); - } - if (sync) { - cacheModules(); - } else { - cacheHandler.post(this::cacheModules); - } - } - - // for system server, cache is not yet ready, we need to query database for it - public boolean shouldSkipSystemServer() { - if (!SELinux.checkSELinuxAccess("u:r:system_server:s0", "u:r:system_server:s0", "process", "execmem")) { - Log.e(TAG, "skip injecting into android because sepolicy was not loaded properly"); - return true; // skip - } - /* - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"modules.mid"}, "app_pkg_name=? AND enabled=1", new String[]{"system"}, null, null, null)) { - return cursor == null || !cursor.moveToNext(); - }*/ - return false; - } - - @SuppressLint("BlockedPrivateApi") - public List getModulesForSystemServer() { - List modules = new LinkedList<>(); - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"module_pkg_name", "apk_path"}, "app_pkg_name=? AND enabled=1", new String[]{"system"}, null, null, null)) { - int apkPathIdx = cursor.getColumnIndex("apk_path"); - int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - while (cursor.moveToNext()) { - var module = new Module(); - module.apkPath = cursor.getString(apkPathIdx); - module.packageName = cursor.getString(pkgNameIdx); - var cached = cachedModule.get(module.packageName); - if (cached != null) { - modules.add(cached); - continue; - } - var statPath = toGlobalNamespace("/data/user_de/0/" + module.packageName).getAbsolutePath(); - try { - module.appId = Os.stat(statPath).st_uid; - } catch (ErrnoException e) { - Log.w(TAG, "cannot stat " + statPath, e); - module.appId = -1; - } - try { - var apkFile = new File(module.apkPath); - var pkg = new PackageParser().parsePackage(apkFile, 0, false); - module.applicationInfo = pkg.applicationInfo; - module.applicationInfo.sourceDir = module.apkPath; - module.applicationInfo.dataDir = statPath; - module.applicationInfo.deviceProtectedDataDir = statPath; - HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(module.applicationInfo, statPath); - module.applicationInfo.processName = module.packageName; - } catch (PackageParser.PackageParserException e) { - Log.w(TAG, "failed to parse " + module.apkPath, e); - } - module.service = new LSPInjectedModuleService(module.packageName); - modules.add(module); - } - } - - return modules.parallelStream().filter(m -> { - var file = ConfigFileManager.loadModule(m.apkPath, dexObfuscate); - if (file == null) { - Log.w(TAG, "Can not load " + m.apkPath + ", skip!"); - return false; - } - m.file = file; - cachedModule.putIfAbsent(m.packageName, m); - return true; - }).collect(Collectors.toList()); - } - - private synchronized void updateConfig() { - Map config = getModulePrefs("lspd", 0, "config"); - - Object bool = config.get("enable_verbose_log"); - verboseLog = bool == null || (boolean) bool; - - bool = config.get("enable_log_watchdog"); - logWatchdog = bool == null || (boolean) bool; - - bool = config.get("enable_dex_obfuscate"); - dexObfuscate = bool == null || (boolean) bool; - - bool = config.get("enable_auto_add_shortcut"); - if (bool != null) { - // TODO: remove - updateModulePrefs("lspd", 0, "config", "enable_auto_add_shortcut", null); - } - - bool = config.get("enable_status_notification"); - enableStatusNotification = bool == null || (boolean) bool; - - var set = (Set) config.get("scope_request_blocked"); - scopeRequestBlocked = set == null ? new HashSet<>() : set; - - // Don't migrate to ConfigFileManager, as XSharedPreferences will be restored soon - String string = (String) config.get("misc_path"); - if (string == null) { - miscPath = Paths.get("/data", "misc", UUID.randomUUID().toString()); - updateModulePrefs("lspd", 0, "config", "misc_path", miscPath.toString()); - } else { - miscPath = Paths.get(string); - } - try { - var perms = PosixFilePermissions.fromString("rwx--x--x"); - Files.createDirectories(miscPath, PosixFilePermissions.asFileAttribute(perms)); - walkFileTree(miscPath, f -> SELinux.setFileContext(f.toString(), "u:object_r:xposed_data:s0")); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - - updateManager(false); - - cacheHandler.post(this::getPreloadDex); - } - - public synchronized void updateManager(boolean uninstalled) { - if (uninstalled) { - managerUid = -1; - return; - } - if (!PackageService.isAlive()) return; - try { - PackageInfo info = PackageService.getPackageInfo(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME, 0, 0); - if (info != null) { - managerUid = info.applicationInfo.uid; - } else { - managerUid = -1; - Log.i(TAG, "manager is not installed"); - } - } catch (RemoteException ignored) { - } - } - - static ConfigManager getInstance() { - if (instance == null) - instance = new ConfigManager(); - boolean needCached; - synchronized (instance.cacheHandler) { - needCached = instance.lastModuleCacheTime == 0 || instance.lastScopeCacheTime == 0; - } - if (needCached) { - if (PackageService.isAlive() && UserService.isAlive()) { - Log.d(TAG, "pm & um are ready, updating cache"); - // must ensure cache is valid for later usage - instance.updateCaches(true); - instance.updateManager(false); - } - } - return instance; - } - - private ConfigManager() { - HandlerThread cacheThread = new HandlerThread("cache"); - cacheThread.start(); - cacheHandler = new Handler(cacheThread.getLooper()); - - initDB(); - updateConfig(); - // must ensure cache is valid for later usage - updateCaches(true); - } - - - private T executeInTransaction(Supplier execution) { - try { - db.beginTransaction(); - var res = execution.get(); - db.setTransactionSuccessful(); - return res; - } finally { - db.endTransaction(); - } - } - - private void executeInTransaction(Runnable execution) { - executeInTransaction((Supplier) () -> { - execution.run(); - return null; - }); - } - - private void initDB() { - db.setForeignKeyConstraintsEnabled(true); - int oldVersion = db.getVersion(); - if (oldVersion >= 4) { - // Database is already up to date. - return; - } - - Log.i(TAG, "Initializing/Upgrading database from version " + oldVersion + " to 4"); - db.beginTransaction(); - try { - if (oldVersion == 0) { - db.execSQL(CREATE_MODULES_TABLE); - db.execSQL(CREATE_SCOPE_TABLE); - db.execSQL(CREATE_CONFIG_TABLE); - - var values = new ContentValues(); - values.put("module_pkg_name", "lspd"); - values.put("apk_path", ConfigFileManager.managerApkPath.toString()); - db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE); - oldVersion = 1; - } - if (oldVersion < 2) { - // Upgrade from 1 to 2: Recreate tables to enforce constraints and clean up. - db.compileStatement("DROP INDEX IF EXISTS configs_idx;").execute(); - db.compileStatement("DROP TABLE IF EXISTS config;").execute(); - db.compileStatement("ALTER TABLE scope RENAME TO old_scope;").execute(); - db.compileStatement("ALTER TABLE configs RENAME TO old_configs;").execute(); - - db.execSQL(CREATE_SCOPE_TABLE); - db.execSQL(CREATE_CONFIG_TABLE); - - try { - db.compileStatement("INSERT INTO scope SELECT * FROM old_scope;").execute(); - } catch (Throwable e) { - Log.w(TAG, "Failed to migrate scope data", e); - } - try { - db.compileStatement("INSERT INTO configs SELECT * FROM old_configs;").execute(); - } catch (Throwable e) { - Log.w(TAG, "Failed to migrate config data", e); - } - - db.compileStatement("DROP TABLE old_scope;").execute(); - db.compileStatement("DROP TABLE old_configs;").execute(); - db.compileStatement("CREATE INDEX IF NOT EXISTS configs_idx ON configs (module_pkg_name, user_id);").execute(); - } - if (oldVersion < 3) { - // Upgrade from 2 to 3: Rename 'android' scope to 'system'. - db.compileStatement("UPDATE scope SET app_pkg_name = 'system' WHERE app_pkg_name = 'android';").execute(); - } - if (oldVersion < 4) { - // Upgrade from 3 to 4: Add the 'auto_include' column to the modules table. - try { - db.compileStatement("ALTER TABLE modules ADD COLUMN auto_include BOOLEAN DEFAULT 0 CHECK (auto_include IN (0, 1));").execute(); - } catch (SQLiteException ex) { - // This might happen if the column already exists from a previous buggy run. - Log.w(TAG, "Could not add auto_include column, it may already exist.", ex); - } - } - db.setVersion(4); - db.setTransactionSuccessful(); - Log.i(TAG, "Database upgrade to version 4 successful."); - } catch (Throwable e) { - Log.e(TAG, "Failed to initialize or upgrade database, transaction rolled back.", e); - } finally { - db.endTransaction(); - } - } - - private List getAssociatedProcesses(Application app) throws RemoteException { - Pair, Integer> result = PackageService.fetchProcessesWithUid(app); - List processes = new ArrayList<>(); - if (app.packageName.equals("android")) { - // this is hardcoded for ResolverActivity - processes.add(new ProcessScope("system:ui", Process.SYSTEM_UID)); - } - for (String processName : result.first) { - var uid = result.second; - if (uid == Process.SYSTEM_UID && processName.equals("system")) { - // code run in system_server - continue; - } - processes.add(new ProcessScope(processName, uid)); - } - return processes; - } - - private @NonNull - Map> fetchModuleConfig(String name, int user_id) { - var config = new ConcurrentHashMap>(); - - try (Cursor cursor = db.query("configs", new String[]{"`group`", "`key`", "data"}, - "module_pkg_name = ? and user_id = ?", new String[]{name, String.valueOf(user_id)}, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "db cache failed"); - return config; - } - int groupIdx = cursor.getColumnIndex("group"); - int keyIdx = cursor.getColumnIndex("key"); - int dataIdx = cursor.getColumnIndex("data"); - while (cursor.moveToNext()) { - var group = cursor.getString(groupIdx); - var key = cursor.getString(keyIdx); - var data = cursor.getBlob(dataIdx); - var object = SerializationUtilsX.deserialize(data); - if (object == null) continue; - config.computeIfAbsent(group, g -> new HashMap<>()).put(key, object); - } - } - return config; - } - - public void updateModulePrefs(String moduleName, int userId, String group, String key, Object value) { - Map values = new HashMap<>(); - values.put(key, value); - updateModulePrefs(moduleName, userId, group, values); - } - - public void updateModulePrefs(String moduleName, int userId, String group, Map values) { - var config = cachedConfig.computeIfAbsent(new Pair<>(moduleName, userId), module -> fetchModuleConfig(module.first, module.second)); - config.compute(group, (g, prefs) -> { - HashMap newPrefs = prefs == null ? new HashMap<>() : new HashMap<>(prefs); - executeInTransaction(() -> { - for (var entry : values.entrySet()) { - var key = entry.getKey(); - var value = entry.getValue(); - if (value instanceof Serializable) { - newPrefs.put(key, value); - var contents = new ContentValues(); - contents.put("`group`", group); - contents.put("`key`", key); - contents.put("data", SerializationUtilsX.serialize((Serializable) value)); - contents.put("module_pkg_name", moduleName); - contents.put("user_id", String.valueOf(userId)); - db.insertWithOnConflict("configs", null, contents, SQLiteDatabase.CONFLICT_REPLACE); - } else { - newPrefs.remove(key); - db.delete("configs", "module_pkg_name=? and user_id=? and `group`=? and `key`=?", new String[]{moduleName, String.valueOf(userId), group, key}); - } - } - var bundle = new Bundle(); - bundle.putSerializable("config", (Serializable) config); - if (bundle.size() > 1024 * 1024) { - throw new IllegalArgumentException("Preference too large"); - } - }); - return newPrefs; - }); - } - - public void deleteModulePrefs(String moduleName, int userId, String group) { - db.delete("configs", "module_pkg_name=? and user_id=? and `group`=?", new String[]{moduleName, String.valueOf(userId), group}); - var config = cachedConfig.getOrDefault(new Pair<>(moduleName, userId), null); - if (config != null) { - config.remove(group); - } - } - - public HashMap getModulePrefs(String moduleName, int userId, String group) { - var config = cachedConfig.computeIfAbsent(new Pair<>(moduleName, userId), module -> fetchModuleConfig(module.first, module.second)); - return config.getOrDefault(group, new HashMap<>()); - } - - private synchronized void clearCache() { - synchronized (cacheHandler) { - lastScopeCacheTime = 0; - lastModuleCacheTime = 0; - } - cachedModule.clear(); - cachedScope.clear(); - } - - private synchronized void cacheModules() { - // skip caching when pm is not yet available - if (!PackageService.isAlive() || !UserService.isAlive()) return; - synchronized (cacheHandler) { - if (lastModuleCacheTime >= requestModuleCacheTime) return; - else lastModuleCacheTime = SystemClock.elapsedRealtime(); - } - Set toClose = ConcurrentHashMap.newKeySet(); - try (Cursor cursor = db.query(true, "modules", new String[]{"module_pkg_name", "apk_path"}, - "enabled = 1", null, null, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "db cache failed"); - return; - } - int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - int apkPathIdx = cursor.getColumnIndex("apk_path"); - Set obsoleteModules = ConcurrentHashMap.newKeySet(); - // packageName, apkPath - Map obsoletePaths = new ConcurrentHashMap<>(); - cachedModule.values().removeIf(m -> { - if (m.apkPath == null || !existsInGlobalNamespace(m.apkPath)) { - toClose.addAll(m.file.preLoadedDexes); - return true; - } - return false; - }); - List modules = new ArrayList<>(); - while (cursor.moveToNext()) { - String packageName = cursor.getString(pkgNameIdx); - String apkPath = cursor.getString(apkPathIdx); - if (packageName.equals("lspd")) continue; - var module = new Module(); - module.packageName = packageName; - module.apkPath = apkPath; - modules.add(module); - } - - modules.stream().parallel().filter(m -> { - var oldModule = cachedModule.get(m.packageName); - PackageInfo pkgInfo = null; - try { - pkgInfo = PackageService.getPackageInfoFromAllUsers(m.packageName, MATCH_ALL_FLAGS).values().stream().findFirst().orElse(null); - } catch (Throwable e) { - Log.w(TAG, "Get package info of " + m.packageName, e); - } - if (pkgInfo == null || pkgInfo.applicationInfo == null) { - Log.w(TAG, "Failed to find package info of " + m.packageName); - obsoleteModules.add(m.packageName); - return false; - } - - if (oldModule != null && - pkgInfo.applicationInfo.sourceDir != null && - m.apkPath != null && oldModule.apkPath != null && - existsInGlobalNamespace(m.apkPath) && - Objects.equals(m.apkPath, oldModule.apkPath) && - Objects.equals(new File(pkgInfo.applicationInfo.sourceDir).getParent(), new File(m.apkPath).getParent())) { - if (oldModule.appId != -1) { - Log.d(TAG, m.packageName + " did not change, skip caching it"); - } else { - // cache from system server, update application info - oldModule.applicationInfo = pkgInfo.applicationInfo; - } - return false; - } - m.apkPath = getModuleApkPath(pkgInfo.applicationInfo); - if (m.apkPath == null) { - Log.w(TAG, "Failed to find path of " + m.packageName); - obsoleteModules.add(m.packageName); - return false; - } else { - obsoletePaths.put(m.packageName, m.apkPath); - } - m.appId = pkgInfo.applicationInfo.uid; - m.applicationInfo = pkgInfo.applicationInfo; - m.service = oldModule != null ? oldModule.service : new LSPInjectedModuleService(m.packageName); - return true; - }).forEach(m -> { - var file = ConfigFileManager.loadModule(m.apkPath, dexObfuscate); - if (file == null) { - Log.w(TAG, "failed to load module " + m.packageName); - obsoleteModules.add(m.packageName); - return; - } - m.file = file; - cachedModule.put(m.packageName, m); - }); - - if (PackageService.isAlive()) { - obsoleteModules.forEach(this::removeModuleWithoutCache); - obsoletePaths.forEach((packageName, path) -> updateModuleApkPath(packageName, path, true)); - } else { - Log.w(TAG, "pm is dead while caching. invalidating..."); - clearCache(); - return; - } - } - Log.d(TAG, "cached modules"); - for (var module : cachedModule.entrySet()) { - Log.d(TAG, module.getKey() + " " + module.getValue().apkPath); - } - cacheScopes(); - toClose.forEach(SharedMemory::close); - } - - private synchronized void cacheScopes() { - // skip caching when pm is not yet available - if (!PackageService.isAlive()) return; - synchronized (cacheHandler) { - if (lastScopeCacheTime >= requestScopeCacheTime) return; - else lastScopeCacheTime = SystemClock.elapsedRealtime(); - } - cachedScope.clear(); - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"app_pkg_name", "module_pkg_name", "user_id"}, - "enabled = 1", null, null, null, null)) { - int appPkgNameIdx = cursor.getColumnIndex("app_pkg_name"); - int modulePkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - int userIdIdx = cursor.getColumnIndex("user_id"); - - final var obsoletePackages = new HashSet(); - final var obsoleteModules = new HashSet(); - final var moduleAvailability = new HashMap, Boolean>(); - final var cachedProcessScope = new HashMap, List>(); - - final var denylist = new HashSet<>(getDenyListPackages()); - while (cursor.moveToNext()) { - Application app = new Application(); - app.packageName = cursor.getString(appPkgNameIdx); - app.userId = cursor.getInt(userIdIdx); - var modulePackageName = cursor.getString(modulePkgNameIdx); - - // check if module is present in this user - if (!moduleAvailability.computeIfAbsent(new Pair<>(modulePackageName, app.userId), n -> { - var available = false; - try { - available = PackageService.isPackageAvailable(n.first, n.second, true) && cachedModule.containsKey(modulePackageName); - } catch (Throwable e) { - Log.w(TAG, "check package availability ", e); - } - if (!available) { - var obsoleteModule = new Application(); - obsoleteModule.packageName = modulePackageName; - obsoleteModule.userId = app.userId; - obsoleteModules.add(obsoleteModule); - } - return available; - })) continue; - - // system server always loads database - if (app.packageName.equals("system")) continue; - - try { - List processesScope = cachedProcessScope.computeIfAbsent(new Pair<>(app.packageName, app.userId), (k) -> { - try { - if (denylist.contains(app.packageName)) - Log.w(TAG, app.packageName + " is on denylist. It may not take effect."); - return getAssociatedProcesses(app); - } catch (RemoteException e) { - return Collections.emptyList(); - } - }); - if (processesScope.isEmpty()) { - obsoletePackages.add(app); - continue; - } - var module = cachedModule.get(modulePackageName); - assert module != null; - for (ProcessScope processScope : processesScope) { - cachedScope.computeIfAbsent(processScope, - ignored -> new LinkedList<>()).add(module); - // Always allow the module to inject itself - if (modulePackageName.equals(app.packageName)) { - var appId = processScope.uid % PER_USER_RANGE; - for (var user : UserService.getUsers()) { - var moduleUid = user.id * PER_USER_RANGE + appId; - if (moduleUid == processScope.uid) continue; // skip duplicate - var moduleSelf = new ProcessScope(processScope.processName, moduleUid); - cachedScope.computeIfAbsent(moduleSelf, - ignored -> new LinkedList<>()).add(module); - } - } - } - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - if (PackageService.isAlive()) { - for (Application obsoletePackage : obsoletePackages) { - Log.d(TAG, "removing obsolete package: " + obsoletePackage.packageName + "/" + obsoletePackage.userId); - removeAppWithoutCache(obsoletePackage); - } - for (Application obsoleteModule : obsoleteModules) { - Log.d(TAG, "removing obsolete module: " + obsoleteModule.packageName + "/" + obsoleteModule.userId); - removeModuleScopeWithoutCache(obsoleteModule); - removeBlockedScopeRequest(obsoleteModule.packageName); - } - } else { - Log.w(TAG, "pm is dead while caching. invalidating..."); - clearCache(); - return; - } - } - Log.d(TAG, "cached scope"); - cachedScope.forEach((ps, modules) -> { - Log.d(TAG, ps.processName + "/" + ps.uid); - modules.forEach(module -> Log.d(TAG, "\t" + module.packageName)); - }); - } - - // This is called when a new process created, use the cached result - public List getModulesForProcess(String processName, int uid) { - return isManager(uid) ? Collections.emptyList() : cachedScope.getOrDefault(new ProcessScope(processName, uid), Collections.emptyList()); - } - - // This is called when a new process created, use the cached result - public boolean shouldSkipProcess(ProcessScope scope) { - return !cachedScope.containsKey(scope) && !isManager(scope.uid); - } - - public boolean isUidHooked(int uid) { - return cachedScope.keySet().stream().reduce(false, (p, scope) -> p || scope.uid == uid, Boolean::logicalOr); - } - - @Nullable - public List getModuleScope(String packageName) { - if (packageName.equals("lspd")) return null; - try (Cursor cursor = db.query("scope INNER JOIN modules ON scope.mid = modules.mid", new String[]{"app_pkg_name", "user_id"}, - "modules.module_pkg_name = ?", new String[]{packageName}, null, null, null)) { - if (cursor == null) { - return null; - } - int userIdIdx = cursor.getColumnIndex("user_id"); - int appPkgNameIdx = cursor.getColumnIndex("app_pkg_name"); - ArrayList result = new ArrayList<>(); - while (cursor.moveToNext()) { - Application scope = new Application(); - scope.packageName = cursor.getString(appPkgNameIdx); - scope.userId = cursor.getInt(userIdIdx); - result.add(scope); - } - return result; - } - } - - @Nullable - public String getModuleApkPath(ApplicationInfo info) { - String[] apks; - if (info.splitSourceDirs != null) { - apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1); - apks[info.splitSourceDirs.length] = info.sourceDir; - } else apks = new String[]{info.sourceDir}; - var apkPath = Arrays.stream(apks).parallel().filter(apk -> { - if (apk == null) { - Log.w(TAG, info.packageName + " has null apk path???"); - return false; - } - try (var zip = new ZipFile(toGlobalNamespace(apk))) { - return zip.getEntry("META-INF/xposed/java_init.list") != null || zip.getEntry("assets/xposed_init") != null; - } catch (IOException e) { - return false; - } - }).findFirst(); - return apkPath.orElse(null); - } - - public boolean updateModuleApkPath(String packageName, String apkPath, boolean force) { - if (apkPath == null || packageName.equals("lspd")) return false; - if (db.inTransaction()) { - Log.w(TAG, "update module apk path should not be called inside transaction"); - return false; - } - - ContentValues values = new ContentValues(); - values.put("module_pkg_name", packageName); - values.put("apk_path", apkPath); - // insert or update in two step since insert or replace will change the autoincrement mid - int count = (int) db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE); - if (count < 0) { - var cached = cachedModule.getOrDefault(packageName, null); - if (force || cached == null || cached.apkPath == null || !cached.apkPath.equals(apkPath)) - count = db.updateWithOnConflict("modules", values, "module_pkg_name=?", new String[]{packageName}, SQLiteDatabase.CONFLICT_IGNORE); - else - count = 0; - } - // force update is because cache is already update to date - // skip caching again - if (!force && count > 0) { - // Called by oneway binder - updateCaches(true); - return true; - } - return count > 0; - } - - // Only be called before updating modules. No need to cache. - private int getModuleId(String packageName) { - if (packageName.equals("lspd")) return -1; - if (db.inTransaction()) { - Log.w(TAG, "get module id should not be called inside transaction"); - return -1; - } - try (Cursor cursor = db.query("modules", new String[]{"mid"}, "module_pkg_name=?", new String[]{packageName}, null, null, null)) { - if (cursor == null) return -1; - if (cursor.getCount() != 1) return -1; - cursor.moveToFirst(); - return cursor.getInt(cursor.getColumnIndexOrThrow("mid")); - } - } - - public boolean setModuleScope(String packageName, List scopes) throws RemoteException { - if (scopes == null) return false; - enableModule(packageName); - int mid = getModuleId(packageName); - if (mid == -1) return false; - executeInTransaction(() -> { - db.delete("scope", "mid = ?", new String[]{String.valueOf(mid)}); - for (Application app : scopes) { - if (app.packageName.equals("system") && app.userId != 0) continue; - ContentValues values = new ContentValues(); - values.put("mid", mid); - values.put("app_pkg_name", app.packageName); - values.put("user_id", app.userId); - db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE); - } - }); - // Called by manager, should be async - updateCaches(false); - return true; - } - - public boolean setModuleScope(String packageName, String scopePackageName, int userId) { - if (scopePackageName == null) return false; - int mid = getModuleId(packageName); - if (mid == -1) return false; - if (scopePackageName.equals("system") && userId != 0) return false; - executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("mid", mid); - values.put("app_pkg_name", scopePackageName); - values.put("user_id", userId); - db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE); - }); - // Called by xposed service, should be async - updateCaches(false); - return true; - } - - public boolean removeModuleScope(String packageName, String scopePackageName, int userId) { - if (scopePackageName == null) return false; - int mid = getModuleId(packageName); - if (mid == -1) return false; - if (scopePackageName.equals("system") && userId != 0) return false; - executeInTransaction(() -> { - db.delete("scope", "mid = ? AND app_pkg_name = ? AND user_id = ?", new String[]{String.valueOf(mid), scopePackageName, String.valueOf(userId)}); - }); - // Called by xposed service, should be async - updateCaches(false); - return true; - } - - - public String[] enabledModules() { - return listModules("enabled"); - } - - public boolean removeModule(String packageName) { - if (removeModuleWithoutCache(packageName)) { - // called by oneway binder - // Called only when the application is completely uninstalled - // If it's a module we need to return as soon as possible to broadcast to the manager - // for updating the module status - updateCaches(false); - return true; - } - return false; - } - - private boolean removeModuleWithoutCache(String packageName) { - if (packageName.equals("lspd")) return false; - boolean res = executeInTransaction(() -> db.delete("modules", "module_pkg_name = ?", new String[]{packageName}) > 0); - try { - for (var user : UserService.getUsers()) { - removeModulePrefs(user.id, packageName); - } - } catch (Throwable e) { - Log.w(TAG, "remove module prefs for " + packageName); - } - return res; - } - - private boolean removeModuleScopeWithoutCache(Application module) { - if (module.packageName.equals("lspd")) return false; - int mid = getModuleId(module.packageName); - if (mid == -1) return false; - boolean res = executeInTransaction(() -> db.delete("scope", "mid = ? and user_id = ?", new String[]{String.valueOf(mid), String.valueOf(module.userId)}) > 0); - try { - removeModulePrefs(module.userId, module.packageName); - } catch (IOException e) { - Log.w(TAG, "removeModulePrefs", e); - } - return res; - } - - private boolean removeAppWithoutCache(Application app) { - return executeInTransaction(() -> db.delete("scope", "app_pkg_name = ? AND user_id=?", - new String[]{app.packageName, String.valueOf(app.userId)}) > 0); - } - - public boolean disableModule(String packageName) { - if (packageName.equals("lspd")) return false; - boolean changed = executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("enabled", 0); - return db.update("modules", values, "module_pkg_name = ?", new String[]{packageName}) > 0; - }); - if (changed) { - // called by manager, should be async - updateCaches(false); - return true; - } else { - return false; - } - } - - public boolean enableModule(String packageName) throws RemoteException { - if (packageName.equals("lspd")) return false; - PackageInfo pkgInfo = PackageService.getPackageInfoFromAllUsers(packageName, PackageService.MATCH_ALL_FLAGS).values().stream().findFirst().orElse(null); - if (pkgInfo == null || pkgInfo.applicationInfo == null) return false; - var modulePath = getModuleApkPath(pkgInfo.applicationInfo); - if (modulePath == null) return false; - boolean changed = updateModuleApkPath(packageName, modulePath, false); - changed = executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("enabled", 1); - return db.update("modules", values, "module_pkg_name = ?", new String[]{packageName}) > 0; - }) || changed; - if (changed) { - // Called by manager, should be async - updateCaches(false); - return true; - } else { - return false; - } - } - - public void updateCache() { - // Called by oneway binder - updateCaches(true); - } - - public void updateAppCache() { - // Called by oneway binder - cacheScopes(); - } - - public void setVerboseLog(boolean on) { - if (BuildConfig.DEBUG) return; - var logcatService = ServiceManager.getLogcatService(); - if (on) { - logcatService.startVerbose(); - } else { - logcatService.stopVerbose(); - } - updateModulePrefs("lspd", 0, "config", "enable_verbose_log", on); - verboseLog = on; - } - - public boolean verboseLog() { - return BuildConfig.DEBUG || verboseLog; - } - - public void setLogWatchdog(boolean on) { - var logcatService = ServiceManager.getLogcatService(); - if (on) { - logcatService.enableWatchdog(); - } else { - logcatService.disableWatchdog(); - } - updateModulePrefs("lspd", 0, "config", "enable_log_watchdog", on); - logWatchdog = on; - } - - public boolean isLogWatchdogEnabled() { - return logWatchdog; - } - - public void setDexObfuscate(boolean on) { - updateModulePrefs("lspd", 0, "config", "enable_dex_obfuscate", on); - } - - public boolean scopeRequestBlocked(String packageName) { - return scopeRequestBlocked.contains(packageName); - } - - public void blockScopeRequest(String packageName) { - var set = new HashSet<>(scopeRequestBlocked); - set.add(packageName); - updateModulePrefs("lspd", 0, "config", "scope_request_blocked", set); - scopeRequestBlocked = set; - } - - public void removeBlockedScopeRequest(String packageName) { - var set = new HashSet<>(scopeRequestBlocked); - set.remove(packageName); - updateModulePrefs("lspd", 0, "config", "scope_request_blocked", set); - scopeRequestBlocked = set; - } - - // this is for manager and should not use the cache result - boolean dexObfuscate() { - var bool = getModulePrefs("lspd", 0, "config").get("enable_dex_obfuscate"); - return bool == null || (boolean) bool; - } - - public boolean enableStatusNotification() { - Log.d(TAG, "show status notification = " + enableStatusNotification); - return enableStatusNotification; - } - - public void setEnableStatusNotification(boolean enable) { - updateModulePrefs("lspd", 0, "config", "enable_status_notification", enable); - enableStatusNotification = enable; - } - - public ParcelFileDescriptor getManagerApk() { - try { - return ConfigFileManager.getManagerApk(); - } catch (Throwable e) { - Log.e(TAG, "failed to open manager apk", e); - return null; - } - } - - public ParcelFileDescriptor getModulesLog() { - try { - var modulesLog = ServiceManager.getLogcatService().getModulesLog(); - if (modulesLog == null) return null; - return ParcelFileDescriptor.open(modulesLog, ParcelFileDescriptor.MODE_READ_ONLY); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - return null; - } - } - - public ParcelFileDescriptor getVerboseLog() { - try { - var verboseLog = ServiceManager.getLogcatService().getVerboseLog(); - if (verboseLog == null) return null; - return ParcelFileDescriptor.open(verboseLog, ParcelFileDescriptor.MODE_READ_ONLY); - } catch (FileNotFoundException e) { - Log.e(TAG, Log.getStackTraceString(e)); - return null; - } - } - - public boolean clearLogs(boolean verbose) { - ServiceManager.getLogcatService().refresh(verbose); - return true; - } - - public boolean isManager(int uid) { - return uid == managerUid; - } - - public boolean isManagerInstalled() { - return managerUid != -1; - } - - public String getPrefsPath(String packageName, int uid) { - int userId = uid / PER_USER_RANGE; - var path = miscPath.resolve("prefs" + (userId == 0 ? "" : String.valueOf(userId))).resolve(packageName); - var module = cachedModule.getOrDefault(packageName, null); - if (module != null && module.appId == uid % PER_USER_RANGE) { - try { - var perms = PosixFilePermissions.fromString("rwx--x--x"); - Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms)); - walkFileTree(path, p -> { - try { - Os.chown(p.toString(), uid, uid); - } catch (ErrnoException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - }); - } catch (IOException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - return path.toString(); - } - - // this is slow, avoid using it - public Module getModule(int uid) { - for (var module : cachedModule.values()) { - if (module.appId == uid % PER_USER_RANGE) return module; - } - return null; - } - - private void walkFileTree(Path rootDir, Consumer action) throws IOException { - if (Files.notExists(rootDir)) return; - Files.walkFileTree(rootDir, new SimpleFileVisitor<>() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - action.accept(dir); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - action.accept(file); - return FileVisitResult.CONTINUE; - } - }); - } - - private void removeModulePrefs(int uid, String packageName) throws IOException { - if (packageName == null) return; - var path = Paths.get(getPrefsPath(packageName, uid)); - ConfigFileManager.deleteFolderIfExists(path); - } - - public List getDenyListPackages() { - List result = new ArrayList<>(); - if (!getApi().equals("Zygisk")) return result; - if (!ConfigFileManager.magiskDbPath.exists()) return result; - try (final SQLiteDatabase magiskDb = - SQLiteDatabase.openDatabase(ConfigFileManager.magiskDbPath, new SQLiteDatabase.OpenParams.Builder().addOpenFlags(SQLiteDatabase.OPEN_READONLY).build())) { - try (Cursor cursor = magiskDb.query("settings", new String[]{"value"}, "`key`=?", new String[]{"denylist"}, null, null, null)) { - if (!cursor.moveToNext()) return result; - int valueIndex = cursor.getColumnIndex("value"); - if (valueIndex >= 0 && cursor.getInt(valueIndex) == 0) return result; - } - try (Cursor cursor = magiskDb.query(true, "denylist", new String[]{"package_name"}, null, null, null, null, null, null, null)) { - if (cursor == null) return result; - int packageNameIdx = cursor.getColumnIndex("package_name"); - while (cursor.moveToNext()) { - result.add(cursor.getString(packageNameIdx)); - } - return result; - } - } catch (Throwable e) { - Log.e(TAG, "get denylist", e); - } - return result; - } - - public void setApi(String api) { - this.api = api; - } - - public String getApi() { - return api; - } - - public void exportScopes(ZipOutputStream os) throws IOException { - os.putNextEntry(new ZipEntry("scopes.txt")); - cachedScope.forEach((scope, modules) -> { - try { - os.write((scope.processName + "/" + scope.uid + "\n").getBytes(StandardCharsets.UTF_8)); - for (var module : modules) { - os.write(("\t" + module.packageName + "\n").getBytes(StandardCharsets.UTF_8)); - for (var cn : module.file.moduleClassNames) { - os.write(("\t\t" + cn + "\n").getBytes(StandardCharsets.UTF_8)); - } - for (var ln : module.file.moduleLibraryNames) { - os.write(("\t\t" + ln + "\n").getBytes(StandardCharsets.UTF_8)); - } - } - } catch (IOException e) { - Log.w(TAG, scope.processName, e); - } - }); - os.closeEntry(); - } - - synchronized SharedMemory getPreloadDex() { - return ConfigFileManager.getPreloadDex(dexObfuscate); - } - - public boolean getAutoInclude(String packageName) { - try (Cursor cursor = db.query("modules", new String[]{"auto_include"}, - "module_pkg_name = ? and auto_include = 1", new String[]{packageName}, null, null, null, null)) { - return cursor == null || cursor.moveToNext(); - } - } - - public boolean setAutoInclude(String packageName, boolean enable) { - boolean changed = executeInTransaction(() -> { - ContentValues values = new ContentValues(); - values.put("auto_include", enable ? 1 : 0); - return db.update("modules", values, "module_pkg_name = ?", new String[]{packageName}) > 0; - }); - return true; - } - - public String[] getAutoIncludeModules() { - return listModules("auto_include"); - } - - private String[] listModules(String column) { - try (Cursor cursor = db.query("modules", new String[]{"module_pkg_name"}, column + " = 1", null, null, null, null)) { - if (cursor == null) { - Log.e(TAG, "query " + column + " modules failed"); - return null; - } - int modulePkgNameIdx = cursor.getColumnIndex("module_pkg_name"); - HashSet result = new HashSet<>(); - while (cursor.moveToNext()) { - var pkgName = cursor.getString(modulePkgNameIdx); - if (pkgName.equals("lspd")) continue; - result.add(pkgName); - } - return result.toArray(new String[0]); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java b/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java deleted file mode 100644 index f4da23ebb..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_CRASHED; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_MOUNT_FAILED; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_OK; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_SELINUX_PERMISSIVE; -import static org.lsposed.lspd.ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT; - -import android.net.LocalServerSocket; -import android.os.Build; -import android.os.FileObserver; -import android.os.Process; -import android.os.SELinux; -import android.system.ErrnoException; -import android.system.Os; -import android.system.OsConstants; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; - -@RequiresApi(Build.VERSION_CODES.Q) -public class Dex2OatService implements Runnable { - private static final String TAG = "LSPosedDex2Oat"; - private static final String WRAPPER32 = "bin/dex2oat32"; - private static final String WRAPPER64 = "bin/dex2oat64"; - private static final String HOOKER32 = "bin/liboat_hook32.so"; - private static final String HOOKER64 = "bin/liboat_hook64.so"; - - private final String[] dex2oatArray = new String[6]; - private final FileDescriptor[] fdArray = new FileDescriptor[6]; - private final FileObserver selinuxObserver; - private int compatibility = DEX2OAT_OK; - - private void openDex2oat(int id, String path) { - try { - var fd = Os.open(path, OsConstants.O_RDONLY, 0); - dex2oatArray[id] = path; - fdArray[id] = fd; - } catch (ErrnoException ignored) { - } - } - - /** - * Checks the ELF header of the target file. - * If 32-bit -> Assigns to Index 0 (Release) or 1 (Debug). - * If 64-bit -> Assigns to Index 2 (Release) or 3 (Debug). - */ - private void checkAndAddDex2Oat(String path) { - if (path == null) - return; - File file = new File(path); - if (!file.exists()) - return; - - try (FileInputStream fis = new FileInputStream(file)) { - byte[] header = new byte[5]; - if (fis.read(header) != 5) - return; - - // 1. Verify ELF Magic: 0x7F 'E' 'L' 'F' - if (header[0] != 0x7F || header[1] != 'E' || header[2] != 'L' || header[3] != 'F') { - return; - } - - // 2. Check Architecture (header[4]): 1 = 32-bit, 2 = 64-bit - boolean is32Bit = (header[4] == 1); - boolean is64Bit = (header[4] == 2); - boolean isDebug = path.contains("dex2oatd"); - - int index = -1; - - if (is32Bit) { - index = isDebug ? 1 : 0; // Index 0/1 maps to r32/d32 in C++ - } else if (is64Bit) { - index = isDebug ? 3 : 2; // Index 2/3 maps to r64/d64 in C++ - } - - // 3. Assign to the detected slot - if (index != -1 && dex2oatArray[index] == null) { - dex2oatArray[index] = path; - try { - // Open the FD for the wrapper to use later - fdArray[index] = Os.open(path, OsConstants.O_RDONLY, 0); - Log.i(TAG, "Detected " + path + " as " + (is64Bit ? "64-bit" : "32-bit") + " -> Assigned Index " - + index); - } catch (ErrnoException e) { - Log.e(TAG, "Failed to open FD for " + path, e); - dex2oatArray[index] = null; - } - } - } catch (IOException e) { - // File not readable, skip - } - } - - public Dex2OatService() { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - // Android 10: Check the standard path. - // Logic will detect if it is 32-bit and put it in Index 0. - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat"); - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd"); - - // Check for explicit 64-bit paths (just in case) - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat64"); - checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd64"); - } else { - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat32"); - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd32"); - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat64"); - checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd64"); - } - - openDex2oat(4, "/data/adb/modules/zygisk_vector/bin/liboat_hook32.so"); - openDex2oat(5, "/data/adb/modules/zygisk_vector/bin/liboat_hook64.so"); - - var enforce = Paths.get("/sys/fs/selinux/enforce"); - var policy = Paths.get("/sys/fs/selinux/policy"); - var list = new ArrayList(); - list.add(enforce.toFile()); - list.add(policy.toFile()); - selinuxObserver = new FileObserver(list, FileObserver.CLOSE_WRITE) { - @Override - public synchronized void onEvent(int i, @Nullable String s) { - Log.d(TAG, "SELinux status changed"); - if (compatibility == DEX2OAT_CRASHED) { - stopWatching(); - return; - } - - boolean enforcing = false; - try (var is = Files.newInputStream(enforce)) { - enforcing = is.read() == '1'; - } catch (IOException ignored) { - } - - if (!enforcing) { - if (compatibility == DEX2OAT_OK) doMount(false); - compatibility = DEX2OAT_SELINUX_PERMISSIVE; - } else if (SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", - "u:object_r:dex2oat_exec:s0", "file", "execute") - || SELinux.checkSELinuxAccess("u:r:untrusted_app:s0", - "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans")) { - if (compatibility == DEX2OAT_OK) doMount(false); - compatibility = DEX2OAT_SEPOLICY_INCORRECT; - } else if (compatibility != DEX2OAT_OK) { - doMount(true); - if (notMounted()) { - doMount(false); - compatibility = DEX2OAT_MOUNT_FAILED; - stopWatching(); - } else { - compatibility = DEX2OAT_OK; - } - } - } - - @Override - public void stopWatching() { - super.stopWatching(); - Log.w(TAG, "SELinux observer stopped"); - } - }; - } - - private boolean notMounted() { - for (int i = 0; i < dex2oatArray.length && i < 4; i++) { - var bin = dex2oatArray[i]; - if (bin == null) continue; - try { - var apex = Os.stat("/proc/1/root" + bin); - var wrapper = Os.stat(i < 2 ? WRAPPER32 : WRAPPER64); - if (apex.st_dev != wrapper.st_dev || apex.st_ino != wrapper.st_ino) { - Log.w(TAG, "Check mount failed for " + bin); - return true; - } - } catch (ErrnoException e) { - Log.e(TAG, "Check mount failed for " + bin, e); - return true; - } - } - Log.d(TAG, "Check mount succeeded"); - return false; - } - - private void doMount(boolean enabled) { - doMountNative(enabled, dex2oatArray[0], dex2oatArray[1], dex2oatArray[2], dex2oatArray[3]); - } - - public void start() { - if (notMounted()) { // Already mounted when restart daemon - doMount(true); - if (notMounted()) { - doMount(false); - compatibility = DEX2OAT_MOUNT_FAILED; - return; - } - } - - var thread = new Thread(this); - thread.setName("dex2oat"); - thread.start(); - selinuxObserver.startWatching(); - selinuxObserver.onEvent(0, null); - } - - @Override - public void run() { - Log.i(TAG, "Dex2oat wrapper daemon start"); - var sockPath = getSockPath(); - Log.d(TAG, "wrapper path: " + sockPath); - var xposed_file = "u:object_r:xposed_file:s0"; - var dex2oat_exec = "u:object_r:dex2oat_exec:s0"; - if (SELinux.checkSELinuxAccess("u:r:dex2oat:s0", dex2oat_exec, - "file", "execute_no_trans")) { - SELinux.setFileContext(WRAPPER32, dex2oat_exec); - SELinux.setFileContext(WRAPPER64, dex2oat_exec); - setSockCreateContext("u:r:dex2oat:s0"); - } else { - SELinux.setFileContext(WRAPPER32, xposed_file); - SELinux.setFileContext(WRAPPER64, xposed_file); - setSockCreateContext("u:r:installd:s0"); - } - SELinux.setFileContext(HOOKER32, xposed_file); - SELinux.setFileContext(HOOKER64, xposed_file); - try (var server = new LocalServerSocket(sockPath)) { - setSockCreateContext(null); - while (true) { - try (var client = server.accept(); - var is = client.getInputStream(); - var os = client.getOutputStream()) { - var id = is.read(); - var fd = new FileDescriptor[]{fdArray[id]}; - client.setFileDescriptorsForSend(fd); - os.write(1); - Log.d(TAG, "Sent fd of " + dex2oatArray[id]); - } - } - } catch (IOException e) { - Log.e(TAG, "Dex2oat wrapper daemon crashed", e); - if (compatibility == DEX2OAT_OK) { - doMount(false); - compatibility = DEX2OAT_CRASHED; - } - } - } - - public int getCompatibility() { - return compatibility; - } - - private native void doMountNative(boolean enabled, - String r32, String d32, String r64, String d64); - - private static native boolean setSockCreateContext(String context); - - private native String getSockPath(); -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java deleted file mode 100644 index f5cd46555..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.os.IBinder; -import android.os.Parcel; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; - -import org.lsposed.lspd.models.Module; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -public class LSPApplicationService extends ILSPApplicationService.Stub { - final static int DEX_TRANSACTION_CODE = ('_' << 24) | ('D' << 16) | ('E' << 8) | 'X'; - final static int OBFUSCATION_MAP_TRANSACTION_CODE = ('_' << 24) | ('O' << 16) | ('B' << 8) | 'F'; - // key: - private final static Map, ProcessInfo> processes = new ConcurrentHashMap<>(); - - static class ProcessInfo implements DeathRecipient { - final int uid; - final int pid; - final String processName; - final IBinder heartBeat; - - ProcessInfo(int uid, int pid, String processName, IBinder heartBeat) throws RemoteException { - this.uid = uid; - this.pid = pid; - this.processName = processName; - this.heartBeat = heartBeat; - heartBeat.linkToDeath(this, 0); - Log.d(TAG, "register " + this); - processes.put(new Pair<>(uid, pid), this); - } - - @Override - public void binderDied() { - Log.d(TAG, this + " is dead"); - heartBeat.unlinkToDeath(this, 0); - processes.remove(new Pair<>(uid, pid), this); - } - - @NonNull - @Override - public String toString() { - return "ProcessInfo{" + - "uid=" + uid + - ", pid=" + pid + - ", processName='" + processName + '\'' + - ", heartBeat=" + heartBeat + - '}'; - } - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - Log.d(TAG, "LSPApplicationService.onTransact: code=" + code); - switch (code) { - case DEX_TRANSACTION_CODE: { - var shm = ConfigManager.getInstance().getPreloadDex(); - if (shm == null) return false; - reply.writeNoException(); - // assume that write only a fd - shm.writeToParcel(reply, 0); - reply.writeLong(shm.getSize()); - return true; - } - case OBFUSCATION_MAP_TRANSACTION_CODE: { - var obfuscation = ConfigManager.getInstance().dexObfuscate(); - var signatures = ObfuscationManager.getSignatures(); - reply.writeNoException(); - reply.writeInt(signatures.size() * 2); - for (Map.Entry entry : signatures.entrySet()) { - reply.writeString(entry.getKey()); - // return val = key if obfuscation disabled - reply.writeString(obfuscation ? entry.getValue() : entry.getKey()); - } - return true; - } - } - return super.onTransact(code, data, reply, flags); - } - - public boolean registerHeartBeat(int uid, int pid, String processName, IBinder heartBeat) { - try { - new ProcessInfo(uid, pid, processName, heartBeat); - return true; - } catch (RemoteException e) { - return false; - } - } - - private List getAllModulesList() throws RemoteException { - var processInfo = ensureRegistered(); - if (processInfo.uid == Process.SYSTEM_UID && processInfo.processName.equals("system")) { - return ConfigManager.getInstance().getModulesForSystemServer(); - } - if (ServiceManager.getManagerService().isRunningManager(processInfo.pid, processInfo.uid)) - return Collections.emptyList(); - return ConfigManager.getInstance().getModulesForProcess(processInfo.processName, processInfo.uid); - } - - @Override - public boolean isLogMuted() throws RemoteException { - return !ServiceManager.getManagerService().isVerboseLog(); - } - - @Override - public List getLegacyModulesList() throws RemoteException { - return getAllModulesList().stream().filter(m -> m.file.legacy).collect(Collectors.toList()); - } - - @Override - public List getModulesList() throws RemoteException { - return getAllModulesList().stream().filter(m -> !m.file.legacy).collect(Collectors.toList()); - } - - @Override - public String getPrefsPath(String packageName) throws RemoteException { - ensureRegistered(); - return ConfigManager.getInstance().getPrefsPath(packageName, getCallingUid()); - } - - @Override - public ParcelFileDescriptor requestInjectedManagerBinder(List binder) throws RemoteException { - var processInfo = ensureRegistered(); - if (ServiceManager.getManagerService().postStartManager(processInfo.pid, processInfo.uid) || - ConfigManager.getInstance().isManager(processInfo.uid)) { - binder.add(ServiceManager.getManagerService().obtainManagerBinder(processInfo.heartBeat, processInfo.pid, processInfo.uid)); - } - return ConfigManager.getInstance().getManagerApk(); - } - - public boolean hasRegister(int uid, int pid) { - return processes.containsKey(new Pair<>(uid, pid)); - } - - @NonNull - private ProcessInfo ensureRegistered() throws RemoteException { - var uid = getCallingUid(); - var pid = getCallingPid(); - var key = new Pair<>(uid, pid); - ProcessInfo processInfo = processes.getOrDefault(key, null); - if (processInfo == null || uid != processInfo.uid || pid != processInfo.pid) { - processes.remove(key, processInfo); - Log.w(TAG, "non-authorized: info=" + processInfo + " uid=" + uid + " pid=" + pid); - throw new RemoteException("Not registered"); - } - return processInfo; - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java deleted file mode 100644 index fa402923c..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.LSPModuleService.FILES_DIR; -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; - -import android.os.Binder; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.util.Log; - -import org.lsposed.lspd.models.Module; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.service.IXposedService; - -public class LSPInjectedModuleService extends ILSPInjectedModuleService.Stub { - - private static final String TAG = "LSPosedInjectedModuleService"; - - private final String mPackageName; - - Map> callbacks = new ConcurrentHashMap<>(); - - LSPInjectedModuleService(String packageName) { - mPackageName = packageName; - } - - @Override - public long getFrameworkProperties() { - var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; - if (ConfigManager.getInstance().dexObfuscate()) { - prop = prop | IXposedService.PROP_RT_API_PROTECTION; - } - return prop; - } - - @Override - public Bundle requestRemotePreferences(String group, IRemotePreferenceCallback callback) { - var bundle = new Bundle(); - var userId = Binder.getCallingUid() / PER_USER_RANGE; - bundle.putSerializable("map", ConfigManager.getInstance().getModulePrefs(mPackageName, userId, group)); - if (callback != null) { - var groupCallbacks = callbacks.computeIfAbsent(group, k -> ConcurrentHashMap.newKeySet()); - groupCallbacks.add(callback); - try { - callback.asBinder().linkToDeath(() -> groupCallbacks.remove(callback), 0); - } catch (RemoteException e) { - Log.w(TAG, "requestRemotePreferences: ", e); - } - } - return bundle; - } - - @Override - public ParcelFileDescriptor openRemoteFile(String path) throws RemoteException { - ConfigFileManager.ensureModuleFilePath(path); - var userId = Binder.getCallingUid() / PER_USER_RANGE; - try { - var dir = ConfigFileManager.resolveModuleDir(mPackageName, FILES_DIR, userId, -1); - return ParcelFileDescriptor.open(dir.resolve(path).toFile(), ParcelFileDescriptor.MODE_READ_ONLY); - } catch (Throwable e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public String[] getRemoteFileList() throws RemoteException { - var userId = Binder.getCallingUid() / PER_USER_RANGE; - try { - var dir = ConfigFileManager.resolveModuleDir(mPackageName, FILES_DIR, userId, -1); - var files = dir.toFile().list(); - return files == null ? new String[0] : files; - - } catch (Throwable e) { - throw new RemoteException(e.getMessage()); - } - } - - void onUpdateRemotePreferences(String group, Bundle diff) { - var groupCallbacks = callbacks.get(group); - if (groupCallbacks != null) { - for (var callback : groupCallbacks) { - try { - callback.onUpdate(diff); - } catch (RemoteException e) { - groupCallbacks.remove(callback); - } - } - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java deleted file mode 100644 index 0ed6b21f7..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java +++ /dev/null @@ -1,569 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static android.content.Context.BIND_AUTO_CREATE; -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.annotation.SuppressLint; -import android.app.IServiceConnection; -import android.content.AttributionSource; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.VersionedPackage; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.os.SELinux; -import android.os.SystemProperties; -import android.system.ErrnoException; -import android.system.Os; -import android.util.Log; -import android.view.IWindowManager; - -import androidx.annotation.NonNull; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.ILSPManagerService; -import org.lsposed.lspd.models.Application; -import org.lsposed.lspd.models.UserInfo; -import org.lsposed.lspd.util.Utils; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import hidden.HiddenApiBridge; -import io.github.libxposed.service.IXposedService; -import rikka.parcelablelist.ParcelableListSlice; - -public class LSPManagerService extends ILSPManagerService.Stub { - - private static Intent managerIntent = null; - private boolean enabled = true; - - public class ManagerGuard implements IBinder.DeathRecipient { - private final @NonNull - IBinder binder; - private final int pid; - private final int uid; - private final IServiceConnection connection = new IServiceConnection.Stub() { - @Override - public void connected(ComponentName name, IBinder service, boolean dead) { - } - }; - - public ManagerGuard(@NonNull IBinder binder, int pid, int uid) { - guard = this; - this.pid = pid; - this.uid = uid; - this.binder = binder; - try { - this.binder.linkToDeath(this, 0); - if (Utils.isMIUI) { - var intent = new Intent(); - intent.setComponent(ComponentName.unflattenFromString("com.miui.securitycore/com.miui.xspace.service.XSpaceService")); - ActivityManagerService.bindService(intent, intent.getType(), connection, BIND_AUTO_CREATE, "android", 0); - } - } catch (Throwable e) { - Log.e(TAG, "manager guard", e); - guard = null; - } - } - - @Override - public void binderDied() { - try { - binder.unlinkToDeath(this, 0); - ActivityManagerService.unbindService(connection); - } catch (Throwable e) { - Log.e(TAG, "manager guard", e); - } - guard = null; - } - - boolean isAlive() { - return binder.isBinderAlive(); - } - } - - public ManagerGuard guard = null; - - // guard to determine the manager or the injected app - // that is to say, to make the parasitic success, - // we should make sure no extra launch after parasitic - // launch is queued and before the process is started - private boolean pendingManager = false; - private int managerPid = -1; - - LSPManagerService() { - } - - private static Intent getManagerIntent() { - if (managerIntent != null) return managerIntent; - try { - var intent = PackageService.getLaunchIntentForPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - if (intent == null) { - var pkgInfo = PackageService.getPackageInfo(BuildConfig.MANAGER_INJECTED_PKG_NAME, PackageManager.GET_ACTIVITIES, 0); - if (pkgInfo != null && pkgInfo.activities != null && pkgInfo.activities.length > 0) { - for (var activityInfo : pkgInfo.activities) { - if (activityInfo.processName.equals(activityInfo.packageName)) { - intent = new Intent(); - intent.setComponent(new ComponentName(activityInfo.packageName, activityInfo.name)); - intent.setAction(Intent.ACTION_MAIN); - break; - } - } - } - } - if (intent != null) { - if (intent.getCategories() != null) intent.getCategories().clear(); - intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER"); - intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - managerIntent = new Intent(intent); - } - } catch (RemoteException e) { - Log.e(TAG, "get Intent", e); - } - return managerIntent; - } - - static void openManager(Uri withData) { - var intent = getManagerIntent(); - if (intent == null) return; - intent = new Intent(intent); - intent.setData(withData); - try { - ActivityManagerService.startActivityAsUserWithFeature("android", null, intent, intent.getType(), null, null, 0, 0, null, null, 0); - } catch (RemoteException e) { - Log.e(TAG, "failed to open manager"); - } - } - - @SuppressLint("WrongConstant") - public static void broadcastIntent(Intent inIntent) { - var intent = new Intent("org.lsposed.manager.NOTIFICATION"); - intent.putExtra(Intent.EXTRA_INTENT, inIntent); - intent.addFlags(0x01000000); //Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND - intent.addFlags(0x00400000); //Intent.FLAG_RECEIVER_FROM_SHELL - intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME); - try { - ActivityManagerService.broadcastIntentWithFeature(null, intent, - null, null, 0, null, null, - null, -1, null, true, false, - 0); - intent.setPackage(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME); - ActivityManagerService.broadcastIntentWithFeature(null, intent, - null, null, 0, null, null, - null, -1, null, true, false, - 0); - } catch (RemoteException t) { - Log.e(TAG, "Broadcast to manager failed: ", t); - } - } - - private void ensureWebViewPermission(File f) { - if (!f.exists()) return; - SELinux.setFileContext(f.getAbsolutePath(), "u:object_r:xposed_file:s0"); - try { - Os.chown(f.getAbsolutePath(), BuildConfig.MANAGER_INJECTED_UID, BuildConfig.MANAGER_INJECTED_UID); - } catch (ErrnoException e) { - Log.e(TAG, "chown of webview", e); - } - if (f.isDirectory()) { - for (var g : f.listFiles()) { - ensureWebViewPermission(g); - } - } - } - - private void ensureWebViewPermission() { - try { - var pkgInfo = PackageService.getPackageInfo(BuildConfig.MANAGER_INJECTED_PKG_NAME, 0, 0); - if (pkgInfo != null) { - var cacheDir = new File(HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(pkgInfo.applicationInfo) + "/cache"); - // The cache directory does not exist after `pm clear` - cacheDir.mkdirs(); - ensureWebViewPermission(cacheDir); - } - } catch (Throwable e) { - Log.w(TAG, "cannot ensure webview dir", e); - } - } - - synchronized boolean preStartManager() { - pendingManager = true; - managerPid = -1; - return true; - } - - // return true to inject manager - synchronized boolean shouldStartManager(int pid, int uid, String processName) { - if (!enabled || uid != BuildConfig.MANAGER_INJECTED_UID || !BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME.equals(processName) || !pendingManager) - return false; - pendingManager = false; - managerPid = pid; - Log.d(TAG, "starting injected manager: pid = " + pid + " uid = " + uid + " processName = " + processName); - return true; - } - - synchronized boolean setEnabled(boolean newValue) { - enabled = newValue; - Log.i(TAG, "manager enabled = " + enabled); - return enabled; - } - - // return true to send manager binder - boolean postStartManager(int pid, int uid) { - return enabled && uid == BuildConfig.MANAGER_INJECTED_UID && pid == managerPid; - } - - public @NonNull - IBinder obtainManagerBinder(@NonNull IBinder heartbeat, int pid, int uid) { - new ManagerGuard(heartbeat, pid, uid); - if (uid == BuildConfig.MANAGER_INJECTED_UID) - ensureWebViewPermission(); - return this; - } - - public boolean isRunningManager(int pid, int uid) { - return false; - } - - void onSystemServerDied() { - guard = null; - } - - @Override - public IBinder asBinder() { - return this; - } - - @Override - public int getXposedApiVersion() { - return IXposedService.LIB_API; - } - - @Override - public long getXposedVersionCode() { - return BuildConfig.VERSION_CODE; - } - - @Override - public String getXposedVersionName() { - return BuildConfig.VERSION_NAME; - } - - @Override - public String getApi() { - return ConfigManager.getInstance().getApi(); - } - - @Override - public ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) throws RemoteException { - return PackageService.getInstalledPackagesFromAllUsers(flags, filterNoProcess); - } - - @Override - public String[] enabledModules() { - return ConfigManager.getInstance().enabledModules(); - } - - @Override - public boolean enableModule(String packageName) throws RemoteException { - return ConfigManager.getInstance().enableModule(packageName); - } - - @Override - public boolean setModuleScope(String packageName, List scope) throws RemoteException { - return ConfigManager.getInstance().setModuleScope(packageName, scope); - } - - @Override - public List getModuleScope(String packageName) { - return ConfigManager.getInstance().getModuleScope(packageName); - } - - @Override - public boolean disableModule(String packageName) { - return ConfigManager.getInstance().disableModule(packageName); - } - - @Override - public boolean isVerboseLog() { - return ConfigManager.getInstance().verboseLog(); - } - - @Override - public void setVerboseLog(boolean enabled) { - ConfigManager.getInstance().setVerboseLog(enabled); - } - - @Override - public ParcelFileDescriptor getVerboseLog() { - return ConfigManager.getInstance().getVerboseLog(); - } - - @Override - public ParcelFileDescriptor getModulesLog() { - ServiceManager.getLogcatService().checkLogFile(); - return ConfigManager.getInstance().getModulesLog(); - } - - @Override - public boolean clearLogs(boolean verbose) { - return ConfigManager.getInstance().clearLogs(verbose); - } - - @Override - public PackageInfo getPackageInfo(String packageName, int flags, int uid) throws RemoteException { - return PackageService.getPackageInfo(packageName, flags, uid); - } - - @Override - public void forceStopPackage(String packageName, int userId) throws RemoteException { - ActivityManagerService.forceStopPackage(packageName, userId); - } - - @Override - public void reboot() throws RemoteException { - PowerService.reboot(false, null, false); - } - - @Override - public boolean uninstallPackage(String packageName, int userId) throws RemoteException { - try { - if (ActivityManagerService.startUserInBackground(userId)) { - var pkg = new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST); - return PackageService.uninstallPackage(pkg, userId); - } else { - return false; - } - } catch (InterruptedException | ReflectiveOperationException e) { - Log.e(TAG, e.getMessage(), e); - return false; - } - } - - @Override - public boolean isSepolicyLoaded() { - return SELinux.checkSELinuxAccess("u:r:dex2oat:s0", "u:object_r:dex2oat_exec:s0", - "file", "execute_no_trans"); - } - - @Override - public List getUsers() throws RemoteException { - var users = new LinkedList(); - for (var user : UserService.getUsers()) { - var info = new UserInfo(); - info.id = user.id; - info.name = user.name; - users.add(info); - } - return users; - } - - @Override - public int installExistingPackageAsUser(String packageName, int userId) { - try { - if (ActivityManagerService.startUserInBackground(userId)) - return PackageService.installExistingPackageAsUser(packageName, userId); - else return PackageService.INSTALL_FAILED_INTERNAL_ERROR; - } catch (Throwable e) { - Log.w(TAG, "install existing package as user: ", e); - return PackageService.INSTALL_FAILED_INTERNAL_ERROR; - } - } - - @Override - public boolean systemServerRequested() { - return ServiceManager.systemServerRequested(); - } - - @Override - public int startActivityAsUserWithFeature(Intent intent, int userId) throws RemoteException { - if (!intent.getBooleanExtra("lsp_no_switch_to_user", false)) { - intent.removeExtra("lsp_no_switch_to_user"); - var currentUser = ActivityManagerService.getCurrentUser(); - if (currentUser == null) return -1; - var parent = UserService.getProfileParent(userId); - if (parent < 0) return -1; - if (currentUser.id != parent) { - if (!ActivityManagerService.switchUser(parent)) return -1; - var window = android.os.ServiceManager.getService(Context.WINDOW_SERVICE); - if (window != null) { - var wm = IWindowManager.Stub.asInterface(window); - wm.lockNow(null); - } - } - } - return ActivityManagerService.startActivityAsUserWithFeature("android", null, intent, intent.getType(), null, null, 0, 0, null, null, userId); - } - - @Override - public ParcelableListSlice queryIntentActivitiesAsUser(Intent intent, int flags, int userId) throws RemoteException { - return PackageService.queryIntentActivities(intent, intent.getType(), flags, userId); - } - - @Override - public boolean dex2oatFlagsLoaded() { - return SystemProperties.get("dalvik.vm.dex2oat-flags").contains("--inline-max-code-units=0"); - } - - @Override - public void setHiddenIcon(boolean hide) { - Bundle args = new Bundle(); - args.putString("value", hide ? "0" : "1"); - args.putString("_user", "0"); - try { - var contentProvider = ActivityManagerService.getContentProvider("settings", 0); - if (contentProvider != null) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - contentProvider.call(new AttributionSource.Builder(1000).setPackageName("android").build(), - "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - contentProvider.call("android", null, "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - contentProvider.call("android", "settings", "PUT_global", "show_hidden_icon_apps_enabled", args); - } - } catch (NoSuchMethodError e) { - Log.w(TAG, "setHiddenIcon: ", e); - } - } - } catch (Throwable e) { - Log.w(TAG, "setHiddenIcon: ", e); - } - } - - @Override - public void getLogs(ParcelFileDescriptor zipFd) { - ConfigFileManager.getLogs(zipFd); - } - - @Override - public void restartFor(Intent intent) throws RemoteException { - } - - @Override - public List getDenyListPackages() { - return ConfigManager.getInstance().getDenyListPackages(); - } - - @Override - public void flashZip(String zipPath, ParcelFileDescriptor outputStream) { - var processBuilder = new ProcessBuilder("magisk", "--install-module", zipPath); - var fd = new File("/proc/self/fd/" + outputStream.getFd()); - processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(fd)); - try (outputStream; var fdw = new FileOutputStream(fd, true)) { - var proc = processBuilder.start(); - if (proc.waitFor(10, TimeUnit.SECONDS)) { - var exit = proc.exitValue(); - if (exit == 0) { - fdw.write("- Reboot after 5s\n".getBytes()); - Thread.sleep(5000); - reboot(); - } else { - var s = "! Flash failed, exit with " + exit + "\n"; - fdw.write(s.getBytes()); - } - } else { - proc.destroy(); - fdw.write("! Timeout, abort\n".getBytes()); - } - } catch (IOException | InterruptedException | RemoteException e) { - Log.e(TAG, "flashZip: ", e); - } - } - - @Override - public void clearApplicationProfileData(String packageName) throws RemoteException { - PackageService.clearApplicationProfileData(packageName); - } - - @Override - public boolean enableStatusNotification() { - return ConfigManager.getInstance().enableStatusNotification(); - } - - @Override - public void setEnableStatusNotification(boolean enable) { - ConfigManager.getInstance().setEnableStatusNotification(enable); - if (enable) { - LSPNotificationManager.notifyStatusNotification(); - } else { - LSPNotificationManager.cancelStatusNotification(); - } - } - - @Override - public boolean performDexOptMode(String packageName) throws RemoteException { - return PackageService.performDexOptMode(packageName); - } - - @Override - public boolean getDexObfuscate() { - return ConfigManager.getInstance().dexObfuscate(); - } - - @Override - public void setDexObfuscate(boolean enabled) { - ConfigManager.getInstance().setDexObfuscate(enabled); - } - - @Override - public int getDex2OatWrapperCompatibility() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return ServiceManager.getDex2OatService().getCompatibility(); - } else { - return 0; - } - } - - @Override - public void setLogWatchdog(boolean enabled) { - ConfigManager.getInstance().setLogWatchdog(enabled); - } - - @Override - public boolean isLogWatchdogEnabled() { - return ConfigManager.getInstance().isLogWatchdogEnabled(); - } - - @Override - public boolean setAutoInclude(String packageName, boolean enabled) { - return ConfigManager.getInstance().setAutoInclude(packageName, enabled); - } - - @Override - public boolean getAutoInclude(String packageName) { - return ConfigManager.getInstance().getAutoInclude(packageName); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java deleted file mode 100644 index 1b770c3cb..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; - -import android.content.AttributionSource; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.ParcelFileDescriptor; -import android.os.RemoteException; -import android.util.ArrayMap; -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.models.Module; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.service.IXposedScopeCallback; -import io.github.libxposed.service.IXposedService; - -public class LSPModuleService extends IXposedService.Stub { - - private final static String TAG = "LSPosedModuleService"; - - private final static Set uidSet = ConcurrentHashMap.newKeySet(); - private final static Map serviceMap = Collections.synchronizedMap(new WeakHashMap<>()); - - public final static String FILES_DIR = "files"; - - private final @NonNull - Module loadedModule; - - static void uidClear() { - uidSet.clear(); - } - - static void uidStarts(int uid) { - if (!uidSet.contains(uid)) { - uidSet.add(uid); - var module = ConfigManager.getInstance().getModule(uid); - if (module != null && module.file != null && !module.file.legacy) { - var service = serviceMap.computeIfAbsent(module, LSPModuleService::new); - service.sendBinder(uid); - } - } - } - - static void uidGone(int uid) { - uidSet.remove(uid); - } - - private void sendBinder(int uid) { - var name = loadedModule.packageName; - try { - int userId = uid / PackageService.PER_USER_RANGE; - var authority = name + AUTHORITY_SUFFIX; - var provider = ActivityManagerService.getContentProvider(authority, userId); - if (provider == null) { - Log.d(TAG, "no service provider for " + name); - return; - } - var extra = new Bundle(); - extra.putBinder("binder", asBinder()); - Bundle reply = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - reply = provider.call(new AttributionSource.Builder(1000).setPackageName("android").build(), authority, SEND_BINDER, null, extra); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - reply = provider.call("android", null, authority, SEND_BINDER, null, extra); - } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - reply = provider.call("android", authority, SEND_BINDER, null, extra); - } else { - reply = provider.call("android", SEND_BINDER, null, extra); - } - if (reply != null) { - Log.d(TAG, "sent module binder to " + name); - } else { - Log.w(TAG, "failed to send module binder to " + name); - } - } catch (Throwable e) { - Log.w(TAG, "failed to send module binder for uid " + uid, e); - } - } - - LSPModuleService(@NonNull Module module) { - loadedModule = module; - } - - private int ensureModule() throws RemoteException { - var appId = Binder.getCallingUid() % PER_USER_RANGE; - if (loadedModule.appId != appId) { - throw new RemoteException("Module " + loadedModule.packageName + " is not for uid " + Binder.getCallingUid()); - } - return Binder.getCallingUid() / PER_USER_RANGE; - } - - @Override - public int getApiVersion() throws RemoteException { - ensureModule(); - return IXposedService.LIB_API; - } - - @Override - public String getFrameworkName() throws RemoteException { - ensureModule(); - return BuildConfig.FRAMEWORK_NAME; - } - - @Override - public String getFrameworkVersion() throws RemoteException { - ensureModule(); - return BuildConfig.VERSION_NAME; - } - - @Override - public long getFrameworkVersionCode() throws RemoteException { - ensureModule(); - return BuildConfig.VERSION_CODE; - } - - @Override - public long getFrameworkProperties() throws RemoteException { - ensureModule(); - var prop = IXposedService.PROP_CAP_SYSTEM | IXposedService.PROP_CAP_REMOTE; - if (ConfigManager.getInstance().dexObfuscate()) { - prop = prop | IXposedService.PROP_RT_API_PROTECTION; - } - return prop; - } - - @Override - public List getScope() throws RemoteException { - ensureModule(); - ArrayList res = new ArrayList<>(); - var scope = ConfigManager.getInstance().getModuleScope(loadedModule.packageName); - if (scope == null) return res; - for (var s : scope) { - res.add(s.packageName); - } - return res; - } - - @Override - public void requestScope(List packages, IXposedScopeCallback callback) throws RemoteException { - var userId = ensureModule(); - if (!ConfigManager.getInstance().scopeRequestBlocked(loadedModule.packageName)) { - for (String packageName : packages) { - LSPNotificationManager.requestModuleScope(loadedModule.packageName, userId, packageName, callback); - } - } else { - callback.onScopeRequestFailed("Scope request blocked by user configuration"); - } - } - - @Override - public void removeScope(List packages) throws RemoteException { - var userId = ensureModule(); - for (String packageName : packages) { - try { - if (!ConfigManager.getInstance().removeModuleScope(loadedModule.packageName, packageName, userId)) { - Log.w(TAG, "Failed to remove scope: " + packageName + " (Invalid request)"); - } - } catch (Throwable e) { - Log.e(TAG, "Error removing scope for " + packageName, e); - } - } - } - - @Override - public Bundle requestRemotePreferences(String group) throws RemoteException { - var userId = ensureModule(); - var bundle = new Bundle(); - bundle.putSerializable("map", ConfigManager.getInstance().getModulePrefs(loadedModule.packageName, userId, group)); - return bundle; - } - - @Override - public void updateRemotePreferences(String group, Bundle diff) throws RemoteException { - var userId = ensureModule(); - Map values = new ArrayMap<>(); - if (diff.containsKey("delete")) { - var deletes = (Set) diff.getSerializable("delete"); - for (var key : deletes) { - values.put((String) key, null); - } - } - if (diff.containsKey("put")) { - try { - var puts = (Map) diff.getSerializable("put"); - for (var entry : puts.entrySet()) { - values.put((String) entry.getKey(), entry.getValue()); - } - } catch (Throwable e) { - Log.e(TAG, "updateRemotePreferences: ", e); - } - } - try { - ConfigManager.getInstance().updateModulePrefs(loadedModule.packageName, userId, group, values); - ((LSPInjectedModuleService) loadedModule.service).onUpdateRemotePreferences(group, diff); - } catch (Throwable e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public void deleteRemotePreferences(String group) throws RemoteException { - var userId = ensureModule(); - ConfigManager.getInstance().deleteModulePrefs(loadedModule.packageName, userId, group); - } - - @Override - public String[] listRemoteFiles() throws RemoteException { - var userId = ensureModule(); - try { - var dir = ConfigFileManager.resolveModuleDir(loadedModule.packageName, FILES_DIR, userId, Binder.getCallingUid()); - var files = dir.toFile().list(); - return files == null ? new String[0] : files; - } catch (IOException e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public ParcelFileDescriptor openRemoteFile(String path) throws RemoteException { - var userId = ensureModule(); - ConfigFileManager.ensureModuleFilePath(path); - try { - var dir = ConfigFileManager.resolveModuleDir(loadedModule.packageName, FILES_DIR, userId, Binder.getCallingUid()); - return ParcelFileDescriptor.open(dir.resolve(path).toFile(), ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_WRITE); - } catch (IOException e) { - throw new RemoteException(e.getMessage()); - } - } - - @Override - public boolean deleteRemoteFile(String path) throws RemoteException { - var userId = ensureModule(); - ConfigFileManager.ensureModuleFilePath(path); - try { - var dir = ConfigFileManager.resolveModuleDir(loadedModule.packageName, FILES_DIR, userId, Binder.getCallingUid()); - return dir.resolve(path).toFile().delete(); - } catch (IOException e) { - throw new RemoteException(e.getMessage()); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java deleted file mode 100644 index e2ee5148e..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java +++ /dev/null @@ -1,332 +0,0 @@ -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.app.INotificationManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ParceledListSlice; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.AdaptiveIconDrawable; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.graphics.drawable.LayerDrawable; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.util.Log; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.daemon.R; -import org.lsposed.lspd.util.FakeContext; - -import java.util.ArrayList; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import io.github.libxposed.service.IXposedScopeCallback; - -public class LSPNotificationManager { - static final String UPDATED_CHANNEL_ID = "lsposed_module_updated"; - static final String SCOPE_CHANNEL_ID = "lsposed_module_scope"; - private static final String STATUS_CHANNEL_ID = "lsposed_status"; - private static final int STATUS_NOTIFICATION_ID = 2000; - private static final String opPkg = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? - "android" : "com.android.settings"; - - private static final Map notificationIds = new ConcurrentHashMap<>(); - private static int previousNotificationId = STATUS_NOTIFICATION_ID; - - static final String openManagerAction = UUID.randomUUID().toString(); - static final String moduleScope = UUID.randomUUID().toString(); - - private static INotificationManager notificationManager = null; - private static IBinder binder = null; - - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "notificationManager is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - notificationManager = null; - } - }; - - private static INotificationManager getNotificationManager() throws RemoteException { - if (binder == null || notificationManager == null) { - binder = android.os.ServiceManager.getService(Context.NOTIFICATION_SERVICE); - binder.linkToDeath(recipient, 0); - notificationManager = INotificationManager.Stub.asInterface(binder); - } - return notificationManager; - } - - private static Bitmap getBitmap(int id) { - var r = ConfigFileManager.getResources(); - var res = r.getDrawable(id, r.newTheme()); - if (res instanceof BitmapDrawable) { - return ((BitmapDrawable) res).getBitmap(); - } else { - if (res instanceof AdaptiveIconDrawable) { - var layers = new Drawable[]{((AdaptiveIconDrawable) res).getBackground(), - ((AdaptiveIconDrawable) res).getForeground()}; - res = new LayerDrawable(layers); - } - var bitmap = Bitmap.createBitmap(res.getIntrinsicWidth(), - res.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - var canvas = new Canvas(bitmap); - res.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - res.draw(canvas); - return bitmap; - } - } - - private static Icon getNotificationIcon() { - return Icon.createWithBitmap(getBitmap(R.drawable.ic_notification)); - } - - private static boolean hasNotificationChannelForSystem( - INotificationManager nm, String channelId) throws RemoteException { - NotificationChannel channel; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - channel = nm.getNotificationChannelForPackage("android", 1000, channelId, null, false); - } else { - channel = nm.getNotificationChannelForPackage("android", 1000, channelId, false); - } - if (channel != null) { - Log.d(TAG, "hasNotificationChannelForSystem: " + channel); - } - return channel != null; - } - - private static void createNotificationChannel(INotificationManager nm) throws RemoteException { - var context = new FakeContext(); - var list = new ArrayList(); - - var updated = new NotificationChannel(UPDATED_CHANNEL_ID, - context.getString(R.string.module_updated_channel_name), - NotificationManager.IMPORTANCE_HIGH); - updated.setShowBadge(false); - if (hasNotificationChannelForSystem(nm, UPDATED_CHANNEL_ID)) { - Log.d(TAG, "update notification channel: " + UPDATED_CHANNEL_ID); - nm.updateNotificationChannelForPackage("android", 1000, updated); - } else { - list.add(updated); - } - - var status = new NotificationChannel(STATUS_CHANNEL_ID, - context.getString(R.string.status_channel_name), - NotificationManager.IMPORTANCE_MIN); - status.setShowBadge(false); - if (hasNotificationChannelForSystem(nm, STATUS_CHANNEL_ID)) { - Log.d(TAG, "update notification channel: " + STATUS_CHANNEL_ID); - nm.updateNotificationChannelForPackage("android", 1000, status); - } else { - list.add(status); - } - - var scope = new NotificationChannel(SCOPE_CHANNEL_ID, - context.getString(R.string.scope_channel_name), - NotificationManager.IMPORTANCE_HIGH); - scope.setShowBadge(false); - if (hasNotificationChannelForSystem(nm, SCOPE_CHANNEL_ID)) { - Log.d(TAG, "update notification channel: " + SCOPE_CHANNEL_ID); - nm.updateNotificationChannelForPackage("android", 1000, scope); - } else { - list.add(scope); - } - - Log.d(TAG, "create notification channels for android: " + list); - nm.createNotificationChannelsForPackage("android", 1000, new ParceledListSlice<>(list)); - } - - static void notifyStatusNotification() { - var intent = new Intent(openManagerAction); - intent.setPackage("android"); - var context = new FakeContext(); - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - var notification = new Notification.Builder(context, STATUS_CHANNEL_ID) - .setContentTitle(context.getString(R.string.lsposed_running_notification_title)) - .setContentText(context.getString(R.string.lsposed_running_notification_content)) - .setSmallIcon(getNotificationIcon()) - .setContentIntent(PendingIntent.getBroadcast(context, 1, intent, flags)) - .setVisibility(Notification.VISIBILITY_SECRET) - .setColor(0xFFF48FB1) - .setOngoing(true) - .setAutoCancel(false) - .build(); - notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); - try { - var nm = getNotificationManager(); - createNotificationChannel(nm); - nm.enqueueNotificationWithTag("android", opPkg, null, - STATUS_NOTIFICATION_ID, notification, 0); - } catch (RemoteException e) { - Log.e(TAG, "notifyStatusNotification: ", e); - } - } - - static void cancelStatusNotification() { - try { - var nm = getNotificationManager(); - createNotificationChannel(nm); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - nm.cancelNotificationWithTag("android", "android", null, STATUS_NOTIFICATION_ID, 0); - } else { - nm.cancelNotificationWithTag("android", null, STATUS_NOTIFICATION_ID, 0); - } - } catch (RemoteException e) { - Log.e(TAG, "cancelStatusNotification: ", e); - } - } - - private static PendingIntent getModuleIntent(String modulePackageName, int moduleUserId) { - var intent = new Intent(openManagerAction); - intent.setPackage("android"); - intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).build()); - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - return PendingIntent.getBroadcast(new FakeContext(), 3, intent, flags); - } - - private static PendingIntent getModuleScopeIntent(String modulePackageName, int moduleUserId, String scopePackageName, String action, IXposedScopeCallback callback) { - var intent = new Intent(moduleScope); - intent.setPackage("android"); - intent.setData(new Uri.Builder().scheme("module").encodedAuthority(modulePackageName + ":" + moduleUserId).encodedPath(scopePackageName).appendQueryParameter("action", action).build()); - var extras = new Bundle(); - extras.putBinder("callback", callback.asBinder()); - intent.putExtras(extras); - int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; - return PendingIntent.getBroadcast(new FakeContext(), 4, intent, flags); - } - - private static String getNotificationIdKey(String channel, String modulePackageName, int moduleUserId) { - return channel + "/" + modulePackageName + ":" + moduleUserId; - } - - private static int pushAndGetNotificationId(String channel, String modulePackageName, int moduleUserId) { - var idKey = getNotificationIdKey(channel, modulePackageName, moduleUserId); - // previousNotificationId start with 2001 - // https://android.googlesource.com/platform/frameworks/base/+/master/proto/src/system_messages.proto - // https://android.googlesource.com/platform/system/core/+/master/libcutils/include/private/android_filesystem_config.h - // (AID_APP_END - AID_APP_START) x10 = 100000 < NOTE_NETWORK_AVAILABLE - return notificationIds.computeIfAbsent(idKey, key -> previousNotificationId++); - } - - static void notifyModuleUpdated(String modulePackageName, - int moduleUserId, - boolean enabled, - boolean systemModule) { - var context = new FakeContext(); - var userName = UserService.getUserName(moduleUserId); - String title = context.getString(enabled ? systemModule ? - R.string.xposed_module_updated_notification_title_system : - R.string.xposed_module_updated_notification_title : - R.string.module_is_not_activated_yet); - String content = context.getString(enabled ? systemModule ? - R.string.xposed_module_updated_notification_content_system : - R.string.xposed_module_updated_notification_content : - (moduleUserId == 0 ? - R.string.module_is_not_activated_yet_main_user_detailed : - R.string.module_is_not_activated_yet_multi_user_detailed), modulePackageName, userName); - - var style = new Notification.BigTextStyle(); - style.bigText(content); - - var notification = new Notification.Builder(context, UPDATED_CHANNEL_ID) - .setContentTitle(title) - .setContentText(content) - .setSmallIcon(getNotificationIcon()) - .setContentIntent(getModuleIntent(modulePackageName, moduleUserId)) - .setVisibility(Notification.VISIBILITY_SECRET) - .setColor(0xFFF48FB1) - .setAutoCancel(true) - .setStyle(style) - .build(); - notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); - try { - var nm = getNotificationManager(); - nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, - pushAndGetNotificationId(UPDATED_CHANNEL_ID, modulePackageName, moduleUserId), - notification, 0); - } catch (RemoteException e) { - Log.e(TAG, "notify module updated", e); - } - } - - static void requestModuleScope(String modulePackageName, int moduleUserId, String scopePackageName, IXposedScopeCallback callback) { - var context = new FakeContext(); - var userName = UserService.getUserName(moduleUserId); - String title = context.getString(R.string.xposed_module_request_scope_title); - String content = context.getString(R.string.xposed_module_request_scope_content, modulePackageName, userName, scopePackageName); - - var style = new Notification.BigTextStyle(); - style.bigText(content); - - var notification = new Notification.Builder(context, SCOPE_CHANNEL_ID) - .setContentTitle(title) - .setContentText(content) - .setSmallIcon(getNotificationIcon()) - .setVisibility(Notification.VISIBILITY_SECRET) - .setColor(0xFFF48FB1) - .setAutoCancel(true) - .setTimeoutAfter(1000 * 60 * 60) - .setStyle(style) - .setDeleteIntent(getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "delete", callback)) - .setActions(new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_baseline_check_24), - context.getString(R.string.scope_approve), - getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "approve", callback)) - .build(), - new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_baseline_close_24), - context.getString(R.string.scope_deny), - getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "deny", callback)) - .build(), - new Notification.Action.Builder( - Icon.createWithResource(context, R.drawable.ic_baseline_block_24), - context.getString(R.string.nerver_ask_again), - getModuleScopeIntent(modulePackageName, moduleUserId, scopePackageName, "block", callback)) - .build() - ).build(); - notification.extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME); - try { - var nm = getNotificationManager(); - nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, - pushAndGetNotificationId(SCOPE_CHANNEL_ID, modulePackageName, moduleUserId), - notification, 0); - } catch (RemoteException e) { - try { - callback.onScopeRequestFailed(e.getMessage()); - } catch (RemoteException ignored) { - } - Log.e(TAG, "request module scope", e); - } - } - - static void cancelNotification(String channel, String modulePackageName, int moduleUserId) { - try { - var idKey = getNotificationIdKey(channel, modulePackageName, moduleUserId); - var idValue = notificationIds.get(idKey); - if (idValue == null) return; - var nm = getNotificationManager(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - nm.cancelNotificationWithTag("android", "android", modulePackageName, idValue, 0); - } else { - nm.cancelNotificationWithTag("android", modulePackageName, idValue, 0); - } - notificationIds.remove(idKey); - } catch (RemoteException e) { - Log.e(TAG, "cancel notification", e); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java deleted file mode 100644 index facf75417..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 - 2022 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.getSystemServiceManager; - -import android.os.Build; -import android.os.IBinder; -import android.os.IServiceCallback; -import android.os.Parcel; -import android.os.RemoteException; -import android.os.SystemProperties; -import android.util.Log; - -public class LSPSystemServerService extends ILSPSystemServerService.Stub implements IBinder.DeathRecipient { - - private final String proxyServiceName; - private IBinder originService = null; - private int requested; - - public boolean systemServerRequested() { - return requested > 0; - } - - public void putBinderForSystemServer() { - android.os.ServiceManager.addService(proxyServiceName, this); - binderDied(); - } - - public LSPSystemServerService(int maxRetry, String serviceName) { - Log.d(TAG, "LSPSystemServerService::LSPSystemServerService with proxy " + serviceName); - proxyServiceName = serviceName; - requested = -maxRetry; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Registers a callback when system is registering an authentic "serial" service - // And we are proxying all requests to that system service - var serviceCallback = new IServiceCallback.Stub() { - @Override - public void onRegistration(String name, IBinder binder) { - Log.d(TAG, "LSPSystemServerService::LSPSystemServerService onRegistration: " + name + " " + binder); - if (name.equals(proxyServiceName) && binder != null && binder != LSPSystemServerService.this) { - Log.d(TAG, "Register " + name + " " + binder); - originService = binder; - LSPSystemServerService.this.linkToDeath(); - } - } - - @Override - public IBinder asBinder() { - return this; - } - }; - try { - getSystemServiceManager().registerForNotifications(proxyServiceName, serviceCallback); - } catch (Throwable e) { - Log.e(TAG, "unregister: ", e); - } - } - } - - @Override - public ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) { - Log.d(TAG, "ILSPApplicationService.requestApplicationService: " + uid + " " + pid + " " + processName + " " + heartBeat); - requested = 1; - if (ConfigManager.getInstance().shouldSkipSystemServer() || uid != 1000 || heartBeat == null || !"system".equals(processName)) - return null; - else - return ServiceManager.requestApplicationService(uid, pid, processName, heartBeat); - } - - @Override - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - Log.d(TAG, "LSPSystemServerService.onTransact: code=" + code); - if (originService != null) { - return originService.transact(code, data, reply, flags); - } - - switch (code) { - case BridgeService.TRANSACTION_CODE -> { - int uid = data.readInt(); - int pid = data.readInt(); - String processName = data.readString(); - IBinder heartBeat = data.readStrongBinder(); - var service = requestApplicationService(uid, pid, processName, heartBeat); - if (service != null) { - Log.d(TAG, "LSPSystemServerService.onTransact requestApplicationService granted: " + service); - reply.writeNoException(); - reply.writeStrongBinder(service.asBinder()); - return true; - } else { - Log.d(TAG, "LSPSystemServerService.onTransact requestApplicationService rejected"); - return false; - } - } - case LSPApplicationService.OBFUSCATION_MAP_TRANSACTION_CODE, LSPApplicationService.DEX_TRANSACTION_CODE -> { - // Proxy LSP dex transaction to Application Binder - return ServiceManager.getApplicationService().onTransact(code, data, reply, flags); - } - default -> { - return super.onTransact(code, data, reply, flags); - } - } - } - - public void linkToDeath() { - try { - originService.linkToDeath(this, 0); - } catch (Throwable e) { - Log.e(TAG, "system server service: link to death", e); - } - } - - @Override - public void binderDied() { - if (originService != null) { - originService.unlinkToDeath(this, 0); - originService = null; - } - } - - public void maybeRetryInject() { - if (requested < 0) { - Log.w(TAG, "System server injection fails, trying a restart"); - ++requested; - if (Build.SUPPORTED_64_BIT_ABIS.length > 0 && Build.SUPPORTED_32_BIT_ABIS.length > 0) { - // Only devices with both 32-bit and 64-bit support have zygote_secondary - SystemProperties.set("ctl.restart", "zygote_secondary"); - } else { - SystemProperties.set("ctl.restart", "zygote"); - } - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java b/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java deleted file mode 100644 index 4af2632a4..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java +++ /dev/null @@ -1,499 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static android.content.Intent.EXTRA_UID; -import static org.lsposed.lspd.service.LSPNotificationManager.SCOPE_CHANNEL_ID; -import static org.lsposed.lspd.service.LSPNotificationManager.UPDATED_CHANNEL_ID; -import static org.lsposed.lspd.service.PackageService.PER_USER_RANGE; -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.getExecutorService; - -import android.app.IApplicationThread; -import android.app.IUidObserver; -import android.content.Context; -import android.content.IIntentReceiver; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.provider.Telephony; -import android.telephony.TelephonyManager; -import android.util.Log; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.models.Application; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.zip.ZipFile; - -import hidden.HiddenApiBridge; -import io.github.libxposed.service.IXposedScopeCallback; - -public class LSPosedService extends ILSPosedService.Stub { - private static final int AID_NOBODY = 9999; - private static final int USER_NULL = -10000; - private static final String ACTION_USER_ADDED = "android.intent.action.USER_ADDED"; - public static final String ACTION_USER_REMOVED = "android.intent.action.USER_REMOVED"; - private static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle"; - private static final String EXTRA_REMOVED_FOR_ALL_USERS = "android.intent.extra.REMOVED_FOR_ALL_USERS"; - private static boolean bootCompleted = false; - private IBinder appThread = null; - - private static boolean isModernModules(ApplicationInfo info) { - String[] apks; - if (info.splitSourceDirs != null) { - apks = Arrays.copyOf(info.splitSourceDirs, info.splitSourceDirs.length + 1); - apks[info.splitSourceDirs.length] = info.sourceDir; - } else apks = new String[]{info.sourceDir}; - for (var apk : apks) { - try (var zip = new ZipFile(apk)) { - if (zip.getEntry("META-INF/xposed/java_init.list") != null) { - return true; - } - } catch (IOException ignored) { - } - } - return false; - } - - @Override - public ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) { - if (Binder.getCallingUid() != 1000) { - Log.w(TAG, "Someone else got my binder!?"); - return null; - } - if (ServiceManager.getApplicationService().hasRegister(uid, pid)) { - Log.d(TAG, "Skipped duplicated request for uid " + uid + " pid " + pid); - return null; - } - if (!ServiceManager.getManagerService().shouldStartManager(pid, uid, processName) && ConfigManager.getInstance().shouldSkipProcess(new ConfigManager.ProcessScope(processName, uid))) { - Log.d(TAG, "Skipped " + processName + "/" + uid); - return null; - } - Log.d(TAG, "returned service"); - return ServiceManager.requestApplicationService(uid, pid, processName, heartBeat); - } - - /** - * This part is quite complex. - * For modules, we never care about its user id, we only care about its apk path. - * So we will only process module's removal when it's removed from all users. - * And FULLY_REMOVE is exactly the one. - *

- * For applications, we care about its user id. - * So we will process application's removal when it's removed from every single user. - * However, PACKAGE_REMOVED will be triggered by `pm hide`, so we use UID_REMOVED instead. - */ - - private void dispatchPackageChanged(Intent intent) { - if (intent == null) return; - int uid = intent.getIntExtra(EXTRA_UID, AID_NOBODY); - if (uid == AID_NOBODY || uid <= 0) return; - int userId = intent.getIntExtra("android.intent.extra.user_handle", USER_NULL); - var intentAction = intent.getAction(); - if (intentAction == null) return; - var allUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false); - if (userId == USER_NULL) userId = uid % PER_USER_RANGE; - Uri uri = intent.getData(); - var module = ConfigManager.getInstance().getModule(uid); - String moduleName = (uri != null) ? uri.getSchemeSpecificPart() : (module != null) ? module.packageName : null; - - ApplicationInfo applicationInfo = null; - if (moduleName != null) { - try { - applicationInfo = PackageService.getApplicationInfo(moduleName, PackageManager.GET_META_DATA | PackageService.MATCH_ALL_FLAGS, 0); - } catch (Throwable ignored) { - } - } - - boolean isXposedModule = applicationInfo != null && ((applicationInfo.metaData != null && applicationInfo.metaData.containsKey("xposedminversion")) || isModernModules(applicationInfo)); - - switch (intentAction) { - case Intent.ACTION_PACKAGE_FULLY_REMOVED -> { - // for module, remove module - // because we only care about when the apk is gone - if (moduleName != null) { - if (allUsers && ConfigManager.getInstance().removeModule(moduleName)) { - isXposedModule = true; - broadcastAndShowNotification(moduleName, userId, intent, true); - } - LSPNotificationManager.cancelNotification(UPDATED_CHANNEL_ID, moduleName, userId); - } - } - case Intent.ACTION_PACKAGE_REMOVED -> { - if (moduleName != null) { - LSPNotificationManager.cancelNotification(UPDATED_CHANNEL_ID, moduleName, userId); - } - break; - } - case Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED -> { - var configManager = ConfigManager.getInstance(); - // make sure that the change is for the complete package, not only a - // component - String[] components = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); - if (components != null && !Arrays.stream(components).reduce(false, (p, c) -> p || c.equals(moduleName), Boolean::logicalOr)) { - return; - } - if (isXposedModule) { - // When installing a new Xposed module, we update the apk path to mark it as a - // module to send a broadcast when modules that have not been activated are - // uninstalled. - // If cache not updated, assume it's not xposed module - isXposedModule = configManager.updateModuleApkPath(moduleName, ConfigManager.getInstance().getModuleApkPath(applicationInfo), false); - } else { - if (configManager.isUidHooked(uid)) { - // it will automatically remove obsolete app from database - configManager.updateAppCache(); - } - if (intentAction.equals(Intent.ACTION_PACKAGE_ADDED) && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { - for (String xposedModule : configManager.getAutoIncludeModules()) { - // For Xposed modules with auto_include set, we always add new applications - // to its scope - var list = configManager.getModuleScope(xposedModule); - if (list != null) { - Application scope = new Application(); - scope.packageName = moduleName; - scope.userId = userId; - list.add(scope); - try { - if (!configManager.setModuleScope(xposedModule, list)) { - Log.e(TAG, "failed to set scope for " + xposedModule); - } - } catch(RemoteException re) { - Log.e(TAG, "failed to set scope for " + xposedModule, re); - } - } - } - } - } - broadcastAndShowNotification(moduleName, userId, intent, isXposedModule); - } - case Intent.ACTION_UID_REMOVED -> { - // when a package is removed (rather than hide) for a single user - // (apk may still be there because of multi-user) - broadcastAndShowNotification(moduleName, userId, intent, isXposedModule); - if (isXposedModule) { - // it will auto remove obsolete app and scope from database - ConfigManager.getInstance().updateCache(); - } else if (ConfigManager.getInstance().isUidHooked(uid)) { - // it will auto remove obsolete scope from database - ConfigManager.getInstance().updateAppCache(); - } - } - } - boolean removed = Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intentAction) || Intent.ACTION_UID_REMOVED.equals(intentAction); - - Log.d(TAG, "Package changed: uid=" + uid + " userId=" + userId + " action=" + intentAction + " isXposedModule=" + isXposedModule + " isAllUsers=" + allUsers); - - if (BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME.equals(moduleName) && userId == 0) { - Log.d(TAG, "Manager updated"); - ConfigManager.getInstance().updateManager(removed); - } - } - - private void broadcastAndShowNotification(String packageName, int userId, Intent intent, boolean isXposedModule) { - Log.d(TAG, "package " + packageName + " changed, dispatching to manager"); - var action = intent.getAction(); - var allUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false); - intent.putExtra("android.intent.extra.PACKAGES", packageName); - intent.putExtra(Intent.EXTRA_USER, userId); - intent.putExtra("isXposedModule", isXposedModule); - LSPManagerService.broadcastIntent(intent); - if (isXposedModule) { - var enabledModules = ConfigManager.getInstance().enabledModules(); - var scope = ConfigManager.getInstance().getModuleScope(packageName); - boolean systemModule = scope != null && scope.parallelStream().anyMatch(app -> app.packageName.equals("system")); - boolean enabled = Arrays.asList(enabledModules).contains(packageName); - if (!(Intent.ACTION_UID_REMOVED.equals(action) || Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action) || allUsers)) - LSPNotificationManager.notifyModuleUpdated(packageName, userId, enabled, systemModule); - } - } - - private void dispatchUserChanged(Intent intent) { - if (intent == null) return; - int uid = intent.getIntExtra(EXTRA_USER_HANDLE, AID_NOBODY); - if (uid == AID_NOBODY || uid <= 0) return; - LSPManagerService.broadcastIntent(intent); - } - - private void dispatchBootCompleted(Intent intent) { - bootCompleted = true; - var configManager = ConfigManager.getInstance(); - if (configManager.enableStatusNotification()) { - LSPNotificationManager.notifyStatusNotification(); - } else { - LSPNotificationManager.cancelStatusNotification(); - } - } - - private void dispatchConfigurationChanged(Intent intent) { - if (!bootCompleted) return; - ConfigFileManager.reloadConfiguration(); - var configManager = ConfigManager.getInstance(); - if (configManager.enableStatusNotification()) { - LSPNotificationManager.notifyStatusNotification(); - } else { - LSPNotificationManager.cancelStatusNotification(); - } - } - - private void dispatchSecretCodeReceive(Intent i) { - LSPManagerService.openManager(null); - } - - private void dispatchOpenManager(Intent intent) { - LSPManagerService.openManager(intent.getData()); - } - - private void dispatchModuleScope(Intent intent) { - Log.d(TAG, "dispatchModuleScope: " + intent); - var data = intent.getData(); - var extras = intent.getExtras(); - if (extras == null || data == null) return; - var callback = extras.getBinder("callback"); - if (callback == null || !callback.isBinderAlive()) return; - var authority = data.getEncodedAuthority(); - if (authority == null) return; - var s = authority.split(":", 2); - if (s.length != 2) return; - var packageName = s[0]; - int userId; - try { - userId = Integer.parseInt(s[1]); - } catch (NumberFormatException e) { - return; - } - var scopePackageName = data.getPath(); - if (scopePackageName == null) return; - scopePackageName = scopePackageName.substring(1); - var action = data.getQueryParameter("action"); - if (action == null) return; - - var iCallback = IXposedScopeCallback.Stub.asInterface(callback); - try { - var applicationInfo = PackageService.getApplicationInfo(scopePackageName, 0, userId); - if (applicationInfo == null) { - iCallback.onScopeRequestFailed("Package not found"); - return; - } - - switch (action) { - case "approve" -> { - ConfigManager.getInstance().setModuleScope(packageName, scopePackageName, userId); - iCallback.onScopeRequestApproved(Collections.singletonList(scopePackageName)); - } - case "deny" -> iCallback.onScopeRequestFailed("Request denied by user"); - case "delete" -> iCallback.onScopeRequestFailed("Request timeout"); - case "block" -> { - ConfigManager.getInstance().blockScopeRequest(packageName); - iCallback.onScopeRequestFailed("Request blocked by configuration"); - } - } - Log.i(TAG, action + " scope " + scopePackageName + " for " + packageName + " in user " + userId); - } catch (RemoteException e) { - try { - iCallback.onScopeRequestFailed(e.getMessage()); - } catch (RemoteException ignored) { - // callback died - } - } - LSPNotificationManager.cancelNotification(SCOPE_CHANNEL_ID, packageName, userId); - } - - private void registerReceiver(List filters, String requiredPermission, int userId, Consumer task, int flag) { - var receiver = new IIntentReceiver.Stub() { - @Override - public void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser) { - getExecutorService().submit(() -> { - try { - task.accept(intent); - } catch (Throwable t) { - Log.e(TAG, "performReceive: ", t); - } - }); - if (!ordered && !Objects.equals(intent.getAction(), Intent.ACTION_LOCKED_BOOT_COMPLETED)) - return; - try { - ActivityManagerService.finishReceiver(this, appThread, resultCode, data, extras, false, intent.getFlags()); - } catch (RemoteException e) { - Log.e(TAG, "finish receiver", e); - } - } - }; - try { - for (var filter : filters) { - ActivityManagerService.registerReceiver("android", null, receiver, filter, requiredPermission, userId, flag); - } - } catch (RemoteException e) { - Log.e(TAG, "register receiver", e); - } - } - - private void registerReceiver(List filters, int userId, Consumer task) { - //noinspection InlinedApi - registerReceiver(filters, "android.permission.BRICK", userId, task, Context.RECEIVER_NOT_EXPORTED); - } - - private void registerPackageReceiver() { - var packageFilter = new IntentFilter(); - packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); - packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); - packageFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); - packageFilter.addDataScheme("package"); - - var uidFilter = new IntentFilter(Intent.ACTION_UID_REMOVED); - - registerReceiver(List.of(packageFilter, uidFilter), -1, this::dispatchPackageChanged); - Log.d(TAG, "registered package receiver"); - } - - private void registerConfigurationReceiver() { - var intentFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); - - registerReceiver(List.of(intentFilter), 0, this::dispatchConfigurationChanged); - Log.d(TAG, "registered configuration receiver"); - } - - private void registerSecretCodeReceiver() { - IntentFilter intentFilter = new IntentFilter(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - intentFilter.addAction(TelephonyManager.ACTION_SECRET_CODE); - } else { - // noinspection InlinedApi - intentFilter.addAction(Telephony.Sms.Intents.SECRET_CODE_ACTION); - } - intentFilter.addDataAuthority("5776733", null); - intentFilter.addDataScheme("android_secret_code"); - - //noinspection InlinedApi - registerReceiver(List.of(intentFilter), "android.permission.CONTROL_INCALL_EXPERIENCE", - 0, this::dispatchSecretCodeReceive, Context.RECEIVER_EXPORTED); - Log.d(TAG, "registered secret code receiver"); - } - - private void registerBootCompleteReceiver() { - var intentFilter = new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED); - intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); - registerReceiver(List.of(intentFilter), 0, this::dispatchBootCompleted); - Log.d(TAG, "registered boot receiver"); - } - - private void registerUserChangeReceiver() { - var userFilter = new IntentFilter(); - userFilter.addAction(ACTION_USER_ADDED); - userFilter.addAction(ACTION_USER_REMOVED); - - registerReceiver(List.of(userFilter), -1, this::dispatchUserChanged); - Log.d(TAG, "registered user info change receiver"); - } - - private void registerOpenManagerReceiver() { - var intentFilter = new IntentFilter(LSPNotificationManager.openManagerAction); - var moduleFilter = new IntentFilter(intentFilter); - moduleFilter.addDataScheme("module"); - - registerReceiver(List.of(intentFilter, moduleFilter), 0, this::dispatchOpenManager); - Log.d(TAG, "registered open manager receiver"); - } - - private void registerModuleScopeReceiver() { - var intentFilter = new IntentFilter(LSPNotificationManager.moduleScope); - intentFilter.addDataScheme("module"); - - registerReceiver(List.of(intentFilter), 0, this::dispatchModuleScope); - Log.d(TAG, "registered module scope receiver"); - } - - private void registerUidObserver() { - try { - var which = HiddenApiBridge.ActivityManager_UID_OBSERVER_ACTIVE() - | HiddenApiBridge.ActivityManager_UID_OBSERVER_GONE() - | HiddenApiBridge.ActivityManager_UID_OBSERVER_IDLE() - | HiddenApiBridge.ActivityManager_UID_OBSERVER_CACHED(); - LSPModuleService.uidClear(); - ActivityManagerService.registerUidObserver(new IUidObserver.Stub() { - @Override - public void onUidActive(int uid) { - LSPModuleService.uidStarts(uid); - } - - @Override - public void onUidCachedChanged(int uid, boolean cached) { - if (!cached) LSPModuleService.uidStarts(uid); - } - - @Override - public void onUidIdle(int uid, boolean disabled) { - LSPModuleService.uidStarts(uid); - } - - @Override - public void onUidGone(int uid, boolean disabled) { - LSPModuleService.uidGone(uid); - } - }, which, HiddenApiBridge.ActivityManager_PROCESS_STATE_UNKNOWN(), null); - } catch (RemoteException e) { - Log.e(TAG, "registerUidObserver", e); - } - } - - @Override - public void dispatchSystemServerContext(IBinder appThread, IBinder activityToken, String api) { - Log.d(TAG, "received system context"); - this.appThread = appThread; - ConfigManager.getInstance().setApi(api); - ActivityManagerService.onSystemServerContext(IApplicationThread.Stub.asInterface(appThread), activityToken); - registerBootCompleteReceiver(); - registerPackageReceiver(); - registerConfigurationReceiver(); - registerSecretCodeReceiver(); - registerUserChangeReceiver(); - registerOpenManagerReceiver(); - registerModuleScopeReceiver(); - registerUidObserver(); - - if (ServiceManager.isLateInject) { - Log.i(TAG, "System already booted during late injection. Manually triggering boot completed."); - dispatchBootCompleted(null); - } - } - - @Override - public boolean preStartManager() { - return ServiceManager.getManagerService().preStartManager(); - } - - @Override - public boolean setManagerEnabled(boolean enabled) throws RemoteException { - return ServiceManager.getManagerService().setEnabled(enabled); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java b/daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java deleted file mode 100644 index 74ba4fae9..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java +++ /dev/null @@ -1,222 +0,0 @@ -package org.lsposed.lspd.service; - -import android.annotation.SuppressLint; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.SELinux; -import android.os.SystemProperties; -import android.system.Os; -import android.util.Log; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.LinkedHashMap; - -public class LogcatService implements Runnable { - private static final String TAG = "LSPosedLogcat"; - private static final int mode = ParcelFileDescriptor.MODE_WRITE_ONLY | - ParcelFileDescriptor.MODE_CREATE | - ParcelFileDescriptor.MODE_TRUNCATE | - ParcelFileDescriptor.MODE_APPEND; - private int modulesFd = -1; - private int verboseFd = -1; - private Thread thread = null; - - static class LogLRU extends LinkedHashMap { - private static final int MAX_ENTRIES = 10; - - public LogLRU() { - super(MAX_ENTRIES, 1f, false); - } - - @Override - synchronized protected boolean removeEldestEntry(Entry eldest) { - if (size() > MAX_ENTRIES && eldest.getKey().delete()) { - Log.d(TAG, "Deleted old log " + eldest.getKey().getAbsolutePath()); - return true; - } - return false; - } - } - - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private final LinkedHashMap moduleLogs = new LogLRU(); - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - private final LinkedHashMap verboseLogs = new LogLRU(); - - @SuppressLint("UnsafeDynamicallyLoadedCode") - public LogcatService() { - String classPath = System.getProperty("java.class.path"); - var abi = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS[0] : Build.SUPPORTED_32_BIT_ABIS[0]; - System.load(classPath + "!/lib/" + abi + "/" + System.mapLibraryName("daemon")); - ConfigFileManager.moveLogDir(); - - // Meizu devices set this prop and prevent debug logs from being recorded - if (SystemProperties.getInt("persist.sys.log_reject_level", 0) > 0) { - SystemProperties.set("persist.sys.log_reject_level", "0"); - } - - getprop(); - dmesg(); - } - - private static void getprop() { - // multithreaded process can not change their context type, - // start a new process to set restricted context to filter privacy props - var cmd = "echo -n u:r:untrusted_app:s0 > /proc/thread-self/attr/current; getprop"; - try { - SELinux.setFSCreateContext("u:object_r:app_data_file:s0"); - new ProcessBuilder("sh", "-c", cmd) - .redirectOutput(ConfigFileManager.getPropsPath()) - .start(); - } catch (IOException e) { - Log.e(TAG, "getprop: ", e); - } finally { - SELinux.setFSCreateContext(null); - } - } - - private static void dmesg() { - try { - new ProcessBuilder("dmesg") - .redirectOutput(ConfigFileManager.getKmsgPath()) - .start(); - } catch (IOException e) { - Log.e(TAG, "dmesg: ", e); - } - } - - private native void runLogcat(); - - @Override - public void run() { - Log.i(TAG, "start running"); - runLogcat(); - Log.i(TAG, "stopped"); - } - - @SuppressWarnings("unused") - private int refreshFd(boolean isVerboseLog) { - try { - File log; - if (isVerboseLog) { - checkFd(verboseFd); - log = ConfigFileManager.getNewVerboseLogPath(); - } else { - checkFd(modulesFd); - log = ConfigFileManager.getNewModulesLogPath(); - } - Log.i(TAG, "New log file: " + log); - ConfigFileManager.chattr0(log.toPath().getParent()); - int fd = ParcelFileDescriptor.open(log, mode).detachFd(); - if (isVerboseLog) { - synchronized (verboseLogs) { - verboseLogs.put(log, new Object()); - } - verboseFd = fd; - } else { - synchronized (moduleLogs) { - moduleLogs.put(log, new Object()); - } - modulesFd = fd; - } - return fd; - } catch (IOException e) { - if (isVerboseLog) verboseFd = -1; - else modulesFd = -1; - Log.w(TAG, "refreshFd", e); - return -1; - } - } - - private static void checkFd(int fd) { - if (fd == -1) return; - try { - var jfd = new FileDescriptor(); - //noinspection JavaReflectionMemberAccess DiscouragedPrivateApi - jfd.getClass().getDeclaredMethod("setInt$", int.class).invoke(jfd, fd); - var stat = Os.fstat(jfd); - if (stat.st_nlink == 0) { - var file = Files.readSymbolicLink(fdToPath(fd)); - var parent = file.getParent(); - if (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) { - if (ConfigFileManager.chattr0(parent)) - Files.deleteIfExists(parent); - } - var name = file.getFileName().toString(); - var originName = name.substring(0, name.lastIndexOf(' ')); - Files.copy(file, parent.resolve(originName)); - } - } catch (Throwable e) { - Log.w(TAG, "checkFd " + fd, e); - } - } - - public boolean isRunning() { - return thread != null && thread.isAlive(); - } - - public void start() { - if (isRunning()) return; - thread = new Thread(this); - thread.setName("logcat"); - thread.setUncaughtExceptionHandler((t, e) -> { - Log.e(TAG, "Crash unexpectedly: ", e); - thread = null; - start(); - }); - thread.start(); - } - - public void startVerbose() { - Log.i(TAG, "!!start_verbose!!"); - } - - public void stopVerbose() { - Log.i(TAG, "!!stop_verbose!!"); - } - - public void enableWatchdog() { - Log.i(TAG, "!!start_watchdog!!"); - } - - public void disableWatchdog() { - Log.i(TAG, "!!stop_watchdog!!"); - } - - public void refresh(boolean isVerboseLog) { - if (isVerboseLog) { - Log.i(TAG, "!!refresh_verbose!!"); - } else { - Log.i(TAG, "!!refresh_modules!!"); - } - } - - private static Path fdToPath(int fd) { - if (fd == -1) return null; - else return Paths.get("/proc/self/fd", String.valueOf(fd)); - } - - public File getVerboseLog() { - var path = fdToPath(verboseFd); - return path == null ? null : path.toFile(); - } - - public File getModulesLog() { - var path = fdToPath(modulesFd); - return path == null ? null : path.toFile(); - } - - public void checkLogFile() { - if (modulesFd == -1) - refresh(false); - if (verboseFd == -1) - refresh(true); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java deleted file mode 100644 index 4446f2366..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.lsposed.lspd.service; - -import android.os.SharedMemory; - -import java.util.HashMap; - -public class ObfuscationManager { - // For module dexes - static native SharedMemory obfuscateDex(SharedMemory memory); - - // generates signature - static native HashMap getSignatures(); -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/PackageService.java b/daemon/src/main/java/org/lsposed/lspd/service/PackageService.java deleted file mode 100644 index eceaa0746..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/PackageService.java +++ /dev/null @@ -1,435 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static android.content.pm.ServiceInfo.FLAG_ISOLATED_PROCESS; -import static org.lsposed.lspd.service.ServiceManager.TAG; -import static org.lsposed.lspd.service.ServiceManager.existsInGlobalNamespace; - -import android.content.IIntentReceiver; -import android.content.IIntentSender; -import android.content.Intent; -import android.content.IntentSender; -import android.content.pm.ApplicationInfo; -import android.content.pm.ComponentInfo; -import android.content.pm.IPackageManager; -import android.content.pm.PackageInfo; -import android.content.pm.PackageInstaller; -import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.pm.VersionedPackage; -import android.os.Build; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemProperties; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.lsposed.lspd.models.Application; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.stream.Collectors; - -import rikka.parcelablelist.ParcelableListSlice; - -public class PackageService { - - static final int INSTALL_FAILED_INTERNAL_ERROR = -110; - static final int INSTALL_REASON_UNKNOWN = 0; - static final int MATCH_ANY_USER = 0x00400000; // PackageManager.MATCH_ANY_USER - - static final int MATCH_ALL_FLAGS = PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_UNINSTALLED_PACKAGES | MATCH_ANY_USER; - public static final int PER_USER_RANGE = 100000; - - private static IPackageManager pm = null; - private static IBinder binder = null; - private static final Method getInstalledPackagesMethod; - - static { - Method method = null; - try { - boolean isLongFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; - Class flagsType = isLongFlags ? long.class : int.class; - - for (Method m : IPackageManager.class.getDeclaredMethods()) { - if (m.getName().equals("getInstalledPackages") && - m.getParameterTypes().length == 2 && - m.getParameterTypes()[0] == flagsType) { - m.setAccessible(true); - method = m; - break; - } - } - } catch (Exception e) { - Log.e("PackageManagerUtils", "Failed to find getInstalledPackages method", e); - } - getInstalledPackagesMethod = method; - } - - private static List getInstalledPackagesReflect(IPackageManager pm, int flags, int userId) { - if (getInstalledPackagesMethod == null || pm == null) - return Collections.emptyList(); - try { - Object flagsObj; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - flagsObj = (long) flags; - } else { - flagsObj = (int) flags; - } - Object result = getInstalledPackagesMethod.invoke(pm, flagsObj, userId); - if (result instanceof ParceledListSlice) { - // noinspection unchecked - return ((ParceledListSlice) result).getList(); - } - } catch (Exception e) { - Log.w("PackageManagerUtils", "Reflection call failed", e); - } - return Collections.emptyList(); - } - - static boolean isAlive() { - var pm = getPackageManager(); - return pm != null && pm.asBinder().isBinderAlive(); - } - - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "pm is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - pm = null; - } - }; - - private static IPackageManager getPackageManager() { - if (binder == null || pm == null) { - binder = ServiceManager.getService("package"); - if (binder == null) return null; - try { - binder.linkToDeath(recipient, 0); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - pm = IPackageManager.Stub.asInterface(binder); - } - return pm; - } - - @Nullable - public static PackageInfo getPackageInfo(String packageName, int flags, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return pm.getPackageInfo(packageName, (long) flags, userId); - } - return pm.getPackageInfo(packageName, flags, userId); - } - - public static @NonNull - Map getPackageInfoFromAllUsers(String packageName, int flags) throws RemoteException { - IPackageManager pm = getPackageManager(); - Map res = new HashMap<>(); - if (pm == null) return res; - for (var user : UserService.getUsers()) { - var info = getPackageInfo(packageName, flags, user.id); - if (info != null && info.applicationInfo != null) res.put(user.id, info); - } - return res; - } - - @Nullable - public static ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return pm.getApplicationInfo(packageName, (long) flags, userId); - } - return pm.getApplicationInfo(packageName, flags, userId); - } - - // Only for manager - public static ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) - throws RemoteException { - List res = new ArrayList<>(); - IPackageManager pm = getPackageManager(); - if (pm == null) return ParcelableListSlice.emptyList(); - // Prepare flags once outside the loop - for (var user : UserService.getUsers()) { - // Use the reflective helper instead of direct AIDL calls - List infos = getInstalledPackagesReflect(pm, flags, user.id); - res.addAll(infos.parallelStream() - .filter(info -> info.applicationInfo != null - && info.applicationInfo.uid / PER_USER_RANGE == user.id) - .filter(info -> { - try { - return isPackageAvailable(info.packageName, user.id, true); - } catch (RemoteException e) { - return false; - } - }) - .collect(Collectors.toList())); - } - if (filterNoProcess) { - return new ParcelableListSlice<>(res.parallelStream().filter(packageInfo -> { - try { - PackageInfo pkgInfo = getPackageInfoWithComponents(packageInfo.packageName, MATCH_ALL_FLAGS, packageInfo.applicationInfo.uid / PER_USER_RANGE); - return !fetchProcesses(pkgInfo).isEmpty(); - } catch (RemoteException e) { - Log.w(TAG, "filter failed", e); - return true; - } - }).collect(Collectors.toList())); - } - return new ParcelableListSlice<>(res); - } - - private static Set fetchProcesses(PackageInfo pkgInfo) { - HashSet processNames = new HashSet<>(); - if (pkgInfo == null) return processNames; - for (ComponentInfo[] componentInfos : new ComponentInfo[][]{pkgInfo.activities, pkgInfo.receivers, pkgInfo.providers}) { - if (componentInfos == null) continue; - for (ComponentInfo componentInfo : componentInfos) { - processNames.add(componentInfo.processName); - } - } - if (pkgInfo.services == null) return processNames; - for (ServiceInfo service : pkgInfo.services) { - if ((service.flags & FLAG_ISOLATED_PROCESS) == 0) { - processNames.add(service.processName); - } - } - return processNames; - } - - public static Pair, Integer> fetchProcessesWithUid(Application app) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return new Pair<>(Collections.emptySet(), -1); - PackageInfo pkgInfo = getPackageInfoWithComponents(app.packageName, MATCH_ALL_FLAGS, app.userId); - if (pkgInfo == null || pkgInfo.applicationInfo == null) - return new Pair<>(Collections.emptySet(), -1); - return new Pair<>(fetchProcesses(pkgInfo), pkgInfo.applicationInfo.uid); - } - - public static boolean isPackageAvailable(String packageName, int userId, boolean ignoreHidden) throws RemoteException { - return pm.isPackageAvailable(packageName, userId) || (ignoreHidden && pm.getApplicationHiddenSettingAsUser(packageName, userId)); - } - - @SuppressWarnings({"ConstantConditions", "SameParameterValue"}) - @Nullable - private static PackageInfo getPackageInfoWithComponents(String packageName, int flags, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - PackageInfo pkgInfo; - try { - pkgInfo = getPackageInfo(packageName, flags | PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES | PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS, userId); - } catch (Exception e) { - pkgInfo = getPackageInfo(packageName, flags, userId); - if (pkgInfo == null) return null; - try { - pkgInfo.activities = getPackageInfo(packageName, flags | PackageManager.GET_ACTIVITIES, userId).activities; - } catch (Exception ignored) { - - } - try { - pkgInfo.services = getPackageInfo(packageName, flags | PackageManager.GET_SERVICES, userId).services; - } catch (Exception ignored) { - - } - try { - pkgInfo.receivers = getPackageInfo(packageName, flags | PackageManager.GET_RECEIVERS, userId).receivers; - } catch (Exception ignored) { - - } - try { - pkgInfo.providers = getPackageInfo(packageName, flags | PackageManager.GET_PROVIDERS, userId).providers; - } catch (Exception ignored) { - - } - } - if (pkgInfo == null || pkgInfo.applicationInfo == null || (!pkgInfo.packageName.equals("android") && (pkgInfo.applicationInfo.sourceDir == null || !existsInGlobalNamespace(pkgInfo.applicationInfo.sourceDir) || !isPackageAvailable(packageName, userId, true)))) - return null; - return pkgInfo; - } - - static abstract class IntentSenderAdaptor extends IIntentSender.Stub { - public abstract void send(Intent intent); - - @Override - public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { - send(intent); - return 0; - } - - @Override - public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { - send(intent); - } - - public IntentSender getIntentSender() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException { - @SuppressWarnings("JavaReflectionMemberAccess") - Constructor intentSenderConstructor = IntentSender.class.getConstructor(IIntentSender.class); - intentSenderConstructor.setAccessible(true); - return intentSenderConstructor.newInstance(this); - } - } - - public static boolean uninstallPackage(VersionedPackage versionedPackage, int userId) throws RemoteException, InterruptedException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { - CountDownLatch latch = new CountDownLatch(1); - final boolean[] result = {false}; - var flag = userId == -1 ? 0x00000002 : 0; //PackageManager.DELETE_ALL_USERS = 0x00000002; UserHandle ALL = new UserHandle(-1); - pm.getPackageInstaller().uninstall(versionedPackage, "android", flag, new IntentSenderAdaptor() { - @Override - public void send(Intent intent) { - int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); - result[0] = status == PackageInstaller.STATUS_SUCCESS; - Log.d(TAG, intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)); - latch.countDown(); - } - }.getIntentSender(), userId == -1 ? 0 : userId); - latch.await(); - return result[0]; - } - - public static int installExistingPackageAsUser(String packageName, int userId) throws RemoteException { - IPackageManager pm = getPackageManager(); - Log.d(TAG, "about to install existing package " + packageName + "/" + userId); - if (pm == null) return INSTALL_FAILED_INTERNAL_ERROR; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return pm.installExistingPackageAsUser(packageName, userId, 0, INSTALL_REASON_UNKNOWN, null); - } else { - return pm.installExistingPackageAsUser(packageName, userId, 0, INSTALL_REASON_UNKNOWN); - } - } - - @Nullable - public static ParcelableListSlice queryIntentActivities(Intent intent, String resolvedType, int flags, int userId) { - try { - IPackageManager pm = getPackageManager(); - if (pm == null) return null; - ParceledListSlice infos; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - infos = pm.queryIntentActivities(intent, resolvedType, (long) flags, userId); - } else { - infos = pm.queryIntentActivities(intent, resolvedType, flags, userId); - } - return new ParcelableListSlice<>(infos.getList()); - } catch (Exception e) { - Log.e(TAG, "queryIntentActivities", e); - return new ParcelableListSlice<>(new ArrayList<>()); - } - } - - @Nullable - public static Intent getLaunchIntentForPackage(String packageName) throws RemoteException { - Intent intentToResolve = new Intent(Intent.ACTION_MAIN); - intentToResolve.addCategory(Intent.CATEGORY_INFO); - intentToResolve.setPackage(packageName); - var ris = queryIntentActivities(intentToResolve, intentToResolve.getType(), 0, 0); - - // Otherwise, try to find a main launcher activity. - if (ris == null || ris.getList().size() == 0) { - // reuse the intent instance - intentToResolve.removeCategory(Intent.CATEGORY_INFO); - intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER); - intentToResolve.setPackage(packageName); - ris = queryIntentActivities(intentToResolve, intentToResolve.getType(), 0, 0); - } - if (ris == null || ris.getList().size() == 0) { - return null; - } - Intent intent = new Intent(intentToResolve); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setClassName(ris.getList().get(0).activityInfo.packageName, - ris.getList().get(0).activityInfo.name); - return intent; - } - - public static void clearApplicationProfileData(String packageName) throws RemoteException { - IPackageManager pm = getPackageManager(); - if (pm == null) return; - pm.clearApplicationProfileData(packageName); - } - - public static boolean performDexOptMode(String packageName) throws RemoteException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - Process process = null; - try { - // The 'speed-profile' filter is a balanced choice for performance. - String command = "cmd package compile -m speed-profile -f " + packageName; - process = Runtime.getRuntime().exec(command); - - // Capture and log the output for debugging. - StringBuilder output = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); - } - } - - int exitCode = process.waitFor(); - Log.i(TAG, "Dexopt command finished for " + packageName + " with exit code: " + exitCode); - - // A successful command returns exit code 0 and typically "Success" in its output. - return exitCode == 0 && output.toString().contains("Success"); - - } catch (Exception e) { - Log.e(TAG, "Failed to execute dexopt shell command for " + packageName, e); - if (e instanceof InterruptedException) { - // Preserve the interrupted status. - Thread.currentThread().interrupt(); - } - return false; - } finally { - if (process != null) { - process.destroy(); - } - } - } else { - // Fallback to the original reflection method for older Android versions. - IPackageManager pm = getPackageManager(); - if (pm == null) return false; - return pm.performDexOptMode(packageName, - SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false), - SystemProperties.get("pm.dexopt.install", "speed-profile"), true, true, null); - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/PowerService.java b/daemon/src/main/java/org/lsposed/lspd/service/PowerService.java deleted file mode 100644 index 7ce1ac5b6..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/PowerService.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * - */ - -package org.lsposed.lspd.service; - -import static android.content.Context.POWER_SERVICE; -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.os.IBinder; -import android.os.IPowerManager; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -public class PowerService { - private static IPowerManager pm = null; - private static IBinder binder = null; - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "PowerManager is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - pm = null; - } - }; - - private static IPowerManager getPowerManager() { - if (binder == null || pm == null) { - binder = ServiceManager.getService(POWER_SERVICE); - if (binder == null) return null; - try { - binder.linkToDeath(recipient, 0); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - pm = IPowerManager.Stub.asInterface(binder); - } - return pm; - } - - public static void reboot(boolean confirm, String reason, boolean wait) throws RemoteException { - IPowerManager pm = getPowerManager(); - if (pm == null) return; - pm.reboot(confirm, reason, wait); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java b/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java deleted file mode 100644 index e0fbc16af..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import android.app.ActivityThread; -import android.app.Notification; -import android.content.Context; -import android.ddm.DdmHandleAppName; -import android.os.Binder; -import android.os.Build; -import android.os.IBinder; -import android.os.IServiceManager; -import android.os.Looper; -import android.os.Parcel; -import android.os.Process; -import android.os.RemoteException; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; - -import com.android.internal.os.BinderInternal; - -import org.lsposed.daemon.BuildConfig; -import org.lsposed.lspd.util.FakeContext; - -import java.io.File; -import java.lang.AbstractMethodError; -import java.lang.Class; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import hidden.HiddenApiBridge; - -public class ServiceManager { - public static final String TAG = "LSPosedService"; - private static final File globalNamespace = new File("/proc/1/root"); - @SuppressWarnings("FieldCanBeLocal") - private static LSPosedService mainService = null; - private static LSPApplicationService applicationService = null; - private static LSPManagerService managerService = null; - private static LSPSystemServerService systemServerService = null; - private static LogcatService logcatService = null; - private static Dex2OatService dex2OatService = null; - - public static boolean isLateInject = false; - public static String proxyServiceName = "serial"; - - private static final ExecutorService executorService = Executors.newSingleThreadExecutor(); - - @RequiresApi(Build.VERSION_CODES.Q) - public static Dex2OatService getDex2OatService() { - return dex2OatService; - } - - public static ExecutorService getExecutorService() { - return executorService; - } - - private static void waitSystemService(String name) { - while (android.os.ServiceManager.getService(name) == null) { - try { - Log.i(TAG, "service " + name + " is not started, wait 1s."); - //noinspection BusyWait - Thread.sleep(1000); - } catch (InterruptedException e) { - Log.i(TAG, Log.getStackTraceString(e)); - } - } - } - - public static IServiceManager getSystemServiceManager() { - return IServiceManager.Stub.asInterface(HiddenApiBridge.Binder_allowBlocking(BinderInternal.getContextObject())); - } - - // call by ourselves - public static void start(String[] args) { - if (!ConfigFileManager.tryLock()) System.exit(0); - - int systemServerMaxRetry = 1; - for (String arg : args) { - if (arg.startsWith("--system-server-max-retry=")) { - try { - systemServerMaxRetry = Integer.parseInt(arg.substring(arg.lastIndexOf('=') + 1)); - } catch (Throwable ignored) { - } - } else if (arg.equals("--late-inject")) { - isLateInject = true; - proxyServiceName = "serial_vector"; - } - } - - Log.i(TAG, "Vector daemon started: lateInject: " + isLateInject); - Log.i(TAG, String.format("version %s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); - - Thread.setDefaultUncaughtExceptionHandler((t, e) -> { - Log.e(TAG, "Uncaught exception", e); - System.exit(1); - }); - - logcatService = new LogcatService(); - logcatService.start(); - - // get config before package service is started - // otherwise getInstance will trigger module/scope cache - var configManager = ConfigManager.getInstance(); - // --- DO NOT call ConfigManager.getInstance later!!! --- - - // Unblock log watchdog before starting anything else - if (configManager.isLogWatchdogEnabled()) - logcatService.enableWatchdog(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) - permissionManagerWorkaround(); - - Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); - Looper.prepareMainLooper(); - - - mainService = new LSPosedService(); - applicationService = new LSPApplicationService(); - managerService = new LSPManagerService(); - systemServerService = new LSPSystemServerService(systemServerMaxRetry, proxyServiceName); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - dex2OatService = new Dex2OatService(); - dex2OatService.start(); - } - - systemServerService.putBinderForSystemServer(); - - ActivityThread.systemMain(); - - DdmHandleAppName.setAppName("org.lsposed.daemon", 0); - - waitSystemService("package"); - waitSystemService("activity"); - waitSystemService(Context.USER_SERVICE); - waitSystemService(Context.APP_OPS_SERVICE); - - ConfigFileManager.reloadConfiguration(); - - notificationWorkaround(); - - BridgeService.send(mainService, new BridgeService.Listener() { - @Override - public void onSystemServerRestarted() { - Log.w(TAG, "system restarted..."); - } - - @Override - public void onResponseFromBridgeService(boolean response) { - if (response) { - Log.i(TAG, "sent service to bridge"); - } else { - Log.w(TAG, "no response from bridge"); - } - systemServerService.maybeRetryInject(); - } - - @Override - public void onSystemServerDied() { - Log.w(TAG, "system server died"); - systemServerService.putBinderForSystemServer(); - managerService.onSystemServerDied(); - } - }); - - // Force logging on boot, now let's see if we need to stop logging - if (!configManager.verboseLog()) { - logcatService.stopVerbose(); - } - - Looper.loop(); - throw new RuntimeException("Main thread loop unexpectedly exited"); - } - - public static LSPApplicationService getApplicationService() { - return applicationService; - } - - public static LSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat) { - if (applicationService.registerHeartBeat(uid, pid, processName, heartBeat)) - return applicationService; - else return null; - } - - public static LSPManagerService getManagerService() { - return managerService; - } - - public static LogcatService getLogcatService() { - return logcatService; - } - - public static boolean systemServerRequested() { - return systemServerService.systemServerRequested(); - } - - public static File toGlobalNamespace(File file) { - return new File(globalNamespace, file.getAbsolutePath()); - } - - public static File toGlobalNamespace(String path) { - if (path == null) return null; - if (path.startsWith("/")) return new File(globalNamespace, path); - else return toGlobalNamespace(new File(path)); - } - - public static boolean existsInGlobalNamespace(File file) { - return toGlobalNamespace(file).exists(); - } - - public static boolean existsInGlobalNamespace(String path) { - return toGlobalNamespace(path).exists(); - } - - private static void permissionManagerWorkaround() { - try { - Field sCacheField = android.os.ServiceManager.class.getDeclaredField("sCache"); - sCacheField.setAccessible(true); - var sCache = (Map) sCacheField.get(null); - sCache.put("permissionmgr", new BinderProxy("permissionmgr")); - sCache.put("legacy_permission", new BinderProxy("legacy_permission")); - sCache.put("appops", new BinderProxy("appops")); - } catch (Throwable e) { - Log.e(TAG, "failed to init permission manager", e); - } - } - - private static void notificationWorkaround() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - try { - Class feature = Class.forName("android.app.FeatureFlagsImpl"); - Field systemui_is_cached = feature.getDeclaredField("systemui_is_cached"); - systemui_is_cached.setAccessible(true); - systemui_is_cached.set(null, true); - Log.d(TAG, "set flag systemui_is_cached to true"); - } catch (Throwable e) { - Log.e(TAG, "failed to change feature flags", e); - } - } - - try { - new Notification.Builder(new FakeContext(), "notification_workaround").build(); - } catch (AbstractMethodError e) { - FakeContext.nullProvider = ! FakeContext.nullProvider; - } catch (Throwable e) { - Log.e(TAG, "failed to build notifications", e); - } - - } - - private static class BinderProxy extends Binder { - private static final Method rawGetService; - - static { - try { - rawGetService = android.os.ServiceManager.class.getDeclaredMethod("rawGetService", String.class); - rawGetService.setAccessible(true); - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - private IBinder mReal = null; - private final String mName; - - BinderProxy(String name) { - mName = name; - } - - @Override - protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { - synchronized (this) { - if (mReal == null) { - try { - mReal = (IBinder) rawGetService.invoke(null, mName); - } catch (IllegalAccessException | InvocationTargetException ignored){ - - } - } - if (mReal != null) { - return mReal.transact(code, data, reply, flags); - } - } - // getSplitPermissions - if (reply != null && mName.equals("permissionmgr")) - reply.writeTypedList(List.of()); - return true; - } - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/service/UserService.java b/daemon/src/main/java/org/lsposed/lspd/service/UserService.java deleted file mode 100644 index f6eee5597..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/service/UserService.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2021 LSPosed Contributors - */ - -package org.lsposed.lspd.service; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.content.Context; -import android.content.pm.UserInfo; -import android.os.Build; -import android.os.IBinder; -import android.os.IUserManager; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -import org.lsposed.lspd.util.Utils; - -import java.util.LinkedList; -import java.util.List; - -public class UserService { - private static IUserManager um = null; - private static IBinder binder = null; - private static final IBinder.DeathRecipient recipient = new IBinder.DeathRecipient() { - @Override - public void binderDied() { - Log.w(TAG, "um is dead"); - binder.unlinkToDeath(this, 0); - binder = null; - um = null; - } - }; - - static boolean isAlive() { - var um = getUserManager(); - return um != null && um.asBinder().isBinderAlive(); - } - - public static IUserManager getUserManager() { - if (binder == null || um == null) { - binder = ServiceManager.getService(Context.USER_SERVICE); - if (binder == null) return null; - try { - binder.linkToDeath(recipient, 0); - } catch (RemoteException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - um = IUserManager.Stub.asInterface(binder); - } - return um; - } - - public static List getUsers() throws RemoteException { - IUserManager um = getUserManager(); - List users = new LinkedList<>(); - if (um == null) return users; - try { - users = um.getUsers(true); - } catch (NoSuchMethodError e) { - users = um.getUsers(true, true, true); - } - if (Utils.isLENOVO) { // lenovo hides user [900, 910) for app cloning - var gotUsers = new boolean[10]; - for (var user : users) { - var residual = user.id - 900; - if (residual >= 0 && residual < 10) gotUsers[residual] = true; - } - for (int i = 900; i <= 909; i++) { - var user = um.getUserInfo(i); - if (user != null && !gotUsers[i - 900]) { - users.add(user); - } - } - } - return users; - } - - public static UserInfo getUserInfo(int userId) throws RemoteException { - IUserManager um = getUserManager(); - if (um == null) return null; - return um.getUserInfo(userId); - } - - public static String getUserName(int userId) { - try { - var userInfo = getUserInfo(userId); - if (userInfo != null) return userInfo.name; - } catch (RemoteException ignored) { - } - return String.valueOf(userId); - } - - public static int getProfileParent(int userId) throws RemoteException { - IUserManager um = getUserManager(); - if (um == null) return -1; - var userInfo = um.getProfileParent(userId); - if (userInfo == null) return userId; - else return userInfo.id; - } - - public static boolean isUserUnlocked(int userId) throws RemoteException { - IUserManager um = getUserManager(); - if (um == null) return false; - return um.isUserUnlocked(userId); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java b/daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java deleted file mode 100644 index ce5d5a3ad..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.lsposed.lspd.util; - -import static org.lsposed.lspd.service.ServiceManager.TAG; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.os.UserHandle; -import android.util.Log; - -import androidx.annotation.Nullable; - -import org.lsposed.lspd.service.ConfigFileManager; -import org.lsposed.lspd.service.PackageService; - -import hidden.HiddenApiBridge; - -public class FakeContext extends ContextWrapper { - static ApplicationInfo systemApplicationInfo = null; - static Resources.Theme theme = null; - - public static Boolean nullProvider = false; - - private String packageName = "android"; - public FakeContext() { - super(null); - } - - public FakeContext(String packageName) { - super(null); - this.packageName = packageName; - } - - @Override - public String getPackageName() { - return packageName; - } - - @Override - public Resources getResources() { - return ConfigFileManager.getResources(); - } - - @Override - public String getOpPackageName() { - return "android"; - } - - @Override - public ApplicationInfo getApplicationInfo() { - try { - if (systemApplicationInfo == null) - systemApplicationInfo = PackageService.getApplicationInfo("android", 0, 0); - } catch (Throwable e) { - Log.e(TAG, "getApplicationInfo", e); - } - return systemApplicationInfo; - } - - @Override - public ContentResolver getContentResolver() { - if (nullProvider) { - return null; - } else { - return new ContentResolver(this) {}; - } - } - - public int getUserId() { - return 0; - } - - public UserHandle getUser() { - return HiddenApiBridge.UserHandle(0); - } - - @Override - public Resources.Theme getTheme() { - if (theme == null) theme = getResources().newTheme(); - return theme; - } - - @Nullable - @Override - public String getAttributionTag() { - return null; - } - - @Override - public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException { - throw new PackageManager.NameNotFoundException(packageName); - } -} diff --git a/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java b/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java deleted file mode 100644 index 1d6b81d15..000000000 --- a/daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.lsposed.lspd.util; - -import static org.lsposed.lspd.util.SignInfo.CERTIFICATE; - -import com.android.apksig.ApkVerifier; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; - -public class InstallerVerifier { - - public static void verifyInstallerSignature(String path) throws IOException { - ApkVerifier verifier = new ApkVerifier.Builder(new File(path)) - .setMinCheckedPlatformVersion(27) - .build(); - try { - ApkVerifier.Result result = verifier.verify(); - if (!result.isVerified()) { - throw new IOException("apk signature not verified"); - } - var mainCert = result.getSignerCertificates().get(0); - if (!Arrays.equals(mainCert.getEncoded(), CERTIFICATE)) { - var dname = mainCert.getSubjectX500Principal().getName(); - throw new IOException("apk signature mismatch: " + dname); - } - } catch (Exception t) { - throw new IOException(t); - } - } -} diff --git a/daemon/src/main/jni/dex2oat.cpp b/daemon/src/main/jni/dex2oat.cpp index 062e41343..32cf77d07 100644 --- a/daemon/src/main/jni/dex2oat.cpp +++ b/daemon/src/main/jni/dex2oat.cpp @@ -1,21 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2023 LSPosed Contributors - */ #include #include #include @@ -28,50 +10,71 @@ #include "logging.h" -extern "C" JNIEXPORT void JNICALL Java_org_lsposed_lspd_service_Dex2OatService_doMountNative( +// Lightweight RAII wrapper to prevent FD leaks +struct UniqueFd { + int fd; + explicit UniqueFd(int fd) : fd(fd) {} + ~UniqueFd() { + if (fd >= 0) close(fd); + } + operator int() const { return fd; } +}; + +extern "C" JNIEXPORT void JNICALL Java_org_matrix_vector_daemon_env_Dex2OatServer_doMountNative( JNIEnv *env, jobject, jboolean enabled, jstring r32, jstring d32, jstring r64, jstring d64) { char dex2oat32[PATH_MAX], dex2oat64[PATH_MAX]; - realpath("bin/dex2oat32", dex2oat32); - realpath("bin/dex2oat64", dex2oat64); + if (realpath("bin/dex2oat32", dex2oat32) == nullptr) { + PLOGE("resolve realpath for bin/dex2oat32"); + } + if (realpath("bin/dex2oat64", dex2oat64) == nullptr) { + PLOGE("resolve realpath for bin/dex2oat64"); + } - if (pid_t pid = fork(); pid > 0) { // parent + const char *r32p = r32 ? env->GetStringUTFChars(r32, nullptr) : nullptr; + const char *d32p = d32 ? env->GetStringUTFChars(d32, nullptr) : nullptr; + const char *r64p = r64 ? env->GetStringUTFChars(r64, nullptr) : nullptr; + const char *d64p = d64 ? env->GetStringUTFChars(d64, nullptr) : nullptr; + + pid_t pid = fork(); + if (pid > 0) { // Parent process waitpid(pid, nullptr, 0); - } else { // child - int ns = open("/proc/1/ns/mnt", O_RDONLY); - setns(ns, CLONE_NEWNS); - close(ns); - const char *r32p, *d32p, *r64p, *d64p; - if (r32) r32p = env->GetStringUTFChars(r32, nullptr); - if (d32) d32p = env->GetStringUTFChars(d32, nullptr); - if (r64) r64p = env->GetStringUTFChars(r64, nullptr); - if (d64) d64p = env->GetStringUTFChars(d64, nullptr); + // Safely release JNI strings in the parent + if (r32p) env->ReleaseStringUTFChars(r32, r32p); + if (d32p) env->ReleaseStringUTFChars(d32, d32p); + if (r64p) env->ReleaseStringUTFChars(r64, r64p); + if (d64p) env->ReleaseStringUTFChars(d64, d64p); + } else if (pid == 0) { // Child process + UniqueFd ns(open("/proc/1/ns/mnt", O_RDONLY)); + if (ns >= 0) { + setns(ns, CLONE_NEWNS); + } if (enabled) { LOGI("Enable dex2oat wrapper"); - if (r32) { + if (r32p) { mount(dex2oat32, r32p, nullptr, MS_BIND, nullptr); mount(nullptr, r32p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } - if (d32) { + if (d32p) { mount(dex2oat32, d32p, nullptr, MS_BIND, nullptr); mount(nullptr, d32p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } - if (r64) { + if (r64p) { mount(dex2oat64, r64p, nullptr, MS_BIND, nullptr); mount(nullptr, r64p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } - if (d64) { + if (d64p) { mount(dex2oat64, d64p, nullptr, MS_BIND, nullptr); mount(nullptr, d64p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } execlp("resetprop", "resetprop", "--delete", "dalvik.vm.dex2oat-flags", nullptr); } else { LOGI("Disable dex2oat wrapper"); - if (r32) umount(r32p); - if (d32) umount(d32p); - if (r64) umount(r64p); - if (d64) umount(d64p); + if (r32p) umount(r32p); + if (d32p) umount(d32p); + if (r64p) umount(r64p); + if (d64p) umount(d64p); execlp("resetprop", "resetprop", "dalvik.vm.dex2oat-flags", "--inline-max-code-units=0", nullptr); } @@ -83,8 +86,9 @@ extern "C" JNIEXPORT void JNICALL Java_org_lsposed_lspd_service_Dex2OatService_d static int setsockcreatecon_raw(const char *context) { std::string path = "/proc/self/task/" + std::to_string(gettid()) + "/attr/sockcreate"; - int fd = open(path.c_str(), O_RDWR | O_CLOEXEC); + UniqueFd fd(open(path.c_str(), O_RDWR | O_CLOEXEC)); if (fd < 0) return -1; + int ret; if (context) { do { @@ -95,20 +99,19 @@ static int setsockcreatecon_raw(const char *context) { ret = write(fd, nullptr, 0); // clear } while (ret < 0 && errno == EINTR); } - close(fd); return ret < 0 ? -1 : 0; } extern "C" JNIEXPORT jboolean JNICALL -Java_org_lsposed_lspd_service_Dex2OatService_setSockCreateContext(JNIEnv *env, jclass, - jstring contextStr) { - const char *context = env->GetStringUTFChars(contextStr, nullptr); +Java_org_matrix_vector_daemon_env_Dex2OatServer_setSockCreateContext(JNIEnv *env, jclass, + jstring contextStr) { + const char *context = contextStr ? env->GetStringUTFChars(contextStr, nullptr) : nullptr; int ret = setsockcreatecon_raw(context); - env->ReleaseStringUTFChars(contextStr, context); + if (context) env->ReleaseStringUTFChars(contextStr, context); return ret == 0; } extern "C" JNIEXPORT jstring JNICALL -Java_org_lsposed_lspd_service_Dex2OatService_getSockPath(JNIEnv *env, jobject) { +Java_org_matrix_vector_daemon_env_Dex2OatServer_getSockPath(JNIEnv *env, jobject) { return env->NewStringUTF("5291374ceda0aef7c5d86cd2a4f6a3ac\0"); } diff --git a/daemon/src/main/jni/logcat.cpp b/daemon/src/main/jni/logcat.cpp index 2e5897a39..4c0a0586c 100644 --- a/daemon/src/main/jni/logcat.cpp +++ b/daemon/src/main/jni/logcat.cpp @@ -5,8 +5,8 @@ #include #include +#include #include -#include #include #include #include @@ -113,8 +113,6 @@ class Logcat { static size_t PrintLogLine(const AndroidLogEntry &entry, FILE *out); - void StartLogWatchDog(); - JNIEnv *env_; jobject thiz_; jmethodID refresh_fd_method_; @@ -130,7 +128,6 @@ class Logcat { pid_t my_pid_ = getpid(); bool verbose_ = true; - std::atomic enable_watchdog = std::atomic(false); }; size_t Logcat::PrintLogLine(const AndroidLogEntry &entry, FILE *out) { @@ -217,23 +214,32 @@ void Logcat::ProcessBuffer(struct log_msg *buf) { if (android_log_processLogBuffer(&buf->entry, &entry) < 0) return; entry.tagLen--; - std::string_view tag(entry.tag, entry.tagLen); bool shortcut = false; - if (tag == "LSPosed-Bridge"sv || tag == "XSharedPreferences"sv || tag == "LSPosedContext") + + if (tag == "VectorLegacyBridge"sv || tag == "XSharedPreferences"sv || tag == "VectorContext"sv) [[unlikely]] { modules_print_count_ += PrintLogLine(entry, modules_file_.get()); shortcut = true; } - if (verbose_ && (shortcut || buf->id() == log_id::LOG_ID_CRASH || entry.pid == my_pid_ || - tag == "APatchD"sv || tag == "Dobby"sv || tag.starts_with("dex2oat"sv) || - tag == "KernelSU"sv || tag == "LSPlant"sv || tag == "LSPlt"sv || - tag.starts_with("LSPosed"sv) || tag == "Magisk"sv || tag == "SELinux"sv || - tag == "TEESimulator"sv || tag.starts_with("Vector"sv) || - tag.starts_with("zygisk"sv))) [[unlikely]] { + + constexpr std::array exact_tags = { + "APatchD"sv, "Dobby"sv, "KernelSU"sv, "LSPlant"sv, + "LSPlt"sv, "Magisk"sv, "SELinux"sv, "TEESimulator"sv}; + constexpr std::array prefix_tags = {"dex2oat"sv, "Vector"sv, "LSPosed"sv, + "zygisk"sv}; + + bool match_exact = + std::any_of(exact_tags.begin(), exact_tags.end(), [&](auto t) { return tag == t; }); + bool match_prefix = std::any_of(prefix_tags.begin(), prefix_tags.end(), + [&](auto t) { return tag.starts_with(t); }); + + if (verbose_ && (shortcut || buf->id() == log_id::LOG_ID_CRASH || match_exact || match_prefix)) + [[unlikely]] { verbose_print_count_ += PrintLogLine(entry, verbose_file_.get()); } - if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] { + + if (entry.pid == my_pid_ && tag == "VectorLogcat"sv) [[unlikely]] { std::string_view msg(entry.message, entry.messageLen); if (msg == "!!start_verbose!!"sv) { verbose_ = true; @@ -244,83 +250,10 @@ void Logcat::ProcessBuffer(struct log_msg *buf) { RefreshFd(false); } else if (msg == "!!refresh_verbose!!"sv) { RefreshFd(true); - } else if (msg == "!!start_watchdog!!"sv) { - if (!enable_watchdog) StartLogWatchDog(); - enable_watchdog = true; - enable_watchdog.notify_one(); - } else if (msg == "!!stop_watchdog!!"sv) { - enable_watchdog = false; - enable_watchdog.notify_one(); - std::system("resetprop -p --delete persist.logd.size"); - std::system("resetprop -p --delete persist.logd.size.crash"); - std::system("resetprop -p --delete persist.logd.size.main"); - std::system("resetprop -p --delete persist.logd.size.system"); - - // Terminate the watchdog thread by exiting __system_property_wait firs firstt - std::system("setprop persist.log.tag V"); - std::system("resetprop -p --delete persist.log.tag"); } } } -void Logcat::StartLogWatchDog() { - constexpr static auto kLogdSizeProp = "persist.logd.size"sv; - constexpr static auto kLogdTagProp = "persist.log.tag"sv; - constexpr static auto kLogdCrashSizeProp = "persist.logd.size.crash"sv; - constexpr static auto kLogdMainSizeProp = "persist.logd.size.main"sv; - constexpr static auto kLogdSystemSizeProp = "persist.logd.size.system"sv; - constexpr static long kErr = -1; - std::thread watchdog([this] { - Log("[LogWatchDog started]\n"); - while (true) { - enable_watchdog.wait(false); // Blocking current thread until enable_watchdog is true; - auto logd_size = GetByteProp(kLogdSizeProp); - auto logd_tag = GetStrProp(kLogdTagProp); - auto logd_crash_size = GetByteProp(kLogdCrashSizeProp); - auto logd_main_size = GetByteProp(kLogdMainSizeProp); - auto logd_system_size = GetByteProp(kLogdSystemSizeProp); - Log("[LogWatchDog running] log.tag: " + logd_tag + - "; logd.[default, crash, main, system].size: [" + std::to_string(logd_size) + "," + - std::to_string(logd_crash_size) + "," + std::to_string(logd_main_size) + "," + - std::to_string(logd_system_size) + "]\n"); - if (!logd_tag.empty() || - !((logd_crash_size == kErr && logd_main_size == kErr && logd_system_size == kErr && - logd_size != kErr && logd_size >= kLogBufferSize) || - (logd_crash_size != kErr && logd_crash_size >= kLogBufferSize && - logd_main_size != kErr && logd_main_size >= kLogBufferSize && - logd_system_size != kErr && logd_system_size >= kLogBufferSize))) { - SetIntProp(kLogdSizeProp, std::max(kLogBufferSize, logd_size)); - SetIntProp(kLogdCrashSizeProp, std::max(kLogBufferSize, logd_crash_size)); - SetIntProp(kLogdMainSizeProp, std::max(kLogBufferSize, logd_main_size)); - SetIntProp(kLogdSystemSizeProp, std::max(kLogBufferSize, logd_system_size)); - SetStrProp(kLogdTagProp, ""); - SetStrProp("ctl.start", "logd-reinit"); - } - const auto *pi = __system_property_find(kLogdTagProp.data()); - uint32_t serial = 0; - if (pi != nullptr) { - __system_property_read_callback( - pi, [](auto *c, auto, auto, auto s) { *reinterpret_cast(c) = s; }, - &serial); - } - if (!__system_property_wait(pi, serial, &serial, nullptr)) break; - if (pi != nullptr) { - if (enable_watchdog) { - Log("\nProp persist.log.tag changed, resetting log settings\n"); - } else { - break; // End current thread as expected - } - } else { - // log tag prop was not found; to avoid frequently trigger wait, sleep for a while - std::this_thread::sleep_for(1s); - } - } - Log("[LogWatchDog stopped]\n"); - }); - pthread_setname_np(watchdog.native_handle(), "watchdog"); - watchdog.detach(); -} - void Logcat::Run() { constexpr size_t tail_after_crash = 10U; size_t tail = 0; @@ -354,14 +287,12 @@ void Logcat::Run() { if (modules_print_count_ >= kMaxLogSize) [[unlikely]] RefreshFd(false); } - OnCrash(errno); } } extern "C" JNIEXPORT void JNICALL -// NOLINTNEXTLINE -Java_org_lsposed_lspd_service_LogcatService_runLogcat(JNIEnv *env, jobject thiz) { +Java_org_matrix_vector_daemon_env_LogcatMonitor_runLogcat(JNIEnv *env, jobject thiz) { jclass clazz = env->GetObjectClass(thiz); jmethodID method = env->GetMethodID(clazz, "refreshFd", "(Z)I"); Logcat logcat(env, thiz, method); diff --git a/daemon/src/main/jni/logcat.h b/daemon/src/main/jni/logcat.h index cc39b58a1..e2560ccc3 100644 --- a/daemon/src/main/jni/logcat.h +++ b/daemon/src/main/jni/logcat.h @@ -1,10 +1,9 @@ #pragma once +#include #include #include -#include - #define NS_PER_SEC 1000000000L #define MS_PER_NSEC 1000000 #define LOGGER_ENTRY_MAX_LEN (5 * 1024) @@ -43,15 +42,13 @@ struct log_msg { struct logger_entry entry; }; #ifdef __cplusplus - log_id_t id() { - return static_cast(entry.lid); - } + log_id_t id() { return static_cast(entry.lid); } #endif }; struct logger; struct logger_list; -long android_logger_get_log_size(struct logger* logger); +long android_logger_get_log_size(struct logger *logger); int android_logger_set_log_size(struct logger *logger, unsigned long size); struct logger_list *android_logger_list_alloc(int mode, unsigned int tail, pid_t pid); void android_logger_list_free(struct logger_list *logger_list); diff --git a/daemon/src/main/jni/logging.h b/daemon/src/main/jni/logging.h index 86a7220ac..e25e000dc 100644 --- a/daemon/src/main/jni/logging.h +++ b/daemon/src/main/jni/logging.h @@ -1,23 +1,3 @@ -/* - * This file is part of LSPosed. - * - * LSPosed is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LSPosed is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LSPosed. If not, see . - * - * Copyright (C) 2020 EdXposed Contributors - * Copyright (C) 2021 LSPosed Contributors - */ - #ifndef _LOGGING_H #define _LOGGING_H @@ -25,7 +5,7 @@ #include #ifndef LOG_TAG -#define LOG_TAG "LSPosed" +#define LOG_TAG "VectorNativeDaemon" #endif #ifdef LOG_DISABLED @@ -36,8 +16,12 @@ #define LOGE(...) 0 #else #ifndef NDEBUG -#define LOGD(fmt, ...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, "%s:%d#%s" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) -#define LOGV(fmt, ...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, "%s:%d#%s" ": " fmt, __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(,) __VA_ARGS__) +#define LOGD(fmt, ...) \ + __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \ + "%s:%d#%s" \ + ": " fmt, \ + __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) #else #define LOGD(...) 0 #define LOGV(...) 0 diff --git a/daemon/src/main/jni/obfuscation.cpp b/daemon/src/main/jni/obfuscation.cpp index 9965f42f1..d6ecc83fa 100644 --- a/daemon/src/main/jni/obfuscation.cpp +++ b/daemon/src/main/jni/obfuscation.cpp @@ -23,10 +23,10 @@ namespace { std::once_flag init_flag; std::map signatures = { - {"Lde/robv/android/xposed/", ""}, {"Landroid/app/AndroidApp", ""}, - {"Landroid/content/res/XRes", ""}, {"Landroid/content/res/XModule", ""}, - {"Lorg/matrix/vector/core/", ""}, {"Lorg/matrix/vector/nativebridge/", ""}, - {"Lorg/matrix/vector/service/", ""}, + {"Lde/robv/android/xposed/", ""}, {"Landroid/app/AndroidApp", ""}, + {"Landroid/content/res/XRes", ""}, {"Landroid/content/res/XModule", ""}, + {"Lio/github/libxposed/api/Xposed", ""}, {"Lorg/matrix/vector/core/", ""}, + {"Lorg/matrix/vector/nativebridge/", ""}, {"Lorg/matrix/vector/service/", ""}, }; jclass class_file_descriptor = nullptr; @@ -112,7 +112,7 @@ static void ensureInitialized(JNIEnv *env) { for (auto &i : signatures) { i.second = regen(i.first); - LOGD("%s => %s", i.first.c_str(), i.second.c_str()); + LOGV("%s => %s", i.first.c_str(), i.second.c_str()); } LOGD("ObfuscationManager init successfully"); @@ -145,7 +145,8 @@ static jobject stringMapToJavaHashMap(JNIEnv *env, const std::map(dex_data), size}; reader.CreateFullIr(); auto ir = reader.GetIr(); @@ -191,29 +191,32 @@ static int obfuscateDexBuffer(const void *dex_data, size_t size) { return allocator.GetFd(); } -extern "C" JNIEXPORT jobject JNICALL Java_org_lsposed_lspd_service_ObfuscationManager_obfuscateDex( - JNIEnv *env, [[maybe_unused]] jclass clazz, jobject memory) { +extern "C" JNIEXPORT jobject JNICALL +Java_org_matrix_vector_daemon_utils_ObfuscationManager_obfuscateDex(JNIEnv *env, + [[maybe_unused]] jclass clazz, + jobject memory) { ensureInitialized(env); int fd = ASharedMemory_dupFromJava(env, memory); if (fd < 0) return nullptr; auto size = ASharedMemory_getSize(fd); - LOGD("obfuscateDex: fd=%d, size=%zu", fd, size); + LOGV("obfuscateDex: fd=%d, size=%zu", fd, size); // CRITICAL: We MUST use MAP_SHARED here, not MAP_PRIVATE. - // 1. Android's SharedMemory is backed by purely virtual IPC buffers (ashmem/memfd). - // If we use MAP_PRIVATE, the kernel attempts to create a Copy-On-Write snapshot. - // Because the Java side just populated this virtual buffer and immediately passed - // it to JNI, mapping it MAP_PRIVATE often results in mapping unpopulated zero-pages, - // which causes Slicer to read a corrupted/empty header and abort. - // 2. Using MAP_SHARED gives us direct pointers to the exact physical memory pages - // populated by Java. - // 3. ZERO-COPY ARCHITECTURE: Because Slicer's IR holds direct pointers to this mapped - // memory, mutating strings in-place (via const_cast) instantly updates the IR - // without allocating new memory. Since the Java caller discards the original - // SharedMemory buffer anyway, this in-place mutation is completely safe and highly - // efficient. + // 1. Android's SharedMemory is backed by ashmem or memfd. Mapping these as + // MAP_PRIVATE creates a Copy-On-Write (COW) layer. In many Android kernel + // configurations, this COW layer does not correctly fault-in the initial + // contents from the shared source, resulting in the JNI side seeing + // unpopulated zero-pages. This causes slicer to fail immediately. + // 2. Using MAP_SHARED ensures we have direct access to the same physical + // pages populated by the Java layer. + // 3. ZERO-COPY MUTATION: Slicer's Intermediate Representation (IR) points + // directly into this mapped memory for string data. By mutating the + // buffer in-place, we update the IR's state without any additional + // heap allocations. This is safe here because the Daemon owns the + // lifecycle of this temporary buffer and the Java caller will discard + // the un-obfuscated original anyway. void *mem = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { LOGE("Failed to map input dex"); @@ -230,7 +233,7 @@ extern "C" JNIEXPORT jobject JNICALL Java_org_lsposed_lspd_service_ObfuscationMa } if (!needs_obfuscation) { - LOGD("No target signatures found in fd=%d, skipping slicer.", fd); + LOGV("No target signatures found in fd=%d, skipping slicer.", fd); munmap(mem, size); // Wrap the duplicated FD into Java objects and return instantly diff --git a/daemon/src/main/jni/obfuscation.h b/daemon/src/main/jni/obfuscation.h index 16b81124c..27861659a 100644 --- a/daemon/src/main/jni/obfuscation.h +++ b/daemon/src/main/jni/obfuscation.h @@ -16,7 +16,7 @@ class DexAllocator : public dex::Writer::Allocator { public: inline void* Allocate(size_t size) override { - LOGD("DexAllocator: attempting to allocate %zu bytes", size); + LOGV("DexAllocator: attempting to allocate %zu bytes", size); fd_ = ASharedMemory_create("obfuscated_dex", size); if (fd_ < 0) { @@ -37,7 +37,7 @@ class DexAllocator : public dex::Writer::Allocator { mapped_mem_ = nullptr; } - LOGD("DexAllocator: success, mapped at %p, fd=%d", mapped_mem_, fd_); + LOGV("DexAllocator: success, mapped at %p, fd=%d", mapped_mem_, fd_); return mapped_mem_; } diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt new file mode 100644 index 000000000..b305fbc11 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/Cli.kt @@ -0,0 +1,364 @@ +package org.matrix.vector.daemon + +import android.net.LocalSocket +import android.net.LocalSocketAddress +import android.os.Process +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.ToNumberPolicy +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.FileDescriptor +import java.io.FileInputStream +import java.util.concurrent.Callable +import kotlin.system.exitProcess +import org.matrix.vector.daemon.data.FileSystem +import picocli.CommandLine +import picocli.CommandLine.* + +// --- IPC Data Models --- +data class CliRequest( + val command: String, + val action: String = "", + val targets: List = emptyList(), + val options: Map = emptyMap() +) + +data class CliResponse( + val success: Boolean, + val data: Any? = null, + val error: String? = null, + val isFdAttached: Boolean = false +) + +// --- IPC Client Logic --- +object VectorIPC { + val gson: Gson = + GsonBuilder() + .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) // Handles Any/Object fields + .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) // Handles Map values + .setPrettyPrinting() + .create() + + fun transmit(request: CliRequest): CliResponse { + val socket = LocalSocket() + return try { + val cliSocket = FileSystem.socketPath.toString() + val socketFile = java.io.File(cliSocket) + + if (!socketFile.exists()) { + System.err.println("Error: Socket file not found at $cliSocket") + System.err.println("Current UID: ${android.os.Process.myUid()}") + } + socket.connect(LocalSocketAddress(cliSocket, LocalSocketAddress.Namespace.FILESYSTEM)) + + val output = DataOutputStream(socket.outputStream) + val input = DataInputStream(socket.inputStream) + + // Send Security Token + output.writeLong(BuildConfig.CLI_TOKEN_MSB) + output.writeLong(BuildConfig.CLI_TOKEN_LSB) + + // Send Request + output.writeUTF(gson.toJson(request)) + + // Read Response + val responseJson = input.readUTF() + val response = gson.fromJson(responseJson, CliResponse::class.java) + + // Handle Log Streaming + if (response.isFdAttached) { + val hasFd = input.readByte() + if (hasFd.toInt() == 1) { + val fds = socket.getAncillaryFileDescriptors() + if (!fds.isNullOrEmpty()) { + streamLog(fds[0], request.options["follow"] as? Boolean ?: false) + } + } + } + response + } catch (e: Exception) { + CliResponse(success = false, error = "Socket Failure: ${e.message}") + } finally { + socket.close() + } + } + + private fun streamLog(fd: java.io.FileDescriptor, follow: Boolean) { + // Wrap the raw FileDescriptor in a FileInputStream. + // 'use' ensures that fis.close() (and thus the FD) is called + // when the block finishes or if an exception is thrown. + FileInputStream(fd).use { fis -> + val reader = fis.bufferedReader() + + try { + while (true) { + val line = reader.readLine() + if (line != null) { + println(line) + } else { + if (!follow) break // EOF reached, exit + + // In follow mode, wait for new data to be written to the log + Thread.sleep(100) + } + + // Check if thread was interrupted (e.g. by a shutdown hook) + if (Thread.interrupted()) break + } + } catch (e: Exception) { + if (e !is InterruptedException) { + System.err.println("Log streaming error: ${e.message}") + } + } + } // FD is closed here automatically + } +} + +// --- UI Formatter --- +object OutputFormatter { + /** + * Auto-formats the Daemon's output. Prints ASCII tables for lists, Key-Value for maps, or raw + * JSON. + */ + @Suppress("UNCHECKED_CAST") + fun print(response: CliResponse, isJson: Boolean): Int { + if (isJson) { + println(VectorIPC.gson.toJson(response)) + return if (response.success) 0 else 1 + } + + if (!response.success) { + System.err.println("Error: ${response.error}") + return 1 + } + + val data = response.data ?: return 0 + + when (data) { + is List<*> -> { + if (data.isEmpty()) { + println("No records found.") + return 0 + } + // Check if it's a list of objects/maps to draw a table + val first = data[0] + if (first is Map<*, *>) { + printTable(data as List>) + } else { + data.forEach { println(" - $it") } + } + } + is Map<*, *> -> { + data.forEach { (k, v) -> println("$k: $v") } + } + else -> println(data.toString()) + } + return 0 + } + + private fun printTable(rows: List>) { + val headers = rows.first().keys.toList() + val columnWidths = headers.associateWith { it.length }.toMutableMap() + + // Calculate maximum width for each column + for (row in rows) { + for (header in headers) { + val length = row[header]?.toString()?.length ?: 0 + if (length > columnWidths[header]!!) { + columnWidths[header] = length + } + } + } + + // Print Headers + val headerRow = headers.joinToString(" ") { it.padEnd(columnWidths[it]!!) } + println(headerRow.uppercase()) + println("-".repeat(headerRow.length)) + + // Print Data + for (row in rows) { + println( + headers.joinToString(" ") { header -> + (row[header]?.toString() ?: "").padEnd(columnWidths[header]!!) + }) + } + } +} + +// --- CLI Commands (picocli) --- +@Command( + name = "vector-cli", + mixinStandardHelpOptions = true, + version = ["Vector CLI ${BuildConfig.VERSION_NAME}"], + description = ["A fast, scriptable CLI for configuring the Vector Framework daemon."], + subcommands = + [ + StatusCommand::class, + ModulesCommand::class, + ScopeCommand::class, + ConfigCommand::class, + LogCommand::class]) +class Cli : Callable { + + @Option( + names = ["--json"], + description = ["Output structured JSON for scripting"], + scope = ScopeType.INHERIT) + var json: Boolean = false + + override fun call(): Int { + CommandLine(this).usage(System.out) + return 0 + } + + companion object { + @JvmStatic + fun main(args: Array) { + val uid = Process.myUid() + if (uid != 0) { + System.err.println("Permission denied: Vector CLI must run as root.") + exitProcess(1) + } + val mainThread = Thread.currentThread() + Runtime.getRuntime() + .addShutdownHook( + Thread { + mainThread.interrupt() // Signal the loop to stop and close the stream + }) + val exitCode = CommandLine(Cli()).execute(*args) + exitProcess(exitCode) + } + } +} + +@Command(name = "status", description = ["Show framework and system health status"]) +class StatusCommand : Callable { + @ParentCommand lateinit var parent: Cli + + override fun call(): Int { + val req = CliRequest(command = "status") + val res = VectorIPC.transmit(req) + return OutputFormatter.print(res, parent.json) + } +} + +@Command(name = "modules", description = ["Manage Xposed modules"]) +class ModulesCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "ls", description = ["List installed modules"]) + fun ls( + @Option(names = ["-e", "--enabled"], description = ["Show only enabled modules"]) + enabled: Boolean, + @Option(names = ["-d", "--disabled"], description = ["Show only disabled modules"]) + disabled: Boolean + ): Int { + val req = + CliRequest( + command = "modules", + action = "ls", + options = mapOf("enabled" to enabled, "disabled" to disabled)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "enable", description = ["Enable one or more modules (batch processing)"]) + fun enable(@Parameters(paramLabel = "PKG", arity = "1..*") pkgs: List): Int { + val req = CliRequest(command = "modules", action = "enable", targets = pkgs) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "disable", description = ["Disable one or more modules (batch processing)"]) + fun disable(@Parameters(paramLabel = "PKG", arity = "1..*") pkgs: List): Int { + val req = CliRequest(command = "modules", action = "disable", targets = pkgs) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} + +@Command(name = "scope", description = ["Manage granular application injection scopes"]) +class ScopeCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "ls", description = ["List apps in a module's scope"]) + fun ls(@Parameters(index = "0", paramLabel = "MODULE_PKG") modulePkg: String): Int { + val req = CliRequest(command = "scope", action = "ls", targets = listOf(modulePkg)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "add", description = ["Append apps to scope (format: pkg/user_id)"]) + fun add( + @Parameters(index = "0", paramLabel = "MODULE_PKG") modulePkg: String, + @Parameters(index = "1..*") apps: List + ): Int { + val req = CliRequest(command = "scope", action = "add", targets = listOf(modulePkg) + apps) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "set", description = ["Overwrite entire scope (format: pkg/user_id)"]) + fun set( + @Parameters(index = "0", paramLabel = "MODULE_PKG") modulePkg: String, + @Parameters(index = "1..*") apps: List + ): Int { + val req = CliRequest(command = "scope", action = "set", targets = listOf(modulePkg) + apps) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} + +@Command(name = "config", description = ["Manage daemon preferences natively"]) +class ConfigCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "get", description = ["Get a config value"]) + fun get(@Parameters(paramLabel = "KEY") key: String): Int { + val req = CliRequest(command = "config", action = "get", targets = listOf(key)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } + + @Command(name = "set", description = ["Set a config value"]) + fun set( + @Parameters(index = "0", paramLabel = "KEY") key: String, + @Parameters(index = "1", paramLabel = "VALUE") value: String + ): Int { + val req = CliRequest(command = "config", action = "set", targets = listOf(key, value)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} + +@Command(name = "log", description = ["Stream or clear framework logs"]) +class LogCommand { + @ParentCommand lateinit var parent: Cli + + @Command(name = "cat", description = ["Dump logs and exit"]) + fun cat( + @Option(names = ["-v", "--verbose"], description = ["Read verbose daemon log"]) + verbose: Boolean + ): Int { + val req = + CliRequest( + command = "log", + action = "stream", + options = mapOf("verbose" to verbose, "follow" to false)) + VectorIPC.transmit(req) + return 0 + } + + @Command(name = "tail", description = ["Follow logs in real-time"]) + fun tail( + @Option(names = ["-v", "--verbose"], description = ["Follow verbose daemon log"]) + verbose: Boolean + ): Int { + val req = + CliRequest( + command = "log", + action = "stream", + options = mapOf("verbose" to verbose, "follow" to true)) + VectorIPC.transmit(req) + return 0 + } + + @Command(name = "clear", description = ["Clear log buffers"]) + fun clear(@Option(names = ["-v", "--verbose"]) verbose: Boolean): Int { + val req = CliRequest(command = "log", action = "clear", options = mapOf("verbose" to verbose)) + return OutputFormatter.print(VectorIPC.transmit(req), parent.json) + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt new file mode 100644 index 000000000..38f225c0e --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorDaemon.kt @@ -0,0 +1,212 @@ +package org.matrix.vector.daemon + +import android.app.ActivityManager +import android.app.ActivityThread +import android.content.Context +import android.ddm.DdmHandleAppName +import android.os.Build +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.Parcel +import android.os.Process +import android.os.ServiceManager +import android.system.Os +import android.util.Log +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.env.CliSocketServer +import org.matrix.vector.daemon.env.Dex2OatServer +import org.matrix.vector.daemon.env.LogcatMonitor +import org.matrix.vector.daemon.ipc.BRIDGE_TRANSACTION_CODE +import org.matrix.vector.daemon.ipc.ManagerService +import org.matrix.vector.daemon.ipc.SystemServerService +import org.matrix.vector.daemon.utils.applyNotificationWorkaround + +private const val TAG = "VectorDaemon" +private const val ACTION_SEND_BINDER = 1 + +object VectorDaemon { + private val exceptionHandler = CoroutineExceptionHandler { context, throwable -> + Log.e(TAG, "Caught fatal coroutine exception in background task!", throwable) + } + + // Dispatchers.IO: Uses the shared background thread pool. + // SupervisorJob(): Ensures one failing task doesn't kill the whole daemon. + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob() + exceptionHandler) + + var isLateInject = false + var proxyServiceName = "serial" + + @JvmStatic + fun main(args: Array) { + if (!FileSystem.tryLock()) kotlin.system.exitProcess(0) + + var systemServerMaxRetry = 1 + for (arg in args) { + if (arg.startsWith("--system-server-max-retry=")) { + systemServerMaxRetry = arg.substringAfter('=').toIntOrNull() ?: 1 + } else if (arg == "--late-inject") { + isLateInject = true + proxyServiceName = "serial_vector" + } + } + + Log.i(TAG, "Vector daemon started: lateInject=$isLateInject, proxy=$proxyServiceName") + Log.i(TAG, "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") + + Thread.setDefaultUncaughtExceptionHandler { _, e -> + Log.e(TAG, "Uncaught exception in Daemon", e) + kotlin.system.exitProcess(1) + } + + // Start Environmental Daemons + LogcatMonitor.start() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Dex2OatServer.start() + CliSocketServer.start() + + // Preload Framework DEX in the background + scope.launch { FileSystem.getPreloadDex(ConfigCache.state.isDexObfuscateEnabled) } + + // Setup Main Looper & System Services + Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND) + @Suppress("DEPRECATION") Looper.prepareMainLooper() + + val systemServerService = SystemServerService(systemServerMaxRetry, proxyServiceName) + systemServerService.putBinderForSystemServer() + + // Initializes system frameworks inside the daemon process + ActivityThread.systemMain() + DdmHandleAppName.setAppName("org.matrix.vector.daemon", 0) + + // Wait for Android Core Services + waitForSystemService("package") + waitForSystemService("activity") + waitForSystemService(Context.USER_SERVICE) + waitForSystemService(Context.APP_OPS_SERVICE) + + applyNotificationWorkaround() + + // Inject Vector into system_server + sendToBridge(VectorService.asBinder(), isRestart = false, systemServerService) + + if (!ManagerService.isVerboseLog()) { + LogcatMonitor.stopVerbose() + } + + Looper.loop() + throw RuntimeException("Main thread loop unexpectedly exited") + } + + private fun waitForSystemService(name: String) = runBlocking { + while (ServiceManager.getService(name) == null) { + Log.i(TAG, "Waiting system service: $name for 1s") + delay(1000) + } + } + + @Suppress("DEPRECATION") + private fun sendToBridge( + binder: IBinder, + isRestart: Boolean, + systemServerService: SystemServerService + ) { + check(Looper.myLooper() == Looper.getMainLooper()) { + "sendToBridge MUST run on the main thread!" + } + + Os.seteuid(0) + + runCatching { + var bridgeService: IBinder? + if (isRestart) Log.w(TAG, "System Server restarted...") + + while (true) { + bridgeService = ServiceManager.getService("activity") + if (bridgeService?.pingBinder() == true) break + Log.i(TAG, "activity service not ready, waiting 1s...") + Thread.sleep(1000) + } + + // Setup death recipient to handle system_server crashes + val deathRecipient = + object : IBinder.DeathRecipient { + override fun binderDied() { + Log.w(TAG, "System Server died! Clearing caches and re-injecting...") + bridgeService.unlinkToDeath(this, 0) + clearSystemCaches() + systemServerService.putBinderForSystemServer() + ManagerService.guard = null // Remove dead guard + Handler(Looper.getMainLooper()).post { + sendToBridge(binder, isRestart = true, systemServerService) + } + } + } + bridgeService.linkToDeath(deathRecipient, 0) + + // Try sending the Binder payload (up to 3 times) + var success = false + for (i in 0 until 3) { + val data = Parcel.obtain() + val reply = Parcel.obtain() + try { + data.writeInt(ACTION_SEND_BINDER) + data.writeStrongBinder(binder) + success = bridgeService.transact(BRIDGE_TRANSACTION_CODE, data, reply, 0) == true + reply.readException() + if (success) break + } finally { + data.recycle() + reply.recycle() + } + Log.w(TAG, "No response from bridge, retrying...") + Thread.sleep(1000) + } + + if (success) Log.i(TAG, "Successfully injected Vector into system_server") + else { + Log.e(TAG, "Failed to inject Vector into system_server") + systemServerService.maybeRetryInject() + } + } + .onFailure { Log.e(TAG, "Error during System Server bridging", it) } + Os.seteuid(1000) + } + + private fun clearSystemCaches() { + Log.i(TAG, "Clearing ServiceManager and ActivityManager caches...") + runCatching { + // Clear ServiceManager.sServiceManager + var field = ServiceManager::class.java.getDeclaredField("sServiceManager") + field.isAccessible = true + field.set(null, null) + + // Clear ServiceManager.sCache + field = ServiceManager::class.java.getDeclaredField("sCache") + field.isAccessible = true + val sCache = field.get(null) + if (sCache is MutableMap<*, *>) { + sCache.clear() + } + + // Clear ActivityManager.IActivityManagerSingleton + field = ActivityManager::class.java.getDeclaredField("IActivityManagerSingleton") + field.isAccessible = true + val singleton = field.get(null) + if (singleton != null) { + val mInstanceField = + Class.forName("android.util.Singleton").getDeclaredField("mInstance") + mInstanceField.isAccessible = true + synchronized(singleton) { mInstanceField.set(singleton, null) } + } + } + .onFailure { Log.w(TAG, "Failed to clear system caches via reflection", it) } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt new file mode 100644 index 000000000..25a9d86ad --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/VectorService.kt @@ -0,0 +1,377 @@ +package org.matrix.vector.daemon + +import android.app.IApplicationThread +import android.content.Context +import android.content.IIntentReceiver +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.os.Binder +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.provider.Telephony +import android.telephony.TelephonyManager +import android.util.Log +import hidden.HiddenApiBridge +import io.github.libxposed.service.IXposedScopeCallback +import kotlinx.coroutines.launch +import org.lsposed.lspd.service.IDaemonService +import org.lsposed.lspd.service.ILSPApplicationService +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.ModuleDatabase +import org.matrix.vector.daemon.data.PreferenceStore +import org.matrix.vector.daemon.data.ProcessScope +import org.matrix.vector.daemon.ipc.ApplicationService +import org.matrix.vector.daemon.ipc.ManagerService +import org.matrix.vector.daemon.ipc.ModuleService +import org.matrix.vector.daemon.system.* + +private const val TAG = "VectorService" + +object VectorService : IDaemonService.Stub() { + + private var bootCompleted = false + @Suppress("DEPRECATION") + private val ACTION_SECRET_CODE = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) TelephonyManager.ACTION_SECRET_CODE + else Telephony.Sms.Intents.SECRET_CODE_ACTION + + override fun dispatchSystemServerContext( + appThread: IBinder?, + activityToken: IBinder?, + ) { + appThread?.let { SystemContext.appThread = IApplicationThread.Stub.asInterface(it) } + SystemContext.token = activityToken + + // Initialize OS Observers using Coroutines for the dispatch blocks + registerReceivers() + + if (VectorDaemon.isLateInject) { + Log.i(TAG, "Late injection detected. Forcing boot completed event.") + dispatchBootCompleted() + } + } + + override fun requestApplicationService( + uid: Int, + pid: Int, + processName: String, + heartBeat: IBinder + ): ILSPApplicationService? { + if (Binder.getCallingUid() != 1000) { + Log.w(TAG, "Unauthorized requestApplicationService call") + return null + } + if (ApplicationService.hasRegister(uid, pid)) return null + + val scope = ProcessScope(processName, uid) + if (!ManagerService.tryRegisterManagerProcess(pid, uid, processName) && + ConfigCache.shouldSkipProcess(scope)) { + Log.d(TAG, "Skipped $processName/$uid") + return null + } + + return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) { + ApplicationService + } else null + } + + override fun preStartManager() = ManagerService.preStartManager() + + private fun createReceiver() = + object : IIntentReceiver.Stub() { + override fun performReceive( + intent: Intent, + resultCode: Int, + data: String?, + extras: Bundle?, + ordered: Boolean, + sticky: Boolean, + sendingUser: Int + ) { + VectorDaemon.scope.launch { + when (intent.action) { + Intent.ACTION_LOCKED_BOOT_COMPLETED -> dispatchBootCompleted() + Intent.ACTION_CONFIGURATION_CHANGED -> dispatchConfigurationChanged() + NotificationManager.openManagerAction -> ManagerService.openManager(intent.data) + ACTION_SECRET_CODE -> ManagerService.openManager(intent.data) + NotificationManager.moduleScopeAction -> dispatchModuleScope(intent) + else -> dispatchPackageChanged(intent) + } + } + + // Critical for ordered broadcasts to avoid freezing the system queue + if (!ordered && intent.action != Intent.ACTION_LOCKED_BOOT_COMPLETED) return + runCatching { + val appThread = SystemContext.appThread + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityManager?.finishReceiver( + appThread?.asBinder(), resultCode, data, extras, false, intent.flags) + } else { + activityManager?.finishReceiver( + this, resultCode, data, extras, false, intent.flags) + } + } + .onFailure { Log.e(TAG, "finishReceiver failed", it) } + } + } + + private fun registerReceivers() { + val configFilter = IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED) + + val packageFilter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_ADDED) + addAction(Intent.ACTION_PACKAGE_CHANGED) + addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED) + addDataScheme("package") + } + + val uidFilter = IntentFilter(Intent.ACTION_UID_REMOVED) + + val bootFilter = + IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED).apply { + priority = IntentFilter.SYSTEM_HIGH_PRIORITY + } + + val openManagerNoDataFilter = IntentFilter(NotificationManager.openManagerAction) + + val openManagerDataFilter = + IntentFilter(NotificationManager.openManagerAction).apply { + addDataScheme("module") + addDataScheme("android_secret_code") + } + + val scopeFilter = + IntentFilter(NotificationManager.moduleScopeAction).apply { addDataScheme("module") } + + val secretCodeFilter = + IntentFilter().apply { + addDataScheme("android_secret_code") + addDataAuthority("5776733", null) + } + + // Define strict Android 14+ flags and the system-only BRICK permission + val notExported = Context.RECEIVER_NOT_EXPORTED + val exported = Context.RECEIVER_EXPORTED + val brickPerm = "android.permission.BRICK" // Restrict senders to Android system only + + // userId = 0 => USER_SYSTEM + activityManager?.registerReceiverCompat( + createReceiver(), configFilter, brickPerm, 0, notExported) + // userId = -1 => USER_ALL + activityManager?.registerReceiverCompat( + createReceiver(), packageFilter, brickPerm, -1, notExported) + activityManager?.registerReceiverCompat(createReceiver(), uidFilter, brickPerm, -1, notExported) + activityManager?.registerReceiverCompat(createReceiver(), bootFilter, brickPerm, 0, notExported) + + activityManager?.registerReceiverCompat( + createReceiver(), openManagerNoDataFilter, brickPerm, 0, notExported) + activityManager?.registerReceiverCompat( + createReceiver(), openManagerDataFilter, brickPerm, 0, notExported) + activityManager?.registerReceiverCompat( + createReceiver(), scopeFilter, brickPerm, 0, notExported) + + // Only the secret dialer code needs to be exported so the phone app can trigger it + activityManager?.registerReceiverCompat( + createReceiver(), + secretCodeFilter, + "android.permission.CONTROL_INCALL_EXPERIENCE", + 0, + exported) + + // UID Observer + val uidObserver = + object : android.app.IUidObserver.Stub() { + override fun onUidActive(uid: Int) = ModuleService.uidStarts(uid) + + override fun onUidCachedChanged(uid: Int, cached: Boolean) { + if (!cached) ModuleService.uidStarts(uid) + } + + override fun onUidIdle(uid: Int, disabled: Boolean) = ModuleService.uidStarts(uid) + + override fun onUidGone(uid: Int, disabled: Boolean) = ModuleService.uidGone(uid) + } + + val which = + HiddenApiBridge.ActivityManager_UID_OBSERVER_ACTIVE() or + HiddenApiBridge.ActivityManager_UID_OBSERVER_GONE() or + HiddenApiBridge.ActivityManager_UID_OBSERVER_IDLE() or + HiddenApiBridge.ActivityManager_UID_OBSERVER_CACHED() + + activityManager?.registerUidObserver( + uidObserver, which, HiddenApiBridge.ActivityManager_PROCESS_STATE_UNKNOWN(), "android") + Log.d(TAG, "Registered all OS Receivers and UID Observers") + } + + private fun dispatchBootCompleted() { + bootCompleted = true + Log.d(TAG, "BOOT_COMPLETED event received.") + if (PreferenceStore.isStatusNotificationEnabled()) { + NotificationManager.notifyStatusNotification() + } + } + + private fun dispatchConfigurationChanged() { + Log.d(TAG, "CONFIGURATION_CHANGED event received.") + + if (!bootCompleted) return + if (PreferenceStore.isStatusNotificationEnabled()) { + NotificationManager.notifyStatusNotification() + } else { + NotificationManager.cancelStatusNotification() + } + } + + private const val EXTRA_REMOVED_FOR_ALL_USERS = "android.intent.extra.REMOVED_FOR_ALL_USERS" + private const val EXTRA_USER_HANDLE = "android.intent.extra.user_handle" + private const val ACTION_MANAGER_NOTIFICATION = + "${BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME}.NOTIFICATION" + private const val FLAG_RECEIVER_INCLUDE_BACKGROUND = 0x01000000 + private const val FLAG_RECEIVER_FROM_SHELL = 0x00400000 + + private fun dispatchPackageChanged(intent: Intent) { + val action = intent.action ?: return + val uid = intent.getIntExtra(Intent.EXTRA_UID, -1) + val userId = intent.getIntExtra(EXTRA_USER_HANDLE, uid / PER_USER_RANGE) + val isRemovedForAllUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false) + + val uri = intent.data + val moduleName = uri?.schemeSpecificPart ?: ConfigCache.getModuleByUid(uid)?.packageName + + Log.d(TAG, "dispatchPackageChanged $action $moduleName [$uid]") + + val appInfo = + moduleName?.let { + packageManager + ?.getPackageInfoCompat(it, MATCH_ALL_FLAGS or PackageManager.GET_META_DATA, 0) + ?.applicationInfo + } + var isXposedModule = + appInfo != null && + (appInfo.metaData?.containsKey("xposedminversion") == true || + ConfigCache.getModuleApkPath(appInfo) != null) + + when (action) { + Intent.ACTION_PACKAGE_FULLY_REMOVED -> { + // When a package is gone, we can't check metadata. + // If it was in our DB and we successfully removed it, we treat it as an Xposed module. + if (moduleName != null && isRemovedForAllUsers) { + if (ModuleDatabase.removeModule(moduleName)) { + isXposedModule = true + } + } + } + Intent.ACTION_PACKAGE_ADDED, + Intent.ACTION_PACKAGE_CHANGED -> { + if (isXposedModule && moduleName != null && appInfo != null) { + // Update the database with the new APK path if it's an Xposed module + isXposedModule = + ModuleDatabase.updateModuleApkPath( + moduleName, ConfigCache.getModuleApkPath(appInfo), false) + } else if (ConfigCache.state.scopes.keys.any { it.uid == uid }) { + // If not a module, but it's an app that was previously a "scope" (target) + // for a module, we need to refresh the cache. + ConfigCache.requestCacheUpdate() + } + } + Intent.ACTION_UID_REMOVED -> { + // If the UID being removed was a module or a scoped app, refresh the cache. + if (isXposedModule || ConfigCache.state.scopes.keys.any { it.uid == uid }) { + ConfigCache.requestCacheUpdate() + } + } + } + + // Special handling if the app being changed is the Vector Manager itself. + val isRemovedAction = + action == Intent.ACTION_PACKAGE_FULLY_REMOVED || action == Intent.ACTION_UID_REMOVED + if (moduleName == BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME && userId == 0) { + Log.d(TAG, "Manager updated") + ConfigCache.updateManager(isRemovedAction) + } + + // Notify the manager (foreground) that a package state changed so it can refresh its view. + if (moduleName != null) { + val notifyIntent = + Intent(ACTION_MANAGER_NOTIFICATION).apply { + putExtra(Intent.EXTRA_INTENT, intent) + putExtra("android.intent.extra.PACKAGES", moduleName) + putExtra(Intent.EXTRA_USER, userId) + putExtra("isXposedModule", isXposedModule) + addFlags(FLAG_RECEIVER_INCLUDE_BACKGROUND or FLAG_RECEIVER_FROM_SHELL) + } + + // Send to both the parasitic manager and the standalone manager + listOf(BuildConfig.MANAGER_INJECTED_PKG_NAME, BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME) + .forEach { pkg -> + activityManager?.broadcastIntentCompat(Intent(notifyIntent).setPackage(pkg)) + } + } + + // If an actual Xposed module was updated (not removed), show a system notification. + if (moduleName != null && isXposedModule && !isRemovedAction && !isRemovedForAllUsers) { + val scopes = ConfigCache.getModuleScope(moduleName) ?: emptyList() + val isSystemModule = scopes.any { it.packageName == "system" } + val isEnabled = ManagerService.enabledModules().contains(moduleName) + + NotificationManager.notifyModuleUpdated(moduleName, userId, isEnabled, isSystemModule) + } + } + + @Suppress("UNCHECKED_CAST") + private fun dispatchModuleScope(intent: Intent) { + val data = intent.data ?: return + val extras = intent.extras ?: return + val callbackBinder = extras.getBinder("callback") ?: return + if (!callbackBinder.isBinderAlive) return + + val authority = data.encodedAuthority ?: return + val parts = authority.split(":", limit = 2) + if (parts.size != 2) return + val packageName = parts[0] + val userId = parts[1].toIntOrNull() ?: return + + val scopePackageName = data.path?.substring(1) ?: return // remove leading '/' + val action = data.getQueryParameter("action") ?: return + + val iCallback = IXposedScopeCallback.Stub.asInterface(callbackBinder) + runCatching { + val appInfo = packageManager?.getPackageInfoCompat(scopePackageName, 0, userId) + if (appInfo == null) { + iCallback.onScopeRequestFailed("Package not found") + return + } + when (action) { + "approve" -> { + val scopes = ConfigCache.getModuleScope(packageName) ?: mutableListOf() + if (scopes.none { it.packageName == scopePackageName && it.userId == userId }) { + scopes.add( + org.lsposed.lspd.models.Application().apply { + this.packageName = scopePackageName + this.userId = userId + }) + ModuleDatabase.setModuleScope(packageName, scopes) + } + iCallback.onScopeRequestApproved(listOf(scopePackageName)) + } + "deny" -> iCallback.onScopeRequestFailed("Request denied by user") + "delete" -> iCallback.onScopeRequestFailed("Request timeout") + "block" -> { + val blocked = + PreferenceStore.getModulePrefs("lspd", 0, "config")["scope_request_blocked"] + as? Set ?: emptySet() + PreferenceStore.updateModulePref( + "lspd", 0, "config", "scope_request_blocked", blocked + packageName) + iCallback.onScopeRequestFailed("Request blocked by configuration") + } + } + } + .onFailure { runCatching { iCallback.onScopeRequestFailed(it.message) } } + + NotificationManager.cancelNotification( + NotificationManager.SCOPE_CHANNEL_ID, packageName, userId) + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt new file mode 100644 index 000000000..2cd2e85e6 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ConfigCache.kt @@ -0,0 +1,438 @@ +package org.matrix.vector.daemon.data + +import android.content.pm.ApplicationInfo +import android.content.pm.PackageParser +import android.system.Os +import android.util.Log +import hidden.HiddenApiBridge +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.attribute.PosixFilePermissions +import java.util.UUID +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.lsposed.lspd.models.Application +import org.lsposed.lspd.models.Module +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.VectorDaemon +import org.matrix.vector.daemon.ipc.InjectedModuleService +import org.matrix.vector.daemon.system.* +import org.matrix.vector.daemon.utils.InstallerVerifier +import org.matrix.vector.daemon.utils.applySqliteHelperWorkaround +import org.matrix.vector.daemon.utils.getRealUsers + +private const val TAG = "VectorConfigCache" + +object ConfigCache { + // Module preference operations are delegated to PreferenceStore + // Writable operations of modules are delegated to ModuleDatabase + + @Volatile + var state = DaemonState() + private set + + val dbHelper = Database() // Kept public for PreferenceStore and ModuleDatabase + + private val cacheUpdateChannel = Channel(Channel.CONFLATED) + + init { + VectorDaemon.scope.launch { + for (request in cacheUpdateChannel) { + performCacheUpdate() + } + } + applySqliteHelperWorkaround() + } + + private fun ensureCacheReady() { + if (!state.isCacheReady && packageManager?.asBinder()?.isBinderAlive == true) { + synchronized(this) { + if (!state.isCacheReady) { + Log.i(TAG, "System services are ready. Mapping modules and scopes.") + updateManager(false) + setupMiscPath() + performCacheUpdate() + state = state.copy(isCacheReady = true) + } + } + } + } + + fun updateManager(uninstalled: Boolean) { + if (uninstalled) { + state = state.copy(managerUid = -1) + return + } + runCatching { + val info = + packageManager?.getPackageInfoCompat(BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME, 0, 0) + val uid = info?.applicationInfo?.uid + val installedApkPath = info?.applicationInfo?.sourceDir + if (uid == null || installedApkPath == null) { + Log.i(TAG, "Manager is not installed") + state = state.copy(managerUid = -1) + return + } + + InstallerVerifier.verifyInstallerSignature(installedApkPath) + Log.i(TAG, "Manager verified and found at UID: $uid") + state = state.copy(managerUid = uid) + } + .onFailure { state = state.copy(managerUid = -1) } + } + + private fun setupMiscPath() { + val pathStr = PreferenceStore.getModulePrefs("lspd", 0, "config")["misc_path"] as? String + val path = + if (pathStr == null) { + val newPath = Paths.get("/data/misc", UUID.randomUUID().toString()) + PreferenceStore.updateModulePref("lspd", 0, "config", "misc_path", newPath.toString()) + newPath + } else { + Paths.get(pathStr) + } + state = state.copy(miscPath = path) + + runCatching { + val perms = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) + Files.createDirectories(state.miscPath!!, perms) + FileSystem.setSelinuxContextRecursive(state.miscPath!!, "u:object_r:xposed_data:s0") + } + .onFailure { Log.e(TAG, "Failed to create misc directory", it) } + } + + fun isManager(uid: Int): Boolean { + ensureCacheReady() + return uid == state.managerUid || uid == BuildConfig.MANAGER_INJECTED_UID + } + + fun requestCacheUpdate() { + cacheUpdateChannel.trySend(Unit) + } + + /** Builds a completely new Immutable State and atomically swaps it. */ + private fun performCacheUpdate() { + if (packageManager == null) return + + Log.d(TAG, "Executing Cache Update...") + val db = dbHelper.readableDatabase + val oldState = state + + val newModules = mutableMapOf() + val obsoleteModules = mutableSetOf() + val obsoletePaths = mutableMapOf() + + db.query( + "modules", + arrayOf("module_pkg_name", "apk_path"), + "enabled = 1", + null, + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val pkgName = cursor.getString(0) + var apkPath = cursor.getString(1) + if (pkgName == "lspd") continue + + val oldModule = oldState.modules[pkgName] + + var pkgInfo: android.content.pm.PackageInfo? = null + val users = userManager?.getRealUsers() ?: emptyList() + for (user in users) { + pkgInfo = packageManager?.getPackageInfoCompat(pkgName, MATCH_ALL_FLAGS, user.id) + if (pkgInfo?.applicationInfo != null) break + } + + if (pkgInfo?.applicationInfo == null) { + Log.w(TAG, "Failed to find package info of $pkgName") + obsoleteModules.add(pkgName) + continue + } + + val appInfo = pkgInfo.applicationInfo + + if (oldModule != null && + appInfo?.sourceDir != null && + apkPath != null && + oldModule.apkPath != null && + FileSystem.toGlobalNamespace(apkPath).exists() && + apkPath == oldModule.apkPath && + File(appInfo.sourceDir).parent == File(apkPath).parent) { + + if (oldModule.appId == -1) oldModule.applicationInfo = appInfo + newModules[pkgName] = oldModule + continue + } + + val realApkPath = getModuleApkPath(appInfo!!) + if (realApkPath == null) { + Log.w(TAG, "Failed to find path of $pkgName") + obsoleteModules.add(pkgName) + continue + } else { + apkPath = realApkPath + obsoletePaths[pkgName] = realApkPath + } + + val preLoadedApk = FileSystem.loadModule(apkPath, state.isDexObfuscateEnabled) + if (preLoadedApk != null) { + val module = + Module().apply { + packageName = pkgName + this.apkPath = apkPath + appId = appInfo.uid + applicationInfo = appInfo + service = oldModule?.service ?: InjectedModuleService(pkgName) + file = preLoadedApk + } + newModules[pkgName] = module + } else { + Log.w(TAG, "Failed to parse DEX/ZIP for $pkgName, skipping.") + obsoleteModules.add(pkgName) + } + } + } + + if (packageManager?.asBinder()?.isBinderAlive == true) { + obsoleteModules.forEach { ModuleDatabase.removeModule(it) } + obsoletePaths.forEach { (pkg, path) -> ModuleDatabase.updateModuleApkPath(pkg, path, true) } + } + + val newScopes = mutableMapOf>() + db.query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("app_pkg_name", "module_pkg_name", "user_id"), + "enabled = 1", + null, + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val appPkg = cursor.getString(0) + val modPkg = cursor.getString(1) + val userId = cursor.getInt(2) + + if (appPkg == "system") continue + + val module = newModules[modPkg] ?: continue + val pkgInfo = + packageManager?.getPackageInfoWithComponents(appPkg, MATCH_ALL_FLAGS, userId) + if (pkgInfo?.applicationInfo == null) continue + + val processNames = pkgInfo.fetchProcesses() + if (processNames.isEmpty()) continue + + val appUid = pkgInfo.applicationInfo!!.uid + + for (processName in processNames) { + val processScope = ProcessScope(processName, appUid) + newScopes.getOrPut(processScope) { mutableListOf() }.add(module) + + if (modPkg == appPkg) { + val appId = appUid % PER_USER_RANGE + userManager?.getRealUsers()?.forEach { user -> + val moduleUid = user.id * PER_USER_RANGE + appId + if (moduleUid != appUid) { + val moduleSelf = ProcessScope(processName, moduleUid) + newScopes.getOrPut(moduleSelf) { mutableListOf() }.add(module) + } + } + } + } + } + } + + // --- ATOMIC STATE SWAP --- + state = oldState.copy(modules = newModules, scopes = newScopes) + + Log.d(TAG, "Cache Update Complete. Map Swap successful.") + // Log.d(TAG, "cached modules:") + // newModules.forEach { (pkg, mod) -> Log.d(TAG, "$pkg ${mod.apkPath}") } + + // Log.d(TAG, "cached scopes:") + // newScopes.forEach { (ps, modules) -> + // Log.d(TAG, "${ps.processName}/${ps.uid}") + // modules.forEach { mod -> Log.d(TAG, "\t${mod.packageName}") } + // } + } + + fun getModuleScope(packageName: String): MutableList? { + if (packageName == "lspd") return null + val result = mutableListOf() + dbHelper.readableDatabase + .query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("app_pkg_name", "user_id"), + "modules.module_pkg_name = ?", + arrayOf(packageName), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + result.add( + Application().apply { + this.packageName = cursor.getString(0) + this.userId = cursor.getInt(1) + }) + } + } + return result + } + + fun getAutoInclude(packageName: String): Boolean { + if (packageName == "lspd") return false + + var isAutoInclude = false + dbHelper.readableDatabase + .query( + "modules", + arrayOf("auto_include"), + "module_pkg_name = ?", + arrayOf(packageName), + null, + null, + null) + .use { cursor -> + if (cursor.moveToFirst()) { + isAutoInclude = cursor.getInt(0) == 1 + } + } + return isAutoInclude + } + + fun getModulesForProcess(processName: String, uid: Int): List { + ensureCacheReady() + return state.scopes[ProcessScope(processName, uid)] ?: emptyList() + } + + fun getModuleByUid(uid: Int): Module? = + state.modules.values.firstOrNull { it.appId == uid % PER_USER_RANGE } + + fun getModulesForSystemServer(): List { + val modules = mutableListOf() + if (!android.os.SELinux.checkSELinuxAccess( + "u:r:system_server:s0", "u:r:system_server:s0", "process", "execmem")) { + Log.e(TAG, "Skipping system_server injection: sepolicy execmem denied") + return modules + } + + val currentState = state + + dbHelper.readableDatabase + .query( + "scope INNER JOIN modules ON scope.mid = modules.mid", + arrayOf("module_pkg_name", "apk_path"), + "app_pkg_name=? AND enabled=1", + arrayOf("system"), + null, + null, + null) + .use { cursor -> + while (cursor.moveToNext()) { + val pkgName = cursor.getString(0) + val apkPath = cursor.getString(1) + + val cached = currentState.modules[pkgName] + if (cached != null) { + modules.add(cached) + continue + } + + val statPath = FileSystem.toGlobalNamespace("/data/user_de/0/$pkgName").absolutePath + val module = + Module().apply { + packageName = pkgName + this.apkPath = apkPath + appId = runCatching { Os.stat(statPath).st_uid }.getOrDefault(-1) + service = InjectedModuleService(pkgName) + } + + runCatching { + @Suppress("DEPRECATION") + val pkg = PackageParser().parsePackage(File(apkPath), 0, false) + module.applicationInfo = pkg.applicationInfo + } + .onFailure { + Log.w(TAG, "PackageParser failed for $apkPath, using fallback ApplicationInfo") + module.applicationInfo = ApplicationInfo().apply { packageName = pkgName } + } + + // Always apply the critical paths manually, even on fallback + module.applicationInfo?.apply { + sourceDir = apkPath + dataDir = statPath + deviceProtectedDataDir = statPath + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(this, statPath) + processName = pkgName + uid = module.appId + } + + FileSystem.loadModule(apkPath, state.isDexObfuscateEnabled)?.let { + module.file = it + modules.add(module) + // We intentionally don't mutate state.modules here. Cache update will catch it. + } + } + } + return modules + } + + fun getModuleApkPath(info: ApplicationInfo): String? { + val apks = mutableListOf() + info.sourceDir?.let { apks.add(it) } + info.splitSourceDirs?.let { apks.addAll(it) } + + return apks.firstOrNull { apk -> + runCatching { + java.util.zip.ZipFile(apk).use { zip -> + zip.getEntry("META-INF/xposed/java_init.list") != null || + zip.getEntry("assets/xposed_init") != null + } + } + .getOrDefault(false) + } + } + + fun getInstalledModules(): List { + val allPackages = + packageManager?.getInstalledPackagesFromAllUsers(MATCH_ALL_FLAGS, false) ?: emptyList() + return allPackages + .mapNotNull { it.applicationInfo } + .filter { info -> getModuleApkPath(info) != null } + } + + fun shouldSkipProcess(scope: ProcessScope): Boolean { + ensureCacheReady() + return !state.scopes.containsKey(scope) + } + + fun getPrefsPath(packageName: String, uid: Int): String { + ensureCacheReady() + val currentState = state + val basePath = state.miscPath ?: throw IllegalStateException("Fatal: miscPath not initialized!") + + val userId = uid / PER_USER_RANGE + val userSuffix = if (userId == 0) "" else userId.toString() + val path = basePath.resolve("prefs$userSuffix").resolve(packageName) + + val module = currentState.modules[packageName] + if (module != null && module.appId == uid % PER_USER_RANGE) { + runCatching { + val perms = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx--x--x")) + Files.createDirectories(path, perms) + Files.walk(path).forEach { p -> Os.chown(p.toString(), uid, uid) } + } + .onFailure { Log.e(TAG, "Failed to prepare prefs path", it) } + } + return path.toString() + } + + fun getDenyListPackages(): List = emptyList() // TODO: implement it +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt new file mode 100644 index 000000000..2e5eef443 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/DaemonState.kt @@ -0,0 +1,21 @@ +package org.matrix.vector.daemon.data + +import java.nio.file.Path +import org.lsposed.lspd.models.Module + +data class ProcessScope(val processName: String, val uid: Int) + +/** + * An immutable snapshot of the Daemon's state. Any updates will generate a new copy of this class + * and atomically swap the reference. + */ +data class DaemonState( + // State non configurable for users + val isDexObfuscateEnabled: Boolean = true, + // States initialized after system services are ready + val isCacheReady: Boolean = false, + val managerUid: Int = -1, + val miscPath: Path? = null, + val modules: Map = emptyMap(), + val scopes: Map> = emptyMap(), +) diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt new file mode 100644 index 000000000..b3d21e60d --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/Database.kt @@ -0,0 +1,93 @@ +package org.matrix.vector.daemon.data + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log +import org.matrix.vector.daemon.utils.FakeContext + +private const val TAG = "VectorDatabase" +private const val DB_VERSION = 4 + +class Database(context: Context? = FakeContext()) : + SQLiteOpenHelper(context, FileSystem.dbPath.absolutePath, null, DB_VERSION) { + + override fun onConfigure(db: SQLiteDatabase) { + super.onConfigure(db) + db.setForeignKeyConstraintsEnabled(true) + db.enableWriteAheadLogging() + // Improve write performance + db.execSQL("PRAGMA synchronous=NORMAL;") + } + + override fun onCreate(db: SQLiteDatabase) { + Log.i(TAG, "Creating new Vector database") + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS modules ( + mid integer PRIMARY KEY AUTOINCREMENT, + module_pkg_name text NOT NULL UNIQUE, + apk_path text NOT NULL, + enabled BOOLEAN DEFAULT 0 CHECK (enabled IN (0, 1)), + auto_include BOOLEAN DEFAULT 0 CHECK (auto_include IN (0, 1)) + ); + """ + .trimIndent()) + + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS scope ( + mid integer, + app_pkg_name text NOT NULL, + user_id integer NOT NULL, + PRIMARY KEY (mid, app_pkg_name, user_id), + CONSTRAINT scope_module_constraint FOREIGN KEY (mid) REFERENCES modules (mid) ON DELETE CASCADE + ); + """ + .trimIndent()) + + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS configs ( + module_pkg_name text NOT NULL, + user_id integer NOT NULL, + `group` text NOT NULL, + `key` text NOT NULL, + data blob NOT NULL, + PRIMARY KEY (module_pkg_name, user_id, `group`, `key`), + CONSTRAINT config_module_constraint FOREIGN KEY (module_pkg_name) REFERENCES modules (module_pkg_name) ON DELETE CASCADE + ); + """ + .trimIndent()) + + db.execSQL("CREATE INDEX IF NOT EXISTS configs_idx ON configs (module_pkg_name, user_id);") + + // Insert self + db.execSQL( + "INSERT OR IGNORE INTO modules (module_pkg_name, apk_path) VALUES ('lspd', ?)", + arrayOf(FileSystem.managerApkPath.toString())) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + Log.i(TAG, "Upgrading database from $oldVersion to $newVersion") + if (oldVersion < 2) { + db.execSQL("DROP INDEX IF EXISTS configs_idx;") + db.execSQL("ALTER TABLE scope RENAME TO old_scope;") + db.execSQL("ALTER TABLE configs RENAME TO old_configs;") + onCreate(db) // Recreate tables with strict constraints + runCatching { db.execSQL("INSERT INTO scope SELECT * FROM old_scope;") } + runCatching { db.execSQL("INSERT INTO configs SELECT * FROM old_configs;") } + db.execSQL("DROP TABLE old_scope;") + db.execSQL("DROP TABLE old_configs;") + } + if (oldVersion < 3) { + db.execSQL("UPDATE scope SET app_pkg_name = 'system' WHERE app_pkg_name = 'android';") + } + if (oldVersion < 4) { + runCatching { + db.execSQL( + "ALTER TABLE modules ADD COLUMN auto_include BOOLEAN DEFAULT 0 CHECK (auto_include IN (0, 1));") + } + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt new file mode 100644 index 000000000..8ac061ce7 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/FileSystem.kt @@ -0,0 +1,415 @@ +package org.matrix.vector.daemon.data + +import android.content.res.AssetManager +import android.content.res.Resources +import android.os.Binder +import android.os.ParcelFileDescriptor +import android.os.Process +import android.os.RemoteException +import android.os.SELinux +import android.os.SharedMemory +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import android.util.Log +import hidden.HiddenApiBridge +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.nio.channels.Channels +import java.nio.channels.FileChannel +import java.nio.channels.FileLock +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.nio.file.attribute.PosixFilePermissions +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import org.lsposed.lspd.models.PreLoadedApk +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.utils.ObfuscationManager + +private const val TAG = "VectorFileSystem" + +object FileSystem { + val basePath: Path = Paths.get("/data/adb/lspd") + val logDirPath: Path = basePath.resolve("log") + val oldLogDirPath: Path = basePath.resolve("log.old") + val modulePath: Path = basePath.resolve("modules") + val socketPath: Path = basePath.resolve(".cli_sock") + val daemonApkPath: Path = Paths.get(System.getProperty("java.class.path", "")) + val managerApkPath: Path = daemonApkPath.parent.resolve("manager.apk") + val configDirPath: Path = basePath.resolve("config") + val dbPath: File = configDirPath.resolve("modules_config.db").toFile() + val magiskDbPath = File("/data/adb/magisk.db") + + @Volatile private var preloadDex: SharedMemory? = null + + private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.systemDefault()) + private val lockPath: Path = basePath.resolve("lock") + private var fileLock: FileLock? = null + private var lockChannel: FileChannel? = null + + init { + runCatching { + Files.createDirectories(basePath) + Os.chmod(basePath.toString(), "700".toInt(8)) + SELinux.setFileContext(basePath.toString(), "u:object_r:system_file:s0") + Files.createDirectories(configDirPath) + } + .onFailure { Log.e(TAG, "Failed to initialize directories", it) } + } + + fun setupCli(): String { + val cliSource = daemonApkPath.parent.resolve("cli").toFile() + val cliDest = basePath.resolve("cli").toFile() + if (cliSource.exists()) { + runCatching { + cliSource.copyTo(cliDest, overwrite = true) + Os.chmod(cliDest.absolutePath, "700".toInt(8)) + } + .onFailure { Log.e(TAG, "Failed to deploy CLI script", it) } + } + + val cliSocket: String = socketPath.toString() + val socketFile = File(cliSocket) + if (socketFile.exists()) { + Log.d(TAG, "Existing $cliSocket deleted") + socketFile.delete() + } + + return cliSocket + } + + /** Tries to lock the daemon lockfile. Returns false if another daemon is running. */ + fun tryLock(): Boolean { + return runCatching { + val permissions = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")) + lockChannel = + FileChannel.open( + lockPath, setOf(StandardOpenOption.CREATE, StandardOpenOption.WRITE), permissions) + fileLock = lockChannel?.tryLock() + fileLock?.isValid == true + } + .getOrDefault(false) + } + + /** Clears all special file attributes (like immutable) on a directory. */ + fun chattr0(path: Path): Boolean { + return runCatching { + val fd = Os.open(path.toString(), OsConstants.O_RDONLY, 0) + // 0x40086602 for 64-bit, 0x40046602 for 32-bit (FS_IOC_SETFLAGS) + val req = if (Process.is64Bit()) 0x40086602 else 0x40046602 + HiddenApiBridge.Os_ioctlInt(fd, req, 0) + Os.close(fd) + true + } + .recover { e -> if (e is ErrnoException && e.errno == OsConstants.ENOTSUP) true else false } + .getOrDefault(false) + } + + /** Recursively sets SELinux context. Crucial for modules to read their data. */ + fun setSelinuxContextRecursive(path: Path, context: String) { + runCatching { + SELinux.setFileContext(path.toString(), context) + if (path.isDirectory()) { + Files.list(path).use { stream -> + stream.forEach { setSelinuxContextRecursive(it, context) } + } + } + } + .onFailure { Log.e(TAG, "Failed to set SELinux context for $path", it) } + } + + /** + * Lazily loads resources from the daemon's APK path via reflection. This allows FakeContext to + * access strings/drawables without a real application context. + */ + val resources: Resources by lazy { + val am = AssetManager::class.java.getDeclaredConstructor().newInstance() + val addAssetPath = + AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java).apply { + isAccessible = true + } + addAssetPath.invoke(am, daemonApkPath.toString()) + @Suppress("DEPRECATION") Resources(am, null, null) + } + + /** Loads a single DEX file into SharedMemory, optionally applying obfuscation. */ + private fun readDex(inputStream: InputStream, obfuscate: Boolean): SharedMemory { + var memory = SharedMemory.create(null, inputStream.available()) + val byteBuffer = memory.mapReadWrite() + Channels.newChannel(inputStream).read(byteBuffer) + SharedMemory.unmap(byteBuffer) + + if (obfuscate) { + val newMemory = ObfuscationManager.obfuscateDex(memory) + if (memory !== newMemory) { + memory.close() + memory = newMemory + } + } + memory.setProtect(OsConstants.PROT_READ) + return memory + } + + /** Parses the module APK, extracts init lists, and loads DEXes into SharedMemory. */ + fun loadModule(apkPath: String, obfuscate: Boolean): PreLoadedApk? { + val file = File(apkPath) + if (!file.exists()) return null + + val preLoadedApk = PreLoadedApk() + val preLoadedDexes = mutableListOf() + val moduleClassNames = mutableListOf() + val moduleLibraryNames = mutableListOf() + var isLegacy = false + + runCatching { + ZipFile(file).use { zip -> + // Read all classes*.dex files + var secondary = 1 + while (true) { + val entryName = if (secondary == 1) "classes.dex" else "classes$secondary.dex" + val dexEntry = zip.getEntry(entryName) ?: break + zip.getInputStream(dexEntry).use { preLoadedDexes.add(readDex(it, obfuscate)) } + secondary++ + } + + // Read initialization lists + fun readList(name: String, dest: MutableList) { + zip.getEntry(name)?.let { entry -> + zip.getInputStream(entry).bufferedReader().useLines { lines -> + lines + .map { it.trim() } + .filter { it.isNotEmpty() && !it.startsWith("#") } + .forEach { dest.add(it) } + } + } + } + + readList("META-INF/xposed/java_init.list", moduleClassNames) + if (moduleClassNames.isEmpty()) { + isLegacy = true + readList("assets/xposed_init", moduleClassNames) + readList("assets/native_init", moduleLibraryNames) + } else { + readList("META-INF/xposed/native_init.list", moduleLibraryNames) + } + } + } + .onFailure { + Log.e(TAG, "Failed to load module $apkPath", it) + return null + } + + if (preLoadedDexes.isEmpty() || moduleClassNames.isEmpty()) return null + + // 3. Apply obfuscation to class names if required + if (obfuscate) { + val signatures = ObfuscationManager.getSignatures() + for (i in moduleClassNames.indices) { + val s = moduleClassNames[i] + signatures.entries + .firstOrNull { s.startsWith(it.key) } + ?.let { moduleClassNames[i] = s.replace(it.key, it.value) } + } + } + + preLoadedApk.preLoadedDexes = preLoadedDexes + preLoadedApk.moduleClassNames = moduleClassNames + preLoadedApk.moduleLibraryNames = moduleLibraryNames + preLoadedApk.legacy = isLegacy + + return preLoadedApk + } + + /** Safely creates the log directory. If a file exists with the same name, it deletes it first. */ + private fun createLogDirPath() { + if (!Files.isDirectory(logDirPath, java.nio.file.LinkOption.NOFOLLOW_LINKS)) { + logDirPath.toFile().deleteRecursively() + } + Files.createDirectories(logDirPath) + } + + /** + * Rotates the log directory by clearing file attributes (chattr 0), deleting the old backup, and + * renaming the current log directory to the backup. + */ + fun moveLogDir() { + runCatching { + if (Files.exists(logDirPath)) { + if (chattr0(logDirPath)) { + // Kotlin's deleteRecursively replaces the verbose Java SimpleFileVisitor + oldLogDirPath.toFile().deleteRecursively() + Files.move(logDirPath, oldLogDirPath) + } + } + Files.createDirectories(logDirPath) + } + .onFailure { Log.e(TAG, "Failed to move log directory", it) } + } + + fun getPropsPath(): File { + createLogDirPath() + return logDirPath.resolve("props.txt").toFile() + } + + fun getKmsgPath(): File { + createLogDirPath() + return logDirPath.resolve("kmsg.log").toFile() + } + + @Synchronized + fun getPreloadDex(obfuscate: Boolean): SharedMemory? { + if (preloadDex == null) { + runCatching { + FileInputStream("framework/lspd.dex").use { preloadDex = readDex(it, obfuscate) } + } + .onFailure { Log.e(TAG, "Failed to load framework dex", it) } + } + return preloadDex + } + + fun ensureModuleFilePath(path: String?) { + if (path == null || path.contains(File.separatorChar) || path == "." || path == "..") { + throw RemoteException("Invalid path: $path") + } + } + + fun resolveModuleDir(packageName: String, dir: String, userId: Int, uid: Int): Path { + val path = modulePath.resolve(userId.toString()).resolve(packageName).resolve(dir).normalize() + path.toFile().mkdirs() + + if (SELinux.getFileContext(path.toString()) != "u:object_r:xposed_data:s0") { + runCatching { + setSelinuxContextRecursive(path, "u:object_r:xposed_data:s0") + if (uid != -1) Os.chown(path.toString(), uid, uid) + Os.chmod(path.toString(), 0x1ed) // 0755 + } + .onFailure { throw RemoteException("Failed to set SELinux context: ${it.message}") } + } + return path + } + + fun toGlobalNamespace(path: String): File { + return if (path.startsWith("/")) File("/proc/1/root", path) else File("/proc/1/root/$path") + } + + fun getLogs(zipFd: ParcelFileDescriptor) { + runCatching { + ZipOutputStream(java.io.FileOutputStream(zipFd.fileDescriptor)).use { os -> + val comment = + "Vector ${BuildConfig.BUILD_TYPE} ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" + os.setComment(comment) + os.setLevel(java.util.zip.Deflater.BEST_COMPRESSION) + + fun addFile(name: String, file: File) { + if (!file.exists() || !file.isFile) return + runCatching { + os.putNextEntry(ZipEntry(name)) + file.inputStream().use { it.copyTo(os) } + os.closeEntry() + } + } + + fun addDir(basePath: String, dir: File) { + if (!dir.exists() || !dir.isDirectory) return + // Kotlin's walkTopDown elegantly replaces Files.walkFileTree + dir.walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relativePath = dir.toPath().relativize(file.toPath()).toString() + val entryName = + if (basePath.isEmpty()) relativePath else "$basePath/$relativePath" + addFile(entryName, file) + } + } + + fun addProcOutput(name: String, vararg cmd: String) { + runCatching { + val proc = ProcessBuilder(*cmd).start() + os.putNextEntry(ZipEntry(name)) + proc.inputStream.use { it.copyTo(os) } + os.closeEntry() + } + } + + // 1. Gather daemon logs and system crash traces + addDir("log", logDirPath.toFile()) + addDir("log.old", oldLogDirPath.toFile()) + addDir("tombstones", File("/data/tombstones")) + addDir("anr", File("/data/anr")) + addDir( + "crash1", File("/data/data/${BuildConfig.MANAGER_INJECTED_PKG_NAME}/cache/crash")) + addDir( + "crash2", + File("/data/data/${BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME}/cache/crash")) + + // 2. Gather system logs directly via shell + addProcOutput("full.log", "logcat", "-b", "all", "-d") + addProcOutput("dmesg.log", "dmesg") + + // 3. Gather Magisk module states safely + val magiskDataDir = File("/data/adb/modules") + if (magiskDataDir.exists() && magiskDataDir.isDirectory) { + magiskDataDir.listFiles()?.forEach { moduleDir -> + val modName = moduleDir.name + listOf("module.prop", "remove", "disable", "update", "sepolicy.rule").forEach { + addFile("modules/$modName/$it", File(moduleDir, it)) + } + } + } + + // 4. Gather memory/mount info for daemon and caller + val proc = File("/proc") + arrayOf("self", Binder.getCallingPid().toString()).forEach { pid -> + val pidPath = File(proc, pid) + listOf("maps", "mountinfo", "status").forEach { + addFile("proc/$pid/$it", File(pidPath, it)) + } + } + + // 5. Gather Database and Scopes + addFile("modules_config.db", dbPath) + runCatching { + os.putNextEntry(ZipEntry("scopes.txt")) + ConfigCache.state.scopes.forEach { (scope, modules) -> + os.write("${scope.processName}/${scope.uid}\n".toByteArray()) + modules.forEach { mod -> + os.write("\t${mod.packageName}\n".toByteArray()) + mod.file?.moduleClassNames?.forEach { cn -> os.write("\t\t$cn\n".toByteArray()) } + mod.file?.moduleLibraryNames?.forEach { ln -> + os.write("\t\t$ln\n".toByteArray()) + } + } + } + os.closeEntry() + } + } + } + .onFailure { Log.e(TAG, "Failed to export logs", it) } + .also { runCatching { zipFd.close() } } + } + + private fun getNewLogFileName(prefix: String): String { + return "${prefix}_${formatter.format(Instant.now())}.log" + } + + fun getNewVerboseLogPath(): File { + createLogDirPath() + return logDirPath.resolve(getNewLogFileName("verbose")).toFile() + } + + fun getNewModulesLogPath(): File { + createLogDirPath() + return logDirPath.resolve(getNewLogFileName("modules")).toFile() + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt new file mode 100644 index 000000000..cfeca4165 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/ModuleDatabase.kt @@ -0,0 +1,146 @@ +package org.matrix.vector.daemon.data + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import android.util.Log +import org.lsposed.lspd.models.Application + +private const val TAG = "VectorModuleDb" + +object ModuleDatabase { + + fun enableModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val db = ConfigCache.dbHelper.writableDatabase + var changed = false + + // First, check if it exists. If not, we need to "discover" it. + val exists = + db.compileStatement("SELECT COUNT(*) FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() > 0 + if (!exists) { + val values = + ContentValues().apply { + put("module_pkg_name", packageName) + put("apk_path", "") // defer to cache updating + put("enabled", 1) + } + db.insert("modules", null, values) + changed = true + } else { + val values = ContentValues().apply { put("enabled", 1) } + changed = db.update("modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + } + + if (changed) ConfigCache.requestCacheUpdate() + return changed + } + + fun disableModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val values = ContentValues().apply { put("enabled", 0) } + val changed = + ConfigCache.dbHelper.writableDatabase.update( + "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (changed) ConfigCache.requestCacheUpdate() + return changed + } + + fun setModuleScope(packageName: String, scope: MutableList): Boolean { + enableModule(packageName) + val db = ConfigCache.dbHelper.writableDatabase + db.beginTransaction() + try { + val mid = + db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() + db.delete("scope", "mid = ?", arrayOf(mid.toString())) + + val values = ContentValues().apply { put("mid", mid) } + for (app in scope) { + if (app.packageName == "system" && app.userId != 0) continue + values.put("app_pkg_name", app.packageName) + values.put("user_id", app.userId) + db.insertWithOnConflict("scope", null, values, SQLiteDatabase.CONFLICT_IGNORE) + } + db.setTransactionSuccessful() + } catch (e: Exception) { + Log.e(TAG, "Failed to set scope", e) + return false + } finally { + db.endTransaction() + } + ConfigCache.requestCacheUpdate() + return true + } + + fun removeModuleScope(packageName: String, scopePackageName: String, userId: Int): Boolean { + if (packageName == "lspd" || (scopePackageName == "system" && userId != 0)) return false + val db = ConfigCache.dbHelper.writableDatabase + val mid = + db.compileStatement("SELECT mid FROM modules WHERE module_pkg_name = ?") + .apply { bindString(1, packageName) } + .simpleQueryForLong() + db.delete( + "scope", + "mid = ? AND app_pkg_name = ? AND user_id = ?", + arrayOf(mid.toString(), scopePackageName, userId.toString())) + ConfigCache.requestCacheUpdate() + return true + } + + fun updateModuleApkPath(packageName: String, apkPath: String?, force: Boolean): Boolean { + if (apkPath == null || packageName == "lspd") return false + val values = + ContentValues().apply { + put("module_pkg_name", packageName) + put("apk_path", apkPath) + } + val db = ConfigCache.dbHelper.writableDatabase + var count = + db.insertWithOnConflict("modules", null, values, SQLiteDatabase.CONFLICT_IGNORE).toInt() + + if (count < 0) { + val cached = ConfigCache.state.modules[packageName] + if (force || cached == null || cached.apkPath != apkPath) { + count = + db.updateWithOnConflict( + "modules", + values, + "module_pkg_name=?", + arrayOf(packageName), + SQLiteDatabase.CONFLICT_IGNORE) + } else count = 0 + } + if (!force && count > 0) ConfigCache.requestCacheUpdate() + return count > 0 + } + + fun removeModule(packageName: String): Boolean { + if (packageName == "lspd") return false + val res = + ConfigCache.dbHelper.writableDatabase.delete( + "modules", "module_pkg_name = ?", arrayOf(packageName)) > 0 + if (res) ConfigCache.requestCacheUpdate() + return res + } + + fun setAutoInclude(packageName: String, enabled: Boolean): Boolean { + if (packageName == "lspd") return false + + val values = ContentValues().apply { put("auto_include", if (enabled) 1 else 0) } + + val changed = + ConfigCache.dbHelper.writableDatabase.update( + "modules", values, "module_pkg_name = ?", arrayOf(packageName)) > 0 + + // If the auto_include flag changes, we should rebuild the scope cache + if (changed) { + ConfigCache.requestCacheUpdate() + } + + return changed + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt new file mode 100644 index 000000000..4f0a6da3a --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/data/PreferenceStore.kt @@ -0,0 +1,92 @@ +package org.matrix.vector.daemon.data + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import org.apache.commons.lang3.SerializationUtilsX + +private const val TAG = "VectorPreferenceStore" + +object PreferenceStore { + + fun getModulePrefs( + packageName: String, + userId: Int, + group: String, + db: SQLiteDatabase = ConfigCache.dbHelper.readableDatabase + ): Map { + val result = mutableMapOf() + + db.query( + "configs", + arrayOf("`key`", "data"), + "module_pkg_name = ? AND user_id = ? AND `group` = ?", + arrayOf(packageName, userId.toString(), group), + null, + null, + null) + .use { cursor -> // We only close the cursor + while (cursor.moveToNext()) { + val key = cursor.getString(0) + val blob = cursor.getBlob(1) + val obj = SerializationUtilsX.deserialize(blob) + if (obj != null) result[key] = obj + } + } + return result + } + + fun updateModulePref(moduleName: String, userId: Int, group: String, key: String, value: Any?) { + updateModulePrefs(moduleName, userId, group, mapOf(key to value)) + } + + fun updateModulePrefs(moduleName: String, userId: Int, group: String, diff: Map) { + val db = ConfigCache.dbHelper.writableDatabase + db.beginTransaction() + try { + for ((key, value) in diff) { + if (value is java.io.Serializable) { + val values = + ContentValues().apply { + put("`group`", group) + put("`key`", key) + put("data", SerializationUtilsX.serialize(value)) + put("module_pkg_name", moduleName) + put("user_id", userId.toString()) + } + db.insertWithOnConflict("configs", null, values, SQLiteDatabase.CONFLICT_REPLACE) + } else { + db.delete( + "configs", + "module_pkg_name=? AND user_id=? AND `group`=? AND `key`=?", + arrayOf(moduleName, userId.toString(), group, key)) + } + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + + fun deleteModulePrefs(moduleName: String, userId: Int, group: String) { + ConfigCache.dbHelper.writableDatabase.delete( + "configs", + "module_pkg_name=? AND user_id=? AND `group`=?", + arrayOf(moduleName, userId.toString(), group)) + } + + fun isStatusNotificationEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_status_notification"] as? Boolean ?: true + + fun setStatusNotification(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_status_notification", enabled) + + fun isVerboseLogEnabled(): Boolean = + getModulePrefs("lspd", 0, "config")["enable_verbose_log"] as? Boolean ?: true + + fun setVerboseLog(enabled: Boolean) = + updateModulePref("lspd", 0, "config", "enable_verbose_log", enabled) + + fun isScopeRequestBlocked(pkg: String): Boolean = + (getModulePrefs("lspd", 0, "config")["scope_request_blocked"] as? Set<*>)?.contains(pkg) == + true +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt new file mode 100644 index 000000000..20359d109 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/CliSocketServer.kt @@ -0,0 +1,130 @@ +package org.matrix.vector.daemon.env + +import android.net.LocalServerSocket +import android.net.LocalSocket +import android.net.LocalSocketAddress +import android.system.Os +import android.util.Log +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import kotlinx.coroutines.launch +import org.matrix.vector.daemon.* +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.ipc.CliHandler + +private const val TAG = "VectorCliSever" + +object CliSocketServer { + + private var isRunning = false + + fun start() { + if (isRunning) return + isRunning = true + + val serverThread = Thread { + // Keep these references outside the loop to prevent GC from closing them + var rootSocket: LocalSocket? = null + var server: LocalServerSocket? = null + var socketFile: File? = null + + try { + val cliSocketPath: String = FileSystem.setupCli() + socketFile = File(cliSocketPath) + + // Create a standard LocalSocket + rootSocket = LocalSocket() + // Bind it to the filesystem path + val address = LocalSocketAddress(cliSocketPath, LocalSocketAddress.Namespace.FILESYSTEM) + rootSocket.bind(address) + + // LocalServerSocket(FileDescriptor) requires the FD to already be listening. + Os.listen(rootSocket.fileDescriptor, 50) + // Wrap the underlying FileDescriptor into a ServerSocket + server = LocalServerSocket(rootSocket.fileDescriptor) + + Log.d(TAG, "CLI server started at $cliSocketPath") + + while (!Thread.currentThread().isInterrupted) { + try { + val clientSocket = server.accept() + VectorDaemon.scope.launch { handleClient(clientSocket) } + } catch (e: IOException) { + if (Thread.currentThread().isInterrupted) break + Log.w(TAG, "Error accepting client", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "Fatal CLI Server error", e) + } finally { + try { + server?.close() + rootSocket?.close() + } catch (ignored: Exception) {} + + if (socketFile?.exists() == true) { + socketFile.delete() + } + isRunning = false + Log.d(TAG, "CLI server stopped") + } + } + + serverThread.name = "VectorCliListener" + serverThread.priority = Thread.MIN_PRIORITY + serverThread.start() + } + + private fun handleClient(socket: LocalSocket) { + try { + val input = DataInputStream(socket.inputStream) + val output = DataOutputStream(socket.outputStream) + + // Read & Verify Security Token (UUID MSB/LSB) + val msb = input.readLong() + val lsb = input.readLong() + if (msb != BuildConfig.CLI_TOKEN_MSB || lsb != BuildConfig.CLI_TOKEN_LSB) { + socket.close() + return + } + + val requestJson = input.readUTF() + val request = VectorIPC.gson.fromJson(requestJson, CliRequest::class.java) + + // Intercept Log Streaming specifically before CliHandler + if (request.command == "log" && request.action == "stream") { + val verbose = request.options["verbose"] as? Boolean ?: false + val logFile = if (verbose) LogcatMonitor.getVerboseLog() else LogcatMonitor.getModulesLog() + + if (logFile != null && logFile.exists()) { + val response = CliResponse(success = true, isFdAttached = true) + output.writeUTF(VectorIPC.gson.toJson(response)) + + // Open file and get raw FileDescriptor + val fis = FileInputStream(logFile) + val fd = fis.fd + + // Attach FD to the next write operation + socket.setFileDescriptorsForSend(arrayOf(fd)) + output.write(1) // Trigger byte to "carry" the ancillary FD data + + // fis is closed when the socket/method finishes + return + } else { + output.writeUTF( + VectorIPC.gson.toJson(CliResponse(success = false, error = "Log file not found."))) + return + } + } + + // Standard commands go to CliHandler as usual + val response = CliHandler.execute(request) + output.writeUTF(VectorIPC.gson.toJson(response)) + } finally { + socket.close() + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt new file mode 100644 index 000000000..b144e62c1 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/Dex2OatServer.kt @@ -0,0 +1,247 @@ +package org.matrix.vector.daemon.env + +import android.net.LocalServerSocket +import android.os.Build +import android.os.FileObserver +import android.os.SELinux +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import android.util.Log +import java.io.File +import java.io.FileDescriptor +import java.io.FileInputStream +import java.nio.file.Files +import java.nio.file.Paths +import kotlinx.coroutines.launch +import org.matrix.vector.daemon.VectorDaemon + +private const val TAG = "VectorDex2Oat" + +// Compatibility states matching Manager expectations +const val DEX2OAT_OK = 0 +const val DEX2OAT_MOUNT_FAILED = 1 +const val DEX2OAT_SEPOLICY_INCORRECT = 2 +const val DEX2OAT_SELINUX_PERMISSIVE = 3 +const val DEX2OAT_CRASHED = 4 + +object Dex2OatServer { + private const val WRAPPER32 = "bin/dex2oat32" + private const val WRAPPER64 = "bin/dex2oat64" + private const val HOOKER32 = "bin/liboat_hook32.so" + private const val HOOKER64 = "bin/liboat_hook64.so" + + private val dex2oatArray = arrayOfNulls(6) + private val fdArray = arrayOfNulls(6) + + @Volatile + var compatibility = DEX2OAT_OK + private set + + private external fun doMountNative( + enabled: Boolean, + r32: String?, + d32: String?, + r64: String?, + d64: String? + ) + + private external fun setSockCreateContext(context: String?): Boolean + + private external fun getSockPath(): String + + private val selinuxObserver = + object : + FileObserver( + listOf(File("/sys/fs/selinux/enforce"), File("/sys/fs/selinux/policy")), + CLOSE_WRITE) { + override fun onEvent(event: Int, path: String?) { + synchronized(this) { + if (compatibility == DEX2OAT_CRASHED) { + stopWatching() + return + } + + val enforcing = + runCatching { + Files.newInputStream(Paths.get("/sys/fs/selinux/enforce")).use { + it.read() == '1'.code + } + } + .getOrDefault(false) + + when { + !enforcing -> { + if (compatibility == DEX2OAT_OK) doMount(false) + compatibility = DEX2OAT_SELINUX_PERMISSIVE + } + hasSePolicyErrors() -> { + if (compatibility == DEX2OAT_OK) doMount(false) + compatibility = DEX2OAT_SEPOLICY_INCORRECT + } + compatibility != DEX2OAT_OK -> { + doMount(true) + if (notMounted()) { + doMount(false) + compatibility = DEX2OAT_MOUNT_FAILED + stopWatching() + } else { + compatibility = DEX2OAT_OK + } + } + } + } + } + } + + init { + // Android 10 vs 11+ path differences + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat") + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd") + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oat64") + checkAndAddDex2Oat("/apex/com.android.runtime/bin/dex2oatd64") + } else { + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat32") + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd32") + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oat64") + checkAndAddDex2Oat("/apex/com.android.art/bin/dex2oatd64") + } + + openDex2oat(4, "/data/adb/modules/zygisk_vector/bin/liboat_hook32.so") + openDex2oat(5, "/data/adb/modules/zygisk_vector/bin/liboat_hook64.so") + } + + private fun hasSePolicyErrors(): Boolean { + return SELinux.checkSELinuxAccess( + "u:r:untrusted_app:s0", "u:object_r:dex2oat_exec:s0", "file", "execute") || + SELinux.checkSELinuxAccess( + "u:r:untrusted_app:s0", "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans") + } + + private fun openDex2oat(id: Int, path: String) { + runCatching { + fdArray[id] = Os.open(path, OsConstants.O_RDONLY, 0) + dex2oatArray[id] = path + } + } + + private fun checkAndAddDex2Oat(path: String) { + val file = File(path) + if (!file.exists()) return + + runCatching { + FileInputStream(file).use { fis -> + val header = ByteArray(5) + if (fis.read(header) != 5) return + // Verify ELF Magic: 0x7F 'E' 'L' 'F' + if (header[0] != 0x7F.toByte() || + header[1] != 'E'.code.toByte() || + header[2] != 'L'.code.toByte() || + header[3] != 'F'.code.toByte()) + return + + val is32Bit = header[4] == 1.toByte() + val is64Bit = header[4] == 2.toByte() + val isDebug = path.contains("dex2oatd") + + val index = + when { + is32Bit -> if (isDebug) 1 else 0 + is64Bit -> if (isDebug) 3 else 2 + else -> -1 + } + + if (index != -1 && dex2oatArray[index] == null) { + dex2oatArray[index] = path + fdArray[index] = Os.open(path, OsConstants.O_RDONLY, 0) + Log.i(TAG, "Detected $path -> Assigned Index $index") + } + } + } + .onFailure { dex2oatArray[dex2oatArray.indexOf(path)] = null } + } + + private fun notMounted(): Boolean { + for (i in 0 until 4) { + val bin = dex2oatArray[i] ?: continue + try { + val apex = Os.stat("/proc/1/root$bin") + val wrapper = Os.stat(if (i < 2) WRAPPER32 else WRAPPER64) + if (apex.st_dev != wrapper.st_dev || apex.st_ino != wrapper.st_ino) { + return true + } + } catch (e: ErrnoException) { + return true + } + } + return false + } + + private fun doMount(enabled: Boolean) { + doMountNative(enabled, dex2oatArray[0], dex2oatArray[1], dex2oatArray[2], dex2oatArray[3]) + } + + fun start() { + if (notMounted()) { + doMount(true) + if (notMounted()) { + doMount(false) + compatibility = DEX2OAT_MOUNT_FAILED + return + } + } + + selinuxObserver.startWatching() + selinuxObserver.onEvent(0, null) + + // Run the socket accept loop in an IO coroutine + VectorDaemon.scope.launch { runSocketLoop() } + } + + private fun runSocketLoop() { + Log.i(TAG, "Dex2oat wrapper daemon start") + val sockPath = getSockPath() + Log.d(TAG, "wrapper path: $sockPath") + + val xposedFile = "u:object_r:xposed_file:s0" + val dex2oatExec = "u:object_r:dex2oat_exec:s0" + + if (SELinux.checkSELinuxAccess("u:r:dex2oat:s0", dex2oatExec, "file", "execute_no_trans")) { + SELinux.setFileContext(WRAPPER32, dex2oatExec) + SELinux.setFileContext(WRAPPER64, dex2oatExec) + setSockCreateContext("u:r:dex2oat:s0") + } else { + SELinux.setFileContext(WRAPPER32, xposedFile) + SELinux.setFileContext(WRAPPER64, xposedFile) + setSockCreateContext("u:r:installd:s0") + } + SELinux.setFileContext(HOOKER32, xposedFile) + SELinux.setFileContext(HOOKER64, xposedFile) + + runCatching { + LocalServerSocket(sockPath).use { server -> + setSockCreateContext(null) + while (true) { + // This blocks until the C++ wrapper connects + server.accept().use { client -> + val input = client.inputStream + val output = client.outputStream + val id = input.read() + if (id in fdArray.indices && fdArray[id] != null) { + client.setFileDescriptorsForSend(arrayOf(fdArray[id]!!)) + output.write(1) + } + } + } + } + } + .onFailure { + Log.e(TAG, "Dex2oat wrapper daemon crashed", it) + if (compatibility == DEX2OAT_OK) { + doMount(false) + compatibility = DEX2OAT_CRASHED + } + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt new file mode 100644 index 000000000..f17e29b23 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/env/LogcatMonitor.kt @@ -0,0 +1,181 @@ +package org.matrix.vector.daemon.env + +import android.annotation.SuppressLint +import android.os.Build +import android.os.ParcelFileDescriptor +import android.os.Process +import android.os.SELinux +import android.os.SystemProperties +import android.system.Os +import android.util.Log +import java.io.File +import java.io.FileDescriptor +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Paths +import kotlinx.coroutines.launch +import org.matrix.vector.daemon.VectorDaemon +import org.matrix.vector.daemon.data.FileSystem + +private const val TAG = "VectorLogcat" +private const val FD_MODE = + ParcelFileDescriptor.MODE_WRITE_ONLY or + ParcelFileDescriptor.MODE_CREATE or + ParcelFileDescriptor.MODE_TRUNCATE or + ParcelFileDescriptor.MODE_APPEND + +object LogcatMonitor { + private var modulesFd = -1 + private var verboseFd = -1 + @Volatile private var isRunning = false + + private external fun runLogcat() + + // Thread-safe LRU implementation for log files + private class ThreadSafeLRU(private val maxEntries: Int = 10) { + private val map = LinkedHashMap(maxEntries, 1f, false) + + @Synchronized + fun add(file: File) { + map[file] = Unit + if (map.size > maxEntries) { + val eldest = map.keys.first() + if (eldest.delete()) { + map.remove(eldest) + } + } + } + } + + private val moduleLogs = ThreadSafeLRU() + private val verboseLogs = ThreadSafeLRU() + + init { + loadNativeLibrary() + FileSystem.moveLogDir() // Defined in FileSystem + + // Meizu log_reject_level workaround + if (SystemProperties.getInt("persist.sys.log_reject_level", 0) > 0) { + SystemProperties.set("persist.sys.log_reject_level", "0") + } + + dumpPropsAndDmesg() + } + + @SuppressLint("UnsafeDynamicallyLoadedCode") + private fun loadNativeLibrary() { + val classPath = System.getProperty("java.class.path", "") + val abi = + if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS[0] else Build.SUPPORTED_32_BIT_ABIS[0] + System.load("$classPath!/lib/$abi/${System.mapLibraryName("daemon")}") + } + + private fun dumpPropsAndDmesg() { + VectorDaemon.scope.launch { + // Filter privacy props by temporarily assuming an untrusted context + runCatching { + SELinux.setFSCreateContext("u:object_r:app_data_file:s0") + ProcessBuilder( + "sh", + "-c", + "echo -n u:r:untrusted_app:s0 > /proc/thread-self/attr/current; getprop") + .redirectOutput(FileSystem.getPropsPath()) // Ensure this exists in FileSystem + .start() + } + .onFailure { Log.e(TAG, "getprop failed", it) } + .also { SELinux.setFSCreateContext(null) } + + runCatching { ProcessBuilder("dmesg").redirectOutput(FileSystem.getKmsgPath()).start() } + .onFailure { Log.e(TAG, "dmesg failed", it) } + } + } + + fun start() { + if (isRunning) return + isRunning = true + VectorDaemon.scope.launch { + runCatching { + Log.i(TAG, "Logcat daemon starting") + runLogcat() // Blocks until the native logcat process dies + Log.i(TAG, "Logcat daemon stopped") + } + .onFailure { Log.e(TAG, "Logcat crashed", it) } + isRunning = false + } + } + + fun getVerboseLog(): File? = fdToPath(verboseFd)?.toFile() + + fun getModulesLog(): File? = fdToPath(modulesFd)?.toFile() + + private fun fdToPath(fd: Int) = if (fd == -1) null else Paths.get("/proc/self/fd", fd.toString()) + + /** Resurrects deleted log files from /proc/self/fd if an external process deletes them. */ + private fun checkFd(fd: Int) { + if (fd == -1) return + runCatching { + val jfd = FileDescriptor() + jfd.javaClass + .getDeclaredMethod("setInt\$", Int::class.java) + .apply { isAccessible = true } + .invoke(jfd, fd) + val stat = Os.fstat(jfd) + + // st_nlink == 0 means the file was deleted but the FD is still held open + if (stat.st_nlink == 0L) { + val file = Files.readSymbolicLink(fdToPath(fd)!!) + val parent = file.parent + if (!Files.isDirectory(parent, LinkOption.NOFOLLOW_LINKS)) { + if (FileSystem.chattr0(parent)) Files.deleteIfExists(parent) + } + val name = file.fileName.toString() + val originName = name.substring(0, name.lastIndexOf(' ')) + Files.copy(file, parent.resolve(originName)) + } + } + .onFailure { Log.w(TAG, "checkFd failed for $fd", it) } + } + + fun startVerbose() = Log.i(TAG, "!!start_verbose!!") + + fun stopVerbose() = Log.i(TAG, "!!stop_verbose!!") + + fun refresh(isVerboseLog: Boolean) { + Log.i(TAG, if (isVerboseLog) "!!refresh_verbose!!" else "!!refresh_modules!!") + } + + fun checkLogFile() { + if (modulesFd == -1) refresh(false) + if (verboseFd == -1) refresh(true) + } + + @Suppress("unused") // Called via JNI + private fun refreshFd(isVerboseLog: Boolean): Int { + return runCatching { + val logFile = + if (isVerboseLog) { + checkFd(verboseFd) + val f = FileSystem.getNewVerboseLogPath() + verboseLogs.add(f) + f + } else { + checkFd(modulesFd) + val f = FileSystem.getNewModulesLogPath() + moduleLogs.add(f) + f + } + + Log.i(TAG, "New log file: $logFile") + FileSystem.chattr0(logFile.toPath().parent) + val fd = ParcelFileDescriptor.open(logFile, FD_MODE).detachFd() + + if (isVerboseLog) verboseFd = fd else modulesFd = fd + fd + } + .onFailure { + if (isVerboseLog) verboseFd = -1 else modulesFd = -1 + Log.w(TAG, "refreshFd failed", it) + } + .getOrDefault(-1) + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt new file mode 100644 index 000000000..fb4f817e8 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ApplicationService.kt @@ -0,0 +1,132 @@ +package org.matrix.vector.daemon.ipc + +import android.os.IBinder +import android.os.Parcel +import android.os.ParcelFileDescriptor +import android.os.Process +import android.os.RemoteException +import android.util.Log +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.models.Module +import org.lsposed.lspd.service.ILSPApplicationService +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.utils.InstallerVerifier +import org.matrix.vector.daemon.utils.ObfuscationManager + +private const val TAG = "VectorAppService" + +// Hardcoded transaction code from BridgeService +const val BRIDGE_TRANSACTION_CODE = + ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code +const val DEX_TRANSACTION_CODE = + ('_'.code shl 24) or ('D'.code shl 16) or ('E'.code shl 8) or 'X'.code +const val OBFUSCATION_MAP_TRANSACTION_CODE = + ('_'.code shl 24) or ('O'.code shl 16) or ('B'.code shl 8) or 'F'.code + +object ApplicationService : ILSPApplicationService.Stub() { + + data class ProcessKey(val uid: Int, val pid: Int) + + private val processes = ConcurrentHashMap() + + private class ProcessInfo(val key: ProcessKey, val processName: String, val heartBeat: IBinder) : + IBinder.DeathRecipient { + init { + heartBeat.linkToDeath(this, 0) + processes[key] = this + } + + override fun binderDied() { + heartBeat.unlinkToDeath(this, 0) + processes.remove(key) + } + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + when (code) { + DEX_TRANSACTION_CODE -> { + val shm = FileSystem.getPreloadDex(ConfigCache.state.isDexObfuscateEnabled) ?: return false + reply?.writeNoException() + reply?.let { shm.writeToParcel(it, 0) } + reply?.writeLong(shm.size.toLong()) + return true + } + OBFUSCATION_MAP_TRANSACTION_CODE -> { + val obfuscation = ConfigCache.state.isDexObfuscateEnabled + val signatures = ObfuscationManager.getSignatures() + reply?.writeNoException() + reply?.writeInt(signatures.size * 2) + for ((key, value) in signatures) { + reply?.writeString(key) + reply?.writeString(if (obfuscation) value else key) + } + return true + } + } + return super.onTransact(code, data, reply, flags) + } + + fun registerHeartBeat(uid: Int, pid: Int, processName: String, heartBeat: IBinder): Boolean { + return runCatching { + ProcessInfo(ProcessKey(uid, pid), processName, heartBeat) + true + } + .getOrDefault(false) + } + + fun hasRegister(uid: Int, pid: Int): Boolean = processes.containsKey(ProcessKey(uid, pid)) + + private fun ensureRegistered(): ProcessInfo { + val key = ProcessKey(getCallingUid(), getCallingPid()) + val info = processes[key] + if (info == null) { + Log.w(TAG, "Unauthorized IPC call from uid=${key.uid} pid=${key.pid}") + throw RemoteException("Not registered") + } + return info + } + + private fun getAllModules(): List { + val info = ensureRegistered() + if (info.key.uid == Process.SYSTEM_UID && info.processName == "system") { + return ConfigCache.getModulesForSystemServer() + } + if (ManagerService.isRunningManager(getCallingPid(), info.key.uid)) { + return emptyList() + } + return ConfigCache.getModulesForProcess(info.processName, info.key.uid) + } + + override fun getModulesList() = getAllModules().filter { !it.file.legacy } + + override fun getLegacyModulesList() = getAllModules().filter { it.file.legacy } + + override fun isLogMuted(): Boolean = !ManagerService.isVerboseLog + + override fun getPrefsPath(packageName: String): String { + val info = ensureRegistered() + return ConfigCache.getPrefsPath(packageName, info.key.uid) + } + + override fun requestInjectedManagerBinder( + binderList: MutableList + ): ParcelFileDescriptor? { + val info = ensureRegistered() + val pid = info.key.pid + val uid = info.key.uid + + if (ManagerService.postStartManager(pid) || ConfigCache.isManager(uid)) { + binderList.add(ManagerService.obtainManagerBinder(info.heartBeat, pid, uid)) + } + + return runCatching { + // Verify the APK signature before serving it + InstallerVerifier.verifyInstallerSignature(FileSystem.managerApkPath.toString()) + ParcelFileDescriptor.open( + FileSystem.managerApkPath.toFile(), ParcelFileDescriptor.MODE_READ_ONLY) + } + .onFailure { Log.e(TAG, "Failed to open or verify manager APK", it) } + .getOrNull() + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt new file mode 100644 index 000000000..d0dab77d2 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/CliHandler.kt @@ -0,0 +1,206 @@ +package org.matrix.vector.daemon.ipc + +import org.lsposed.lspd.models.Application +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.CliRequest +import org.matrix.vector.daemon.CliResponse +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.ModuleDatabase +import org.matrix.vector.daemon.data.PreferenceStore +import org.matrix.vector.daemon.system.* + +object CliHandler { + + /** + * Executes the requested CLI command within the daemon's memory space. Returns a structured + * CliResponse. + */ + fun execute(request: CliRequest): CliResponse { + return try { + val responseData = + when (request.command) { + "status" -> handleStatus() + "modules" -> handleModules(request) + "scope" -> handleScope(request) + "config" -> handleConfig(request) + "log" -> handleLog(request) + else -> throw IllegalArgumentException("Unknown command: ${request.command}") + } + CliResponse(success = true, data = responseData) + } catch (e: Exception) { + CliResponse(success = false, error = e.message ?: "Unknown error occurred") + } + } + + private fun handleStatus(): Map { + return mapOf( + "Framework Version" to BuildConfig.VERSION_NAME, + "Version Code" to BuildConfig.VERSION_CODE, + "Enabled Modules" to ConfigCache.state.modules.size, + "Status Notification" to PreferenceStore.isStatusNotificationEnabled()) + } + + private fun isPackageInstalled(pkg: String, userId: Int = 0): Boolean { + return runCatching { packageManager?.getPackageInfo(pkg, 0, userId) != null } + .getOrDefault(false) + } + + private fun handleModules(request: CliRequest): Any { + return when (request.action) { + "ls" -> { + val enabledOnly = request.options["enabled"] as? Boolean ?: false + val disabledOnly = request.options["disabled"] as? Boolean ?: false + + // Get the current immutable snapshot of enabled modules + val enabledModuleKeys = ConfigCache.state.modules.keys + // Get all installed modules from the system + val installed = ConfigCache.getInstalledModules() + + // Map to the CLI view model + installed + .mapNotNull { info -> + val pkg = info.packageName + val isEnabled = enabledModuleKeys.contains(pkg) + + // Filter based on CLI flags + if (enabledOnly && !isEnabled) return@mapNotNull null + if (disabledOnly && isEnabled) return@mapNotNull null + + mapOf( + "PACKAGE" to pkg, + "UID" to info.uid, + "STATUS" to (if (isEnabled) "enabled" else "disabled")) + } + .sortedBy { it["PACKAGE"] as String } + } + "enable" -> { + if (request.targets.isEmpty()) + throw IllegalArgumentException("No packages provided to enable.") + val success = mutableListOf() + val failed = mutableListOf() + request.targets.forEach { pkg -> + if (ModuleDatabase.enableModule(pkg)) success.add(pkg) else failed.add(pkg) + } + mapOf("Enabled" to success, "Failed" to failed) + } + "disable" -> { + if (request.targets.isEmpty()) + throw IllegalArgumentException("No packages provided to disable.") + val success = mutableListOf() + val failed = mutableListOf() + request.targets.forEach { pkg -> + if (ModuleDatabase.disableModule(pkg)) success.add(pkg) else failed.add(pkg) + } + mapOf("Disabled" to success, "Failed" to failed) + } + else -> throw IllegalArgumentException("Unknown module action: ${request.action}") + } + } + + private fun handleScope(request: CliRequest): Any { + if (request.targets.isEmpty()) throw IllegalArgumentException("Module package name required.") + val modulePkg = request.targets[0] + val apps = request.targets.drop(1) + + return when (request.action) { + "ls" -> { + val scope = + ConfigCache.getModuleScope(modulePkg) + ?: throw IllegalArgumentException("Module not found: $modulePkg") + scope.map { mapOf("APP_PACKAGE" to it.packageName, "USER_ID" to it.userId) } + } + "add" -> { + if (apps.isEmpty()) throw IllegalArgumentException("No target apps provided.") + val scope = ConfigCache.getModuleScope(modulePkg) ?: mutableListOf() + + apps.forEach { appStr -> + val parts = appStr.split("/") + val pkg = parts[0] + val user = parts.getOrNull(1)?.toIntOrNull() ?: 0 + if (scope.none { it.packageName == pkg && it.userId == user }) { + scope.add( + Application().apply { + packageName = pkg + userId = user + }) + } + } + ModuleDatabase.setModuleScope(modulePkg, scope) + "Successfully appended ${apps.size} apps to $modulePkg scope." + } + "set" -> { + if (apps.isEmpty()) + throw IllegalArgumentException("No target apps provided for scope overwrite.") + val scope = mutableListOf() + apps.forEach { appStr -> + val parts = appStr.split("/") + val pkg = parts[0] + val user = parts.getOrNull(1)?.toIntOrNull() ?: 0 + scope.add( + Application().apply { + packageName = pkg + userId = user + }) + } + ModuleDatabase.setModuleScope(modulePkg, scope) + "Successfully overwrote scope for $modulePkg (${apps.size} apps)." + } + "rm" -> { + if (apps.isEmpty()) throw IllegalArgumentException("No target apps provided to remove.") + var removedCount = 0 + apps.forEach { appStr -> + val parts = appStr.split("/") + val pkg = parts[0] + val user = parts.getOrNull(1)?.toIntOrNull() ?: 0 + if (ModuleDatabase.removeModuleScope(modulePkg, pkg, user)) removedCount++ + } + "Successfully removed $removedCount apps from $modulePkg scope." + } + else -> throw IllegalArgumentException("Unknown scope action: ${request.action}") + } + } + + private fun handleConfig(request: CliRequest): Any { + val keys = request.targets + return when (request.action) { + "get" -> { + if (keys.isEmpty()) throw IllegalArgumentException("Config key required.") + val key = keys[0] + val value = + when (key) { + "status-notification" -> ManagerService.enableStatusNotification() + "verbose-log" -> ManagerService.isVerboseLog + else -> throw IllegalArgumentException("Unknown config key: $key") + } + mapOf("KEY" to key, "VALUE" to value) + } + "set" -> { + if (keys.size < 2) throw IllegalArgumentException("Key and value required.") + val key = keys[0] + val value = + keys[1].toBooleanStrictOrNull() + ?: throw IllegalArgumentException("Value must be 'true' or 'false'.") + + when (key) { + "status-notification" -> ManagerService.setEnableStatusNotification(value) + "verbose-log" -> ManagerService.setVerboseLog(value) + else -> throw IllegalArgumentException("Unknown config key: $key") + } + "Successfully set $key to $value." + } + else -> throw IllegalArgumentException("Unknown config action: ${request.action}") + } + } + + private fun handleLog(request: CliRequest): Any { + return when (request.action) { + "clear" -> { + val verbose = request.options["verbose"] as? Boolean ?: false + ManagerService.clearLogs(verbose) + "Logs cleared successfully." + } + // "stream" is handled in SystemServerService.kt to attach the FileDescriptor + else -> throw IllegalArgumentException("Unknown log action: ${request.action}") + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt new file mode 100644 index 000000000..cff900d01 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/InjectedModuleService.kt @@ -0,0 +1,77 @@ +package org.matrix.vector.daemon.ipc + +import android.os.Binder +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.os.RemoteException +import android.util.Log +import io.github.libxposed.service.IXposedService +import java.io.Serializable +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.service.ILSPInjectedModuleService +import org.lsposed.lspd.service.IRemotePreferenceCallback +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.PreferenceStore +import org.matrix.vector.daemon.system.PER_USER_RANGE + +private const val TAG = "VectorInjectedModuleService" + +class InjectedModuleService(private val packageName: String) : ILSPInjectedModuleService.Stub() { + + // Tracks active RemotePreferenceCallbacks linked by config group + private val callbacks = ConcurrentHashMap>() + + override fun getFrameworkProperties(): Long { + var prop = IXposedService.PROP_CAP_SYSTEM or IXposedService.PROP_CAP_REMOTE + if (ConfigCache.state.isDexObfuscateEnabled) { + prop = prop or IXposedService.PROP_RT_API_PROTECTION + } + return prop + } + + override fun requestRemotePreferences( + group: String, + callback: IRemotePreferenceCallback? + ): Bundle { + val bundle = Bundle() + val userId = Binder.getCallingUid() / PER_USER_RANGE + bundle.putSerializable( + "map", PreferenceStore.getModulePrefs(packageName, userId, group) as Serializable) + + if (callback != null) { + val groupCallbacks = callbacks.getOrPut(group) { ConcurrentHashMap.newKeySet() } + groupCallbacks.add(callback) + runCatching { callback.asBinder().linkToDeath({ groupCallbacks.remove(callback) }, 0) } + .onFailure { Log.w(TAG, "requestRemotePreferences linkToDeath failed", it) } + } + return bundle + } + + override fun openRemoteFile(path: String): ParcelFileDescriptor { + FileSystem.ensureModuleFilePath(path) + val userId = Binder.getCallingUid() / PER_USER_RANGE + return runCatching { + val dir = FileSystem.resolveModuleDir(packageName, "files", userId, -1) + ParcelFileDescriptor.open(dir.resolve(path).toFile(), ParcelFileDescriptor.MODE_READ_ONLY) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun getRemoteFileList(): Array { + val userId = Binder.getCallingUid() / PER_USER_RANGE + return runCatching { + val dir = FileSystem.resolveModuleDir(packageName, "files", userId, -1) + dir.toFile().list() ?: emptyArray() + } + .getOrElse { throw RemoteException(it.message) } + } + + // Called by ModuleService when prefs are updated globally + fun onUpdateRemotePreferences(group: String, diff: Bundle) { + val groupCallbacks = callbacks[group] ?: return + for (callback in groupCallbacks) { + runCatching { callback.onUpdate(diff) }.onFailure { groupCallbacks.remove(callback) } + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt new file mode 100644 index 000000000..ccce28ca6 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ManagerService.kt @@ -0,0 +1,481 @@ +package org.matrix.vector.daemon.ipc + +import android.content.ComponentName +import android.content.Context +import android.content.IIntentSender +import android.content.Intent +import android.content.IntentSender +import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.pm.VersionedPackage +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.os.ParcelFileDescriptor +import android.os.SELinux +import android.os.SystemProperties +import android.util.Log +import android.view.IWindowManager +import hidden.HiddenApiBridge +import io.github.libxposed.service.IXposedService +import java.io.File +import java.io.FileOutputStream +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.lsposed.lspd.ILSPManagerService +import org.lsposed.lspd.models.Application +import org.lsposed.lspd.models.UserInfo +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.ModuleDatabase +import org.matrix.vector.daemon.data.PreferenceStore +import org.matrix.vector.daemon.env.Dex2OatServer +import org.matrix.vector.daemon.env.LogcatMonitor +import org.matrix.vector.daemon.system.* +import org.matrix.vector.daemon.utils.applyXspaceWorkaround +import org.matrix.vector.daemon.utils.getRealUsers +import rikka.parcelablelist.ParcelableListSlice + +private const val TAG = "VectorManagerService" + +object ManagerService : ILSPManagerService.Stub() { + + private var managerPid = -1 + private var pendingManager = false + + private var managerIntent: Intent? = null + + var guard: ManagerGuard? = null + internal set + + class ManagerGuard(private val binder: IBinder, val pid: Int, val uid: Int) : + IBinder.DeathRecipient { + private val connection = + object : android.app.IServiceConnection.Stub() { + override fun connected(name: ComponentName?, service: IBinder?, dead: Boolean) {} + } + + init { + ManagerService.guard = this + runCatching { + binder.linkToDeath(this, 0) + applyXspaceWorkaround(connection) + } + .onFailure { + Log.e(TAG, "ManagerGuard initialization failed", it) + ManagerService.guard = null + } + } + + override fun binderDied() { + runCatching { + binder.unlinkToDeath(this, 0) + activityManager?.unbindService(connection) + } + ManagerService.guard = null + } + } + + @Synchronized + fun preStartManager(): Boolean { + Log.v(TAG, "Pre-start parasitic manager.") + pendingManager = true + managerPid = -1 + return true + } + + @Synchronized + fun tryRegisterManagerProcess(pid: Int, uid: Int, processName: String): Boolean { + if (ConfigCache.isManager(uid) && processName == BuildConfig.DEFAULT_MANAGER_PACKAGE_NAME) { + if (pendingManager) { + Log.v(TAG, "Parasitic manager registered.") + pendingManager = false + } else { + Log.v(TAG, "Starting user-installed manager process.") + } + managerPid = pid + return true + } + return false + } + + fun postStartManager(pid: Int): Boolean = pid == managerPid + + private fun getManagerIntent(): Intent? { + if (managerIntent != null) return managerIntent + runCatching { + var intent = + Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_INFO) + setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME) + } + var ris = packageManager?.queryIntentActivitiesCompat(intent, intent.type, 0, 0) + + if (ris.isNullOrEmpty()) { + intent.removeCategory(Intent.CATEGORY_INFO) + intent.addCategory(Intent.CATEGORY_LAUNCHER) + ris = packageManager?.queryIntentActivitiesCompat(intent, intent.type, 0, 0) + } + + if (ris.isNullOrEmpty()) { + val pkgInfo = + packageManager?.getPackageInfoCompat( + BuildConfig.MANAGER_INJECTED_PKG_NAME, PackageManager.GET_ACTIVITIES, 0) + val activity = pkgInfo?.activities?.firstOrNull { it.processName == it.packageName } + if (activity != null) { + intent = + Intent(Intent.ACTION_MAIN).apply { + component = ComponentName(activity.packageName, activity.name) + } + } else return null + } else { + val activity = ris.first().activityInfo + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.component = ComponentName(activity.packageName, activity.name) + } + + intent.categories?.clear() + intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER") + intent.setPackage(BuildConfig.MANAGER_INJECTED_PKG_NAME) + managerIntent = Intent(intent) + } + .onFailure { Log.e(TAG, "Failed to build manager intent", it) } + return managerIntent + } + + fun openManager(withData: Uri?) { + val intent = getManagerIntent() ?: return + val launchIntent = Intent(intent).apply { data = withData } + runCatching { + activityManager?.startActivityAsUserWithFeature( + SystemContext.appThread, + "android", + null, + launchIntent, + launchIntent.type, + null, + null, + 0, + 0, + null, + null, + 0) + } + .onFailure { Log.e(TAG, "Failed to open manager", it) } + } + + /** Fixes permissions for the WebView cache. */ + private fun fixWebViewPermissions(file: File, targetUid: Int) { + if (!file.exists()) return + + // Set the SELinux label that allows apps to read/write shared xposed data + SELinux.setFileContext(file.absolutePath, "u:object_r:xposed_file:s0") + + // Change ownership to the target UID (e.g., 2000) + runCatching { android.system.Os.chown(file.absolutePath, targetUid, targetUid) } + .onFailure { Log.e(TAG, "Failed to chown ${file.path}", it) } + + // Recurse into directories + if (file.isDirectory) { + file.listFiles()?.forEach { fixWebViewPermissions(it, targetUid) } + } + } + + private fun ensureWebViewPermission() { + val targetUid = BuildConfig.MANAGER_INJECTED_UID + runCatching { + val pkgInfo = + packageManager?.getPackageInfoCompat(BuildConfig.MANAGER_INJECTED_PKG_NAME, 0, 0) + ?: return@runCatching + + val dataDir = + HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(pkgInfo.applicationInfo) + val cacheDir = File(dataDir, "cache") + + if (!cacheDir.exists()) cacheDir.mkdirs() + fixWebViewPermissions(cacheDir, targetUid) + } + .onFailure { Log.w(TAG, "WebView permission fix failed", it) } + } + + fun obtainManagerBinder(heartbeat: IBinder, pid: Int, uid: Int): IBinder { + ManagerGuard(heartbeat, pid, uid) + if (ConfigCache.isManager(uid)) { + ensureWebViewPermission() + } + return this + } + + fun isRunningManager(pid: Int, uid: Int): Boolean = false + + override fun getXposedApiVersion() = IXposedService.LIB_API + + override fun getXposedVersionCode() = BuildConfig.VERSION_CODE + + override fun getXposedVersionName() = BuildConfig.VERSION_NAME + + override fun getInstalledPackagesFromAllUsers( + flags: Int, + filterNoProcess: Boolean + ): ParcelableListSlice { + return ParcelableListSlice( + packageManager?.getInstalledPackagesFromAllUsers(flags, filterNoProcess) ?: emptyList()) + } + + override fun enabledModules() = ConfigCache.state.modules.keys.toTypedArray() + + override fun enableModule(packageName: String) = ModuleDatabase.enableModule(packageName) + + override fun disableModule(packageName: String) = ModuleDatabase.disableModule(packageName) + + override fun setModuleScope(packageName: String, scope: MutableList) = + ModuleDatabase.setModuleScope(packageName, scope) + + override fun getModuleScope(packageName: String) = ConfigCache.getModuleScope(packageName) + + override fun isVerboseLog() = PreferenceStore.isVerboseLogEnabled() || BuildConfig.DEBUG + + override fun setVerboseLog(enabled: Boolean) { + if (isVerboseLog()) LogcatMonitor.startVerbose() else LogcatMonitor.stopVerbose() + PreferenceStore.setVerboseLog(enabled) + } + + override fun getVerboseLog() = + LogcatMonitor.getVerboseLog()?.let { + ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY) + } + + override fun getModulesLog(): ParcelFileDescriptor? { + LogcatMonitor.checkLogFile() + return LogcatMonitor.getModulesLog()?.let { + ParcelFileDescriptor.open(it, ParcelFileDescriptor.MODE_READ_ONLY) + } + } + + override fun clearLogs(verbose: Boolean): Boolean { + LogcatMonitor.refresh(verbose) + return true + } + + override fun getPackageInfo(packageName: String, flags: Int, uid: Int) = + packageManager?.getPackageInfoCompat(packageName, flags, uid) + + override fun forceStopPackage(packageName: String, userId: Int) { + activityManager?.forceStopPackage(packageName, userId) + } + + override fun reboot() { + powerManager?.reboot(false, null, false) + } + + override fun uninstallPackage(packageName: String, userId: Int): Boolean { + val latch = CountDownLatch(1) + var result = false + + val sender = + object : IIntentSender.Stub() { + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + whitelistToken: IBinder?, + finishedReceiver: android.content.IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ) { + val status = + intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + result = status == PackageInstaller.STATUS_SUCCESS + latch.countDown() + } + + override fun send( + code: Int, + intent: Intent, + resolvedType: String?, + finishedReceiver: android.content.IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ): Int { + send(code, intent, resolvedType, null, finishedReceiver, requiredPermission, options) + return 0 + } + } + + // Using reflection to wrap the AIDL stub into an Android IntentSender + val intentSender = + runCatching { + val constructor = + IntentSender::class.java.getDeclaredConstructor(IIntentSender::class.java) + constructor.isAccessible = true + constructor.newInstance(sender) + } + .getOrNull() ?: return false + + val pkg = VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST) + val flag = if (userId == -1) 0x00000002 else 0 // DELETE_ALL_USERS flag + + runCatching { + packageManager + ?.packageInstaller + ?.uninstall(pkg, "android", flag, intentSender, if (userId == -1) 0 else userId) + } + .onFailure { + return false + } + + latch.await() + return result + } + + override fun isSepolicyLoaded() = + SELinux.checkSELinuxAccess( + "u:r:dex2oat:s0", "u:object_r:dex2oat_exec:s0", "file", "execute_no_trans") + + override fun getUsers(): List { + return userManager?.getRealUsers()?.map { + UserInfo().apply { + id = it.id + name = it.name + } + } ?: emptyList() + } + + override fun installExistingPackageAsUser(packageName: String, userId: Int): Int { + return runCatching { + packageManager?.installExistingPackageAsUser(packageName, userId, 0, 0, null) ?: -110 + } + .getOrDefault(-110) + } + + override fun systemServerRequested() = SystemServerService.systemServerRequested + + override fun startActivityAsUserWithFeature(intent: Intent, userId: Int): Int { + if (!intent.getBooleanExtra("lsp_no_switch_to_user", false)) { + intent.removeExtra("lsp_no_switch_to_user") + val currentUser = activityManager?.currentUser + val parent = userManager?.getProfileParent(userId)?.id ?: userId + if (currentUser != null && currentUser.id != parent) { + if (activityManager?.switchUser(parent) == false) return -1 + val wm = + IWindowManager.Stub.asInterface( + android.os.ServiceManager.getService(Context.WINDOW_SERVICE)) + wm?.lockNow(null) + } + } + return activityManager?.startActivityAsUserWithFeature( + SystemContext.appThread, + "android", + null, + intent, + intent.type, + null, + null, + 0, + 0, + null, + null, + userId) ?: -1 + } + + override fun queryIntentActivitiesAsUser( + intent: Intent, + flags: Int, + userId: Int + ): ParcelableListSlice { + return ParcelableListSlice( + packageManager?.queryIntentActivitiesCompat(intent, intent.type, flags, userId) + ?: emptyList()) + } + + override fun dex2oatFlagsLoaded() = + SystemProperties.get("dalvik.vm.dex2oat-flags").contains("--inline-max-code-units=0") + + override fun setHiddenIcon(hide: Boolean) { + val args = + Bundle().apply { + putString("value", if (hide) "0" else "1") + putString("_user", "0") + } + runCatching { + val provider = + activityManager + ?.getContentProviderExternal("settings", 0, SystemContext.token, null) + ?.provider + provider?.call("android", "settings", "PUT_global", "show_hidden_icon_apps_enabled", args) + } + .onFailure { Log.w(TAG, "setHiddenIcon failed", it) } + } + + override fun getLogs(zipFd: ParcelFileDescriptor) { + FileSystem.getLogs(zipFd) + } + + override fun restartFor(intent: Intent) {} // No-op matching original + + override fun getDenyListPackages() = ConfigCache.getDenyListPackages() + + /** + * Executes Magisk via ProcessBuilder and redirects output directly to the passed + * ParcelFileDescriptor using the /proc/self/fd/ pseudo-filesystem. + */ + override fun flashZip(zipPath: String, outputStream: ParcelFileDescriptor) { + val fdFile = File("/proc/self/fd/${outputStream.fd}") + val processBuilder = + ProcessBuilder("magisk", "--install-module", zipPath) + .redirectOutput(ProcessBuilder.Redirect.appendTo(fdFile)) + + runCatching { + outputStream.use { _ -> + FileOutputStream(fdFile, true).use { fdw -> + val proc = processBuilder.start() + if (proc.waitFor(10, TimeUnit.SECONDS)) { + if (proc.exitValue() == 0) { + fdw.write("- Reboot after 5s\n".toByteArray()) + Thread.sleep(5000) + reboot() + } else { + fdw.write("! Flash failed, exit with ${proc.exitValue()}\n".toByteArray()) + } + } else { + proc.destroy() + fdw.write("! Timeout, abort\n".toByteArray()) + } + } + } + } + .onFailure { Log.e(TAG, "flashZip failed", it) } + } + + override fun clearApplicationProfileData(packageName: String) { + packageManager?.clearApplicationProfileData(packageName) + } + + override fun enableStatusNotification() = PreferenceStore.isStatusNotificationEnabled() + + override fun setEnableStatusNotification(enable: Boolean) { + val isEnabled = enableStatusNotification() + PreferenceStore.setStatusNotification(enable) + if (isEnabled && !enable) { + NotificationManager.cancelStatusNotification() + } + if (!isEnabled && enable) { + NotificationManager.notifyStatusNotification() + } + } + + override fun performDexOptMode(packageName: String) = + org.matrix.vector.daemon.utils.performDexOptMode(packageName) + + override fun getDex2OatWrapperCompatibility() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Dex2OatServer.compatibility else 0 + + override fun setAutoInclude(packageName: String, enabled: Boolean) = + ModuleDatabase.setAutoInclude(packageName, enabled) + + override fun getAutoInclude(packageName: String) = ConfigCache.getAutoInclude(packageName) +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt new file mode 100644 index 000000000..26d0a25bb --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/ModuleService.kt @@ -0,0 +1,213 @@ +package org.matrix.vector.daemon.ipc + +import android.content.AttributionSource +import android.os.Binder +import android.os.Build +import android.os.Bundle +import android.os.ParcelFileDescriptor +import android.os.RemoteException +import android.util.Log +import io.github.libxposed.service.IXposedScopeCallback +import io.github.libxposed.service.IXposedService +import java.io.Serializable +import java.util.Collections +import java.util.WeakHashMap +import java.util.concurrent.ConcurrentHashMap +import org.lsposed.lspd.models.Module +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.data.ConfigCache +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.data.ModuleDatabase +import org.matrix.vector.daemon.data.PreferenceStore +import org.matrix.vector.daemon.system.NotificationManager +import org.matrix.vector.daemon.system.PER_USER_RANGE +import org.matrix.vector.daemon.system.activityManager + +private const val TAG = "VectorModuleService" + +class ModuleService(private val loadedModule: Module) : IXposedService.Stub() { + + companion object { + private val uidSet = ConcurrentHashMap.newKeySet() + private val serviceMap = Collections.synchronizedMap(WeakHashMap()) + + fun uidClear() { + uidSet.clear() + } + + fun uidStarts(uid: Int) { + if (uidSet.add(uid)) { + val module = ConfigCache.getModuleByUid(uid) + if (module?.file?.legacy == false) { + val service = serviceMap.getOrPut(module) { ModuleService(module) } + service.sendBinder(uid) + } + } + } + + fun uidGone(uid: Int) { + uidSet.remove(uid) + } + } + + /** + * Forges a ContentProvider call to force the module's target app process to receive this Binder + * IPC endpoint without standard Context.bindService() limits. + */ + private fun sendBinder(uid: Int) { + val name = loadedModule.packageName + runCatching { + val userId = uid / PER_USER_RANGE + val authority = name + AUTHORITY_SUFFIX + val provider = + activityManager?.getContentProviderExternal(authority, userId, null, null)?.provider + + if (provider == null) { + Log.d(TAG, "No service provider for $name") + return + } + + val extra = Bundle().apply { putBinder("binder", asBinder()) } + val reply: Bundle? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + provider.call( + AttributionSource.Builder(1000).setPackageName("android").build(), + authority, + SEND_BINDER, + null, + extra) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + provider.call("android", null, authority, SEND_BINDER, null, extra) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + provider.call("android", authority, SEND_BINDER, null, extra) + } else { + provider.call("android", SEND_BINDER, null, extra) + } + + if (reply != null) Log.d(TAG, "Sent module binder to $name") + else Log.w(TAG, "Failed to send module binder to $name") + } + .onFailure { Log.w(TAG, "Failed to send module binder for uid $uid", it) } + } + + private fun ensureModule(): Int { + val appId = Binder.getCallingUid() % PER_USER_RANGE + if (loadedModule.appId != appId) { + throw RemoteException( + "Module ${loadedModule.packageName} is not for uid ${Binder.getCallingUid()}") + } + return Binder.getCallingUid() / PER_USER_RANGE + } + + override fun getApiVersion() = ensureModule().let { IXposedService.LIB_API } + + override fun getFrameworkName() = ensureModule().let { BuildConfig.FRAMEWORK_NAME } + + override fun getFrameworkVersion() = ensureModule().let { BuildConfig.VERSION_NAME } + + override fun getFrameworkVersionCode() = ensureModule().let { BuildConfig.VERSION_CODE } + + override fun getFrameworkProperties(): Long { + ensureModule() + var prop = IXposedService.PROP_CAP_SYSTEM or IXposedService.PROP_CAP_REMOTE + if (ConfigCache.state.isDexObfuscateEnabled) + prop = prop or IXposedService.PROP_RT_API_PROTECTION + return prop + } + + override fun getScope(): List { + ensureModule() + return ConfigCache.getModuleScope(loadedModule.packageName)?.map { it.packageName } + ?: emptyList() + } + + override fun requestScope(packages: List, callback: IXposedScopeCallback) { + val userId = ensureModule() + if (!PreferenceStore.isScopeRequestBlocked(loadedModule.packageName)) { + packages.forEach { pkg -> + NotificationManager.requestModuleScope(loadedModule.packageName, userId, pkg, callback) + } + } else { + callback.onScopeRequestFailed("Scope request blocked by user configuration") + } + } + + override fun removeScope(packages: List) { + val userId = ensureModule() + packages.forEach { pkg -> + runCatching { ModuleDatabase.removeModuleScope(loadedModule.packageName, pkg, userId) } + .onFailure { Log.e(TAG, "Error removing scope for $pkg", it) } + } + } + + override fun requestRemotePreferences(group: String): Bundle { + val userId = ensureModule() + return Bundle().apply { + putSerializable( + "map", + PreferenceStore.getModulePrefs(loadedModule.packageName, userId, group) as Serializable) + } + } + + @Suppress("DEPRECATION") + override fun updateRemotePreferences(group: String, diff: Bundle) { + val userId = ensureModule() + val values = mutableMapOf() + + diff.getSerializable("delete")?.let { deletes -> + (deletes as Set<*>).forEach { values[it as String] = null } + } + diff.getSerializable("put")?.let { puts -> + (puts as Map<*, *>).forEach { (k, v) -> values[k as String] = v } + } + + runCatching { + PreferenceStore.updateModulePrefs(loadedModule.packageName, userId, group, values) + (loadedModule.service as? InjectedModuleService)?.onUpdateRemotePreferences(group, diff) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun deleteRemotePreferences(group: String) { + PreferenceStore.deleteModulePrefs(loadedModule.packageName, ensureModule(), group) + } + + override fun listRemoteFiles(): Array { + val userId = ensureModule() + return runCatching { + FileSystem.resolveModuleDir( + loadedModule.packageName, "files", userId, Binder.getCallingUid()) + .toFile() + .list() ?: emptyArray() + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun openRemoteFile(path: String): ParcelFileDescriptor { + val userId = ensureModule() + FileSystem.ensureModuleFilePath(path) + return runCatching { + val file = + FileSystem.resolveModuleDir( + loadedModule.packageName, "files", userId, Binder.getCallingUid()) + .resolve(path) + .toFile() + ParcelFileDescriptor.open( + file, ParcelFileDescriptor.MODE_CREATE or ParcelFileDescriptor.MODE_READ_WRITE) + } + .getOrElse { throw RemoteException(it.message) } + } + + override fun deleteRemoteFile(path: String): Boolean { + val userId = ensureModule() + FileSystem.ensureModuleFilePath(path) + return runCatching { + FileSystem.resolveModuleDir( + loadedModule.packageName, "files", userId, Binder.getCallingUid()) + .resolve(path) + .toFile() + .delete() + } + .getOrElse { throw RemoteException(it.message) } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt new file mode 100644 index 000000000..74c94a353 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/ipc/SystemServerService.kt @@ -0,0 +1,121 @@ +package org.matrix.vector.daemon.ipc + +import android.os.Build +import android.os.IBinder +import android.os.IServiceCallback +import android.os.Parcel +import android.os.ServiceManager +import android.os.SystemProperties +import android.util.Log +import org.lsposed.lspd.service.ILSPApplicationService +import org.lsposed.lspd.service.ILSPSystemServerService +import org.matrix.vector.daemon.* +import org.matrix.vector.daemon.system.getSystemServiceManager + +private const val TAG = "VectorSystemServer" + +class SystemServerService(private val maxRetry: Int, private val proxyServiceName: String) : + ILSPSystemServerService.Stub(), IBinder.DeathRecipient { + + private var originService: IBinder? = null + private var requestedRetryCount = -maxRetry + + companion object { + var systemServerRequested = false + } + + init { + Log.d(TAG, "registering via proxy $proxyServiceName") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val callback = + object : IServiceCallback.Stub() { + override fun onRegistration(name: String, binder: IBinder?) { + if (name == proxyServiceName && + binder != null && + binder !== this@SystemServerService) { + Log.d(TAG, "Intercepted system service registration: $name") + originService = binder + runCatching { binder.linkToDeath(this@SystemServerService, 0) } + } + } + + override fun asBinder(): IBinder = this + } + runCatching { getSystemServiceManager().registerForNotifications(proxyServiceName, callback) } + .onFailure { Log.e(TAG, "Failed to register IServiceCallback", it) } + } + } + + fun putBinderForSystemServer() { + ServiceManager.addService(proxyServiceName, this) + binderDied() + } + + override fun requestApplicationService( + uid: Int, + pid: Int, + processName: String, + heartBeat: IBinder? + ): ILSPApplicationService? { + if (uid != 1000 || heartBeat == null || processName != "system") return null + systemServerRequested = true + + // Return the ApplicationService singleton if successfully registered + return if (ApplicationService.registerHeartBeat(uid, pid, processName, heartBeat)) { + ApplicationService + } else null + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + originService?.let { + // This should however never happen, as service registration enforces later replacements + Log.i(TAG, "Original service $proxyServiceName alive, transmitting requests") + return it.transact(code, data, reply, flags) + } + + when (code) { + BRIDGE_TRANSACTION_CODE -> { + val uid = data.readInt() + val pid = data.readInt() + val processName = data.readString() ?: "" + val heartBeat = data.readStrongBinder() + + val service = requestApplicationService(uid, pid, processName, heartBeat) + if (service != null) { + reply?.writeNoException() + reply?.writeStrongBinder(service.asBinder()) + return true + } + return false + } + DEX_TRANSACTION_CODE, + OBFUSCATION_MAP_TRANSACTION_CODE -> { + return ApplicationService.onTransact(code, data, reply, flags) + } + else -> { + return super.onTransact(code, data, reply, flags) + } + } + } + + override fun binderDied() { + originService?.unlinkToDeath(this, 0) + originService = null + } + + fun maybeRetryInject() { + if (requestedRetryCount < 0) { + Log.w(TAG, "System server injection fails, triggering restart...") + requestedRetryCount++ + val restartTarget = + if (Build.SUPPORTED_64_BIT_ABIS.isNotEmpty() && + Build.SUPPORTED_32_BIT_ABIS.isNotEmpty()) { + "zygote_secondary" + } else { + "zygote" + } + SystemProperties.set("ctl.restart", restartTarget) + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt new file mode 100644 index 000000000..56e9a80c2 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/NotificationManager.kt @@ -0,0 +1,269 @@ +package org.matrix.vector.daemon.system + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.AdaptiveIconDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Icon +import android.graphics.drawable.LayerDrawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.util.Log +import io.github.libxposed.service.IXposedScopeCallback +import java.util.UUID +import org.matrix.vector.daemon.BuildConfig +import org.matrix.vector.daemon.R +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.utils.FakeContext + +private const val TAG = "VectorNotificationManager" +private const val STATUS_CHANNEL_ID = "vector_status" +private const val UPDATED_CHANNEL_ID = "vector_module_updated" +private const val STATUS_NOTIF_ID = BuildConfig.MANAGER_INJECTED_UID + +object NotificationManager { + val openManagerAction = UUID.randomUUID().toString() + val moduleScopeAction = UUID.randomUUID().toString() + + val SCOPE_CHANNEL_ID = "vector_module_scope" + + private val nm: android.app.INotificationManager? by + SystemService( + Context.NOTIFICATION_SERVICE, android.app.INotificationManager.Stub::asInterface) + private val opPkg = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) "android" else "com.android.settings" + + private fun createChannels() { + val context = FakeContext() + val list = + listOf( + NotificationChannel( + STATUS_CHANNEL_ID, + context.getString(R.string.status_channel_name), + android.app.NotificationManager.IMPORTANCE_MIN) + .apply { setShowBadge(false) }, + NotificationChannel( + UPDATED_CHANNEL_ID, + context.getString(R.string.module_updated_channel_name), + android.app.NotificationManager.IMPORTANCE_HIGH) + .apply { setShowBadge(false) }, + NotificationChannel( + SCOPE_CHANNEL_ID, + context.getString(R.string.scope_channel_name), + android.app.NotificationManager.IMPORTANCE_HIGH) + .apply { setShowBadge(false) }) + runCatching { + nm?.createNotificationChannelsForPackage( + "android", 1000, android.content.pm.ParceledListSlice(list)) + } + .onFailure { Log.e(TAG, "Failed to create notification channels", it) } + } + + private fun getBitmap(id: Int): Bitmap { + val r = FileSystem.resources + var res = r.getDrawable(id, r.newTheme()) + if (res is BitmapDrawable) { + return res.bitmap + } else { + if (res is AdaptiveIconDrawable) { + res = LayerDrawable(arrayOf(res.background, res.foreground)) + } + val bitmap = + Bitmap.createBitmap(res.intrinsicWidth, res.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + res.setBounds(0, 0, canvas.width, canvas.height) + res.draw(canvas) + return bitmap + } + } + + private fun getNotificationIcon(): Icon { + return Icon.createWithBitmap(getBitmap(R.drawable.ic_notification)) + } + + fun notifyStatusNotification() { + val context = FakeContext() + val intent = Intent(openManagerAction).apply { setPackage("android") } + val pi = + PendingIntent.getBroadcast( + context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + val notif = + Notification.Builder(context, STATUS_CHANNEL_ID) + .setContentTitle(context.getString(R.string.vector_running_notification_title)) + .setContentText(context.getString(R.string.vector_running_notification_content)) + .setSmallIcon(getNotificationIcon()) + .setContentIntent(pi) + .setVisibility(Notification.VISIBILITY_SECRET) + .setOngoing(true) + .build() + .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } + + createChannels() + runCatching { + nm?.enqueueNotificationWithTag("android", opPkg, null, STATUS_NOTIF_ID, notif, 0) + } + } + + fun cancelStatusNotification() { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + nm?.cancelNotificationWithTag("android", "android", null, STATUS_NOTIF_ID, 0) + } else { + nm?.cancelNotificationWithTag("android", null, STATUS_NOTIF_ID, 0) + } + } + } + + fun cancelNotification(channel: String, modulePkg: String, moduleUserId: Int) { + runCatching { + // We use the module package name's hash code as the notification ID + // to match how we enqueued it in requestModuleScope and notifyModuleUpdated. + val notifId = modulePkg.hashCode() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + nm?.cancelNotificationWithTag("android", "android", modulePkg, notifId, 0) + } else { + nm?.cancelNotificationWithTag("android", modulePkg, notifId, 0) + } + } + .onFailure { Log.e(TAG, "Failed to cancel notification", it) } + } + + fun requestModuleScope( + modulePkg: String, + moduleUserId: Int, + scopePkg: String, + callback: IXposedScopeCallback + ) { + val context = FakeContext() + val userName = userManager?.getUserName(moduleUserId) ?: moduleUserId.toString() + + fun createActionIntent(actionParams: String, requestCode: Int): PendingIntent { + val intent = + Intent(moduleScopeAction).apply { + setPackage("android") + data = + Uri.Builder() + .scheme("module") + .encodedAuthority("$modulePkg:$moduleUserId") + .encodedPath(scopePkg) + .appendQueryParameter("action", actionParams) + .build() + putExtras(Bundle().apply { putBinder("callback", callback.asBinder()) }) + } + return PendingIntent.getBroadcast( + context, + requestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + } + + val notif = + Notification.Builder(context, SCOPE_CHANNEL_ID) + .setContentTitle(context.getString(R.string.xposed_module_request_scope_title)) + .setContentText( + context.getString( + R.string.xposed_module_request_scope_content, modulePkg, userName, scopePkg)) + .setSmallIcon(getNotificationIcon()) + .addAction( + Notification.Action.Builder( + null, + context.getString(R.string.scope_approve), + createActionIntent("approve", 4)) + .build()) + .addAction( + Notification.Action.Builder( + null, context.getString(R.string.scope_deny), createActionIntent("deny", 5)) + .build()) + .addAction( + Notification.Action.Builder( + null, + context.getString(R.string.never_ask_again), + createActionIntent("block", 6)) + .build()) + .setAutoCancel(true) + .setStyle( + Notification.BigTextStyle() + .bigText( + context.getString( + R.string.xposed_module_request_scope_content, + modulePkg, + userName, + scopePkg))) + .build() + .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } + + createChannels() + runCatching { + nm?.enqueueNotificationWithTag("android", opPkg, modulePkg, modulePkg.hashCode(), notif, 0) + } + } + + fun notifyModuleUpdated( + modulePackageName: String, + moduleUserId: Int, + enabled: Boolean, + systemModule: Boolean + ) { + val context = FakeContext() + val userName = userManager?.getUserName(moduleUserId) ?: moduleUserId.toString() + + val title = + context.getString( + if (enabled) { + if (systemModule) R.string.xposed_module_updated_notification_title_system + else R.string.xposed_module_updated_notification_title + } else R.string.module_is_not_activated_yet) + + val content = + context.getString( + if (enabled) { + if (systemModule) R.string.xposed_module_updated_notification_content_system + else R.string.xposed_module_updated_notification_content + } else { + if (moduleUserId == 0) R.string.module_is_not_activated_yet_main_user_detailed + else R.string.module_is_not_activated_yet_multi_user_detailed + }, + modulePackageName, + userName) + + val intent = + Intent(openManagerAction).apply { + setPackage("android") + data = + Uri.Builder() + .scheme("module") + .encodedAuthority("$modulePackageName:$moduleUserId") + .build() + } + val pi = + PendingIntent.getBroadcast( + context, 3, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + val notif = + Notification.Builder(context, UPDATED_CHANNEL_ID) + .setContentTitle(title) + .setContentText(content) + .setSmallIcon(getNotificationIcon()) + .setContentIntent(pi) + .setVisibility(Notification.VISIBILITY_SECRET) + .setAutoCancel(true) + .setStyle(Notification.BigTextStyle().bigText(content)) + .build() + .apply { extras.putString("android.substName", BuildConfig.FRAMEWORK_NAME) } + + createChannels() + runCatching { + nm?.enqueueNotificationWithTag( + "android", opPkg, modulePackageName, modulePackageName.hashCode(), notif, 0) + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt new file mode 100644 index 000000000..6c5227848 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemBinders.kt @@ -0,0 +1,68 @@ +package org.matrix.vector.daemon.system + +import android.app.IActivityManager +import android.content.Context +import android.content.pm.IPackageManager +import android.os.IBinder +import android.os.IPowerManager +import android.os.IServiceManager +import android.os.IUserManager +import android.os.RemoteException +import android.os.ServiceManager +import com.android.internal.os.BinderInternal +import hidden.HiddenApiBridge +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * A thread-safe, lazy property delegate that fetches an Android system service Binder. + * Automatically links a DeathRecipient to clear the cache if the service dies. + */ +class SystemService(private val name: String, private val asInterface: (IBinder) -> T) : + ReadOnlyProperty { + @Volatile private var instance: T? = null + + private val deathRecipient = IBinder.DeathRecipient { instance = null } + + override fun getValue(thisRef: Any?, property: KProperty<*>): T? { + instance?.let { + return it + } + return synchronized(this) { + instance?.let { + return it + } + val binder = ServiceManager.getService(name) ?: return null + try { + binder.linkToDeath(deathRecipient, 0) + instance = asInterface(binder) + instance + } catch (e: RemoteException) { + null + } + } + } +} + +// --- Top-level System Binders --- +val activityManager: IActivityManager? by + SystemService(Context.ACTIVITY_SERVICE, IActivityManager.Stub::asInterface) +val packageManager: IPackageManager? by SystemService("package", IPackageManager.Stub::asInterface) +val userManager: IUserManager? by + SystemService(Context.USER_SERVICE, IUserManager.Stub::asInterface) +val powerManager: IPowerManager? by + SystemService(Context.POWER_SERVICE, IPowerManager.Stub::asInterface) + +/** + * Holds global state received from system_server during the late injection phase. Used for forging + * calls to ActivityManager that require a valid caller context. + */ +object SystemContext { + @Volatile var appThread: android.app.IApplicationThread? = null + @Volatile var token: IBinder? = null +} + +fun getSystemServiceManager(): IServiceManager { + return IServiceManager.Stub.asInterface( + HiddenApiBridge.Binder_allowBlocking(BinderInternal.getContextObject())) +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt new file mode 100644 index 000000000..9ab34d25b --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/system/SystemExtensions.kt @@ -0,0 +1,313 @@ +package org.matrix.vector.daemon.system + +import android.app.IActivityManager +import android.content.IIntentReceiver +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.IPackageManager +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.ParceledListSlice +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IUserManager +import android.util.Log +import java.io.File +import java.lang.reflect.Method +import java.util.stream.Collectors +import org.matrix.vector.daemon.utils.getRealUsers + +private const val TAG = "VectorSystem" +const val PER_USER_RANGE = 100000 +const val MATCH_ANY_USER = 0x00400000 // PackageManager.MATCH_ANY_USER +const val MATCH_ALL_FLAGS = + PackageManager.MATCH_DISABLED_COMPONENTS or + PackageManager.MATCH_DIRECT_BOOT_AWARE or + PackageManager.MATCH_DIRECT_BOOT_UNAWARE or + PackageManager.MATCH_UNINSTALLED_PACKAGES or + MATCH_ANY_USER + +@Throws(Exception::class) +private fun IPackageManager.getPackageInfoCompatThrows( + packageName: String, + flags: Int, + userId: Int +): PackageInfo? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getPackageInfo(packageName, flags.toLong(), userId) + } else { + getPackageInfo(packageName, flags, userId) + } +} + +fun IPackageManager.getPackageInfoCompat( + packageName: String, + flags: Int, + userId: Int +): PackageInfo? { + return try { + getPackageInfoCompatThrows(packageName, flags, userId) + } catch (e: Exception) { + null + } +} + +/** + * Checks if the package is truly available for the given user. Apps can be "installed" but + * disabled/hidden by profile owners. + */ +fun IPackageManager.isPackageAvailable( + packageName: String, + userId: Int, + ignoreHidden: Boolean +): Boolean { + return runCatching { + isPackageAvailable(packageName, userId) || + (ignoreHidden && getApplicationHiddenSettingAsUser(packageName, userId)) + } + .getOrDefault(false) +} + +/** Fetches PackageInfo alongside its components (Activities, Services, Receivers, Providers). */ +fun IPackageManager.getPackageInfoWithComponents( + packageName: String, + flags: Int, + userId: Int +): PackageInfo? { + val fullFlags = + flags or + PackageManager.GET_ACTIVITIES or + PackageManager.GET_SERVICES or + PackageManager.GET_RECEIVERS or + PackageManager.GET_PROVIDERS + + var pkgInfo: PackageInfo? = null + + try { + // If the binder buffer overflows, it will throw an exception here. + pkgInfo = getPackageInfoCompatThrows(packageName, fullFlags, userId) + } catch (e: Exception) { + // Fallback path: Fetch sequentially if the initial query threw an Exception + pkgInfo = + try { + getPackageInfoCompatThrows(packageName, flags, userId) + } catch (ignored: Exception) { + null + } + + if (pkgInfo != null) { + runCatching { + pkgInfo.activities = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_ACTIVITIES, userId) + ?.activities + } + runCatching { + pkgInfo.services = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_SERVICES, userId) + ?.services + } + runCatching { + pkgInfo.receivers = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_RECEIVERS, userId) + ?.receivers + } + runCatching { + pkgInfo.providers = + getPackageInfoCompatThrows(packageName, flags or PackageManager.GET_PROVIDERS, userId) + ?.providers + } + } + } + + if (pkgInfo?.applicationInfo == null) return null + if (pkgInfo.packageName != "android") { + val sourceDir = pkgInfo.applicationInfo?.sourceDir + if (sourceDir == null || + !File(sourceDir).exists() || + !isPackageAvailable(packageName, userId, true)) { + return null + } + } + + return pkgInfo +} + +/** Extracts all unique process names associated with a package's components. */ +fun PackageInfo.fetchProcesses(): Set { + val processNames = mutableSetOf() + + val componentArrays = arrayOf(activities, receivers, providers) + for (components in componentArrays) { + components?.forEach { processNames.add(it.processName) } + } + + services?.forEach { service -> + // Ignore isolated processes as they shouldn't be hooked in the same way + if ((service.flags and ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) { + processNames.add(service.processName) + } + } + + return processNames +} + +fun IPackageManager.queryIntentActivitiesCompat( + intent: Intent, + resolvedType: String?, + flags: Int, + userId: Int +): List { + return runCatching { + val slice = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + queryIntentActivities(intent, resolvedType, flags.toLong(), userId) + } else { + queryIntentActivities(intent, resolvedType, flags, userId) + } + slice?.list ?: emptyList() + } + .getOrElse { + Log.e(TAG, "queryIntentActivitiesCompat failed", it) + emptyList() + } +} + +/** Cached method reference to avoid repeated reflection lookups in loops. */ +private val getInstalledPackagesMethod: Method? by lazy { + val isLongFlags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + android.content.pm.IPackageManager::class + .java + .declaredMethods + .find { + it.name == "getInstalledPackages" && + it.parameterTypes.size == 2 && + it.parameterTypes[0] == + (if (isLongFlags) Long::class.javaPrimitiveType else Int::class.javaPrimitiveType) + } + ?.apply { isAccessible = true } +} + +/** Reflectively calls getInstalledPackages and casts to ParceledListSlice. */ +private fun IPackageManager.getInstalledPackagesReflect( + flags: Any, + userId: Int +): List { + val method = getInstalledPackagesMethod ?: return emptyList() + return runCatching { + val result = method.invoke(this, flags, userId) + @Suppress("UNCHECKED_CAST") (result as? ParceledListSlice)?.list + } + .getOrNull() ?: emptyList() +} + +fun IPackageManager.getInstalledPackagesFromAllUsers( + flags: Int, + filterNoProcess: Boolean +): List { + val result = mutableListOf() + // Assuming userManager is available in this scope as in original code + val users = userManager?.getRealUsers() ?: emptyList() + + for (user in users) { + // We pass flags as Any so the reflective invoke handles Long or Int correctly + val flagParam: Any = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) flags.toLong() else flags + + val infos = getInstalledPackagesReflect(flagParam, user.id) + if (infos.isEmpty()) continue + + val validUserApps = + infos + .parallelStream() + .filter { + it.applicationInfo != null && (it.applicationInfo!!.uid / PER_USER_RANGE) == user.id + } + .filter { isPackageAvailable(it.packageName, user.id, true) } + .collect(Collectors.toList()) + + result.addAll(validUserApps) + } + + if (filterNoProcess) { + return result + .parallelStream() + .filter { + getPackageInfoWithComponents( + it.packageName, MATCH_ALL_FLAGS, it.applicationInfo!!.uid / PER_USER_RANGE) + ?.fetchProcesses() + ?.isNotEmpty() == true + } + .collect(Collectors.toList()) + } + + return result +} + +fun IActivityManager.registerReceiverCompat( + receiver: IIntentReceiver, + filter: IntentFilter, + requiredPermission: String?, + userId: Int, + flags: Int +): Intent? { + val appThread = SystemContext.appThread ?: return null + return runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + registerReceiverWithFeature( + appThread, + "android", + null, + "null", + receiver, + filter, + requiredPermission, + userId, + flags) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + registerReceiverWithFeature( + appThread, "android", null, receiver, filter, requiredPermission, userId, flags) + } else { + registerReceiver( + appThread, "android", receiver, filter, requiredPermission, userId, flags) + } + } + .onFailure { Log.e(TAG, "registerReceiver failed", it) } + .getOrNull() +} + +fun IActivityManager.broadcastIntentCompat(intent: Intent) { + val appThread = SystemContext.appThread + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + broadcastIntentWithFeature( + appThread, + null, + intent, + null, + null, + 0, + null, + null, + null, + null, + null, + -1, + null, + true, + false, + 0) + } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + broadcastIntentWithFeature( + appThread, null, intent, null, null, 0, null, null, null, -1, null, true, false, 0) + } else { + broadcastIntent( + appThread, intent, null, null, 0, null, null, null, -1, null, true, false, 0) + } + } + .onFailure { Log.e(TAG, "broadcastIntent failed", it) } +} + +fun IUserManager.getUserName(userId: Int): String { + return runCatching { getUserInfo(userId)?.name }.getOrNull() ?: userId.toString() +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt new file mode 100644 index 000000000..8f4f6db10 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/FakeContext.kt @@ -0,0 +1,86 @@ +package org.matrix.vector.daemon.utils + +import android.content.ContentResolver +import android.content.ContextWrapper +import android.content.pm.ApplicationInfo +import android.content.res.Resources +import android.database.DatabaseErrorHandler +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteDatabase.CursorFactory +import android.os.Build +import hidden.HiddenApiBridge +import java.io.File +import org.matrix.vector.daemon.data.FileSystem +import org.matrix.vector.daemon.system.packageManager as sysPackageManager + +/** A stub context used by the daemon to initilize database, forge intents and notifications. */ +class FakeContext(private val fakePackageName: String = "android") : ContextWrapper(null) { + + companion object { + @Volatile var nullProvider = false + private var systemAppInfo: ApplicationInfo? = null + private var fakeTheme: Resources.Theme? = null + } + + override fun getPackageName(): String = fakePackageName + + override fun getOpPackageName(): String = "android" + + fun getUserId(): Int { + return 0 + } + + fun getUser(): android.os.UserHandle { + return HiddenApiBridge.UserHandle(0) + } + + override fun getApplicationInfo(): ApplicationInfo { + if (systemAppInfo == null) { + systemAppInfo = + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + sysPackageManager?.getApplicationInfo("android", 0L, 0) + } else { + sysPackageManager?.getApplicationInfo("android", 0, 0) + } + } + .getOrNull() + } + return systemAppInfo ?: ApplicationInfo() + } + + override fun getContentResolver(): ContentResolver? { + return if (nullProvider) null else object : ContentResolver(this) {} + } + + override fun getTheme(): Resources.Theme { + if (fakeTheme == null) fakeTheme = resources.newTheme() + return fakeTheme!! + } + + override fun getResources(): Resources = FileSystem.resources + + // Required for Android 12+ + override fun getAttributionTag(): String? = null + + override fun getDatabasePath(name: String): File { + return java.io.File(name) // We pass absolute paths, so just return it directly + } + + override fun openOrCreateDatabase( + name: String, + mode: Int, + factory: CursorFactory? + ): SQLiteDatabase { + return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), factory) + } + + override fun openOrCreateDatabase( + name: String, + mode: Int, + factory: CursorFactory, + errorHandler: DatabaseErrorHandler? + ): SQLiteDatabase { + return SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name).path, factory, errorHandler) + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt new file mode 100644 index 000000000..dc7d00596 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/InstallerVerifier.kt @@ -0,0 +1,28 @@ +package org.matrix.vector.daemon.utils + +import com.android.apksig.ApkVerifier +import java.io.File +import java.io.IOException + +object InstallerVerifier { + + @Throws(IOException::class) + fun verifyInstallerSignature(path: String) { + val verifier = ApkVerifier.Builder(File(path)).setMinCheckedPlatformVersion(27).build() + + try { + val result = verifier.verify() + if (!result.isVerified) { + throw IOException("APK signature not verified") + } + + val mainCert = result.signerCertificates[0] + if (!mainCert.encoded.contentEquals(SignInfo.CERTIFICATE)) { + val dname = mainCert.subjectX500Principal.name + throw IOException("APK signature mismatch: $dname") + } + } catch (e: Exception) { + throw IOException(e) + } + } +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt new file mode 100644 index 000000000..ca6400bd4 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/ObfuscationManager.kt @@ -0,0 +1,9 @@ +package org.matrix.vector.daemon.utils + +import android.os.SharedMemory + +object ObfuscationManager { + @JvmStatic external fun obfuscateDex(memory: SharedMemory): SharedMemory + + @JvmStatic external fun getSignatures(): Map +} diff --git a/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt new file mode 100644 index 000000000..5cf69c960 --- /dev/null +++ b/daemon/src/main/kotlin/org/matrix/vector/daemon/utils/Workarounds.kt @@ -0,0 +1,164 @@ +package org.matrix.vector.daemon.utils + +import android.app.IServiceConnection +import android.app.Notification +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.UserInfo +import android.os.Build +import android.os.IUserManager +import android.util.Log +import java.io.BufferedReader +import java.io.InputStreamReader +import java.lang.ClassNotFoundException +import org.matrix.vector.daemon.system.* + +private const val TAG = "VectorWorkarounds" +private val isLenovo = Build.MANUFACTURER.equals("lenovo", ignoreCase = true) +private val isXiaomi = Build.MANUFACTURER.equals("xiaomi", ignoreCase = true) + +fun IUserManager.getRealUsers(): List { + val users = + runCatching { getUsers(true, true, true) } + .recoverCatching { t -> if (t is NoSuchMethodError) getUsers(true) else throw t } + .onFailure { Log.e(TAG, "All user retrieval attempts failed", it) } + .getOrDefault(emptyList()) + .toMutableList() + + if (isLenovo) { + val existingIds = users.map { it.id }.toSet() + for (i in 900..909) { + if (i !in existingIds) { + runCatching { getUserInfo(i) } + .onFailure { Log.e(TAG, "Failed to apply Lenovo's app cloning workaround", it) } + .getOrNull() + ?.let { users.add(it) } + } + } + } + return users +} + +/** Android 16 DP1 SystemUI FeatureFlag and Notification Builder workaround. */ +fun applyNotificationWorkaround() { + if (Build.VERSION.SDK_INT == 36) { + runCatching { + val feature = Class.forName("android.app.FeatureFlagsImpl") + val field = feature.getDeclaredField("systemui_is_cached").apply { isAccessible = true } + field.set(null, true) + } + .onFailure { + if (it !is ClassNotFoundException) + Log.e(TAG, "Failed to bypass systemui_is_cached flag", it) + } + } + + runCatching { Notification.Builder(FakeContext(), "notification_workaround").build() } + .onFailure { + if (it is AbstractMethodError) { + FakeContext.nullProvider = !FakeContext.nullProvider + } else { + Log.e(TAG, "Failed to build dummy notification", it) + } + } +} + +/** + * UpsideDownCake (Android 14) requires executing a shell command for dexopt, whereas older versions + * use reflection/IPC. + */ +fun performDexOptMode(packageName: String): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + return runCatching { + val process = + Runtime.getRuntime().exec("cmd package compile -m speed-profile -f $packageName") + val output = BufferedReader(InputStreamReader(process.inputStream)).use { it.readText() } + val exitCode = process.waitFor() + exitCode == 0 && output.contains("Success") + } + .onFailure { Log.e(TAG, "Failed to exectute dexopt via cmd", it) } + .getOrDefault(false) + } else { + return runCatching { + packageManager?.performDexOptMode( + packageName, + false, // useJitProfiles + "speed-profile", + true, + true, + null) == true + } + .onFailure { Log.e(TAG, "Failed to invoke IPackageManager.performDexOptMode", it) } + .getOrDefault(false) + } +} + +fun applyXspaceWorkaround(connection: IServiceConnection) { + if (isXiaomi) { + val intent = + Intent().apply { + component = + ComponentName.unflattenFromString( + "com.miui.securitycore/com.miui.xspace.service.XSpaceService") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activityManager?.bindService( + SystemContext.appThread, + SystemContext.token, + intent, + intent.type, + connection, + Context.BIND_AUTO_CREATE.toLong(), + "android", + 0) + } else { + activityManager?.bindService( + SystemContext.appThread, + SystemContext.token, + intent, + intent.type, + connection, + Context.BIND_AUTO_CREATE, + "android", + 0) + } + } +} + +fun applySqliteHelperWorkaround() { + // OnePlus compare current package with BenchAppList to decide sync mode + runCatching { + val globalClass = Class.forName("android.database.sqlite.SQLiteGlobal") + val syncModeField = globalClass.getDeclaredField("sDefaultSyncMode") + syncModeField.isAccessible = true + + // Prevents from calling getPkgs() + if (syncModeField.get(null) == null) { + syncModeField.set(null, "NORMAL") + Log.i(TAG, "SQLiteGlobal.sDefaultSyncMode initialized to NORMAL.") + } + } + .onFailure { Log.v(TAG, "SQLiteGlobal workaround not applied: ${it.message}") } + + // Fix AOSP Settings.Global dependency (API 28+ but not recent Android versions) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + runCatching { + val walClass = Class.forName("android.database.sqlite.SQLiteCompatibilityWalFlags") + + // Mark as initialized so initIfNeeded() returns immediately + walClass.getDeclaredField("sInitialized").apply { + isAccessible = true + set(null, true) + } + + // Mark as 'Currently Calling' as a secondary recursion guard + walClass.getDeclaredField("sCallingGlobalSettings").apply { + isAccessible = true + set(null, true) + } + Log.i(TAG, "SQLiteCompatibilityWalFlags successfully bypassed.") + } + .onFailure { Log.v(TAG, "Could not disable SQLiteCompatibilityWalFlags: ${it.message}") } + } +} diff --git a/daemon/src/main/res/values-af/strings.xml b/daemon/src/main/res/values-af/strings.xml index 38e9953c4..8b9d290e5 100644 --- a/daemon/src/main/res/values-af/strings.xml +++ b/daemon/src/main/res/values-af/strings.xml @@ -9,13 +9,13 @@ Xposed-module is opgedateer, stelselherlaai vereis %s is opgedateer, aangesien die omvang System Framework bevat, vereis herlaai om veranderinge toe te pas Module-opdatering voltooi - LSPosed status - LSPosed gelaai - Tik die kennisgewing om bestuurder oop te maak + Vector status + Vector gelaai + Tik die kennisgewing om bestuurder oop te maak Omvang Versoek %1$s op gebruiker %2$s versoek om %3$s by sy omvang te voeg. Omvang versoek Keur goed Ontken - Moet nooit vra nie + Moet nooit vra nie diff --git a/daemon/src/main/res/values-ar/strings.xml b/daemon/src/main/res/values-ar/strings.xml index 013f1e30a..153b6a658 100644 --- a/daemon/src/main/res/values-ar/strings.xml +++ b/daemon/src/main/res/values-ar/strings.xml @@ -9,13 +9,13 @@ تم تحديث وحدة Xposed، مطلوب إعادة تشغيل النظام %s تم تحديثه، نظراً لأن النطاق يحتوي على إطار النظام، يتطلب إعادة التشغيل لتطبيق التغييرات اكتمل تحديث الوحدة - حالة LSPosed - تم تحميل LSPosed - اضغط على الإشعار لفتح المدير + حالة Vector + تم تحميل Vector + اضغط على الإشعار لفتح المدير Scope Request %1$s على %2$s طلبات المستخدم لإضافة %3$s إلى نطاقه. طلب النطاق اوافق رفض - لا تسأل أبدا + لا تسأل أبدا diff --git a/daemon/src/main/res/values-bg/strings.xml b/daemon/src/main/res/values-bg/strings.xml index 1d931004a..af52dbf0e 100644 --- a/daemon/src/main/res/values-bg/strings.xml +++ b/daemon/src/main/res/values-bg/strings.xml @@ -10,12 +10,12 @@ %s е актуализиран, тъй като обхватът съдържа System Framework, необходимо е рестартиране, за да се приложат промените Актуализация на модула е завършена Предложен статус на LSP - LSPosed заредени - Докоснете известието, за да отворите мениджъра + Vector заредени + Докоснете известието, за да отворите мениджъра Заявка за обхват %1$s при заявки от страна на потребителя %2$s за добавяне на %3$s към неговия обхват. Искане за обхват Одобряване на Отказ - Никога не питайте + Никога не питайте diff --git a/daemon/src/main/res/values-bn/strings.xml b/daemon/src/main/res/values-bn/strings.xml index ff94cfddf..36914d24a 100644 --- a/daemon/src/main/res/values-bn/strings.xml +++ b/daemon/src/main/res/values-bn/strings.xml @@ -10,12 +10,12 @@ %s আপডেট করা হয়েছে, যেহেতু সুযোগে সিস্টেম ফ্রেমওয়ার্ক রয়েছে, পরিবর্তনগুলি প্রয়োগ করার জন্য রিবুট প্রয়োজন মডিউল আপডেট সম্পূর্ণ LSPপোজড স্ট্যাটাস - LSPosed লোড - ম্যানেজার খুলতে বিজ্ঞপ্তিতে ট্যাপ করুন + Vector লোড + ম্যানেজার খুলতে বিজ্ঞপ্তিতে ট্যাপ করুন সুযোগ অনুরোধ ব্যবহারকারী %2$s এর সুযোগে %3$s যোগ করার অনুরোধে %1$s। সুযোগ অনুরোধ অনুমোদন করুন অস্বীকার করুন - কখনো জিজ্ঞাসা করবেন না + কখনো জিজ্ঞাসা করবেন না diff --git a/daemon/src/main/res/values-ca/strings.xml b/daemon/src/main/res/values-ca/strings.xml index 961589d2d..b5d53bc16 100644 --- a/daemon/src/main/res/values-ca/strings.xml +++ b/daemon/src/main/res/values-ca/strings.xml @@ -9,13 +9,13 @@ Mòdul Xposed actualitzat, cal reiniciar el sistema %s s\'ha actualitzat, ja que l\'abast conté System Framework, cal reiniciar per aplicar els canvis S\'ha completat l\'actualització del mòdul - Estat LSPosed - LSPosed carregat - Toqueu la notificació per obrir el gestor + Estat Vector + Vector carregat + Toqueu la notificació per obrir el gestor Sol·licitud d\'abast %1$s a l\'usuari %2$s demana afegir %3$s al seu abast. Sol·licitud d\'abast Aprovar Negar - Mai pregunteu + Mai pregunteu diff --git a/daemon/src/main/res/values-cs/strings.xml b/daemon/src/main/res/values-cs/strings.xml index 89c6aab41..fd96e36b9 100644 --- a/daemon/src/main/res/values-cs/strings.xml +++ b/daemon/src/main/res/values-cs/strings.xml @@ -9,13 +9,13 @@ Xposed modul aktualizován, je vyžadován restart systému %s byl aktualizován, a protože se provedly změny v souvislosti se Systémovým Frameworkem, je vyžadován restart pro aplikaci změn Aktualizace modulu dokončena - Stav LSPosed - LPosed načten - Klepnutím na oznámení otevřete správce + Stav Vector + LPosed načten + Klepnutím na oznámení otevřete správce Žádost o rozsah %1$s pro uživatele %2$s požaduje přidání %3$s do jeho rozsahu. Žádost o rozsah Schválit Odmítnout - Nikdy se neptat + Nikdy se neptat diff --git a/daemon/src/main/res/values-da/strings.xml b/daemon/src/main/res/values-da/strings.xml index 2a8485100..4bd015211 100644 --- a/daemon/src/main/res/values-da/strings.xml +++ b/daemon/src/main/res/values-da/strings.xml @@ -9,13 +9,13 @@ Xposed modul opdateret, system genstart kræves %s er blevet opdateret, da anvendelsesområdet indeholder System Framework, krævede genstart for at anvende ændringer Modulopdatering afsluttet - LSPosed status - LSPosed indlæst - Tryk på meddelelsen for at åbne administratoren + Vector status + Vector indlæst + Tryk på meddelelsen for at åbne administratoren Anmodning om anvendelsesområde %1$s på anmodninger fra brugeren %2$s om at tilføje %3$s til sit anvendelsesområde. Anmodning om anvendelsesområde Godkend Afvis - Spørg aldrig + Spørg aldrig diff --git a/daemon/src/main/res/values-de/strings.xml b/daemon/src/main/res/values-de/strings.xml index df200b793..f2e35f8e8 100644 --- a/daemon/src/main/res/values-de/strings.xml +++ b/daemon/src/main/res/values-de/strings.xml @@ -9,13 +9,13 @@ Xposed-Modul aktualisiert, Systemneustart erforderlich %s wurde aktualisiert, da der Geltungsbereich System-Framework enthält, ist ein Neustart erforderlich, damit die Änderungen übernommen werden Modulaktualisierung abgeschlossen - LSPosed-Status - LSPosed geladen - Auf die Benachrichtigung tippen, um den Manager zu öffnen + Vector-Status + Vector geladen + Auf die Benachrichtigung tippen, um den Manager zu öffnen Scope-Anfrage %1$s von Benutzer %2$s fordert an, %3$s zu seinem Scope-Bereich hinzuzufügen. Scope-Anfrage Genehmigen Verweigern - Niemals fragen + Niemals fragen diff --git a/daemon/src/main/res/values-el/strings.xml b/daemon/src/main/res/values-el/strings.xml index 85797fc8f..a2e25dd6b 100644 --- a/daemon/src/main/res/values-el/strings.xml +++ b/daemon/src/main/res/values-el/strings.xml @@ -9,13 +9,13 @@ Το πρόσθετο Xposed ενημερώθηκε, απαιτείται επανεκκίνηση συστήματος %s έχει ενημερωθεί, δεδομένου ότι το πεδίο εφαρμογής περιέχει Πλαίσιο Συστήματος, απαιτείται επανεκκίνηση για να εφαρμοστούν οι αλλαγές Η ενημέρωση πρόσθετου ολοκληρώθηκε - Κατάσταση LSPosed - LSPosed φορτώθηκε - Πατήστε την ειδοποίηση για άνοιγμα διαχειριστή + Κατάσταση Vector + Vector φορτώθηκε + Πατήστε την ειδοποίηση για άνοιγμα διαχειριστή Αίτηση για το πεδίο εφαρμογής %1$s στις αιτήσεις του χρήστη %2$s για την προσθήκη του %3$s στο πεδίο εφαρμογής του. Αίτημα πεδίου εφαρμογής Έγκριση Άρνηση - Ποτέ μην ρωτάς + Ποτέ μην ρωτάς diff --git a/daemon/src/main/res/values-es/strings.xml b/daemon/src/main/res/values-es/strings.xml index 5a4c91961..f0e39ee78 100644 --- a/daemon/src/main/res/values-es/strings.xml +++ b/daemon/src/main/res/values-es/strings.xml @@ -10,12 +10,12 @@ %s ha sido actualizado, ya que el ámbito contiene la estructura del sistema, requiere reiniciar para aplicar cambios Módulo de actualización completo LSPosición de estado - LSPosed cargado - Toca la notificación para abrir el gestor + Vector cargado + Toca la notificación para abrir el gestor Solicitud de alcance %1$s cuando el usuario %2$s solicita añadir %3$s a su ámbito. Solicitud de alcance Aprobar Denegar - Nunca preguntes + Nunca preguntes diff --git a/daemon/src/main/res/values-et/strings.xml b/daemon/src/main/res/values-et/strings.xml index 5e3a66898..da3e617f2 100644 --- a/daemon/src/main/res/values-et/strings.xml +++ b/daemon/src/main/res/values-et/strings.xml @@ -9,13 +9,13 @@ Xposed moodul uuendatud, süsteemi taaskäivitamine vajalik %s on uuendatud, kuna reguleerimisala sisaldab System Framework, vajalik taaskäivitamine, et rakendada muudatusi Mooduli uuendamine lõpetatud - LSPosedi staatus - LSPosed laaditud - Halduri avamiseks puudutage märguannet + Vectori staatus + Vector laaditud + Halduri avamiseks puudutage märguannet Ulatuse Taotlus %1$s kasutajal %2$s taotleb %3$s lisamist oma ulatusse. Ulatuse taotlus Kinnita Keela - Ära Enam Küsi + Ära Enam Küsi diff --git a/daemon/src/main/res/values-fa/strings.xml b/daemon/src/main/res/values-fa/strings.xml index 339c0e5de..cda35c5fe 100644 --- a/daemon/src/main/res/values-fa/strings.xml +++ b/daemon/src/main/res/values-fa/strings.xml @@ -9,13 +9,13 @@ ماژول Xposed به‌روزرسانی شد، نیاز به راه‌اندازی مجدد سیستم %s به‌روزرسانی شده است؛ از آنجا که محدوده شامل چارچوب سیستم است، برای اعمال تغییرات نیاز به راه‌اندازی مجدد سیستم است به‌روزرسانی ماژول کامل شد - وضعیت LSPosed - LSPosed بارگذاری شد - برای باز کردن مدیر روی اعلان ضربه بزنید + وضعیت Vector + Vector بارگذاری شد + برای باز کردن مدیر روی اعلان ضربه بزنید درخواست محدوده %1$s روی کاربر %2$s درخواست افزودن %3$s به محدوده خود را دارد. درخواست محدوده تأیید رد - هرگز نپرس + هرگز نپرس diff --git a/daemon/src/main/res/values-fi/strings.xml b/daemon/src/main/res/values-fi/strings.xml index b591d36b3..b21473c9e 100644 --- a/daemon/src/main/res/values-fi/strings.xml +++ b/daemon/src/main/res/values-fi/strings.xml @@ -9,13 +9,13 @@ Xposed moduuli päivitetty, järjestelmän uudelleenkäynnistys vaaditaan %s on päivitetty, koska soveltamisala sisältää järjestelmän kehyksen, vaaditaan uudelleenkäynnistys muutosten käyttöön Moduulin päivitys valmis - LSPosed status - LSPosed ladattu - Avaa manager napauttamalla ilmoitusta + Vector status + Vector ladattu + Avaa manager napauttamalla ilmoitusta Soveltamisalaa koskeva pyyntö %1$s käyttäjän %2$s pyynnöistä lisätä %3$s sen toimialueeseen. Laajuuspyyntö Hyväksy Kiellä - Älä koskaan kysy + Älä koskaan kysy diff --git a/daemon/src/main/res/values-fr/strings.xml b/daemon/src/main/res/values-fr/strings.xml index 2ca96bea9..0c3564c4b 100644 --- a/daemon/src/main/res/values-fr/strings.xml +++ b/daemon/src/main/res/values-fr/strings.xml @@ -1,7 +1,7 @@ - Le module LSPosed n\’est pas encore actif + Le module Vector n\’est pas encore actif %1$s a été installé, mais n\'a pas été encore activé %1$s a été installé pour l\'utilisateur %2$s, mais n\'a pas été encore activé Module Xposed mis à jour @@ -9,13 +9,13 @@ Module Xposed mis à jour, redémarrage du système requis %s a été mis à jour, étant donné que le champ d\'application est étendu au sous système, un redémarrage est nécessaire pour appliquer les changements Mise à jour du module terminée - Statut LSPosed - LSPosed chargé - Appuyer sur la notification pour ouvrir le gestionnaire + Statut Vector + Vector chargé + Appuyer sur la notification pour ouvrir le gestionnaire Demande d\'Extension de Portée %1$s sur l\'utilisateur %2$s demande d\'ajouter %3$s à son périmètre d\'action. Demande d\'Extension de Portée Approuver Refuser - Ne jamais demander + Ne jamais demander diff --git a/daemon/src/main/res/values-hi/strings.xml b/daemon/src/main/res/values-hi/strings.xml index 5d0eae764..b18a1f757 100644 --- a/daemon/src/main/res/values-hi/strings.xml +++ b/daemon/src/main/res/values-hi/strings.xml @@ -10,12 +10,12 @@ %s को अपडेट कर दिया गया है, क्योंकि स्कोप में सिस्टम फ्रेमवर्क है, परिवर्तनों को लागू करने के लिए रीबूट की आवश्यकता है मॉड्यूल अद्यतन पूर्ण एलएसपोस्ड स्थिति - LSPosed लोड किया गया - मैनेजर खोलने के लिए नोटिफिकेशन पर टैप करें + Vector लोड किया गया + मैनेजर खोलने के लिए नोटिफिकेशन पर टैप करें गुंजाइश अनुरोध उपयोगकर्ता %2$s पर %1$s इसके दायरे में %3$s जोड़ने का अनुरोध करता है। दायरा अनुरोध मंज़ूरी देना अस्वीकार करना - कभी मत पूछो + कभी मत पूछो diff --git a/daemon/src/main/res/values-hr/strings.xml b/daemon/src/main/res/values-hr/strings.xml index fa1f789f9..963882940 100644 --- a/daemon/src/main/res/values-hr/strings.xml +++ b/daemon/src/main/res/values-hr/strings.xml @@ -9,13 +9,13 @@ Xposed modul je ažuriran, potrebno je ponovno pokretanje sustava %s je ažuriran, budući da opseg sadrži System Framework, potrebno je ponovno pokretanje za primjenu promjena Ažuriranje modula dovršeno - LSPosed status - LSPosed je učitan - Dodirnite obavijest da otvorite upravitelja + Vector status + Vector je učitan + Dodirnite obavijest da otvorite upravitelja Zahtjev za opseg %1$s na korisniku %2$s zahtijeva dodavanje %3$s svom opsegu. Zahtjev za opseg Odobriti Poreći - Nikad ne pitaj + Nikad ne pitaj diff --git a/daemon/src/main/res/values-hu/strings.xml b/daemon/src/main/res/values-hu/strings.xml index bebddf236..f834c508e 100644 --- a/daemon/src/main/res/values-hu/strings.xml +++ b/daemon/src/main/res/values-hu/strings.xml @@ -9,13 +9,13 @@ Xposed modul frissítve, rendszer újraindítás szükséges %s frissítve lett, mivel a hatókör tartalmazza a System Framework-et, a változások alkalmazásához szükséges újraindítás szükséges. A modul frissítése befejeződött - LSPosed állapot - LSPosed betöltve - Érintse meg az értesítést a menedzser megnyitásához + Vector állapot + Vector betöltve + Érintse meg az értesítést a menedzser megnyitásához Hatókör kérés %1$s a felhasználó %2$s kérésére a %3$s hozzáadására a hatóköréhez. Hatókör kérés Jóváhagyás Megtagadás - Soha ne kérdezz rá + Soha ne kérdezz rá diff --git a/daemon/src/main/res/values-in/strings.xml b/daemon/src/main/res/values-in/strings.xml index 1f178a95d..f061972db 100644 --- a/daemon/src/main/res/values-in/strings.xml +++ b/daemon/src/main/res/values-in/strings.xml @@ -9,13 +9,13 @@ Modul Xposed diperbarui, diperlukan memulai ulang sistem %s telah diperbarui, karena cakupannya berisi Kerangka Sistem, diperlukan mulai ulang untuk menerapkan perubahan Pembaruan modul selesai - Status LSPosed - LSPosed dimuat - Ketuk notifikasi untuk membuka pengelola + Status Vector + Vector dimuat + Ketuk notifikasi untuk membuka pengelola Permintaan Cakupan %1$s pada pengguna %2$s meminta untuk menambahkan %3$s ke dalam cakupannya. Permintaan cakupan Menyetujui Menolak - Jangan Pernah Bertanya + Jangan Pernah Bertanya diff --git a/daemon/src/main/res/values-it/strings.xml b/daemon/src/main/res/values-it/strings.xml index 9646afd2d..9c611e04f 100644 --- a/daemon/src/main/res/values-it/strings.xml +++ b/daemon/src/main/res/values-it/strings.xml @@ -9,13 +9,13 @@ Modulo Xposed aggiornato, è necessario il riavvio del sistema %s è stato aggiornato. Poiché è abilitato per il framework di sistema, è necessario riavviare per applicare le modifiche Aggiornamento del modulo completato - Stato LSPosed - LSPosed caricato - Tocca la notifica per aprire il manager + Stato Vector + Vector caricato + Tocca la notifica per aprire il manager Richiesta attivazione %1$s sull\'utente %2$s richiede di aggiungere %3$s alle sue attivazioni. Richiesta attivazione Approva Nega - Non chiedere mai + Non chiedere mai diff --git a/daemon/src/main/res/values-iw/strings.xml b/daemon/src/main/res/values-iw/strings.xml index c9bb62b6a..a37e77231 100644 --- a/daemon/src/main/res/values-iw/strings.xml +++ b/daemon/src/main/res/values-iw/strings.xml @@ -1,21 +1,21 @@ - מודול LSPosed עדיין לא הופעל + מודול Vector עדיין לא הופעל %1$s הותקן, אך אינו מופעל עדיין %1$s הותקן למשתמש %2$s, אך אינו מופעל עדיין - מודול LSPosed עודכן + מודול Vector עודכן %s עודכן מודול Xposed עודכן, נדרש אתחול המערכת %s עודכן, מכיוון שההיקף מכיל System Framework, נדרש אתחול כדי להחיל שינויים עדכון המודול הושלם סטטוס LSPost - LSPosed נטען - הקש על ההודעה כדי לפתוח את המנהל + Vector נטען + הקש על ההודעה כדי לפתוח את המנהל Xposed_מודול_מבקש_כותרת_תחום %1$s על משתמש %2$s מבקש להוסיף %3$s להיקף שלו. תחום_שם_ערוץ אישור_תחום לְהַכּחִישׁ - לעולם אל תשאל + לעולם אל תשאל diff --git a/daemon/src/main/res/values-ja/strings.xml b/daemon/src/main/res/values-ja/strings.xml index 3b99590ab..9a567bbfd 100644 --- a/daemon/src/main/res/values-ja/strings.xml +++ b/daemon/src/main/res/values-ja/strings.xml @@ -9,13 +9,13 @@ Xposed モジュールが更新されました。システムの再起動が必要です %s が更新されました。スコープにシステムフレームワークが含まれているため、変更を適用するには再起動が必要です モジュールの更新完了通知 - LSPosed のステータス通知 - LSPosed の読み込み完了通知 - 通知をタップしてマネージャーを開きます + Vector のステータス通知 + Vector の読み込み完了通知 + 通知をタップしてマネージャーを開きます スコープのリクエスト ユーザー %2$s の %1$s が %3$s をそのスコープに追加するようリクエストしています。 スコープのリクエスト 許可 拒否 - 再度表示しない + 再度表示しない diff --git a/daemon/src/main/res/values-ko/strings.xml b/daemon/src/main/res/values-ko/strings.xml index 82a50715d..25be4ca5a 100644 --- a/daemon/src/main/res/values-ko/strings.xml +++ b/daemon/src/main/res/values-ko/strings.xml @@ -10,12 +10,12 @@ 범위에 시스템 프레임워크가 포함되어 있으므로 %s 이 업데이트되었습니다. 변경 사항을 적용하려면 재부팅해야 합니다. 모듈 업데이트 완료 LS포즈 상태 - LSPosed 로드됨 - 알림을 탭하여 관리자 열기 + Vector 로드됨 + 알림을 탭하여 관리자 열기 범위 요청 사용자 %2$s 의 %1$s 은 해당 범위에 %3$s 을 추가하도록 요청합니다. 범위 요청 승인 거부 - 다시 묻지 않음 + 다시 묻지 않음 diff --git a/daemon/src/main/res/values-ku/strings.xml b/daemon/src/main/res/values-ku/strings.xml index d442b1ae4..aef735d9b 100644 --- a/daemon/src/main/res/values-ku/strings.xml +++ b/daemon/src/main/res/values-ku/strings.xml @@ -10,12 +10,12 @@ %s hate nûve kirin, ji ber ku çarçove Çarçoveya Pergalê dihewîne, ji bo sepandina guhertinan ji nû ve destpêkirinê hewce dike Nûvekirina modulê qediya statûya LSP - LSP hate barkirin - Daxuyaniyê bikirtînin da ku rêveberê vekin + LSP hate barkirin + Daxuyaniyê bikirtînin da ku rêveberê vekin Scope Daxwaza %1$s li ser bikarhêner %2$s daxwaz dike ku %3$s li qada xwe zêde bike. Daxwaza Scope Destûrdan Înkarkirin - Never Ask + Never Ask diff --git a/daemon/src/main/res/values-lt/strings.xml b/daemon/src/main/res/values-lt/strings.xml index cd2045bca..60a491ffd 100644 --- a/daemon/src/main/res/values-lt/strings.xml +++ b/daemon/src/main/res/values-lt/strings.xml @@ -10,12 +10,12 @@ %s buvo atnaujintas, nes srityje yra System Framework, reikalingas perkrovimas, kad būtų galima taikyti pakeitimus Modulio atnaujinimas baigtas LSPatvirtintas statusas - LSPpateiktas pakrautas - Bakstelėkite pranešimą, kad atidarytumėte tvarkytuvę + LSPpateiktas pakrautas + Bakstelėkite pranešimą, kad atidarytumėte tvarkytuvę Apimties prašymas %1$s pagal naudotojo %2$s užklausas įtraukti %3$s į jo taikymo sritį. Apimties prašymas Patvirtinti Atsisakyti - Niekada neklauskite + Niekada neklauskite diff --git a/daemon/src/main/res/values-nl/strings.xml b/daemon/src/main/res/values-nl/strings.xml index 6e68eb0d0..333730fa9 100644 --- a/daemon/src/main/res/values-nl/strings.xml +++ b/daemon/src/main/res/values-nl/strings.xml @@ -1,21 +1,21 @@ - LSPosed module is nog niet geactiveerd + Vector module is nog niet geactiveerd %1$s is geïnstalleerd, maar nog niet geactiveerd %1$s is geïnstalleerd bij gebruiker %2$s, maar is nog niet geactiveerd - LSPosed module bijgewerkt + Vector module bijgewerkt %1$s is geupdate Xposed-module bijgewerkt, systeem opnieuw opstarten vereist %s is bijgewerkt, omdat het bereik een systeemkader bevat, moet je opnieuw opstarten om wijzigingen toe te passen Module update voltooid - LSPosed status - LSPosed geladen - Tik op de melding om manager te openen + Vector status + Vector geladen + Tik op de melding om manager te openen Scope verzoek %1$s op gebruiker %2$s verzoeken om %3$s toe te voegen aan het toepassingsgebied. Scope verzoek Goedkeuren Weiger - Nooit vragen + Nooit vragen diff --git a/daemon/src/main/res/values-no/strings.xml b/daemon/src/main/res/values-no/strings.xml index d275ef53a..c30428507 100644 --- a/daemon/src/main/res/values-no/strings.xml +++ b/daemon/src/main/res/values-no/strings.xml @@ -10,12 +10,12 @@ %s er blitt oppdatert, siden omfanget inneholder systemramme, nødvendig for omstart av endringene Moduloppdatering fullført LSPosert status - LSPosert lastet - Trykk på varselet for å åpne administrator + LSPosert lastet + Trykk på varselet for å åpne administrator Forespørsel om omfang %1$s på bruker %2$s ber om å legge til %3$s i omfanget. Forespørsel om omfang Vedta Benekte - Spør aldri + Spør aldri diff --git a/daemon/src/main/res/values-pl/strings.xml b/daemon/src/main/res/values-pl/strings.xml index a63aa91d8..07c40cec7 100644 --- a/daemon/src/main/res/values-pl/strings.xml +++ b/daemon/src/main/res/values-pl/strings.xml @@ -9,13 +9,13 @@ Zaktualizowano moduł Xposed, wymagane ponowne uruchomienie systemu %s został zaktualizowany, ponieważ zakres zawiera System Framework, wymagany restart aby zastosować zmiany Aktualizowanie modułu zakończone - Status LSPosed - LSPosed załadowany - Kliknij powiadomienie, by włączyć menadżer + Status Vector + Vector załadowany + Kliknij powiadomienie, by włączyć menadżer Żądanie Zakresu %1$s w użytkowniku %2$s żąda dodania %3$s do jego zakresu. Żądanie zakresu Zatwierdź Odrzuć - Nigdy nie pytaj + Nigdy nie pytaj diff --git a/daemon/src/main/res/values-pt-rBR/strings.xml b/daemon/src/main/res/values-pt-rBR/strings.xml index ff5357dc2..844219a7a 100644 --- a/daemon/src/main/res/values-pt-rBR/strings.xml +++ b/daemon/src/main/res/values-pt-rBR/strings.xml @@ -9,13 +9,13 @@ Módulo atualizado. É necessário reiniciar o sistema. %s foi atualizado. Como o escopo contém o Framework do Sistema, é necessário reiniciar para aplicar as mudanças. Atualização do módulo concluída - Status do LSPosed - LSPosed carregado - Toque na notificação para abrir o gerenciador + Status do Vector + Vector carregado + Toque na notificação para abrir o gerenciador Solicitação de escopo %1$s do usuário %2$s está solicitando para adicionar %3$s no seu escopo. Solicitação de escopo Permitir Negar - Nunca perguntar + Nunca perguntar diff --git a/daemon/src/main/res/values-pt/strings.xml b/daemon/src/main/res/values-pt/strings.xml index c036f9f32..680c9213b 100644 --- a/daemon/src/main/res/values-pt/strings.xml +++ b/daemon/src/main/res/values-pt/strings.xml @@ -9,13 +9,13 @@ Módulo Atualizado. É necessário reiniciar o sistema %s foi atualizado. Como o escopo contém o Framework do Sistema, é necessário reiniciar para aplicar as mudanças Atualização do módulo concluída - Estado do LSPosed - LSPosed carregado - Toque na notificação para abrir o gerenciador + Estado do Vector + Vector carregado + Toque na notificação para abrir o gerenciador Pedido de âmbito de aplicação %1$s sobre o utilizador %2$s pede para acrescentar %3$s ao seu âmbito de aplicação. Pedido de âmbito de aplicação Aprovar Negar - Nunca Pergunte + Nunca Pergunte diff --git a/daemon/src/main/res/values-ro/strings.xml b/daemon/src/main/res/values-ro/strings.xml index 0e7151ac6..e50349466 100644 --- a/daemon/src/main/res/values-ro/strings.xml +++ b/daemon/src/main/res/values-ro/strings.xml @@ -9,13 +9,13 @@ Modulul Xposed a fost actualizat, este necesară repornirea sistemului Modulul %s a fost actualizat. Este necesară repornirea dispozitivului, deoarece Sistemul Android face parte din configurația modulului. Actualizarea modulelor este completă - Stare LSPosed - LSPosed încărcat - Apăsați pentru a deschide managerul + Stare Vector + Vector încărcat + Apăsați pentru a deschide managerul Cerere de modificare configurație Modulul %1$s, instalat pentru utilizatorul %2$s, dorește să adauge %3$s în configurația sa. Cerere de modificare configurație Aprobați Refuzați - Nu afișați din nou + Nu afișați din nou diff --git a/daemon/src/main/res/values-ru/strings.xml b/daemon/src/main/res/values-ru/strings.xml index 75691c8d8..bca097f38 100644 --- a/daemon/src/main/res/values-ru/strings.xml +++ b/daemon/src/main/res/values-ru/strings.xml @@ -9,13 +9,13 @@ Модуль Xposed обновлён, требуется перезагрузка устройства %s обновлён; ввиду того, что системный фреймворк (System Framework) в его «охвате», требуется перезагрузка для применения изменений Обновление модуля завершено - Статус LSPosed - LSPosed загружен - Нажмите уведомление, чтобы открыть LSPosed Manager + Статус Vector + Vector загружен + Нажмите уведомление, чтобы открыть Vector Manager Запрос «охвата» %1$s (пользователь %2$s): запрашивается добавление %3$s в «охват». Запрос «охвата» Принять Отклонить - Больше не спрашивать + Больше не спрашивать diff --git a/daemon/src/main/res/values-si/strings.xml b/daemon/src/main/res/values-si/strings.xml index 14af856a0..5a2bf8bbe 100644 --- a/daemon/src/main/res/values-si/strings.xml +++ b/daemon/src/main/res/values-si/strings.xml @@ -10,12 +10,12 @@ %s යාවත්කාලීන කර ඇත, විෂය පථයේ පද්ධති රාමුව අඩංගු බැවින්, වෙනස්කම් යෙදීමට නැවත පණගැන්වීම අවශ්‍ය වේ මොඩියුල යාවත්කාලීන කිරීම සම්පූර්ණයි එල්එස්පී තත්ත්වය - LSPposed පටවා ඇත - කළමනාකරු විවෘත කිරීමට දැනුම්දීම තට්ටු කරන්න + LSPposed පටවා ඇත + කළමනාකරු විවෘත කිරීමට දැනුම්දීම තට්ටු කරන්න විෂය පථය ඉල්ලීම පරිශීලක %2$s හි %1$s එහි විෂය පථයට %3$s එකතු කරන ලෙස ඉල්ලා සිටී. විෂය පථය ඉල්ලීම අනුමත කරන්න ප්රතික්ෂේප කරන්න - කවදාවත් අහන්න එපා + කවදාවත් අහන්න එපා diff --git a/daemon/src/main/res/values-sk/strings.xml b/daemon/src/main/res/values-sk/strings.xml index bc8f4db64..9e71934c0 100644 --- a/daemon/src/main/res/values-sk/strings.xml +++ b/daemon/src/main/res/values-sk/strings.xml @@ -10,12 +10,12 @@ %s bola aktualizovaná, pretože rozsah obsahuje System Framework, potrebný reštart na uplatnenie zmien Aktualizácia modulu dokončená LSPonúkaný stav - LSPosed naložené - Ťuknutím na oznámenie otvorte správcu + Vector naložené + Ťuknutím na oznámenie otvorte správcu Žiadosť o rozsah %1$s na žiadosti používateľa %2$s o pridanie stránky %3$s do jej rozsahu. Žiadosť o rozsah Schváliť Odmietnuť - Nikdy sa nepýtajte + Nikdy sa nepýtajte diff --git a/daemon/src/main/res/values-sv/strings.xml b/daemon/src/main/res/values-sv/strings.xml index 6ba7be981..e5b63bc52 100644 --- a/daemon/src/main/res/values-sv/strings.xml +++ b/daemon/src/main/res/values-sv/strings.xml @@ -9,13 +9,13 @@ Xposed modul uppdaterad, systemomstart krävs %s har uppdaterats, eftersom omfattningen innehåller Systemramverk, krävs omstart för att tillämpa ändringar Uppdatering av modulen slutförd - LSPosed status - LSPosed laddad - Tryck på meddelandet för att öppna administratören + Vector status + Vector laddad + Tryck på meddelandet för att öppna administratören Begäran om tillämpningsområde %1$s om användaren %2$s begär att %3$s ska läggas till i dess räckvidd. Begäran om tillämpningsområde Godkänna Förneka - Fråga aldrig + Fråga aldrig diff --git a/daemon/src/main/res/values-th/strings.xml b/daemon/src/main/res/values-th/strings.xml index a00562388..38ac73703 100644 --- a/daemon/src/main/res/values-th/strings.xml +++ b/daemon/src/main/res/values-th/strings.xml @@ -9,13 +9,13 @@ อัปเดตโมดูล Xposed จำเป็นต้องรีสตาร์ทเครื่อง %s ได้รับการอัปเดตแล้ว เนื่องจาก Scope มี System Framework จึงจำเป็นต้องรีสตาร์ทเครื่องเพื่อใช้การเปลี่ยนแปลง การอัปเดตโมดูลเสร็จสมบูรณ์ - สถานะ LSPosed - LSPosed โหลดแล้ว - แตะการแจ้งเตือนเพื่อเปิดตัวจัดการ + สถานะ Vector + Vector โหลดแล้ว + แตะการแจ้งเตือนเพื่อเปิดตัวจัดการ คำขอ Scope %1$s ผูัใช้นี้ %2$s ขอให้เพิ่ม %3$s ใน List ของ scope. คำขอ Scope. อนุมัติ ปฎิเสธ - ไม่เคยถาม + ไม่เคยถาม diff --git a/daemon/src/main/res/values-tr/strings.xml b/daemon/src/main/res/values-tr/strings.xml index ebea2a4f6..e28cbb835 100644 --- a/daemon/src/main/res/values-tr/strings.xml +++ b/daemon/src/main/res/values-tr/strings.xml @@ -9,13 +9,13 @@ Xposed modülü güncellendi, sistemin yeniden başlatılması gerekiyor %s Güncelleme kapsamı Sistem Çerçevesi içerdiğinden, değişiklikleri uygulamak için yeniden başlatma gereklidir Modül güncellemesi tamamlandı - LSPosed durumu - LSPosed yüklendi - Yöneticiyi açmak için bildirime dokunun + Vector durumu + Vector yüklendi + Yöneticiyi açmak için bildirime dokunun Kapsam Talebi Kullanıcı %2$s %1$s kapsamına %3$s eklemek ister. Kapsam talebi Onayla Reddet - Asla Sorma + Asla Sorma diff --git a/daemon/src/main/res/values-uk/strings.xml b/daemon/src/main/res/values-uk/strings.xml index e4e6037df..73daa8ed7 100644 --- a/daemon/src/main/res/values-uk/strings.xml +++ b/daemon/src/main/res/values-uk/strings.xml @@ -9,13 +9,13 @@ Модуль Xposed оновлено, потрібно перезавантаження системи %s було оновлено, оскільки область містить System Framework, необхідне перезавантаження для застосування змін Оновлення модуля завершено - Статус LSPosed - LSPosed завантажено - Натисніть на повідомлення, щоб відкрити менеджер + Статус Vector + Vector завантажено + Натисніть на повідомлення, щоб відкрити менеджер Запит на визначення обсягу робіт %1$s на запит користувача %2$s з проханням додати %3$s до своєї області видимості. Запит обсягу робіт Затвердити Відхилити - Ніколи не питай + Ніколи не питай diff --git a/daemon/src/main/res/values-ur/strings.xml b/daemon/src/main/res/values-ur/strings.xml index 485184ea0..fc9739ed4 100644 --- a/daemon/src/main/res/values-ur/strings.xml +++ b/daemon/src/main/res/values-ur/strings.xml @@ -10,12 +10,12 @@ %s کو اپ ڈیٹ کر دیا گیا ہے، چونکہ دائرہ کار میں سسٹم فریم ورک ہے، تبدیلیاں لاگو کرنے کے لیے ریبوٹ کی ضرورت ہے۔ ماڈیول اپ ڈیٹ مکمل ہو گیا۔ ایل ایس پیز کی حیثیت - ایل ایس پیز لوڈ شدہ - مینیجر کو کھولنے کے لیے نوٹیفکیشن کو تھپتھپائیں۔ + ایل ایس پیز لوڈ شدہ + مینیجر کو کھولنے کے لیے نوٹیفکیشن کو تھپتھپائیں۔ دائرہ کار کی درخواست صارف %2$s پر %1$s اپنے دائرہ کار میں %3$s شامل کرنے کی درخواست کرتا ہے۔ دائرہ کار کی درخواست منظور کرو انکار کرنا - کبھی نہ پوچھیں۔ + کبھی نہ پوچھیں۔ diff --git a/daemon/src/main/res/values-vi/strings.xml b/daemon/src/main/res/values-vi/strings.xml index d33acebe9..f8f6e96f1 100644 --- a/daemon/src/main/res/values-vi/strings.xml +++ b/daemon/src/main/res/values-vi/strings.xml @@ -10,12 +10,12 @@ %s đã được cập nhật, vì phạm vi bao gồm Framework Hệ thống, thì khởi động lại là cần thiết để áp dụng các thay đổi Tiện ích bổ sung cập nhật hoàn tất Trạng thái hoạt động - Ứng dụng đã được tải - Nhấn để mở trình quản lý + Ứng dụng đã được tải + Nhấn để mở trình quản lý Yêu cầu phạm vi %1$s khi người dùng %2$s yêu cầu thêm %3$s vào phạm vi của nó. Phạm vi yêu cầu Chấp thuận Từ chối - Không hỏi lại + Không hỏi lại diff --git a/daemon/src/main/res/values-zh-rCN/strings.xml b/daemon/src/main/res/values-zh-rCN/strings.xml index 0642027b6..ace294bba 100644 --- a/daemon/src/main/res/values-zh-rCN/strings.xml +++ b/daemon/src/main/res/values-zh-rCN/strings.xml @@ -9,13 +9,13 @@ Xposed 模块已更新,需要重新启动 %s 已更新,由于作用域包含系统框架,需重启以应用更改 模块更新完成 - LSPosed 状态 - LSPosed 已加载 - 点按通知以打开管理器 + Vector 状态 + Vector 已加载 + 点按通知以打开管理器 作用域请求 用户 %2$s 上的 %1$s 请求将 %3$s 添加至其作用域。 作用域请求 允许 拒绝 - 不再询问 + 不再询问 diff --git a/daemon/src/main/res/values-zh-rHK/strings.xml b/daemon/src/main/res/values-zh-rHK/strings.xml index 5919b74e3..02b34d381 100644 --- a/daemon/src/main/res/values-zh-rHK/strings.xml +++ b/daemon/src/main/res/values-zh-rHK/strings.xml @@ -9,13 +9,13 @@ Xposed 模組已更新,需要重新啟動。 %s 已更新,由於作用範圍包含系統架構,需要重新啟動以套用修改。 模块更新完成 - LSPosed 狀態 - LSPosed 已載入 - 輕觸通知以開啟管理員 + Vector 狀態 + Vector 已載入 + 輕觸通知以開啟管理員 作用範圍要求 用戶 %2$s 上的 %1$s 要求將 %3$s 新增至其作用範圍。 作用範圍要求 核准 拒絕 - 永不詢問 + 永不詢問 diff --git a/daemon/src/main/res/values-zh-rTW/strings.xml b/daemon/src/main/res/values-zh-rTW/strings.xml index dd2f463be..d085f2620 100644 --- a/daemon/src/main/res/values-zh-rTW/strings.xml +++ b/daemon/src/main/res/values-zh-rTW/strings.xml @@ -9,13 +9,13 @@ Xposed 模組已更新,需要重新啟動。 %s 已更新,由於作用域包含系統框架,需要重新啟動以套用修改。 模組更新完成 - LSPosed 狀態 - LSPosed 已載入 - 輕觸通知以開啟管理員 + Vector 狀態 + Vector 已載入 + 輕觸通知以開啟管理員 作用範圍要求 使用者 %2$s 上的 %1$s 要求將 %3$s 新增至其作用範圍。 作用範圍要求 核准 拒絕 - 永不詢問 + 永不詢問 diff --git a/daemon/src/main/res/values/strings.xml b/daemon/src/main/res/values/strings.xml index 0d195d2dd..1b68ced85 100644 --- a/daemon/src/main/res/values/strings.xml +++ b/daemon/src/main/res/values/strings.xml @@ -1,20 +1,20 @@ - Xposed module is not activated yet + Module is not activated yet %1$s has been installed, but is not activated yet %1$s has been installed to user %2$s, but is not activated yet - Xposed module updated + Module updated %s has been updated, please force stop and restart apps in its scope - Xposed module updated, system reboot required + Module updated, system reboot required %s has been updated, since the scope contains System Framework, required reboot to apply changes Module update complete - LSPosed status - LSPosed loaded - Tap the notification to open manager + Vector status + Vector loaded + Tap the notification to open manager Scope Request %1$s on user %2$s requests to add %3$s to its scope. Scope request Approve Deny - Never Ask + Never Ask diff --git a/dex2oat/src/main/cpp/include/logging.h b/dex2oat/src/main/cpp/include/logging.h index ab66ef34c..36069bdd4 100644 --- a/dex2oat/src/main/cpp/include/logging.h +++ b/dex2oat/src/main/cpp/include/logging.h @@ -15,12 +15,12 @@ #define LOGE(...) 0 #else #ifndef NDEBUG -#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) -#define LOGV(fmt, ...) \ - __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, \ +#define LOGD(fmt, ...) \ + __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \ "%s:%d#%s" \ ": " fmt, \ __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) #else #define LOGD(...) 0 #define LOGV(...) 0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ec38ab0f..530c2248d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ appcenter = "5.0.5" glide = "5.0.5" okhttp = "5.3.2" ktfmt = "0.25.0" +coroutines = "1.10.2" [plugins] agp-lib = { id = "com.android.library", version.ref = "agp" } @@ -15,7 +16,6 @@ nav-safeargs = { id = "androidx.navigation.safeargs", version.ref = "nav" } autoresconfig = { id = "dev.rikka.tools.autoresconfig", version = "1.2.2" } ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } materialthemebuilder = { id = "dev.rikka.tools.materialthemebuilder", version = "1.5.1" } -lsplugin-resopt = { id = "org.lsposed.lsplugin.resopt", version = "1.6" } lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version = "1.4" } [libraries] @@ -55,4 +55,6 @@ material = { module = "com.google.android.material:material", version = "1.12.0" gson = { module = "com.google.code.gson:gson", version = "2.13.2" } hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version = "6.1" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.10.2" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +picocli = { module = "info.picocli:picocli", version = "4.7.7" } diff --git a/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java b/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java index 279e89754..15578dc7c 100644 --- a/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java +++ b/hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java @@ -38,13 +38,6 @@ PackageInfo getPackageInfo(String packageName, long flags, int userId) String[] getPackagesForUid(int uid) throws RemoteException; - ParceledListSlice getInstalledPackages(int flags, int userId) - throws RemoteException; - - @RequiresApi(33) - ParceledListSlice getInstalledPackages(long flags, int userId) - throws RemoteException; - ParceledListSlice getInstalledApplications(int flags, int userId) throws RemoteException; diff --git a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java index 4e37cf25b..5e85d36c6 100644 --- a/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java +++ b/legacy/src/main/java/de/robv/android/xposed/XposedBridge.java @@ -42,7 +42,7 @@ public final class XposedBridge { /** * @hide */ - public static final String TAG = "LSPosed-Bridge"; + public static final String TAG = "VectorLegacyBridge"; /** * @deprecated Use {@link #getXposedVersion()} instead. diff --git a/native/include/common/logging.h b/native/include/common/logging.h index 25533e4fb..a06585bc5 100644 --- a/native/include/common/logging.h +++ b/native/include/common/logging.h @@ -77,14 +77,13 @@ inline void LogToAndroid(int prio, const char *tag, fmt::format_string fmt } // namespace vector::native::detail #ifndef NDEBUG -#define LOGV(fmt, ...) \ - ::vector::native::detail::LogToAndroid(ANDROID_LOG_VERBOSE, LOG_TAG, "{}:{} ({}): " fmt, \ - __FILE_NAME__, __LINE__, \ - __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) #define LOGD(fmt, ...) \ ::vector::native::detail::LogToAndroid(ANDROID_LOG_DEBUG, LOG_TAG, "{}:{} ({}): " fmt, \ __FILE_NAME__, __LINE__, \ __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) +#define LOGV(fmt, ...) \ + ::vector::native::detail::LogToAndroid(ANDROID_LOG_VERBOSE, LOG_TAG, \ + fmt __VA_OPT__(, ) __VA_ARGS__) #else #define LOGV(...) ((void)0) #define LOGD(...) ((void)0) diff --git a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/IDaemonService.aidl similarity index 71% rename from services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl rename to services/daemon-service/src/main/aidl/org/lsposed/lspd/service/IDaemonService.aidl index 4dff667c3..8ac84bd34 100644 --- a/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl +++ b/services/daemon-service/src/main/aidl/org/lsposed/lspd/service/IDaemonService.aidl @@ -2,12 +2,10 @@ package org.lsposed.lspd.service; import org.lsposed.lspd.service.ILSPApplicationService; -interface ILSPosedService { +interface IDaemonService { ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat); - oneway void dispatchSystemServerContext(in IBinder activityThread, in IBinder activityToken, String api); + oneway void dispatchSystemServerContext(in IBinder activityThread, in IBinder activityToken); boolean preStartManager(); - - boolean setManagerEnabled(boolean enabled); } diff --git a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl index 11e2385e9..3a67757b6 100644 --- a/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl +++ b/services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl @@ -12,8 +12,6 @@ interface ILSPManagerService { const int DEX2OAT_SELINUX_PERMISSIVE = 3; const int DEX2OAT_SEPOLICY_INCORRECT = 4; - String getApi() = 1; - ParcelableListSlice getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) = 2; String[] enabledModules() = 3; @@ -76,10 +74,6 @@ interface ILSPManagerService { List getDenyListPackages() = 41; - boolean getDexObfuscate() = 42; - - void setDexObfuscate(boolean enable) = 43; - int getDex2OatWrapperCompatibility() = 44; void clearApplicationProfileData(in String packageName) = 45; @@ -88,10 +82,6 @@ interface ILSPManagerService { void setEnableStatusNotification(boolean enable) = 48; - void setLogWatchdog(boolean enable) = 49; - - boolean isLogWatchdogEnabled() = 50; - boolean getAutoInclude(String packageName) = 51; boolean setAutoInclude(String packageName, boolean enable) = 52; diff --git a/zygisk/README.md b/zygisk/README.md index b1f44786d..b3ddf34b6 100644 --- a/zygisk/README.md +++ b/zygisk/README.md @@ -1,75 +1,57 @@ -# Vector Zygisk Module & Framework Loader +# Vector Zygisk Module and Framework Loader ## Overview -This sub-project constitutes the injection engine of the Vector framework. It acts as the bridge between the Android Zygote process and the high-level Xposed API. +This subsystem constitutes the injection engine of the Vector framework. It acts as the bridge between the Android Zygote process and the high-level Java/Kotlin Xposed API. The architecture is designed to avoid standard Android service registration and disk-based class loading, operating entirely through in-memory execution, JNI-level Binder interception, and process identity transplantation. -The project is a hybrid system consisting of two distinct layers: -1. **Native Layer (C++)**: A Zygisk module that hooks process creation, filters targets, and bootstraps the environment. -2. **Loader Layer (Kotlin)**: The initial Java-world payload that initializes the Xposed bridge, establishes high-level IPC, and manages the "Parasitic" execution environment for the Manager. +The subsystem is divided into two discrete layers: +1. _Native Zygisk Layer_ (C++): Hooks process creation via Zygisk, filters target processes, establishes the initial IPC bridge, and bootstraps the Dalvik environment from memory. +2. _Framework Loader_ (Kotlin): The Java-world payload. It handles high-level Android framework manipulation, manages the custom Binder routing service, and orchestrates the Parasitic Manager execution. -Its primary responsibility is to inject the Vector framework into the target process's memory at the earliest possible stage of its lifecycle, ensuring a robust and stealthy environment. +## IPC Architecture and Binder Relay ---- +Vector utilizes a two-phase IPC routing mechanism to establish communication between injected applications and the root daemon. Instead of registering standard AIDL endpoints with ServiceManager, it intercepts the lowest level of Java Binder communication within the Dalvik VM. -## Part 1: The Native Zygisk Layer +### The JNI Binder Trap +In `ipc_bridge.cpp`, the module uses the ART internal function `SetTableOverride` to replace the JNI function `CallBooleanMethodV`. This override intercepts all native calls to `android.os.Binder.execTransact` system-wide. -The native layer (`libzygisk.so`) is the entry point. It hooks into the Zygote process creation lifecycle via the Zygisk API (e.g., `preAppSpecialize`, `postAppSpecialize`). It is architected to have minimal internal logic, delegating heavy lifting (like ART hooking and ELF parsing) to the core [native](../native) library. +When a transaction occurs, the hook inspects the transaction code. If it matches the constant kBridgeTransactionCode (`_VEC`), the transaction is diverted to the static Kotlin method `BridgeService.execTransact`. All other transactions pass through to the original Android framework unmodified. -### Core Responsibilities -* **Target Filtering**: Implements logic to skip isolated processes, application zygotes, and non-target system components to minimize footprint. -* **IPC Communication**: Establishes a secure Binder IPC connection with the daemon manager service via a "Rendezvous" system service to fetch the framework DEX and configuration data (e.g., obfuscation maps). -* **DEX Loading**: Uses `InMemoryDexClassLoader` to load the framework's bytecode directly from memory, avoiding disk I/O signatures. -* **JNI Interception**: Installs a low-level JNI hook on `CallBooleanMethodV`. This intercepts `Binder.execTransact` calls, allowing the framework to patch into the system's IPC flow without registering standard Android Services. +### Phase 1: System Server Initialization +The `system_server` acts as the primary proxy router for the framework. During the `postServerSpecialize` callback, the following sequence occurs: +1. The native module queries `ServiceManager` for the `serial` service (or `serial_vector` for late-inject scenarios). This service acts as a temporary rendezvous point. +2. The module sends a `_VEC` transaction to retrieve a temporary binder, which it uses to fetch the framework DEX file descriptor and the obfuscation map. +3. The module installs the JNI Binder Trap (`HookBridge`) and bootstraps the Kotlin layer via `Main.forkCommon`. +4. Concurrently, the root daemon initiates a Binder transaction directly to the `system_server`. The JNI trap intercepts this, and BridgeService processes the `SEND_BINDER` action, storing the daemon's primary `IDaemonService` binder, sending back `system_server` context and linking a `DeathRecipient`. -### Key Components (C++) -* **`VectorModule` (`module.cpp`)**: The central orchestrator implementing `zygisk::ModuleBase`. It manages the injection state machine and inherits from `vector::native::Context` to gain core injection capabilities. -* **`IPCBridge` (`ipc_bridge.cpp`)**: A singleton handling raw Binder transactions. It manages the two-step connection protocol (Rendezvous -> Dedicated Binder) and contains the JNI table override logic. +### Phase 2: User Application Rendezvous +Standard applications initialize their IPC connection by routing requests through the `system_server`. +1. In `postAppSpecialize`, the application queries `ServiceManager` for the `activity` service (which resides in `system_server`). +2. The application sends a `_VEC` transaction containing the `GET_BINDER` action, its process name, and a newly allocated heartbeat `BBinder`. +3. The JNI trap inside `system_server` intercepts this transaction before the Activity Manager processes it. +4. The `system_server`'s BridgeService forwards the application's UID, PID, and heartbeat binder to the root daemon via the `IDaemonService` binder acquired in Phase 1. +5. The daemon evaluates the request against its internal scope state. If approved, it generates an `ILSPApplicationService` binder and returns it to the `system_server`, which writes it back to the waiting application's reply parcel. +6. The application uses this dedicated binder to fetch its specific framework DEX and obfuscation map. ---- +### The Heartbeat Mechanism +To manage process lifecycles without polling, the native module generates a dummy Binder object (`heartbeat_binder`) during both initialization phases. This object is passed to the daemon and kept alive in the application process via a JNI Global Reference (`env->NewGlobalRef`). If the application or `system_server` terminates normally or is killed by the kernel, the global reference is destroyed, the binder node is released, and the daemon's `DeathRecipient` triggers immediate resource cleanup. -## Part 2: The Kotlin Framework Loader +## Memory Execution and Obfuscation Synchronization -Once the native layer successfully loads the DEX, control is handed off to the Kotlin layer via JNI. This layer handles high-level Android framework manipulation, Xposed initialization, and identity spoofing. +Vector does not write framework code to the /data partition. -### Core Responsibilities -* **Bootstrapping**: `Main.forkCommon` acts as the Java entry point. It differentiates between the `system_server` and standard applications. -* **Parasitic Injection**: Implements the logic to run the full LSPosed Manager application inside a host process (currently `com.android.shell`). This allows the Manager to run with elevated privileges without being installed as a system app. -* **Manual Bridge Service**: Provides the Java-side handling for the intercepted Binder transactions. +1. Asset Delivery: The root daemon provides the framework DEX via a `SharedMemory` file descriptor, using `kDexTransactionCode`. The C++ layer wraps this file descriptor in a `java.nio.DirectByteBuffer` and initializes a `dalvik.system.InMemoryDexClassLoader`. +2. Dynamic Relinking: The daemon randomizes framework class names on each boot. The native module fetches a serialized dictionary over IPC, using `kObfuscationMapTransactionCode`. `SetupEntryClass` uses this map to locate the randomized entry point (`org.matrix.vector.core.Main`) and BridgeService, enabling the framework to link correctly at runtime. -### Key Components (Kotlin) -* **`Main`**: The singleton entry point. It initializes the Xposed bridge (`Startup`) and decides whether to load the standard Xposed environment or the Parasitic Manager. -* **`BridgeService`**: The peer to the C++ `IPCBridge`. It decodes custom `_LSP` transactions, manages the distribution of the system service binder, and handles communication between the injected framework and the root daemon. -* **`ParasiticManagerHooker`**: The complex logic for identity transplantation. - * **App Swap**: Swaps the host's `ApplicationInfo` with the Manager's info during `handleBindApplication`. - * **State Persistence**: Since the Android System is unaware the host process is running Manager activities, this component manually captures and restores `Bundle` states to prevent data loss during lifecycle events. - * **Resource Spoofing**: Hooks `WebView` and `ContentProvider` installation to satisfy package name validations. +## Parasitic Manager and Identity Transplantation ---- +The Vector Manager application is not installed as a standard package. It runs by hollowing out a host process (e.g., `com.android.shell`) using a parasitic execution model. -## Injection & Execution Flow +### System Server Intent Redirection +Within the `system_server`, `ParasiticManagerSystemHooker` intercepts `ActivityTaskSupervisor.resolveActivity`. When it detects an Intent tagged with the `LAUNCH_MANAGER` category, it dynamically modifies the returned ActivityInfo. It forces the system to launch the host package while setting the processName to the Manager's package name and adjusting theme and recents flags to mimic a standalone application. -The full lifecycle of a Vector-instrumented process follows this sequence: - -1. **Zygote Fork**: Zygisk triggers the `preAppSpecialize` callback in C++. -2. **Native Decision**: `VectorModule` checks the UID/Process Name. If valid, it initializes the `IPCBridge`. -3. **DEX Fetch**: The C++ layer connects to the root daemon, fetches the Framework DEX file descriptor and the Obfuscation Map. -4. **Memory Loading**: `postAppSpecialize` triggers the creation of an `InMemoryDexClassLoader`. -5. **JNI Hand-off**: The native module calls the static Kotlin method `org.lsposed.lspd.core.Main.forkCommon`. -6. **Identity Check (Kotlin)**: - * **If Manager Package**: `ParasiticManagerHooker.start()` is called. The process is "hijacked" to run the Manager UI. - * **If Standard App**: `Startup.bootstrapXposed()` is called. Third-party modules are loaded. -7. **Live Interception**: Throughout the process life, the C++ JNI hook redirects specific `Binder.execTransact` calls to `BridgeService.execTransact` in Kotlin. - ---- - -## Maintenance & Technical Notes - -### The IPC Protocol -The communication between the native loader and the Kotlin framework relies on specific conventions: -* **Transaction Code**: The custom code `_VEC` (bitwise constructed) must remain synchronized between `ipc_bridge.cpp` (Native) and `BridgeService.kt` (Kotlin). -* **The "Out-Parameter" List**: In `ParasiticManagerHooker.start()`, you will see an empty list `mutableListOf()`. -It is used as an "out-parameter" for the Binder call, allowing the root daemon to push the Manager Service Binder back to the loader. - -### System Server Hooks -The `ParasiticManagerSystemHooker` runs *only* in the `system_server`. It uses `XposedHooker` to intercept `ActivityTaskSupervisor.resolveActivity`. It detects Intents tagged with `LAUNCH_MANAGER` and forcefully redirects them to the parasitic process (e.g., `Shell`), modifying the `ActivityInfo` on the fly to ensure the Manager launches correctly. +### Application Host Hijacking +When the native module detects the host package UID and the Manager process name during `preAppSpecialize`, it injects `GID_INET` (3003) into the process's GID array to ensure network access. Control is then passed to `ParasiticManagerHooker.kt`, which performs the identity transplantation: +1. Code Injection: It intercepts `LoadedApk.getClassLoader` and `ActivityThread.handleBindApplication`, swapping the host's ApplicationInfo with a hybrid object constructed from the manager's APK (provided via file descriptor). The manager's DEX is injected into the host's `PathClassLoader`. +2. State Forgery: The system ActivityManager is unaware of the spoofed Manager activities. To prevent data loss during lifecycle transitions (e.g., screen rotation), the hooker intercepts `performStopActivityInner` to manually capture Bundle and PersistableBundle states into static concurrent maps. These states are reinjected during `scheduleLaunchActivity`. +3. Context Spoofing: It intercepts `ActivityThread.installProvider` and `WebViewFactory.getProvider` to construct forged `ContextImpl` instances, bypassing internal Android and Chromium package-name validation checks. diff --git a/zygisk/build.gradle.kts b/zygisk/build.gradle.kts index 1cb090d84..1757ab170 100644 --- a/zygisk/build.gradle.kts +++ b/zygisk/build.gradle.kts @@ -21,6 +21,7 @@ android { defaultConfig { multiDexEnabled = false + buildConfigField("int", "HostPackageUid", "${injectedPackageUid}") buildConfigField("String", "InjectedPackageName", """"${injectedPackageName}"""") buildConfigField("String", "ManagerPackageName", """"${defaultManagerPackageName}"""") diff --git a/zygisk/module/cli b/zygisk/module/cli new file mode 100644 index 000000000..f4ade4f10 --- /dev/null +++ b/zygisk/module/cli @@ -0,0 +1,27 @@ +#!/system/bin/sh + +tmpDaemonApk="/data/local/tmp/daemon.apk" + +# Safely check for debug APK and set classpath +if [ -r "$tmpDaemonApk" ]; then + java_options="-Djava.class.path=$tmpDaemonApk" +else + dex_path="" + for DEXDIR in /data/adb/modules $(magisk --path 2>/dev/null)/.magisk/modules; do + if [ -d "$DEXDIR/zygisk_vector" ]; then + dex_path="$DEXDIR/zygisk_vector" + break + fi + done + + if [ -z "$dex_path" ]; then + echo "No vector module path found" + exit 1 + fi + + dex_path="$dex_path/daemon.apk" + java_options="-Djava.class.path=$dex_path" +fi + +# Launch the cli +exec /system/bin/app_process $java_options /system/bin --nice-name=VectorCli org.matrix.vector.daemon.Cli "$@" diff --git a/zygisk/module/customize.sh b/zygisk/module/customize.sh index 7a9451dc8..44707fee7 100644 --- a/zygisk/module/customize.sh +++ b/zygisk/module/customize.sh @@ -82,7 +82,7 @@ esac ui_print "- Device platform: $ARCH ($ABI32 / $ABI64)" ui_print "- Extracting root module files" -for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framework/lspd.dex daemon.apk daemon manager.apk; do +for file in module.prop action.sh service.sh uninstall.sh sepolicy.rule framework/lspd.dex cli daemon.apk daemon manager.apk; do extract "$ZIPFILE" "$file" "$MODPATH" done @@ -132,6 +132,7 @@ set_perm_recursive "$MODPATH" 0 0 0755 0644 [ -d "$MODPATH/bin" ] && set_perm_recursive "$MODPATH/bin" 0 2000 0755 0755 u:object_r:xposed_file:s0 set_perm "$MODPATH/daemon" 0 0 0744 +set_perm "$MODPATH/cli" 0 0 0744 if [ "$(grep_prop ro.maple.enable)" = "1" ]; then ui_print "- Add ro.maple.enable=0" diff --git a/zygisk/module/daemon b/zygisk/module/daemon index 514eac7c0..faca6adef 100644 --- a/zygisk/module/daemon +++ b/zygisk/module/daemon @@ -43,4 +43,4 @@ fi [ "$debug" = "true" ] && log -p d -t "Vector" "Starting daemon $*" # Launch the daemon -exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.lsposed.lspd.Main "$@" >/dev/null 2>&1 +exec /system/bin/app_process $java_options /system/bin --nice-name=lspd org.matrix.vector.daemon.VectorDaemon "$@" >/dev/null 2>&1 diff --git a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt index b8037f12f..59aec5148 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt @@ -123,7 +123,7 @@ object ParasiticManagerHooker { /** * Passes the IPC binder to the Manager's internal [Constants] class so it can communicate back - * to the LSPosed system service. + * to the Vector system service. */ private fun sendBinderToManager(classLoader: ClassLoader, binder: IBinder) { runCatching { diff --git a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt index 279e1e90b..93b7113f1 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt @@ -44,11 +44,13 @@ object Main { .onFailure { t -> Utils.logE("Failed to configure logs from service", t) } // Check if this process is the designated Vector Manager. - // If so, we perform "parasitic" injection into a host (com.android.shell) - // and terminate further standard Xposed loading for this specific process. - if (niceName == BuildConfig.ManagerPackageName && ParasiticManagerHooker.start()) { - Utils.logI("Parasitic manager loaded into host, skipping standard bootstrap.") - return + if (niceName == BuildConfig.ManagerPackageName) { + val type = + if (Process.myUid() == BuildConfig.HostPackageUid) "parasitic" else "user-installed" + if (ParasiticManagerHooker.start()) { + Utils.logI("Manager ($type) loaded into host, skipping standard bootstrap.") + return + } } // Standard Xposed module loading for third-party apps diff --git a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt index 5aeb86a20..e9d13724f 100644 --- a/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt +++ b/zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt @@ -5,10 +5,9 @@ import android.os.Binder import android.os.IBinder import android.os.IBinder.DeathRecipient import android.os.Parcel -import android.os.Process import hidden.HiddenApiBridge.Binder_allowBlocking import hidden.HiddenApiBridge.Context_getActivityToken -import org.lsposed.lspd.service.ILSPosedService +import org.lsposed.lspd.service.IDaemonService import org.lsposed.lspd.util.Utils.Log /** @@ -20,35 +19,34 @@ import org.lsposed.lspd.util.Utils.Log object BridgeService { private const val TRANSACTION_CODE = ('_'.code shl 24) or ('V'.code shl 16) or ('E'.code shl 8) or 'C'.code - private const val TAG = "VectorBridge" + private const val TAG = "VectorZygiskBridge" /** Actions supported by the manual IPC bridge. */ private enum class Action { UNKNOWN, SEND_BINDER, // Daemon sending the system service binder GET_BINDER, // Process requesting its specific application service - ENABLE_MANAGER, // Toggle manager state } @Volatile private var serviceBinder: IBinder? = null - @Volatile private var service: ILSPosedService? = null + @Volatile private var service: IDaemonService? = null - /** Cleans up service references if the remote LSPosed process crashes. */ + /** Cleans up service references if the remote Vector daemon crashes. */ private val serviceRecipient: DeathRecipient = DeathRecipient { - Log.e(TAG, "LSPosed system service died.") + Log.e(TAG, "Vector daemin service died.") serviceBinder?.unlinkToDeath(this.serviceRecipient, 0) serviceBinder = null service = null } - /** Returns the active LSPosed system service interface. */ - @JvmStatic fun getService(): ILSPosedService? = service + /** Returns the active Vector daemin service interface. */ + @JvmStatic fun getService(): IDaemonService? = service /** - * Initializes the client-side connection to the LSPosed system service. + * Initializes the client-side connection to the Vector daemin service. * - * @param binder The raw binder for [ILSPosedService]. + * @param binder The raw binder for [IDaemonService]. */ private fun receiveFromBridge(binder: IBinder?) { if (binder == null) { @@ -67,7 +65,7 @@ object BridgeService { // Allow blocking calls since we are often in a synchronous fork path val blockingBinder = Binder_allowBlocking(binder) serviceBinder = blockingBinder - service = ILSPosedService.Stub.asInterface(blockingBinder) + service = IDaemonService.Stub.asInterface(blockingBinder) runCatching { blockingBinder.linkToDeath(serviceRecipient, 0) } .onFailure { Log.e(TAG, "Failed to link to service death", it) } @@ -78,15 +76,11 @@ object BridgeService { val at = activityThread.applicationThread as android.app.IApplicationThread val atBinder = at.asBinder() val systemCtx = activityThread.systemContext - service?.dispatchSystemServerContext( - atBinder, - Context_getActivityToken(systemCtx), - "Zygisk", - ) + service?.dispatchSystemServerContext(atBinder, Context_getActivityToken(systemCtx)) } .onFailure { Log.e(TAG, "Failed to dispatch system context", it) } - Log.i(TAG, "LSPosed system service binder linked.") + Log.i(TAG, "Vector daemin service binder linked.") } /** Handles manual parcel transactions. Called via reflection/JNI from the native hook. */ @@ -125,21 +119,6 @@ object BridgeService { true } else false } - - Action.ENABLE_MANAGER -> { - val uid = Binder.getCallingUid() - // Restricted to Root, System, or Shell - if ( - (uid == 0 || uid == Process.SHELL_UID || uid == Process.SYSTEM_UID) && - service != null - ) { - val enabled = data.readInt() == 1 - val result = service?.setManagerEnabled(enabled) ?: false - reply?.writeInt(if (result) 1 else 0) - true - } else false - } - else -> false } } catch (e: Throwable) {