Repository: JingMatrix/LSPosed Branch: master Commit: 8db19217d300 Files: 560 Total size: 2.5 MB Directory structure: gitextract_f3szflt5/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ ├── dependabot.yml │ └── workflows/ │ ├── core.yml │ └── crowdin.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ └── webview/ │ │ ├── colors_dark.css │ │ ├── colors_light.css │ │ ├── markdown.css │ │ ├── syntax.css │ │ ├── syntax_dark.css │ │ ├── template.html │ │ └── template_dark.html │ ├── java/ │ │ ├── com/ │ │ │ └── google/ │ │ │ └── android/ │ │ │ └── material/ │ │ │ ├── appbar/ │ │ │ │ └── SubtitleCollapsingToolbarLayout.java │ │ │ └── internal/ │ │ │ └── SubtitleCollapsingTextHelper.java │ │ └── org/ │ │ └── lsposed/ │ │ └── manager/ │ │ ├── App.java │ │ ├── ConfigManager.java │ │ ├── Constants.java │ │ ├── adapters/ │ │ │ ├── AppHelper.java │ │ │ └── ScopeAdapter.java │ │ ├── receivers/ │ │ │ └── LSPManagerServiceHolder.java │ │ ├── repo/ │ │ │ ├── RepoLoader.java │ │ │ └── model/ │ │ │ ├── Collaborator.java │ │ │ ├── OnlineModule.java │ │ │ ├── Release.java │ │ │ └── ReleaseAsset.java │ │ ├── ui/ │ │ │ ├── activity/ │ │ │ │ ├── MainActivity.java │ │ │ │ └── base/ │ │ │ │ └── BaseActivity.java │ │ │ ├── dialog/ │ │ │ │ ├── BlurBehindDialogBuilder.java │ │ │ │ ├── FlashDialogBuilder.java │ │ │ │ └── WelcomeDialog.java │ │ │ ├── fragment/ │ │ │ │ ├── AppListFragment.java │ │ │ │ ├── BaseFragment.java │ │ │ │ ├── CompileDialogFragment.java │ │ │ │ ├── HomeFragment.java │ │ │ │ ├── LogsFragment.java │ │ │ │ ├── ModulesFragment.java │ │ │ │ ├── RecyclerViewDialogFragment.java │ │ │ │ ├── RepoFragment.java │ │ │ │ ├── RepoItemFragment.java │ │ │ │ └── SettingsFragment.java │ │ │ └── widget/ │ │ │ ├── EmptyStateRecyclerView.java │ │ │ ├── ExpandableTextView.java │ │ │ ├── LinkifyTextView.java │ │ │ ├── ScrollWebView.java │ │ │ └── StatefulRecyclerView.java │ │ └── util/ │ │ ├── AccessibilityUtils.java │ │ ├── AppIconModelLoader.java │ │ ├── AppModule.java │ │ ├── BackupUtils.java │ │ ├── CloudflareDNS.java │ │ ├── EmptyAccessibilityDelegate.java │ │ ├── ModuleUtil.java │ │ ├── NavUtil.java │ │ ├── NoSniFactory.java │ │ ├── ShortcutUtil.java │ │ ├── SimpleStatefulAdaptor.java │ │ ├── ThemeUtil.java │ │ ├── UpdateUtil.java │ │ └── chrome/ │ │ ├── CustomTabsURLSpan.java │ │ └── LinkTransformationMethod.java │ └── res/ │ ├── anim/ │ │ ├── fragment_enter.xml │ │ ├── fragment_enter_pop.xml │ │ ├── fragment_exit.xml │ │ └── fragment_exit_pop.xml │ ├── drawable/ │ │ ├── ic_assignment_checkable.xml │ │ ├── ic_attach_file.xml │ │ ├── ic_baseline_add_24.xml │ │ ├── ic_baseline_arrow_back_24.xml │ │ ├── ic_baseline_assignment_24.xml │ │ ├── ic_baseline_chat_24.xml │ │ ├── ic_baseline_extension_24.xml │ │ ├── ic_baseline_get_app_24.xml │ │ ├── ic_baseline_home_24.xml │ │ ├── ic_baseline_info_24.xml │ │ ├── ic_baseline_search_24.xml │ │ ├── ic_baseline_settings_24.xml │ │ ├── ic_baseline_settings_backup_restore_24.xml │ │ ├── ic_extension_checkable.xml │ │ ├── ic_get_app_checkable.xml │ │ ├── ic_home_checkable.xml │ │ ├── ic_keyboard_arrow_down.xml │ │ ├── ic_launcher.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_round.xml │ │ ├── ic_open_in_browser.xml │ │ ├── ic_outline_android_24.xml │ │ ├── ic_outline_app_shortcut_24.xml │ │ ├── ic_outline_assignment_24.xml │ │ ├── ic_outline_dark_mode_24.xml │ │ ├── ic_outline_dns_24.xml │ │ ├── ic_outline_extension_24.xml │ │ ├── ic_outline_format_color_fill_24.xml │ │ ├── ic_outline_get_app_24.xml │ │ ├── ic_outline_groups_24.xml │ │ ├── ic_outline_home_24.xml │ │ ├── ic_outline_invert_colors_24.xml │ │ ├── ic_outline_language_24.xml │ │ ├── ic_outline_merge_type_24.xml │ │ ├── ic_outline_palette_24.xml │ │ ├── ic_outline_restore_24.xml │ │ ├── ic_outline_settings_24.xml │ │ ├── ic_outline_shield_24.xml │ │ ├── ic_outline_speaker_notes_24.xml │ │ ├── ic_outline_translate_24.xml │ │ ├── ic_round_bug_report_24.xml │ │ ├── ic_round_check_circle_24.xml │ │ ├── ic_round_error_outline_24.xml │ │ ├── ic_round_settings_24.xml │ │ ├── ic_round_update_24.xml │ │ ├── ic_round_warning_24.xml │ │ ├── ic_save.xml │ │ ├── ic_settings_checkable.xml │ │ ├── shortcut_ic_logs.xml │ │ ├── shortcut_ic_modules.xml │ │ ├── shortcut_ic_repo.xml │ │ ├── shortcut_ic_settings.xml │ │ └── simple_menu_background.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── dialog_about.xml │ │ ├── dialog_item.xml │ │ ├── dialog_title.xml │ │ ├── fragment_app_list.xml │ │ ├── fragment_compile_dialog.xml │ │ ├── fragment_home.xml │ │ ├── fragment_pager.xml │ │ ├── fragment_repo.xml │ │ ├── fragment_settings.xml │ │ ├── item_log_textview.xml │ │ ├── item_master_switch.xml │ │ ├── item_module.xml │ │ ├── item_onlinemodule.xml │ │ ├── item_repo_loadmore.xml │ │ ├── item_repo_readme.xml │ │ ├── item_repo_recyclerview.xml │ │ ├── item_repo_release.xml │ │ ├── item_repo_title_description.xml │ │ ├── preference_recyclerview.xml │ │ ├── scrollable_dialog.xml │ │ └── swiperefresh_recyclerview.xml │ ├── layout-sw600dp/ │ │ └── activity_main.xml │ ├── menu/ │ │ ├── context_menu_modules.xml │ │ ├── menu_app_item.xml │ │ ├── menu_app_list.xml │ │ ├── menu_home.xml │ │ ├── menu_logs.xml │ │ ├── menu_modules.xml │ │ ├── menu_repo.xml │ │ ├── menu_repo_item.xml │ │ └── navigation_menu.xml │ ├── menu-sw600dp/ │ │ └── navigation_menu.xml │ ├── navigation/ │ │ ├── main_nav.xml │ │ ├── modules_nav.xml │ │ └── repo_nav.xml │ ├── values/ │ │ ├── arrays.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── integer.xml │ │ ├── settings.xml │ │ ├── strings.xml │ │ ├── strings_untranslatable.xml │ │ ├── styles.xml │ │ ├── themes.xml │ │ ├── themes_custom.xml │ │ ├── themes_overlay.xml │ │ └── themes_override.xml │ ├── values-af/ │ │ └── strings.xml │ ├── values-ar/ │ │ └── strings.xml │ ├── values-bg/ │ │ └── strings.xml │ ├── values-bn/ │ │ └── strings.xml │ ├── values-ca/ │ │ └── strings.xml │ ├── values-cs/ │ │ └── strings.xml │ ├── values-da/ │ │ └── strings.xml │ ├── values-de/ │ │ └── strings.xml │ ├── values-el/ │ │ └── strings.xml │ ├── values-es/ │ │ └── strings.xml │ ├── values-et/ │ │ └── strings.xml │ ├── values-fa/ │ │ └── strings.xml │ ├── values-fi/ │ │ └── strings.xml │ ├── values-fr/ │ │ └── strings.xml │ ├── values-hi/ │ │ └── strings.xml │ ├── values-hr/ │ │ └── strings.xml │ ├── values-hu/ │ │ └── strings.xml │ ├── values-in/ │ │ └── strings.xml │ ├── values-it/ │ │ └── strings.xml │ ├── values-iw/ │ │ └── strings.xml │ ├── values-ja/ │ │ └── strings.xml │ ├── values-ko/ │ │ └── strings.xml │ ├── values-ku/ │ │ └── strings.xml │ ├── values-lt/ │ │ └── strings.xml │ ├── values-night/ │ │ ├── colors.xml │ │ └── styles.xml │ ├── values-night-v31/ │ │ └── colors.xml │ ├── values-nl/ │ │ └── strings.xml │ ├── values-no/ │ │ └── strings.xml │ ├── values-pl/ │ │ └── strings.xml │ ├── values-pt/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ro/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-si/ │ │ └── strings.xml │ ├── values-sk/ │ │ └── strings.xml │ ├── values-sv/ │ │ └── strings.xml │ ├── values-sw600dp/ │ │ └── integer.xml │ ├── values-th/ │ │ └── strings.xml │ ├── values-tr/ │ │ └── strings.xml │ ├── values-uk/ │ │ └── strings.xml │ ├── values-ur/ │ │ └── strings.xml │ ├── values-v28/ │ │ ├── dimens.xml │ │ └── themes.xml │ ├── values-v29/ │ │ └── settings.xml │ ├── values-v30/ │ │ └── themes.xml │ ├── values-v31/ │ │ └── colors.xml │ ├── values-vi/ │ │ └── strings.xml │ ├── values-zh-rCN/ │ │ └── strings.xml │ ├── values-zh-rHK/ │ │ └── strings.xml │ ├── values-zh-rTW/ │ │ └── strings.xml │ └── xml/ │ ├── prefs.xml │ └── shortcuts.xml ├── build.gradle.kts ├── core/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ └── java/ │ ├── android/ │ │ ├── app/ │ │ │ └── AndroidAppHelper.java │ │ └── content/ │ │ └── res/ │ │ ├── XModuleResources.java │ │ ├── XResForwarder.java │ │ └── XResources.java │ ├── de/ │ │ └── robv/ │ │ └── android/ │ │ └── xposed/ │ │ ├── IXposedHookCmdInit.java │ │ ├── IXposedHookInitPackageResources.java │ │ ├── IXposedHookLoadPackage.java │ │ ├── IXposedHookZygoteInit.java │ │ ├── IXposedMod.java │ │ ├── SELinuxHelper.java │ │ ├── XC_MethodHook.java │ │ ├── XC_MethodReplacement.java │ │ ├── XSharedPreferences.java │ │ ├── XposedBridge.java │ │ ├── XposedHelpers.java │ │ ├── XposedInit.java │ │ ├── callbacks/ │ │ │ ├── IXUnhook.java │ │ │ ├── XC_InitPackageResources.java │ │ │ ├── XC_LayoutInflated.java │ │ │ ├── XC_LoadPackage.java │ │ │ └── XCallback.java │ │ └── services/ │ │ ├── BaseService.java │ │ ├── DirectAccessService.java │ │ └── FileResult.java │ └── org/ │ └── lsposed/ │ └── lspd/ │ ├── core/ │ │ ├── ApplicationServiceClient.java │ │ └── Startup.java │ ├── deopt/ │ │ ├── InlinedMethodCallers.java │ │ └── PrebuiltMethodsDeopter.java │ ├── hooker/ │ │ ├── AttachHooker.java │ │ ├── CrashDumpHooker.java │ │ ├── HandleSystemServerProcessHooker.java │ │ ├── LoadedApkCreateCLHooker.java │ │ ├── LoadedApkCtorHooker.java │ │ ├── OpenDexFileHooker.java │ │ └── StartBootstrapServicesHooker.java │ ├── impl/ │ │ ├── LSPosedBridge.java │ │ ├── LSPosedContext.java │ │ ├── LSPosedHelper.java │ │ ├── LSPosedHookCallback.java │ │ └── LSPosedRemotePreferences.java │ └── util/ │ ├── ClassPathURLStreamHandler.java │ ├── Hookers.java │ ├── LspModuleClassLoader.java │ └── MetaDataReader.java ├── crowdin.yml ├── daemon/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── org/ │ │ └── lsposed/ │ │ └── lspd/ │ │ ├── Main.java │ │ ├── service/ │ │ │ ├── ActivityManagerService.java │ │ │ ├── BridgeService.java │ │ │ ├── ConfigFileManager.java │ │ │ ├── ConfigManager.java │ │ │ ├── Dex2OatService.java │ │ │ ├── LSPApplicationService.java │ │ │ ├── LSPInjectedModuleService.java │ │ │ ├── LSPManagerService.java │ │ │ ├── LSPModuleService.java │ │ │ ├── LSPNotificationManager.java │ │ │ ├── LSPSystemServerService.java │ │ │ ├── LSPosedService.java │ │ │ ├── LogcatService.java │ │ │ ├── ObfuscationManager.java │ │ │ ├── PackageService.java │ │ │ ├── PowerService.java │ │ │ ├── ServiceManager.java │ │ │ └── UserService.java │ │ └── util/ │ │ ├── FakeContext.java │ │ └── InstallerVerifier.java │ ├── jni/ │ │ ├── CMakeLists.txt │ │ ├── dex2oat.cpp │ │ ├── logcat.cpp │ │ ├── logcat.h │ │ ├── logging.h │ │ ├── obfuscation.cpp │ │ └── obfuscation.h │ └── res/ │ ├── drawable/ │ │ ├── ic_baseline_block_24.xml │ │ ├── ic_baseline_check_24.xml │ │ ├── ic_baseline_close_24.xml │ │ └── ic_notification.xml │ ├── values/ │ │ └── strings.xml │ ├── values-af/ │ │ └── strings.xml │ ├── values-ar/ │ │ └── strings.xml │ ├── values-bg/ │ │ └── strings.xml │ ├── values-bn/ │ │ └── strings.xml │ ├── values-ca/ │ │ └── strings.xml │ ├── values-cs/ │ │ └── strings.xml │ ├── values-da/ │ │ └── strings.xml │ ├── values-de/ │ │ └── strings.xml │ ├── values-el/ │ │ └── strings.xml │ ├── values-es/ │ │ └── strings.xml │ ├── values-et/ │ │ └── strings.xml │ ├── values-fa/ │ │ └── strings.xml │ ├── values-fi/ │ │ └── strings.xml │ ├── values-fr/ │ │ └── strings.xml │ ├── values-hi/ │ │ └── strings.xml │ ├── values-hr/ │ │ └── strings.xml │ ├── values-hu/ │ │ └── strings.xml │ ├── values-in/ │ │ └── strings.xml │ ├── values-it/ │ │ └── strings.xml │ ├── values-iw/ │ │ └── strings.xml │ ├── values-ja/ │ │ └── strings.xml │ ├── values-ko/ │ │ └── strings.xml │ ├── values-ku/ │ │ └── strings.xml │ ├── values-lt/ │ │ └── strings.xml │ ├── values-nl/ │ │ └── strings.xml │ ├── values-no/ │ │ └── strings.xml │ ├── values-pl/ │ │ └── strings.xml │ ├── values-pt/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ro/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-si/ │ │ └── strings.xml │ ├── values-sk/ │ │ └── strings.xml │ ├── values-sv/ │ │ └── strings.xml │ ├── values-th/ │ │ └── strings.xml │ ├── values-tr/ │ │ └── strings.xml │ ├── values-uk/ │ │ └── strings.xml │ ├── values-ur/ │ │ └── strings.xml │ ├── values-vi/ │ │ └── strings.xml │ ├── values-zh-rCN/ │ │ └── strings.xml │ ├── values-zh-rHK/ │ │ └── strings.xml │ └── values-zh-rTW/ │ └── strings.xml ├── dex2oat/ │ ├── .gitignore │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── cpp/ │ ├── CMakeLists.txt │ ├── dex2oat.cpp │ ├── include/ │ │ ├── base_macros.h │ │ ├── logging.h │ │ ├── macros.h │ │ └── oat.h │ └── oat_hook.cpp ├── external/ │ ├── CMakeLists.txt │ ├── README.md │ ├── apache/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── local/ │ │ └── MemberUtilsX.java │ └── axml/ │ └── build.gradle.kts ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── hiddenapi/ │ ├── bridge/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── hidden/ │ │ ├── ByteBufferDexClassLoader.java │ │ └── HiddenApiBridge.java │ └── stubs/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ ├── android/ │ │ ├── annotation/ │ │ │ ├── NonNull.java │ │ │ └── Nullable.java │ │ ├── app/ │ │ │ ├── ActivityManager.java │ │ │ ├── ActivityThread.java │ │ │ ├── Application.java │ │ │ ├── ContentProviderHolder.java │ │ │ ├── ContextImpl.java │ │ │ ├── IActivityController.java │ │ │ ├── IActivityManager.java │ │ │ ├── IApplicationThread.java │ │ │ ├── INotificationManager.java │ │ │ ├── IServiceConnection.java │ │ │ ├── IUidObserver.java │ │ │ ├── LoadedApk.java │ │ │ ├── Notification.java │ │ │ ├── NotificationChannel.java │ │ │ ├── ProfilerInfo.java │ │ │ └── ResourcesManager.java │ │ ├── content/ │ │ │ ├── AttributionSource.java │ │ │ ├── BroadcastReceiver.java │ │ │ ├── ComponentName.java │ │ │ ├── Context.java │ │ │ ├── IContentProvider.java │ │ │ ├── IIntentReceiver.java │ │ │ ├── IIntentSender.java │ │ │ ├── Intent.java │ │ │ ├── IntentFilter.java │ │ │ ├── IntentSender.java │ │ │ ├── pm/ │ │ │ │ ├── ApplicationInfo.java │ │ │ │ ├── BaseParceledListSlice.java │ │ │ │ ├── IPackageInstaller.java │ │ │ │ ├── IPackageManager.java │ │ │ │ ├── PackageInfo.java │ │ │ │ ├── PackageInstaller.java │ │ │ │ ├── PackageManager.java │ │ │ │ ├── PackageParser.java │ │ │ │ ├── ParceledListSlice.java │ │ │ │ ├── ResolveInfo.java │ │ │ │ ├── UserInfo.java │ │ │ │ └── VersionedPackage.java │ │ │ └── res/ │ │ │ ├── AssetManager.java │ │ │ ├── CompatibilityInfo.java │ │ │ ├── Configuration.java │ │ │ ├── Resources.java │ │ │ ├── ResourcesImpl.java │ │ │ ├── ResourcesKey.java │ │ │ └── TypedArray.java │ │ ├── ddm/ │ │ │ └── DdmHandleAppName.java │ │ ├── graphics/ │ │ │ ├── Movie.java │ │ │ └── drawable/ │ │ │ └── Drawable.java │ │ ├── os/ │ │ │ ├── Binder.java │ │ │ ├── Build.java │ │ │ ├── Bundle.java │ │ │ ├── Environment.java │ │ │ ├── Handler.java │ │ │ ├── IBinder.java │ │ │ ├── IInterface.java │ │ │ ├── IPowerManager.java │ │ │ ├── IServiceCallback.java │ │ │ ├── IServiceManager.java │ │ │ ├── IUserManager.java │ │ │ ├── Parcel.java │ │ │ ├── Parcelable.java │ │ │ ├── PersistableBundle.java │ │ │ ├── RemoteException.java │ │ │ ├── ResultReceiver.java │ │ │ ├── SELinux.java │ │ │ ├── ServiceManager.java │ │ │ ├── ShellCallback.java │ │ │ ├── ShellCommand.java │ │ │ ├── SystemProperties.java │ │ │ ├── UserHandle.java │ │ │ └── UserManager.java │ │ ├── permission/ │ │ │ └── IPermissionManager.java │ │ ├── system/ │ │ │ ├── ErrnoException.java │ │ │ ├── Int32Ref.java │ │ │ └── Os.java │ │ ├── util/ │ │ │ ├── DisplayMetrics.java │ │ │ ├── MutableInt.java │ │ │ └── TypedValue.java │ │ ├── view/ │ │ │ └── IWindowManager.java │ │ └── webkit/ │ │ ├── WebViewDelegate.java │ │ ├── WebViewFactory.java │ │ └── WebViewFactoryProvider.java │ ├── androidx/ │ │ └── annotation/ │ │ ├── IntRange.java │ │ └── RequiresApi.java │ ├── com/ │ │ └── android/ │ │ ├── internal/ │ │ │ ├── os/ │ │ │ │ ├── BinderInternal.java │ │ │ │ └── ZygoteInit.java │ │ │ └── util/ │ │ │ └── XmlUtils.java │ │ └── server/ │ │ ├── LocalServices.java │ │ ├── SystemService.java │ │ ├── SystemServiceManager.java │ │ └── am/ │ │ ├── ActivityManagerService.java │ │ └── ProcessRecord.java │ ├── dalvik/ │ │ └── system/ │ │ ├── BaseDexClassLoader.java │ │ └── VMRuntime.java │ ├── org/ │ │ └── xmlpull/ │ │ └── v1/ │ │ └── XmlPullParserException.java │ ├── sun/ │ │ ├── misc/ │ │ │ └── CompoundEnumeration.java │ │ └── net/ │ │ └── www/ │ │ ├── ParseUtil.java │ │ └── protocol/ │ │ └── jar/ │ │ └── Handler.java │ └── xposed/ │ └── dummy/ │ ├── XResourcesSuperClass.java │ └── XTypedArraySuperClass.java ├── magisk-loader/ │ └── update/ │ ├── changelog.md │ └── zygisk.json ├── native/ │ ├── CMakeLists.txt │ ├── README.md │ ├── include/ │ │ ├── common/ │ │ │ ├── config.h │ │ │ └── logging.h │ │ ├── core/ │ │ │ ├── config_bridge.h │ │ │ ├── context.h │ │ │ └── native_api.h │ │ ├── elf/ │ │ │ ├── elf_image.h │ │ │ └── symbol_cache.h │ │ ├── framework/ │ │ │ └── android_types.h │ │ └── jni/ │ │ ├── jni_bridge.h │ │ └── jni_hooks.h │ └── src/ │ ├── core/ │ │ ├── context.cpp │ │ └── native_api.cpp │ ├── elf/ │ │ ├── elf_image.cpp │ │ └── symbol_cache.cpp │ └── jni/ │ ├── dex_parser_bridge.cpp │ ├── hook_bridge.cpp │ ├── native_api_bridge.cpp │ └── resources_hook.cpp ├── services/ │ ├── daemon-service/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── aidl/ │ │ │ └── org/ │ │ │ └── lsposed/ │ │ │ └── lspd/ │ │ │ ├── models/ │ │ │ │ ├── Module.aidl │ │ │ │ └── PreLoadedApk.aidl │ │ │ └── service/ │ │ │ ├── ILSPApplicationService.aidl │ │ │ ├── ILSPInjectedModuleService.aidl │ │ │ ├── ILSPSystemServerService.aidl │ │ │ ├── ILSPosedService.aidl │ │ │ └── IRemotePreferenceCallback.aidl │ │ └── java/ │ │ └── org/ │ │ └── lsposed/ │ │ └── lspd/ │ │ └── util/ │ │ └── Utils.java │ └── manager-service/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── aidl/ │ └── org/ │ └── lsposed/ │ └── lspd/ │ ├── ILSPManagerService.aidl │ └── models/ │ ├── Application.aidl │ └── UserInfo.aidl ├── settings.gradle.kts ├── xposed/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── org/ │ └── matrix/ │ └── vector/ │ ├── impl/ │ │ └── utils/ │ │ └── VectorDexParser.kt │ └── nativebridge/ │ ├── DexParserBridge.kt │ ├── HookBridge.kt │ ├── NativeAPI.kt │ └── ResourcesHook.kt └── zygisk/ ├── .gitignore ├── README.md ├── build.gradle.kts ├── module/ │ ├── META-INF/ │ │ └── com/ │ │ └── google/ │ │ └── android/ │ │ ├── update-binary │ │ └── updater-script │ ├── action.sh │ ├── customize.sh │ ├── daemon │ ├── module.prop │ ├── sepolicy.rule │ ├── service.sh │ ├── system.prop │ └── uninstall.sh ├── proguard-rules.pro ├── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── cpp/ │ │ ├── CMakeLists.txt │ │ ├── include/ │ │ │ ├── ipc_bridge.h │ │ │ └── zygisk.hpp │ │ ├── ipc_bridge.cpp │ │ └── module.cpp │ └── kotlin/ │ └── org/ │ └── matrix/ │ └── vector/ │ ├── ParasiticManagerHooker.kt │ ├── ParasiticManagerSystemHooker.kt │ ├── core/ │ │ └── Main.kt │ └── service/ │ ├── BridgeService.kt │ └── ParcelUtils.kt └── zygisk.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Set the default behavior, in case people don't have core.autocrlf set. * text=auto eol=lf # Declare files that will always have CRLF line endings on checkout. *.cmd text eol=crlf *.bat text eol=crlf # Denote all files that are truly binary and should not be modified. *.so binary *.dex binary *.jar binary *.png binary ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report/反馈 Bug description: Report errors or unexpected behavior./反馈错误或异常行为。 labels: [bug] body: - type: markdown attributes: value: | Thanks for reporting issues of LSPosed! To make it easier for us to help you, please read all pinned issues and provide the following details. 感谢给 LSPosed 汇报问题! 为了使我们更好地帮助你,请务必阅读所有已置顶的 Issues 并提供以下细节。 为了防止重复汇报,标题请务必使用英文。 - type: textarea attributes: label: Steps to reproduce/复现步骤 placeholder: | 1. 2. 3. validations: required: true - type: textarea attributes: label: Expected behaviour/预期行为 placeholder: Tell us what should happen/正常情况下应该发生什么 validations: required: true - type: textarea attributes: label: Actual behaviour/实际行为 placeholder: Tell us what happens instead/实际上发生了什么 validations: required: true - type: textarea attributes: label: Xposed Module List/Xposed 模块列表 render: shell validations: required: true - type: input attributes: label: Root implementation/Root 方案 description: Common root implementations include Magisk, KernelSU or APatch. Please specify your current implementation with exact version./常见的 Root 方案有 Magisk,KernelSU 以及 APatch。请注明您当前的方案以及详细版本。 validations: required: true - type: textarea attributes: label: System Module List/系统模块列表 description: Modules installed through your root implementation manager/通过您 Root 方案的管理器所安装的模块 render: shell validations: required: true - type: input attributes: label: LSPosed version/LSPosed 版本 description: Don't use 'latest'. Specify actual version with 4 digits, otherwise your issue will be closed./不要填用“最新版”。给出四位版本号,不然 issue 会被关闭。 validations: required: true - type: input attributes: label: Android version/Android 版本 description: If you are running a custom OS, please also note it./如果使用了非官方的操作系统,请一并注明。 validations: required: true - type: checkboxes id: latest attributes: label: Version requirement/版本要求 options: - label: I am using the latest debug build from [GitHub Actions](https://github.com/JingMatrix/LSPosed/actions?query=branch%3Amaster)./我正在使用 [GitHub Actions](https://github.com/JingMatrix/LSPosed/actions?query=branch%3Amaster) 中最新的调试版本。 required: true - type: textarea attributes: label: Logs/日志 description: For usage issues, please provide the log zip saved from manager; for activation issues, please provide [bugreport](https://developer.android.com/studio/debug/bug-report). Without logs zip, the issue will be closed. /使用问题请提供从管理器保存的日志压缩包;激活问题请提供 [bugreport](https://developer.android.google.cn/studio/debug/bug-report?hl=zh-cn) 日志。没有日志附件的问题会被关闭。 placeholder: Upload logs zip by clicking the bar on the bottom. Uploading logs to other websites or using external links is prohibited. /点击文本框底栏上传日志压缩包,禁止上传到其它网站或使用外链提供日志。 validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask a question/提问 url: https://github.com/JingMatrix/LSPosed/discussions/new?category=Q-A about: Please ask and answer questions here./如果有任何疑问请在这里提问 ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ --- name: Feature request/新特性请求 description: Suggest an idea./提出建议 labels: [enhancement] body: - type: textarea attributes: label: Is your feature request related to a problem?/你的请求是否与某个问题相关? placeholder: A clear and concise description of what the problem is./请清晰准确表述该问题。 validations: required: true - type: textarea attributes: label: Describe the solution you'd like/描述你想要的解决方案 placeholder: A clear and concise description of what you want to happen./请清晰准确描述新特性的预期行为 validations: required: true - type: textarea attributes: label: Additional context/其他信息 placeholder: Add any other context or screenshots about the feature request here./其他关于新特性的信息或者截图 validations: required: false ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: monthly groups: actions: patterns: - "*" - package-ecosystem: gitsubmodule directory: / schedule: interval: monthly groups: submodule: patterns: - "*" - package-ecosystem: gradle directory: / schedule: interval: daily groups: maven: patterns: - "*" ================================================ FILE: .github/workflows/core.yml ================================================ name: Core on: workflow_dispatch: push: branches: [ master ] tags: [ v* ] pull_request: merge_group: jobs: build: runs-on: ubuntu-latest env: CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" CCACHE_NOHASHDIR: "true" CCACHE_HARDLINK: "true" CCACHE_BASEDIR: "${{ github.workspace }}" steps: - name: Checkout uses: actions/checkout@v6 with: submodules: recursive fetch-depth: 0 - name: Write key if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/master' ) || github.ref_type == 'tag' }} run: | if [ ! -z "${{ secrets.KEY_STORE }}" ]; then echo androidStorePassword='${{ secrets.KEY_STORE_PASSWORD }}' >> gradle.properties echo androidKeyAlias='${{ secrets.ALIAS }}' >> gradle.properties echo androidKeyPassword='${{ secrets.KEY_PASSWORD }}' >> gradle.properties echo androidStoreFile='key.jks' >> gradle.properties echo ${{ secrets.KEY_STORE }} | base64 --decode > key.jks fi - name: Setup Java uses: actions/setup-java@v5 with: distribution: temurin java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Configure Gradle properties run: | echo 'android.native.buildOutput=verbose' >> ~/.gradle/gradle.properties echo 'org.gradle.parallel=true' >> ~/.gradle/gradle.properties echo 'org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC' >> ~/.gradle/gradle.properties echo 'android.native.buildOutput=verbose' >> ~/.gradle/gradle.properties - name: Setup ninja uses: seanmiddleditch/gha-setup-ninja@v6 with: version: 1.12.1 - name: Setup ccache uses: actions/cache@v5 with: path: | ~/.ccache ${{ github.workspace }}/.ccache key: ${{ runner.os }}-ccache-${{ hashFiles('**/build.gradle') }}-${{ hashFiles('**/CMakeLists.txt') }} restore-keys: | ${{ runner.os }}-ccache- - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Remove Android's cmake shell: bash run: rm -rf $ANDROID_HOME/cmake - name: Build with Gradle run: | ./gradlew zipAll - name: Prepare artifact if: success() id: prepareArtifact run: | zygiskReleaseName=`ls zygisk/release/Vector-v*-Release.zip | awk -F '(/|.zip)' '{print $3}'` && echo "zygiskReleaseName=$zygiskReleaseName" >> $GITHUB_OUTPUT zygiskDebugName=`ls zygisk/release/Vector-v*-Debug.zip | awk -F '(/|.zip)' '{print $3}'` && echo "zygiskDebugName=$zygiskDebugName" >> $GITHUB_OUTPUT unzip zygisk/release/Vector-v*-Release.zip -d Vector-Release unzip zygisk/release/Vector-v*-Debug.zip -d Vector-Debug - name: Upload zygisk release uses: actions/upload-artifact@v6 with: name: ${{ steps.prepareArtifact.outputs.zygiskReleaseName }} path: "./Vector-Release/*" - name: Upload zygisk debug uses: actions/upload-artifact@v6 with: name: ${{ steps.prepareArtifact.outputs.zygiskDebugName }} path: "./Vector-Debug/*" - name: Upload mappings uses: actions/upload-artifact@v6 with: name: mappings path: | zygisk/build/outputs/mapping app/build/outputs/mapping - name: Upload symbols uses: actions/upload-artifact@v6 with: name: symbols path: build/symbols ================================================ FILE: .github/workflows/crowdin.yml ================================================ name: Crowdin Action on: workflow_dispatch: push: branches: [ master ] paths: - app/src/main/res/values/strings.xml - daemon/src/main/res/values/strings.xml jobs: synchronize-with-crowdin: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@main - name: crowdin action uses: crowdin/github-action@master with: upload_translations: true download_translations: false upload_sources: true config: 'crowdin.yml' crowdin_branch_name: master env: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }} ================================================ FILE: .gitignore ================================================ .project .settings .cache *.iml .gradle /local.properties /.idea .DS_Store build /captures bin/ ================================================ FILE: .gitmodules ================================================ [submodule "external/lsplant"] path = external/lsplant url = https://github.com/JingMatrix/LSPlant.git [submodule "external/dobby"] path = external/dobby url = https://github.com/JingMatrix/Dobby.git [submodule "external/fmt"] path = external/fmt url = https://github.com/fmtlib/fmt.git [submodule "external/xz-embedded"] path = external/xz-embedded url = https://github.com/tukaani-project/xz-embedded.git [submodule "external/lsplt"] path = external/lsplt url = https://github.com/JingMatrix/LSPlt [submodule "services/libxposed"] path = services/libxposed url = https://github.com/libxposed/service.git [submodule "xposed/libxposed"] path = xposed/libxposed url = https://github.com/libxposed/api.git [submodule "external/apache/commons-lang"] path = external/apache/commons-lang url = https://github.com/apache/commons-lang.git [submodule "external/axml/manifest-editor"] path = external/axml/manifest-editor url = https://github.com/JingMatrix/ManifestEditor.git ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program 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. This program 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 this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # LSPosed Framework [![Build](https://img.shields.io/github/actions/workflow/status/JingMatrix/LSPosed/core.yml?branch=master&event=push&logo=github&label=Build)](https://github.com/JingMatrix/LSPosed/actions/workflows/core.yml?query=event%3Apush+branch%3Amaster+is%3Acompleted) [![Crowdin](https://img.shields.io/badge/Localization-Crowdin-blueviolet?logo=Crowdin)](https://crowdin.com/project/lsposed_jingmatrix) [![Download](https://img.shields.io/github/v/release/JingMatrix/LSPosed?color=orange&logoColor=orange&label=Download&logo=DocuSign)](https://github.com/JingMatrix/LSPosed/releases/latest) [![Total](https://shields.io/github/downloads/JingMatrix/LSPosed/total?logo=Bookmeter&label=Counts&logoColor=yellow&color=yellow)](https://github.com/JingMatrix/LSPosed/releases) ## Introduction A Zygisk module trying to provide an ART hooking framework which delivers consistent APIs with the OG Xposed, leveraging LSPlant hooking framework. > Xposed is a framework for modules that can change the behavior of the system and apps without touching any APKs. That's great because it means that modules can work for different versions and even ROMs without any changes (as long as the original code was not changed too much). It's also easy to undo. As all changes are done in the memory, you just need to deactivate the module and reboot to get your original system back. There are many other advantages, but here is just one more: multiple modules can do changes to the same part of the system or app. With modified APKs, you have to choose one. No way to combine them, unless the author builds multiple APKs with different combinations. ## Supported Versions Android 8.1 ~ 16 ## Install 1. Install Magisk v26+ 2. [Download](#download) and install LSPosed in Magisk app 3. Reboot 4. Open LSPosed manager from notification 5. Have fun :) ## Download - For stable releases, please go to [Github Releases page](https://github.com/JingMatrix/LSPosed/releases) - For canary build, please check [Github Actions](https://github.com/JingMatrix/LSPosed/actions/workflows/core.yml?query=branch%3Amaster) Note: debug builds are only available in Github Actions. ## Get Help **Only bug reports from **THE LATEST DEBUG BUILD** will be accepted.** - GitHub issues: [Issues](https://github.com/JingMatrix/LSPosed/issues/) - (For Chinese speakers) 本项目只接受英语**标题**的issue。如果您不懂英语,请使用[翻译工具](https://www.deepl.com/zh/translator) ## For Developers Developers are welcome to write Xposed modules with hooks based on LSPosed Framework. A module based on LSPosed framework is fully compatible with the original Xposed Framework, and vice versa, a Xposed Framework-based module will work well with LSPosed framework too. - [Xposed Framework API](https://api.xposed.info/) We use our own module repository. We welcome developers to submit modules to our repository, and then modules can be downloaded in LSPosed. - [LSPosed Module Repository](https://github.com/Xposed-Modules-Repo) ## Community Discussion [Troubleshooting guide](https://github.com/JingMatrix/LSPosed/issues/123) and [Disscusions](https://github.com/JingMatrix/LSPosed/discussions). ## Translation Contributing You can contribute translation [here](https://crowdin.com/project/lsposed_jingmatrix). ## Credits - [Magisk](https://github.com/topjohnwu/Magisk/): makes all these possible - [XposedBridge](https://github.com/rovo89/XposedBridge): the OG Xposed framework APIs - [LSPlant](https://github.com/JingMatrix/LSPlant): the core ART hooking framework - [Dobby](https://github.com/JingMatrix/Dobby): inline hooker for `LSPlant` and `native_api` implement - [EdXposed](https://github.com/ElderDrivers/EdXposed): fork source - [xz-embedded](https://github.com/tukaani-project/xz-embedded): decompress `.gnu_debugdata` header section of stripped `libart.so` - ~~[Riru](https://github.com/RikkaApps/Riru): provides a way to inject code into zygote process~~ - ~[SandHook](https://github.com/ganyao114/SandHook/): ART hooking framework for SandHook variant~ - ~[YAHFA](https://github.com/rk700/YAHFA): previous ART hooking framework~ - ~[dexmaker](https://github.com/linkedin/dexmaker) and [dalvikdx](https://github.com/JakeWharton/dalvik-dx): to dynamically generate YAHFA hooker classes~ - ~[DexBuilder](https://github.com/LSPosed/DexBuilder): to dynamically generate YAHFA hooker classes~ ## License LSPosed is licensed under the **GNU General Public License v3 (GPL-3)** (http://www.gnu.org/copyleft/gpl.html). ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle.kts ================================================ /* * 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 */ import java.time.Instant plugins { alias(libs.plugins.agp.app) alias(libs.plugins.nav.safeargs) alias(libs.plugins.autoresconfig) alias(libs.plugins.materialthemebuilder) alias(libs.plugins.lsplugin.resopt) alias(libs.plugins.lsplugin.apksign) } apksign { storeFileProperty = "androidStoreFile" storePasswordProperty = "androidStorePassword" keyAliasProperty = "androidKeyAlias" keyPasswordProperty = "androidKeyPassword" } val defaultManagerPackageName: String by rootProject.extra android { buildFeatures { viewBinding = true buildConfig = true } defaultConfig { applicationId = defaultManagerPackageName buildConfigField("long", "BUILD_TIME", Instant.now().epochSecond.toString()) } packaging { resources { excludes += "META-INF/**" excludes += "okhttp3/**" excludes += "kotlin/**" excludes += "org/**" excludes += "**.properties" excludes += "**.bin" } } dependenciesInfo.includeInApk = false buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles("proguard-rules.pro") } } sourceSets { named("main") { res { srcDirs("src/common/res") } } } namespace = defaultManagerPackageName } autoResConfig { generateClass = true generateRes = false generatedClassFullName = "org.lsposed.manager.util.LangList" generatedArrayFirstItem = "SYSTEM" } materialThemeBuilder { themes { for ((name, color) in listOf( "Red" to "F44336", "Pink" to "E91E63", "Purple" to "9C27B0", "DeepPurple" to "673AB7", "Indigo" to "3F51B5", "Blue" to "2196F3", "LightBlue" to "03A9F4", "Cyan" to "00BCD4", "Teal" to "009688", "Green" to "4FAF50", "LightGreen" to "8BC3A4", "Lime" to "CDDC39", "Yellow" to "FFEB3B", "Amber" to "FFC107", "Orange" to "FF9800", "DeepOrange" to "FF5722", "Brown" to "795548", "BlueGrey" to "607D8F", "Sakura" to "FF9CA8", )) { create("Material$name") { lightThemeFormat = "ThemeOverlay.Light.%s" darkThemeFormat = "ThemeOverlay.Dark.%s" primaryColor = "#$color" } } } // Add Material Design 3 color tokens (such as palettePrimary100) in generated theme // rikka.material:material >= 2.0.0 provides such attributes // Enable this if your are using rikka.material:material generatePalette = true } dependencies { annotationProcessor(libs.glide.compiler) implementation(libs.androidx.activity) implementation(libs.androidx.browser) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.core) implementation(libs.androidx.fragment) implementation(libs.androidx.navigation.fragment) implementation(libs.androidx.navigation.ui) implementation(libs.androidx.preference) implementation(libs.androidx.recyclerview) implementation(libs.androidx.swiperefreshlayout) implementation(libs.glide) implementation(libs.material) implementation(libs.gson) implementation(libs.okhttp) implementation(libs.okhttp.dnsoverhttps) implementation(libs.okhttp.logging.interceptor) implementation(libs.rikkax.appcompat) implementation(libs.rikkax.core) implementation(libs.rikkax.insets) implementation(libs.rikkax.material) implementation(libs.rikkax.material.preference) implementation(libs.rikkax.recyclerview) implementation(libs.rikkax.widget.borderview) implementation(libs.rikkax.widget.mainswitchbar) implementation(libs.rikkax.layoutinflater) implementation(libs.appiconloader) implementation(libs.hiddenapibypass) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.core) implementation(projects.services.managerService) } configurations.all { exclude("org.jetbrains", "annotations") exclude("androidx.appcompat", "appcompat") exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7") exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") } ================================================ FILE: app/proguard-rules.pro ================================================ -keep class org.lsposed.manager.Constants { public static boolean setBinder(android.os.IBinder); } -assumenosideeffects class kotlin.jvm.internal.Intrinsics { public static void check*(...); public static void throw*(...); } -assumenosideeffects class android.util.Log { public static *** v(...); public static *** d(...); } -keepclasseswithmembers,allowobfuscation class * { @com.google.gson.annotations.SerializedName ; } -repackageclasses -allowaccessmodification -overloadaggressively # Gson uses generic type information stored in a class file when working with fields. Proguard # removes such information by default, so configure it to keep all of it. -keepattributes Signature,InnerClasses,EnclosingMethod -dontwarn org.jetbrains.annotations.NotNull -dontwarn org.jetbrains.annotations.Nullable -dontwarn org.bouncycastle.jsse.BCSSLParameters -dontwarn org.bouncycastle.jsse.BCSSLSocket -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider -dontwarn org.conscrypt.Conscrypt* -dontwarn org.conscrypt.ConscryptHostnameVerifier -dontwarn org.openjsse.javax.net.ssl.SSLParameters -dontwarn org.openjsse.javax.net.ssl.SSLSocket -dontwarn org.openjsse.net.ssl.OpenJSSE -keepclassmembers class * implements android.os.Parcelable { public static final ** CREATOR; } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/assets/webview/colors_dark.css ================================================ /* Primer Colors */ /* Please also update colors_light.css with light mode appropriate colors when modifying this file. */ :root { --blue-900: #082a52; --blue-800: #063366; --blue-700: #0b498f; --blue-600: #0c65c9; --blue-500: #0d6edb; --blue-400: #2e8fff; --blue-300: #85beff; --blue-200: #d1e6ff; --blue-100: #e5f2ff; --blue-000: #f5faff; --gray-1000: #050505; --gray-950: #0b0b0d; --gray-900: #17181a; --gray-850: #242528; --gray-800: #2e2f37; --gray-750: #383a42; --gray-700: #41434e; --gray-650: #4b4d58; --gray-600: #525560; --gray-550: #5e616e; --gray-500: #6c6f7e; --gray-450: #787c8c; --gray-400: #9194a1; --gray-350: #a9abb6; --gray-300: #bfc1c9; --gray-250: #d6d7dc; --gray-200: #e3e4e8; --gray-150: #eff0f5; --gray-100: #f7f7f9; --gray-050: #fbfbfc; --gray-000: #ffffff; --green-900: #184d25; --green-800: #1b612b; --green-700: #1e7533; --green-600: #2b8f43; --green-500: #32b24f; --green-400: #40d663; --green-300: #95f0ab; --green-200: #cbf7d5; --green-100: #e5ffeb; --green-000: #f5fff8; --yellow-900: #755f13; --yellow-800: #b89007; --yellow-700: #e0b112; --yellow-600: #ffcb1a; --yellow-500: #ffd74d; --yellow-400: #ffe166; --yellow-300: #ffec8a; --yellow-200: #fff6b8; --yellow-100: #fffce5; --yellow-000: #fffef5; --orange-900: #a84603; --orange-800: #c75204; --orange-700: #d65c09; --orange-600: #eb680e; --orange-500: #fa6f0f; --orange-400: #ff8a38; --orange-300: #ffae75; --orange-200: #ffd5b2; --orange-100: #ffeee0; --orange-000: #fffaf5; --red-900: #8f1d22; --red-800: #a82229; --red-700: #bd222d; --red-600: #d62b38; --red-500: #e04352; --red-400: #f55363; --red-300: #ff808d; --red-200: #ffb2bb; --red-100: #ffe0e4; --red-000: #fff0f2; --pink-900: #702653; --pink-800: #9e3674; --pink-700: #c2428e; --pink-600: #d63c99; --pink-500: #f051b0; --pink-400: #f576c2; --pink-300: #fa9bd4; --pink-200: #ffbde4; --pink-100: #fee0f2; --pink-000: #fff0f9; --purple-900: #2e1757; --purple-800: #3f2175; --purple-700: #522e8f; --purple-600: #6139a8; --purple-500: #7548c7; --purple-400: #916bd6; --purple-300: #b899f0; --purple-200: #dac7ff; --purple-100: #eae0ff; --purple-000: #f6f2ff; --textPrimary: var(--gray-050); --textSecondary: var(--gray-300); --textTertiary: var(--gray-400); --textPlaceholder: rgba(145, 148, 161, 0.5); --link: var(--blue-400); --appBackground: var(--gray-1000); --backgroundSecondary: var(--gray-900); --backgroundTertiary: var(--gray-850); --border: rgba(191, 193, 201, 0.16); --borderOpaque: var(--gray-700); --iconPrimary: var(--gray-300); --iconSecondary: var(--gray-500); --inputBackground: rgba(191, 193, 201, 0.12); --backgroundPrimary: var(--gray-1000); --backgroundElevatedPrimary: var(--gray-900); --backgroundElevatedSecondary: rgba(191, 193, 201, 0.04); --backgroundElevatedTertiary: rgba(191, 193, 201, 0.08); --backgroundInset: var(--gray-900); --link-hover: var(--gray-800); --color-icon-success: var(--green-600); --color-text-danger: var(--red-600); --diffLineNumberAdditionBackground: #08260f; --diffLineNumberAdditionText: #95f0ab; --diffLineAdditionBackground: #061c0b; --diffLineNumberDeletionBackground: #3b0507; --diffLineNumberDeletionText: #ff808d; --diffLineDeletionBackground: #300406; --suggestedChangeDeletionText: #ffffff; --suggestedChangeAdditionText: #ffffff; --suggestedChangeDeletionBackground: rgba(218,54,51,0.6); --suggestedChangeAdditionBackground: rgba(46,160,67,0.6); --videoBackground: #000000; } /* Custom Base Styles */ :root { --link-highlight: rgba(46, 143, 255, 0.08); --pre-background: var(--backgroundElevatedTertiary); --code-background: var(--pre-background); --hr-background: var(--borderOpaque); --thead-background: var(--pre-background); --thead-border: var(--hr-background); --tr-border: var(--gray-300); --tr-alt-background: var(--pre-background); --kbd-background: var(--pre-background); --kbd-color: var(--gray-350); --kbd-border: var(--gray-750); --blockquote-color: var(--gray-400); --blockquote-border: var(--hr-background); --heading-color: var(--gray-100); --h6-color: var(--gray-500); --frame-border: var(--hr-background); --frame-color: var(--gray-200); --mention-color: var(--textPrimary); --email-toggle-color: var(--kbd-color); --email-toggle-background: var(--blockquote-border); --email-quoted-color: var(--blockquote-color); --keyword-color: var(--gray-600); --code-font: ui-monospace, Menlo, monospace; } ================================================ FILE: app/src/main/assets/webview/colors_light.css ================================================ /* Primer Colors */ /* Please also update colors_dark.css with dark mode appropriate colors when modifying this file. */ :root { --blue-900: #05264c; --blue-800: #032f62; --blue-700: #044289; --blue-600: #005cc5; --blue-500: #0366d6; --blue-400: #2188ff; --blue-300: #79b8ff; --blue-200: #c8e1ff; --blue-100: #dbedff; --blue-000: #f1f8ff; --gray-1000: #050505; --gray-950: #0b0b0d; --gray-900: #17181a; --gray-850: #242528; --gray-800: #2f3037; --gray-750: #383a42; --gray-700: #41434e; --gray-650: #4b4d58; --gray-600: #525560; --gray-550: #5e616e; --gray-500: #6a6d7c; --gray-450: #787c8c; --gray-400: #9194a1; --gray-350: #a9abb6; --gray-300: #bfc1c9; --gray-250: #d6d7dc; --gray-200: #e3e4e8; --gray-150: #eff0f5; --gray-100: #f7f7f9; --gray-050: #fbfbfc; --gray-000: #ffffff; --green-900: #144620; --green-800: #165c26; --green-700: #176f2c; --green-600: #22863a; --green-500: #28a745; --green-400: #34d058; --green-300: #85e89d; --green-200: #bef5cb; --green-100: #dcffe4; --green-000: #f0fff4; --yellow-900: #735c0f; --yellow-800: #b08800; --yellow-700: #dbab09; --yellow-600: #f9c513; --yellow-500: #ffd33d; --yellow-400: #ffdf5d; --yellow-300: #ffea7f; --yellow-200: #fff5b1; --yellow-100: #fffbdd; --yellow-000: #fffdef; --orange-900: #a04100; --orange-800: #c24e00; --orange-700: #d15704; --orange-600: #e36209; --orange-500: #f66a0a; --orange-400: #fb8532; --orange-300: #ffab70; --orange-200: #ffd1ac; --orange-100: #ffebda; --orange-000: #fff8f2; --red-900: #86181d; --red-800: #9e1c23; --red-700: #b31d28; --red-600: #cb2431; --red-500: #d73a49; --red-400: #ea4a5a; --red-300: #f97583; --red-200: #fdaeb7; --red-100: #ffdce0; --red-000: #ffeef0; --pink-900: #6d224f; --pink-800: #99306f; --pink-700: #b93a86; --pink-600: #d03592; --pink-500: #ea4aaa; --pink-400: #ec6cb9; --pink-300: #f692ce; --pink-200: #f9b3dd; --pink-100: #fedbf0; --pink-000: #ffeef8; --purple-900: #29134e; --purple-800: #3a1d6e; --purple-700: #4c2888; --purple-600: #5a32a3; --purple-500: #6f42c1; --purple-400: #8a63d2; --purple-300: #b392f0; --purple-200: #d1bcf9; --purple-100: #e6dcfd; --purple-000: #f5f0ff; --textPrimary: var(--gray-1000); --textSecondary: var(--gray-700); --textTertiary: var(--gray-500); --textPlaceholder: rgba(82, 85, 96, 0.5); --link: var(--blue-500); --appBackground: var(--gray-000); --backgroundSecondary: var(--gray-000); --backgroundTertiary: var(--gray-000); --border: rgba(65, 67, 78, 0.25); --borderOpaque: var(--gray-300); --iconPrimary: var(--gray-600); --iconSecondary: var(--gray-400); --inputBackground: rgba(65, 67, 78, 0.12); --backgroundPrimary: var(--gray-150); --backgroundElevatedPrimary: var(--gray-150); --backgroundElevatedSecondary: var(--gray-000); --backgroundElevatedTertiary: var(--gray-000); --backgroundInset: var(--gray-200); --link-hover: var(--gray-200); --color-icon-success: var(--green-600); --color-text-danger: var(--red-600); --diffLineNumberAdditionBackground: #dcffe4; --diffLineNumberAdditionText: #22863a; --diffLineAdditionBackground: #f0fff4; --diffLineNumberDeletionBackground: #ffdce0; --diffLineNumberDeletionText: #cb2431; --diffLineDeletionBackground: #ffeef0; --suggestedChangeDeletionText: #ffffff; --suggestedChangeAdditionText: #ffffff; --suggestedChangeDeletionBackground: rgba(218,54,51,0.6); --suggestedChangeAdditionBackground: rgba(46,160,67,0.6); --videoBackground: #000000; } /* Custom Base Styles */ :root { --link-highlight: rgba(3, 102, 214, 0.08); --pre-background: var(--gray-100); --code-background: var(--pre-background); --hr-background: var(--gray-200); --thead-background: var(--pre-background); --thead-border: var(--hr-background); --tr-border: var(--gray-300); --tr-alt-background: var(--pre-background); --kbd-background: var(--pre-background); --kbd-color: var(--gray-650); --kbd-border: var(--gray-250); --blockquote-color: var(--gray-500); --blockquote-border: var(--hr-background); --heading-color: var(--gray-900); --h6-color: var(--gray-500); --frame-border: var(--hr-background); --frame-color: var(--gray-850); --mention-color: var(--gray-850); --email-toggle-color: var(--kbd-color); --email-toggle-background: var(--blockquote-border); --email-quoted-color: var(--blockquote-color); --keyword-color: var(--gray-400); --code-font: ui-monospace, Menlo, monospace; } ================================================ FILE: app/src/main/assets/webview/markdown.css ================================================ /* Shared styles between light & dark mode so all colors should be variables */ * { box-sizing: border-box; } input:disabled { touch-action: none; } html { -webkit-text-size-adjust: none; text-size-adjust: none; font: -apple-system-body; } body { color: var(--textPrimary); background-color: var(--background); } a { color: var(--link); text-decoration: none; -webkit-tap-highlight-color: var(--link-highlight); word-break: break-word; } a:not([target]):hover { border-radius: 5px; background-color: var(--link-hover); transition-duration: 0.2s; transform: scale(1.015); } /* Web views hold on to their hover event if the app is backgrounded. We need to disable custom hover effects by setting a class on body and overriding them in CSS when we apply this workaround. When the mouse enters the web view again, we can disable our override. */ body.hover-override a:not([target]) { background-color: transparent; transform: scale(1); } details summary { outline: 0; } table { border-spacing: 0; border-collapse: collapse; } blockquote { margin: 0; } table, table *, pre { touch-action: pan-x; } .markdown-body ul.contains-task-list { list-style: none; padding-left: 0; } .task-list-item { padding-left: 40px; margin-left: -16px; } .task-list-item-checkbox { margin-left: -24px } pre, code, kbd { font-size: 1em; font-family: var(--code-font); } .issue-keyword { border-bottom: 1px dotted var(--keyword-color); } .team-mention, .user-mention { font-weight: 600; color: var(--mention-color); white-space: nowrap; } .email-hidden-toggle, .email-hidden-reply { display: none; } /* Fix checkboxes looking cut off when they render larger than the default size */ input[type="checkbox"] { transform: translate(0px); } /* --- */ .markdown-body { font-size: inherit; line-height: 1.5; word-wrap: break-word; } .markdown-body kbd { display: inline-block; padding: 0.18em 0.31em; font-size: 0.7em; line-height: 1.2em; color: var(--kbd-color); vertical-align: middle; background-color: var(--kbd-background); border: 1px solid var(--kbd-border); border-radius: 0.25em; box-shadow: inset 0 -1px 0 var(--kbd-border); margin-right: 2px; } .markdown-body:after, .markdown-body:before { display: table; content: "" } .markdown-body:after { clear: both; } .markdown-body > :first-child { margin-top: 0 !important; } .markdown-body > :last-child { margin-bottom: 0 !important; } .markdown-body a:not([href]) { color: inherit; text-decoration: none; } .markdown-body .absent { color: var(--red-600); } .markdown-body .anchor { float: left; padding-right: 4px; margin-left: -20px; line-height: 1; } .markdown-body .anchor:focus { outline: none; } .markdown-body blockquote, .markdown-body details, .markdown-body dl, .markdown-body ol, .markdown-body p, .markdown-body pre, .markdown-body table, .markdown-body ul { margin-top: 0; margin-bottom: 16px; } .markdown-body hr { height: .25em; padding: 0; margin: 24px 0; background-color: var(--hr-background); border: 0; } .markdown-body blockquote { padding-left: 1em; color: var(--blockquote-color); position: relative; } .markdown-body blockquote::before { content: ''; width: 2px; position: absolute; top: 0; bottom: 0; left: 0; background-color: var(--blockquote-border); border-radius: 2px; } .markdown-body blockquote > :first-child { margin-top: 0; } .markdown-body blockquote > :last-child { margin-bottom: 0; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: 24px; margin-bottom: 16px; font-weight: 600; line-height: 1.25; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: var(--heading-color); vertical-align: middle; visibility: hidden; } .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { text-decoration: none; } .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { visibility: visible; } .markdown-body h1 code, .markdown-body h1 tt, .markdown-body h2 code, .markdown-body h2 tt, .markdown-body h3 code, .markdown-body h3 tt, .markdown-body h4 code, .markdown-body h4 tt, .markdown-body h5 code, .markdown-body h5 tt, .markdown-body h6 code, .markdown-body h6 tt { font-size: inherit; } .markdown-body h1 { font-size: 2em; } .markdown-body h1, .markdown-body h2 { padding-bottom: .3em; border-bottom: 1px solid var(--border); } .markdown-body h2 { font-size: 1.5em; } .markdown-body h3 { font-size: 1.25em; } .markdown-body h4 { font-size: 1em; } .markdown-body h5 { font-size: .875em; } .markdown-body h6 { font-size: .85em; color: var(--h6-color); } .markdown-body ul { padding-left: 1.5em; } .markdown-body ol.no-list, .markdown-body ul.no-list { padding: 0; list-style-type: none; } .markdown-body ol ol, .markdown-body ol ul, .markdown-body ul ol, .markdown-body ul ul { margin-top: 0; margin-bottom: 0; } .markdown-body li { word-wrap: break-all; } .markdown-body li > p { margin-top: 16px; } .markdown-body li + li { margin-top: .25em; } .markdown-body dl { padding: 0; } .markdown-body dl dt { padding: 0; margin-top: 16px; font-size: 1em; font-style: italic; font-weight: 600; } .markdown-body dl dd { padding: 0 16px; margin-bottom: 16px; } .markdown-body table { display: block; width: 100%; overflow: auto; } .markdown-body table th { font-weight: 600; } .markdown-body table td, .markdown-body table th { padding: 6px 13px; border: 1px solid var(--thead-border); } .markdown-body table tr { background-color: var(--background); border-top: 1px solid var(--tr-border); } .markdown-body table tr:nth-child(2n) { background-color: var(--tr-alt-background); } .markdown-body table img { background-color: initial; } .markdown-body img { max-width: 100%; box-sizing: initial; background-color: var(--background); } .markdown-body img[align=right] { padding-left: 20px; } .markdown-body img[align=left] { padding-right: 20px; } .markdown-body video { max-width: 100%; box-sizing: initial; background-color: var(--videoBackground); } .markdown-body .emoji { max-width: none; vertical-align: text-top; background-color: initial; } .markdown-body span.frame { display: block; overflow: hidden; } .markdown-body span.frame > span { display: block; float: left; width: auto; padding: 7px; margin: 13px 0 0; overflow: hidden; border: 1px solid var(--frame-border); } .markdown-body span.frame span img { display: block; float: left; } .markdown-body span.frame span span { display: block; padding: 5px 0 0; clear: both; color: var(--frame-color); } .markdown-body span.align-center { display: block; overflow: hidden; clear: both; } .markdown-body span.align-center > span { display: block; margin: 13px auto 0; overflow: hidden; text-align: center; } .markdown-body span.align-center span img { margin: 0 auto; text-align: center; } .markdown-body span.align-right { display: block; overflow: hidden; clear: both; } .markdown-body span.align-right > span { display: block; margin: 13px 0 0; overflow: hidden; text-align: right; } .markdown-body span.align-right span img { margin: 0; text-align: right; } .markdown-body span.float-left { display: block; float: left; margin-right: 13px; overflow: hidden; } .markdown-body span.float-left span { margin: 13px 0 0; } .markdown-body span.float-right { display: block; float: right; margin-left: 13px; overflow: hidden; } .markdown-body span.float-right > span { display: block; margin: 13px auto 0; overflow: hidden; text-align: right; } .markdown-body code, .markdown-body tt { padding: .2em .4em; margin: 0; font-size: 85%; background-color: var(--code-background); border-radius: 6px; } .markdown-body code br, .markdown-body tt br { display: none; } .markdown-body del code { text-decoration: inherit; } .markdown-body pre { word-wrap: normal; } .markdown-body pre > code { padding: 0; margin: 0; font-size: 100%; word-break: normal; white-space: pre; background: transparent; border: 0; } .markdown-body .highlight { margin-bottom: 16px; } .markdown-body .highlight pre { margin-bottom: 0; word-break: normal; } .markdown-body .highlight pre, .markdown-body pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: var(--pre-background); border-radius: 6px; } .markdown-body pre code, .markdown-body pre tt { display: inline; max-width: auto; padding: 0; margin: 0; overflow: visible; line-height: inherit; word-wrap: normal; background-color: initial; border: 0; } .markdown-body .csv-data td, .markdown-body .csv-data th { padding: 5px; overflow: hidden; font-size: 12px; line-height: 1; text-align: left; white-space: nowrap; } .markdown-body .csv-data .blob-num { padding: 10px 8px 9px; text-align: right; background: var(--background); border: 0; } .markdown-body .csv-data tr { border-top: 0; } .markdown-body .csv-data th { font-weight: 600; background: var(--thead-background); border-top: 0; } .open.octicon, .draft.octicon, .closed.octicon, .merged.octicon, .color-text-secondary.octicon { display: inline-block; margin-top: 0.15em; vertical-align: text-top; fill: currentColor; width: 1em; height: 1em; font: -apple-system-body; } .open.octicon { color: var(--color-icon-success); } .draft.octicon { color: var(--textTertiary); } .closed.octicon { color: var(--color-text-danger); } .merged.octicon { color: var(--purple-500); } .color-text-secondary.octicon { color: var(--textSecondary); } .reference { white-space: nowrap; } .issue-link { font-weight: 600; color: var(--mention-color); white-space: normal; } .issue-shorthand { font-weight: 400; color: var(--textTertiary); } .mr-1 { margin-right: 4px; } .ml-1 { margin-left: 4px; } .d-inline-block { display: inline-block; } .v-align-middle { vertical-align: middle; } .Box { border-radius: 6px; } ================================================ FILE: app/src/main/assets/webview/syntax.css ================================================ /* From https://github.com/primer/github-syntax-light/blob/master/lib/github-light.css */ .pl-c /* comment, punctuation.definition.comment, string.comment */ { color: #6a737d; } .pl-c1 /* constant, entity.name.constant, variable.other.constant, variable.language, support, meta.property-name, support.constant, support.variable, meta.module-reference, markup.raw, meta.diff.header, meta.output */, .pl-s .pl-v /* string variable */ { color: #005cc5; } .pl-e /* entity */, .pl-en /* entity.name */ { color: #6f42c1; } .pl-smi /* variable.parameter.function, storage.modifier.package, storage.modifier.import, storage.type.java, variable.other */, .pl-s .pl-s1 /* string source */ { color: #24292e; } .pl-ent /* entity.name.tag, markup.quote */ { color: #22863a; } .pl-k /* keyword, storage, storage.type */ { color: #d73a49; } .pl-s /* string */, .pl-pds /* punctuation.definition.string, source.regexp, string.regexp.character-class */, .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, .pl-sr /* string.regexp */, .pl-sr .pl-cce /* string.regexp constant.character.escape */, .pl-sr .pl-sre /* string.regexp source.ruby.embedded */, .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */ { color: #032f62; } .pl-v /* variable */, .pl-smw /* sublimelinter.mark.warning */ { color: #e36209; } .pl-bu /* invalid.broken, invalid.deprecated, invalid.unimplemented, message.error, brackethighlighter.unmatched, sublimelinter.mark.error */ { color: #b31d28; } .pl-ii /* invalid.illegal */ { color: #fafbfc; background-color: #b31d28; } .pl-c2 /* carriage-return */ { color: #fafbfc; background-color: #d73a49; } .pl-c2::before /* carriage-return */ { content: "^M"; } .pl-sr .pl-cce /* string.regexp constant.character.escape */ { font-weight: bold; color: #22863a; } .pl-ml /* markup.list */ { color: #735c0f; } .pl-mh /* markup.heading */, .pl-mh .pl-en /* markup.heading entity.name */, .pl-ms /* meta.separator */ { font-weight: bold; color: #005cc5; } .pl-mi /* markup.italic */ { font-style: italic; color: #24292e; } .pl-mb /* markup.bold */ { font-weight: bold; color: #24292e; } .pl-md /* markup.deleted, meta.diff.header.from-file, punctuation.definition.deleted */ { color: #b31d28; background-color: #ffeef0; } .pl-mi1 /* markup.inserted, meta.diff.header.to-file, punctuation.definition.inserted */ { color: #22863a; background-color: #f0fff4; } .pl-mc /* markup.changed, punctuation.definition.changed */ { color: #e36209; background-color: #ffebda; } .pl-mi2 /* markup.ignored, markup.untracked */ { color: #f6f8fa; background-color: #005cc5; } .pl-mdr /* meta.diff.range */ { font-weight: bold; color: #6f42c1; } .pl-ba /* brackethighlighter.tag, brackethighlighter.curly, brackethighlighter.round, brackethighlighter.square, brackethighlighter.angle, brackethighlighter.quote */ { color: #586069; } .pl-sg /* sublimelinter.gutter-mark */ { color: #959da5; } .pl-corl /* constant.other.reference.link, string.other.link */ { text-decoration: underline; color: #032f62; } ================================================ FILE: app/src/main/assets/webview/syntax_dark.css ================================================ /* From https://github.com/primer/github-syntax-dark/blob/master/lib/github-dark.css */ .pl-c /* comment, punctuation.definition.comment, string.comment */ { color: #959da5; } .pl-c1 /* constant, entity.name.constant, variable.other.constant, variable.language, support, meta.property-name, support.constant, support.variable, meta.module-reference, markup.quote, markup.raw, meta.diff.header */, .pl-s .pl-v /* string variable */ { color: #c8e1ff; } .pl-e /* entity */, .pl-en /* entity.name */ { color: #b392f0; } .pl-smi /* variable.parameter.function, storage.modifier.package, storage.modifier.import, storage.type.java, variable.other */, .pl-s .pl-s1 /* string source */ { color: #f6f8fa; } .pl-ent /* entity.name.tag */ { color: #7bcc72; } .pl-k /* keyword, storage, storage.type */ { color: #ea4a5a; } .pl-s /* string */, .pl-pds /* punctuation.definition.string, source.regexp, string.regexp.character-class */, .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, .pl-sr /* string.regexp */, .pl-sr .pl-cce /* string.regexp constant.character.escape */, .pl-sr .pl-sre /* string.regexp source.ruby.embedded */, .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */ { color: #79b8ff; } .pl-v /* variable */, .pl-ml /* markup.list, sublimelinter.mark.warning */ { color: #fb8532; } .pl-bu /* invalid.broken, invalid.deprecated, invalid.unimplemented, message.error, brackethighlighter.unmatched, sublimelinter.mark.error */ { color: #d73a49; } .pl-ii /* invalid.illegal */ { color: #fafbfc; background-color: #d73a49; } .pl-c2 /* carriage-return */ { color: #fafbfc; background-color: #d73a49; } .pl-c2::before /* carriage-return */ { content: "^M"; } .pl-sr .pl-cce /* string.regexp constant.character.escape */ { font-weight: bold; color: #7bcc72; } .pl-mh /* markup.heading */, .pl-mh .pl-en /* markup.heading entity.name */, .pl-ms /* meta.separator */ { font-weight: bold; color: #0366d6; } .pl-mi /* markup.italic */ { font-style: italic; color: #f6f8fa; } .pl-mb /* markup.bold */ { font-weight: bold; color: #f6f8fa; } .pl-md /* markup.deleted, meta.diff.header.from-file, punctuation.definition.deleted */ { color: #ffdcd7; background-color: #67060c; } .pl-mi1 /* markup.inserted, meta.diff.header.to-file, punctuation.definition.inserted */ { color: #aff5b4; background-color: #033a16; } .pl-mc /* markup.changed, punctuation.definition.changed */ { color: #b08800; background-color: #fffdef; } .pl-mi2 /* markup.ignored, markup.untracked */ { color: #2f363d; background-color: #959da5; } .pl-mdr /* meta.diff.range */ { font-weight: bold; color: #b392f0; } .pl-mo /* meta.output */ { color: #0366d6; } .pl-ba /* brackethighlighter.tag, brackethighlighter.curly, brackethighlighter.round, brackethighlighter.square, brackethighlighter.angle, brackethighlighter.quote */ { color: #ffeef0; } .pl-sg /* sublimelinter.gutter-mark */ { color: #6a737d; } .pl-corl /* constant.other.reference.link, string.other.link */ { text-decoration: underline; color: #79b8ff; } ================================================ FILE: app/src/main/assets/webview/template.html ================================================
@body@
================================================ FILE: app/src/main/assets/webview/template_dark.html ================================================
@body@
================================================ FILE: app/src/main/java/com/google/android/material/appbar/SubtitleCollapsingToolbarLayout.java ================================================ package com.google.android.material.appbar; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.FrameLayout; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StyleRes; import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.math.MathUtils; import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.animation.AnimationUtils; import com.google.android.material.internal.DescendantOffsetUtils; import com.google.android.material.internal.SubtitleCollapsingTextHelper; import com.google.android.material.internal.ThemeEnforcement; import org.lsposed.manager.R; /** * @see CollapsingToolbarLayout */ public class SubtitleCollapsingToolbarLayout extends FrameLayout { private static final int DEFAULT_SCRIM_ANIMATION_DURATION = 600; private boolean refreshToolbar = true; private int toolbarId; @Nullable private Toolbar toolbar; @Nullable private View toolbarDirectChild; private View dummyView; private int expandedMarginStart; private int expandedMarginTop; private int expandedMarginEnd; private int expandedMarginBottom; private final Rect tmpRect = new Rect(); @NonNull final SubtitleCollapsingTextHelper collapsingTextHelper; private boolean collapsingTitleEnabled; private boolean drawCollapsingTitle; @Nullable private Drawable contentScrim; @Nullable Drawable statusBarScrim; private int scrimAlpha; private boolean scrimsAreShown; private ValueAnimator scrimAnimator; private long scrimAnimationDuration; private int scrimVisibleHeightTrigger = -1; private AppBarLayout.OnOffsetChangedListener onOffsetChangedListener; int currentOffset; @Nullable WindowInsetsCompat lastInsets; public SubtitleCollapsingToolbarLayout(@NonNull Context context) { this(context, null); } public SubtitleCollapsingToolbarLayout(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SubtitleCollapsingToolbarLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); collapsingTextHelper = new SubtitleCollapsingTextHelper(this); collapsingTextHelper.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR); collapsingTextHelper.setRtlTextDirectionHeuristicsEnabled(false); TypedArray a = ThemeEnforcement.obtainStyledAttributes( context, attrs, R.styleable.SubtitleCollapsingToolbarLayout, defStyleAttr, R.style.Widget_Design_SubtitleCollapsingToolbar); collapsingTextHelper.setExpandedTextGravity(a.getInt( R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleGravity, GravityCompat.START | Gravity.BOTTOM)); collapsingTextHelper.setCollapsedTextGravity(a.getInt( R.styleable.SubtitleCollapsingToolbarLayout_collapsedTitleGravity, GravityCompat.START | Gravity.CENTER_VERTICAL)); expandedMarginStart = expandedMarginTop = expandedMarginEnd = expandedMarginBottom = a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMargin, 0); if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginStart)) { expandedMarginStart = a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginStart, 0); } if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd)) { expandedMarginEnd = a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd, 0); } if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginTop)) { expandedMarginTop = a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginTop, 0); } if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginBottom)) { expandedMarginBottom = a.getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleMarginBottom, 0); } collapsingTitleEnabled = a.getBoolean(R.styleable.SubtitleCollapsingToolbarLayout_titleEnabled, true); setTitle(a.getText(R.styleable.SubtitleCollapsingToolbarLayout_title)); setSubtitle(a.getText(R.styleable.SubtitleCollapsingToolbarLayout_subtitle)); // First load the default text appearances collapsingTextHelper.setExpandedTitleTextAppearance( R.style.TextAppearance_Design_SubtitleCollapsingToolbar_ExpandedTitle); collapsingTextHelper.setCollapsedTitleTextAppearance( androidx.appcompat.R.style.TextAppearance_AppCompat_Widget_ActionBar_Title); collapsingTextHelper.setExpandedSubtitleTextAppearance( R.style.TextAppearance_Design_SubtitleCollapsingToolbar_ExpandedSubtitle); collapsingTextHelper.setCollapsedSubtitleTextAppearance( androidx.appcompat.R.style.TextAppearance_AppCompat_Widget_ActionBar_Subtitle); // Now overlay any custom text appearances if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleTextAppearance)) { collapsingTextHelper.setExpandedTitleTextAppearance( a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_expandedTitleTextAppearance, 0)); } if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_collapsedTitleTextAppearance)) { collapsingTextHelper.setCollapsedTitleTextAppearance( a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_collapsedTitleTextAppearance, 0)); } if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_expandedSubtitleTextAppearance)) { collapsingTextHelper.setExpandedSubtitleTextAppearance( a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_expandedSubtitleTextAppearance, 0)); } if (a.hasValue(R.styleable.SubtitleCollapsingToolbarLayout_collapsedSubtitleTextAppearance)) { collapsingTextHelper.setCollapsedSubtitleTextAppearance( a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_collapsedSubtitleTextAppearance, 0)); } scrimVisibleHeightTrigger = a .getDimensionPixelSize(R.styleable.SubtitleCollapsingToolbarLayout_scrimVisibleHeightTrigger, -1); scrimAnimationDuration = a.getInt( R.styleable.SubtitleCollapsingToolbarLayout_scrimAnimationDuration, DEFAULT_SCRIM_ANIMATION_DURATION); setContentScrim(a.getDrawable(R.styleable.SubtitleCollapsingToolbarLayout_contentScrim)); setStatusBarScrim(a.getDrawable(R.styleable.SubtitleCollapsingToolbarLayout_statusBarScrim)); toolbarId = a.getResourceId(R.styleable.SubtitleCollapsingToolbarLayout_toolbarId, -1); a.recycle(); setWillNotDraw(false); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); // Add an OnOffsetChangedListener if possible final ViewParent parent = getParent(); if (parent instanceof AppBarLayout) { // Copy over from the ABL whether we should fit system windows ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent)); if (onOffsetChangedListener == null) { onOffsetChangedListener = new OffsetUpdateListener(); } ((AppBarLayout) parent).addOnOffsetChangedListener(onOffsetChangedListener); // We're attached, so lets request an inset dispatch ViewCompat.requestApplyInsets(this); } } @Override protected void onDetachedFromWindow() { // Remove our OnOffsetChangedListener if possible and it exists final ViewParent parent = getParent(); if (onOffsetChangedListener != null && parent instanceof AppBarLayout) { ((AppBarLayout) parent).removeOnOffsetChangedListener(onOffsetChangedListener); } super.onDetachedFromWindow(); } @Override public void draw(@NonNull Canvas canvas) { super.draw(canvas); // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below. // Instead, we draw it here, before our collapsing text. ensureToolbar(); if (toolbar == null && contentScrim != null && scrimAlpha > 0) { contentScrim.mutate().setAlpha(scrimAlpha); contentScrim.draw(canvas); } // Let the collapsing text helper draw its text if (collapsingTitleEnabled && drawCollapsingTitle) { collapsingTextHelper.draw(canvas); } // Now draw the status bar scrim if (statusBarScrim != null && scrimAlpha > 0) { final int topInset = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0; if (topInset > 0) { statusBarScrim.setBounds(0, -currentOffset, getWidth(), topInset - currentOffset); statusBarScrim.mutate().setAlpha(scrimAlpha); statusBarScrim.draw(canvas); } } } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present), // but in front of any other children which are behind it. To do this we intercept the // drawChild() call, and draw our scrim just before the Toolbar is drawn boolean invalidated = false; if (contentScrim != null && scrimAlpha > 0 && isToolbarChild(child)) { contentScrim.mutate().setAlpha(scrimAlpha); contentScrim.draw(canvas); invalidated = true; } return super.drawChild(canvas, child, drawingTime) || invalidated; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); if (contentScrim != null) { contentScrim.setBounds(0, 0, w, h); } } private void ensureToolbar() { if (!refreshToolbar) { return; } // First clear out the current Toolbar this.toolbar = null; toolbarDirectChild = null; if (toolbarId != -1) { // If we have an ID set, try and find it and it's direct parent to us this.toolbar = findViewById(toolbarId); if (this.toolbar != null) { toolbarDirectChild = findDirectChild(this.toolbar); } } if (this.toolbar == null) { // If we don't have an ID, or couldn't find a Toolbar with the correct ID, try and find // one from our direct children Toolbar toolbar = null; for (int i = 0, count = getChildCount(); i < count; i++) { final View child = getChildAt(i); if (child instanceof Toolbar) { toolbar = (Toolbar) child; break; } } this.toolbar = toolbar; } updateDummyView(); refreshToolbar = false; } private boolean isToolbarChild(View child) { return (toolbarDirectChild == null || toolbarDirectChild == this) ? child == toolbar : child == toolbarDirectChild; } /** * Returns the direct child of this layout, which itself is the ancestor of the given view. */ @NonNull private View findDirectChild(@NonNull final View descendant) { View directChild = descendant; for (ViewParent p = descendant.getParent(); p != this && p != null; p = p.getParent()) { if (p instanceof View) { directChild = (View) p; } } return directChild; } private void updateDummyView() { if (!collapsingTitleEnabled && dummyView != null) { // If we have a dummy view and we have our title disabled, remove it from its parent final ViewParent parent = dummyView.getParent(); if (parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(dummyView); } } if (collapsingTitleEnabled && toolbar != null) { if (dummyView == null) { dummyView = new View(getContext()); } if (dummyView.getParent() == null) { toolbar.addView(dummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ensureToolbar(); super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int mode = MeasureSpec.getMode(heightMeasureSpec); final int topInset = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0; if (mode == MeasureSpec.UNSPECIFIED && topInset > 0) { // If we have a top inset and we're set to wrap_content height we need to make sure // we add the top inset to our height, therefore we re-measure heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() + topInset, MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } // Set our minimum height to enable proper AppBarLayout collapsing if (toolbar != null) { if (toolbarDirectChild == null || toolbarDirectChild == this) { setMinimumHeight(getHeightWithMargins(toolbar)); } else { setMinimumHeight(getHeightWithMargins(toolbarDirectChild)); } } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (lastInsets != null) { // Shift down any views which are not set to fit system windows final int insetTop = lastInsets.getSystemWindowInsetTop(); for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); if (!ViewCompat.getFitsSystemWindows(child)) { if (child.getTop() < insetTop) { // If the child isn't set to fit system windows but is drawing within // the inset offset it down ViewCompat.offsetTopAndBottom(child, insetTop); } } } } // Update our child view offset helpers so that they track the correct layout coordinates for (int i = 0, z = getChildCount(); i < z; i++) { getViewOffsetHelper(getChildAt(i)).onViewLayout(); } // Update the collapsed bounds by getting its transformed bounds if (collapsingTitleEnabled && dummyView != null) { // We only draw the title if the dummy view is being displayed (Toolbar removes // views if there is no space) drawCollapsingTitle = ViewCompat.isAttachedToWindow(dummyView) && dummyView.getVisibility() == VISIBLE; if (drawCollapsingTitle) { final boolean isRtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; // Update the collapsed bounds final int maxOffset = getMaxOffsetForPinChild(toolbarDirectChild != null ? toolbarDirectChild : toolbar); DescendantOffsetUtils.getDescendantRect(this, dummyView, tmpRect); collapsingTextHelper.setCollapsedBounds( tmpRect.left + (isRtl ? toolbar.getTitleMarginEnd() : toolbar.getTitleMarginStart()), tmpRect.top + maxOffset + toolbar.getTitleMarginTop(), tmpRect.right - (isRtl ? toolbar.getTitleMarginStart() : toolbar.getTitleMarginEnd()), tmpRect.bottom + maxOffset - toolbar.getTitleMarginBottom()); // Update the expanded bounds collapsingTextHelper.setExpandedBounds( isRtl ? expandedMarginEnd : expandedMarginStart, tmpRect.top + expandedMarginTop, right - left - (isRtl ? expandedMarginStart : expandedMarginEnd), bottom - top - expandedMarginBottom); // Now recalculate using the new bounds collapsingTextHelper.recalculate(); } } if (toolbar != null) { if (collapsingTitleEnabled && TextUtils.isEmpty(collapsingTextHelper.getTitle())) { // If we do not currently have a title, try and grab it from the Toolbar setTitle(toolbar.getTitle()); setSubtitle(toolbar.getSubtitle()); } } updateScrimVisibility(); // Apply any view offsets, this should be done at the very end of layout for (int i = 0, z = getChildCount(); i < z; i++) { getViewOffsetHelper(getChildAt(i)).applyOffsets(); } } private static int getHeightWithMargins(@NonNull final View view) { final ViewGroup.LayoutParams lp = view.getLayoutParams(); if (lp instanceof MarginLayoutParams) { final MarginLayoutParams mlp = (MarginLayoutParams) lp; return view.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin; } return view.getMeasuredHeight(); } static ViewOffsetHelper getViewOffsetHelper(View view) { ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(com.google.android.material.R.id.view_offset_helper); if (offsetHelper == null) { offsetHelper = new ViewOffsetHelper(view); view.setTag(com.google.android.material.R.id.view_offset_helper, offsetHelper); } return offsetHelper; } /** * Sets the title to be displayed by this view, if enabled. * * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_title * @see #setTitleEnabled(boolean) * @see #getTitle() */ public void setTitle(@Nullable CharSequence title) { collapsingTextHelper.setTitle(title); updateContentDescriptionFromTitle(); } /** * Returns the title currently being displayed by this view. If the title is not enabled, then * this will return {@code null}. * * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_title */ @Nullable public CharSequence getTitle() { return collapsingTitleEnabled ? collapsingTextHelper.getTitle() : null; } /** * Sets the subtitle to be displayed by this view, if enabled. * * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_subtitle * @see #setTitleEnabled(boolean) * @see #getSubtitle() */ public void setSubtitle(@Nullable CharSequence subtitle) { collapsingTextHelper.setSubtitle(subtitle); updateContentDescriptionFromTitle(); } /** * Returns the subtitle currently being displayed by this view. If the title is not enabled, then * this will return {@code null}. * * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_subtitle */ @Nullable public CharSequence getSubtitle() { return collapsingTitleEnabled ? collapsingTextHelper.getSubtitle() : null; } /** * Sets whether this view should display its own title and subtitle. *

*

The title and subtitle displayed by this view will shrink and grow based on the scroll offset. * * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_titleEnabled * @see #setTitle(CharSequence) * @see #setSubtitle(CharSequence) * @see #isTitleEnabled() */ public void setTitleEnabled(boolean enabled) { if (enabled != collapsingTitleEnabled) { collapsingTitleEnabled = enabled; updateContentDescriptionFromTitle(); updateDummyView(); requestLayout(); } } /** * Returns whether this view is currently displaying its own title and subtitle. * * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_titleEnabled * @see #setTitleEnabled(boolean) */ public boolean isTitleEnabled() { return collapsingTitleEnabled; } /** * Set whether the content scrim and/or status bar scrim should be shown or not. Any change in the * vertical scroll may overwrite this value. Any visibility change will be animated if this view * has already been laid out. * * @param shown whether the scrims should be shown * @see #getStatusBarScrim() * @see #getContentScrim() */ public void setScrimsShown(boolean shown) { setScrimsShown(shown, ViewCompat.isLaidOut(this) && !isInEditMode()); } /** * Set whether the content scrim and/or status bar scrim should be shown or not. Any change in the * vertical scroll may overwrite this value. * * @param shown whether the scrims should be shown * @param animate whether to animate the visibility change * @see #getStatusBarScrim() * @see #getContentScrim() */ public void setScrimsShown(boolean shown, boolean animate) { if (scrimsAreShown != shown) { if (animate) { animateScrim(shown ? 0xFF : 0x0); } else { setScrimAlpha(shown ? 0xFF : 0x0); } scrimsAreShown = shown; } } private void animateScrim(int targetAlpha) { ensureToolbar(); if (scrimAnimator == null) { scrimAnimator = new ValueAnimator(); scrimAnimator.setDuration(scrimAnimationDuration); scrimAnimator.setInterpolator(targetAlpha > scrimAlpha ? AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR : AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR); scrimAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { setScrimAlpha((int) animator.getAnimatedValue()); } }); } else if (scrimAnimator.isRunning()) { scrimAnimator.cancel(); } scrimAnimator.setIntValues(scrimAlpha, targetAlpha); scrimAnimator.start(); } void setScrimAlpha(int alpha) { if (alpha != scrimAlpha) { final Drawable contentScrim = this.contentScrim; if (contentScrim != null && toolbar != null) { ViewCompat.postInvalidateOnAnimation(toolbar); } scrimAlpha = alpha; ViewCompat.postInvalidateOnAnimation(SubtitleCollapsingToolbarLayout.this); } } int getScrimAlpha() { return scrimAlpha; } /** * Set the drawable to use for the content scrim from resources. Providing null will disable the * scrim functionality. * * @param drawable the drawable to display * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_contentScrim * @see #getContentScrim() */ public void setContentScrim(@Nullable Drawable drawable) { if (contentScrim != drawable) { if (contentScrim != null) { contentScrim.setCallback(null); } contentScrim = drawable != null ? drawable.mutate() : null; if (contentScrim != null) { contentScrim.setBounds(0, 0, getWidth(), getHeight()); contentScrim.setCallback(this); contentScrim.setAlpha(scrimAlpha); } ViewCompat.postInvalidateOnAnimation(this); } } /** * Set the color to use for the content scrim. * * @param color the color to display * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_contentScrim * @see #getContentScrim() */ public void setContentScrimColor(@ColorInt int color) { setContentScrim(new ColorDrawable(color)); } /** * Set the drawable to use for the content scrim from resources. * * @param resId drawable resource id * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_contentScrim * @see #getContentScrim() */ public void setContentScrimResource(@DrawableRes int resId) { setContentScrim(ContextCompat.getDrawable(getContext(), resId)); } /** * Returns the drawable which is used for the foreground scrim. * * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_contentScrim * @see #setContentScrim(Drawable) */ @Nullable public Drawable getContentScrim() { return contentScrim; } /** * Set the drawable to use for the status bar scrim from resources. Providing null will disable * the scrim functionality. *

*

This scrim is only shown when we have been given a top system inset. * * @param drawable the drawable to display * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_statusBarScrim * @see #getStatusBarScrim() */ public void setStatusBarScrim(@Nullable Drawable drawable) { if (statusBarScrim != drawable) { if (statusBarScrim != null) { statusBarScrim.setCallback(null); } statusBarScrim = drawable != null ? drawable.mutate() : null; if (statusBarScrim != null) { if (statusBarScrim.isStateful()) { statusBarScrim.setState(getDrawableState()); } DrawableCompat.setLayoutDirection(statusBarScrim, ViewCompat.getLayoutDirection(this)); statusBarScrim.setVisible(getVisibility() == VISIBLE, false); statusBarScrim.setCallback(this); statusBarScrim.setAlpha(scrimAlpha); } ViewCompat.postInvalidateOnAnimation(this); } } @Override protected void drawableStateChanged() { super.drawableStateChanged(); final int[] state = getDrawableState(); boolean changed = false; Drawable d = statusBarScrim; if (d != null && d.isStateful()) { changed |= d.setState(state); } d = contentScrim; if (d != null && d.isStateful()) { changed |= d.setState(state); } if (collapsingTextHelper != null) { changed |= collapsingTextHelper.setState(state); } if (changed) { invalidate(); } } @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || who == contentScrim || who == statusBarScrim; } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); final boolean visible = visibility == VISIBLE; if (statusBarScrim != null && statusBarScrim.isVisible() != visible) { statusBarScrim.setVisible(visible, false); } if (contentScrim != null && contentScrim.isVisible() != visible) { contentScrim.setVisible(visible, false); } } /** * Set the color to use for the status bar scrim. *

*

This scrim is only shown when we have been given a top system inset. * * @param color the color to display * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_statusBarScrim * @see #getStatusBarScrim() */ public void setStatusBarScrimColor(@ColorInt int color) { setStatusBarScrim(new ColorDrawable(color)); } /** * Set the drawable to use for the content scrim from resources. * * @param resId drawable resource id * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_statusBarScrim * @see #getStatusBarScrim() */ public void setStatusBarScrimResource(@DrawableRes int resId) { setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId)); } /** * Returns the drawable which is used for the status bar scrim. * * @attr ref R.styleable#SubtitleCollapsingToolbarLayout_statusBarScrim * @see #setStatusBarScrim(Drawable) */ @Nullable public Drawable getStatusBarScrim() { return statusBarScrim; } /** * Sets the text color and size for the collapsed title from the specified TextAppearance * resource. * * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_collapsedTitleTextAppearance */ public void setCollapsedTitleTextAppearance(@StyleRes int resId) { collapsingTextHelper.setCollapsedTitleTextAppearance(resId); } /** * Sets the text color of the collapsed title. * * @param color The new text color in ARGB format */ public void setCollapsedTitleTextColor(@ColorInt int color) { setCollapsedTitleTextColor(ColorStateList.valueOf(color)); } /** * Sets the text colors of the collapsed title. * * @param colors ColorStateList containing the new text colors */ public void setCollapsedTitleTextColor(@NonNull ColorStateList colors) { collapsingTextHelper.setCollapsedTitleTextColor(colors); } /** * Sets the text color and size for the collapsed subtitle from the specified TextAppearance * resource. * * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_collapsedSubtitleTextAppearance */ public void setCollapsedSubtitleTextAppearance(@StyleRes int resId) { collapsingTextHelper.setCollapsedSubtitleTextAppearance(resId); } /** * Sets the text color of the collapsed subtitle. * * @param color The new text color in ARGB format */ public void setCollapsedSubtitleTextColor(@ColorInt int color) { setCollapsedSubtitleTextColor(ColorStateList.valueOf(color)); } /** * Sets the text colors of the collapsed subtitle. * * @param colors ColorStateList containing the new text colors */ public void setCollapsedSubtitleTextColor(@NonNull ColorStateList colors) { collapsingTextHelper.setCollapsedSubtitleTextColor(colors); } /** * Sets the horizontal alignment of the collapsed title and the vertical gravity that will be used * when there is extra space in the collapsed bounds beyond what is required for the title itself. * * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_collapsedTitleGravity */ public void setCollapsedTitleGravity(int gravity) { collapsingTextHelper.setCollapsedTextGravity(gravity); } /** * Returns the horizontal and vertical alignment for title when collapsed. * * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_collapsedTitleGravity */ public int getCollapsedTitleGravity() { return collapsingTextHelper.getCollapsedTextGravity(); } /** * Sets the text color and size for the expanded title from the specified TextAppearance resource. * * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleTextAppearance */ public void setExpandedTitleTextAppearance(@StyleRes int resId) { collapsingTextHelper.setExpandedTitleTextAppearance(resId); } /** * Sets the text color of the expanded title. * * @param color The new text color in ARGB format */ public void setExpandedTitleTextColor(@ColorInt int color) { setExpandedTitleTextColor(ColorStateList.valueOf(color)); } /** * Sets the text colors of the expanded title. * * @param colors ColorStateList containing the new text colors */ public void setExpandedTitleTextColor(@NonNull ColorStateList colors) { collapsingTextHelper.setExpandedTitleTextColor(colors); } /** * Sets the text color and size for the expanded subtitle from the specified TextAppearance resource. * * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedSubtitleTextAppearance */ public void setExpandedSubtitleTextAppearance(@StyleRes int resId) { collapsingTextHelper.setExpandedSubtitleTextAppearance(resId); } /** * Sets the text color of the expanded subtitle. * * @param color The new text color in ARGB format */ public void setExpandedSubtitleTextColor(@ColorInt int color) { setExpandedSubtitleTextColor(ColorStateList.valueOf(color)); } /** * Sets the text colors of the expanded subtitle. * * @param colors ColorStateList containing the new text colors */ public void setExpandedSubtitleTextColor(@NonNull ColorStateList colors) { collapsingTextHelper.setExpandedSubtitleTextColor(colors); } /** * Sets the horizontal alignment of the expanded title and the vertical gravity that will be used * when there is extra space in the expanded bounds beyond what is required for the title itself. * * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleGravity */ public void setExpandedTitleGravity(int gravity) { collapsingTextHelper.setExpandedTextGravity(gravity); } /** * Returns the horizontal and vertical alignment for title when expanded. * * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleGravity */ public int getExpandedTitleGravity() { return collapsingTextHelper.getExpandedTextGravity(); } /** * Set the typeface to use for the collapsed title. * * @param typeface typeface to use, or {@code null} to use the default. */ public void setCollapsedTitleTypeface(@Nullable Typeface typeface) { collapsingTextHelper.setCollapsedTitleTypeface(typeface); } /** * Returns the typeface used for the collapsed title. */ @NonNull public Typeface getCollapsedTitleTypeface() { return collapsingTextHelper.getCollapsedTitleTypeface(); } /** * Set the typeface to use for the expanded title. * * @param typeface typeface to use, or {@code null} to use the default. */ public void setExpandedTitleTypeface(@Nullable Typeface typeface) { collapsingTextHelper.setExpandedTitleTypeface(typeface); } /** * Returns the typeface used for the expanded title. */ @NonNull public Typeface getExpandedTitleTypeface() { return collapsingTextHelper.getExpandedTitleTypeface(); } /** * Set the typeface to use for the collapsed title. * * @param typeface typeface to use, or {@code null} to use the default. */ public void setCollapsedSubtitleTypeface(@Nullable Typeface typeface) { collapsingTextHelper.setCollapsedSubtitleTypeface(typeface); } /** * Returns the typeface used for the collapsed title. */ @NonNull public Typeface getCollapsedSubtitleTypeface() { return collapsingTextHelper.getCollapsedSubtitleTypeface(); } /** * Set the typeface to use for the expanded title. * * @param typeface typeface to use, or {@code null} to use the default. */ public void setExpandedSubtitleTypeface(@Nullable Typeface typeface) { collapsingTextHelper.setExpandedSubtitleTypeface(typeface); } /** * Returns the typeface used for the expanded title. */ @NonNull public Typeface getExpandedSubtitleTypeface() { return collapsingTextHelper.getExpandedSubtitleTypeface(); } /** * Sets the expanded title margins. * * @param start the starting title margin in pixels * @param top the top title margin in pixels * @param end the ending title margin in pixels * @param bottom the bottom title margin in pixels * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMargin * @see #getExpandedTitleMarginStart() * @see #getExpandedTitleMarginTop() * @see #getExpandedTitleMarginEnd() * @see #getExpandedTitleMarginBottom() */ public void setExpandedTitleMargin(int start, int top, int end, int bottom) { expandedMarginStart = start; expandedMarginTop = top; expandedMarginEnd = end; expandedMarginBottom = bottom; requestLayout(); } /** * @return the starting expanded title margin in pixels * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginStart * @see #setExpandedTitleMarginStart(int) */ public int getExpandedTitleMarginStart() { return expandedMarginStart; } /** * Sets the starting expanded title margin in pixels. * * @param margin the starting title margin in pixels * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginStart * @see #getExpandedTitleMarginStart() */ public void setExpandedTitleMarginStart(int margin) { expandedMarginStart = margin; requestLayout(); } /** * @return the top expanded title margin in pixels * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginTop * @see #setExpandedTitleMarginTop(int) */ public int getExpandedTitleMarginTop() { return expandedMarginTop; } /** * Sets the top expanded title margin in pixels. * * @param margin the top title margin in pixels * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginTop * @see #getExpandedTitleMarginTop() */ public void setExpandedTitleMarginTop(int margin) { expandedMarginTop = margin; requestLayout(); } /** * @return the ending expanded title margin in pixels * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd * @see #setExpandedTitleMarginEnd(int) */ public int getExpandedTitleMarginEnd() { return expandedMarginEnd; } /** * Sets the ending expanded title margin in pixels. * * @param margin the ending title margin in pixels * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd * @see #getExpandedTitleMarginEnd() */ public void setExpandedTitleMarginEnd(int margin) { expandedMarginEnd = margin; requestLayout(); } /** * @return the bottom expanded title margin in pixels * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginBottom * @see #setExpandedTitleMarginBottom(int) */ public int getExpandedTitleMarginBottom() { return expandedMarginBottom; } /** * Sets the bottom expanded title margin in pixels. * * @param margin the bottom title margin in pixels * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginBottom * @see #getExpandedTitleMarginBottom() */ public void setExpandedTitleMarginBottom(int margin) { expandedMarginBottom = margin; requestLayout(); } /** * Sets whether {@code TextDirectionHeuristics} should be used to determine whether the title text * is RTL. Experimental Feature. */ public void setRtlTextDirectionHeuristicsEnabled(boolean rtlTextDirectionHeuristicsEnabled) { collapsingTextHelper.setRtlTextDirectionHeuristicsEnabled(rtlTextDirectionHeuristicsEnabled); } /** * Gets whether {@code TextDirectionHeuristics} should be used to determine whether the title text * is RTL. Experimental Feature. */ public boolean isRtlTextDirectionHeuristicsEnabled() { return collapsingTextHelper.isRtlTextDirectionHeuristicsEnabled(); } /** * Set the amount of visible height in pixels used to define when to trigger a scrim visibility * change. *

*

If the visible height of this view is less than the given value, the scrims will be made * visible, otherwise they are hidden. * * @param height value in pixels used to define when to trigger a scrim visibility change * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_expandedTitleMarginEnd */ public void setScrimVisibleHeightTrigger(@IntRange(from = 0) final int height) { if (scrimVisibleHeightTrigger != height) { scrimVisibleHeightTrigger = height; // Update the scrim visibility updateScrimVisibility(); } } /** * Returns the amount of visible height in pixels used to define when to trigger a scrim * visibility change. * * @see #setScrimVisibleHeightTrigger(int) */ public int getScrimVisibleHeightTrigger() { if (scrimVisibleHeightTrigger >= 0) { // If we have one explicitly set, return it return scrimVisibleHeightTrigger; } // Otherwise we'll use the default computed value final int insetTop = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0; final int minHeight = ViewCompat.getMinimumHeight(this); if (minHeight > 0) { // If we have a minHeight set, lets use 2 * minHeight (capped at our height) return Math.min((minHeight * 2) + insetTop, getHeight()); } // If we reach here then we don't have a min height set. Instead we'll take a // guess at 1/3 of our height being visible return getHeight() / 3; } /** * Set the duration used for scrim visibility animations. * * @param duration the duration to use in milliseconds * @attr ref com.google.android.material.R.styleable#SubtitleCollapsingToolbarLayout_scrimAnimationDuration */ public void setScrimAnimationDuration(@IntRange(from = 0) final long duration) { scrimAnimationDuration = duration; } /** * Returns the duration in milliseconds used for scrim visibility animations. */ public long getScrimAnimationDuration() { return scrimAnimationDuration; } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @Override protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } public static class LayoutParams extends CollapsingToolbarLayout.LayoutParams { public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int width, int height, int gravity) { super(width, height, gravity); } public LayoutParams(ViewGroup.LayoutParams p) { super(p); } public LayoutParams(MarginLayoutParams source) { super(source); } @RequiresApi(19) public LayoutParams(FrameLayout.LayoutParams source) { super(source); } } /** * Show or hide the scrims if needed */ final void updateScrimVisibility() { if (contentScrim != null || statusBarScrim != null) { setScrimsShown(getHeight() + currentOffset < getScrimVisibleHeightTrigger()); } } final int getMaxOffsetForPinChild(View child) { final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); return getHeight() - offsetHelper.getLayoutTop() - child.getHeight() - lp.bottomMargin; } private void updateContentDescriptionFromTitle() { // Set this layout's contentDescription to match the title if it's shown by CollapsingTextHelper setContentDescription(getTitle()); } private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener { OffsetUpdateListener() { } @Override public void onOffsetChanged(AppBarLayout layout, int verticalOffset) { currentOffset = verticalOffset; final int insetTop = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0; for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child); switch (lp.collapseMode) { case LayoutParams.COLLAPSE_MODE_PIN: offsetHelper.setTopAndBottomOffset( MathUtils.clamp(-verticalOffset, 0, getMaxOffsetForPinChild(child))); break; case LayoutParams.COLLAPSE_MODE_PARALLAX: offsetHelper.setTopAndBottomOffset(Math.round(-verticalOffset * lp.parallaxMult)); break; default: break; } } // Show or hide the scrims if needed updateScrimVisibility(); if (statusBarScrim != null && insetTop > 0) { ViewCompat.postInvalidateOnAnimation(SubtitleCollapsingToolbarLayout.this); } // Update the collapsing text's fraction final int expandRange = getHeight() - ViewCompat.getMinimumHeight(SubtitleCollapsingToolbarLayout.this) - insetTop; collapsingTextHelper.setExpansionFraction(Math.abs(verticalOffset) / (float) expandRange); } } } ================================================ FILE: app/src/main/java/com/google/android/material/internal/SubtitleCollapsingTextHelper.java ================================================ package com.google.android.material.internal; import android.animation.TimeInterpolator; import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.os.Build; import android.text.TextPaint; import android.text.TextUtils; import android.view.Gravity; import android.view.View; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.math.MathUtils; import androidx.core.text.TextDirectionHeuristicsCompat; import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import com.google.android.material.animation.AnimationUtils; import com.google.android.material.resources.CancelableFontCallback; import com.google.android.material.resources.TextAppearance; /** * Helper class for {@link com.google.android.material.appbar.SubtitleCollapsingToolbarLayout}. * * @see CollapsingTextHelper */ public final class SubtitleCollapsingTextHelper { // Pre-JB-MR2 doesn't support HW accelerated canvas scaled title so we will workaround it // by using our own texture private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18; private static final boolean DEBUG_DRAW = false; @NonNull private static final Paint DEBUG_DRAW_PAINT; static { DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null; if (DEBUG_DRAW_PAINT != null) { DEBUG_DRAW_PAINT.setAntiAlias(true); DEBUG_DRAW_PAINT.setColor(Color.MAGENTA); } } private final View view; private boolean drawTitle; private float expandedFraction; @NonNull private final Rect expandedBounds; @NonNull private final Rect collapsedBounds; @NonNull private final RectF currentBounds; private int expandedTextGravity = Gravity.CENTER_VERTICAL; private int collapsedTextGravity = Gravity.CENTER_VERTICAL; private float expandedTitleTextSize, expandedSubtitleTextSize = 15; private float collapsedTitleTextSize, collapsedSubtitleTextSize = 15; private ColorStateList expandedTitleTextColor, expandedSubtitleTextColor; private ColorStateList collapsedTitleTextColor, collapsedSubtitleTextColor; private float expandedTitleDrawY, expandedSubtitleDrawY; private float collapsedTitleDrawY, collapsedSubtitleDrawY; private float expandedTitleDrawX, expandedSubtitleDrawX; private float collapsedTitleDrawX, collapsedSubtitleDrawX; private float currentTitleDrawX, currentSubtitleDrawX; private float currentTitleDrawY, currentSubtitleDrawY; private Typeface collapsedTitleTypeface, collapsedSubtitleTypeface; private Typeface expandedTitleTypeface, expandedSubtitleTypeface; private Typeface currentTitleTypeface, currentSubtitleTypeface; private CancelableFontCallback expandedTitleFontCallback, expandedSubtitleFontCallback; private CancelableFontCallback collapsedTitleFontCallback, collapsedSubtitleFontCallback; @Nullable private CharSequence title, subtitle; @Nullable private CharSequence titleToDraw, subtitleToDraw; private boolean isRtl; private boolean isRtlTextDirectionHeuristicsEnabled = true; private boolean useTexture; @Nullable private Bitmap expandedTitleTexture, expandedSubtitleTexture; private Paint titleTexturePaint, subtitleTexturePaint; private float titleTextureAscent, subtitleTextureAscent; private float titleTextureDescent, subtitleTextureDescent; private float titleScale, subtitleScale; private float currentTitleTextSize, currentSubtitleTextSize; private int[] state; private boolean boundsChanged; @NonNull private final TextPaint titleTextPaint, subtitleTextPaint; @NonNull private final TextPaint titleTmpPaint, subtitleTmpPaint; private TimeInterpolator positionInterpolator; private TimeInterpolator textSizeInterpolator; private float collapsedTitleShadowRadius, collapsedSubtitleShadowRadius; private float collapsedTitleShadowDx, collapsedSubtitleShadowDx; private float collapsedTitleShadowDy, collapsedSubtitleShadowDy; private ColorStateList collapsedTitleShadowColor, collapsedSubtitleShadowColor; private float expandedTitleShadowRadius, expandedSubtitleShadowRadius; private float expandedTitleShadowDx, expandedSubtitleShadowDx; private float expandedTitleShadowDy, expandedSubtitleShadowDy; private ColorStateList expandedTitleShadowColor, expandedSubtitleShadowColor; public SubtitleCollapsingTextHelper(View view) { this.view = view; titleTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); titleTmpPaint = new TextPaint(titleTextPaint); subtitleTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); subtitleTmpPaint = new TextPaint(subtitleTextPaint); collapsedBounds = new Rect(); expandedBounds = new Rect(); currentBounds = new RectF(); } public void setTextSizeInterpolator(TimeInterpolator interpolator) { textSizeInterpolator = interpolator; recalculate(); } public void setPositionInterpolator(TimeInterpolator interpolator) { positionInterpolator = interpolator; recalculate(); } public void setExpandedTitleTextSize(float textSize) { if (expandedTitleTextSize != textSize) { expandedTitleTextSize = textSize; recalculate(); } } public void setCollapsedTitleTextSize(float textSize) { if (collapsedTitleTextSize != textSize) { collapsedTitleTextSize = textSize; recalculate(); } } public void setExpandedSubtitleTextSize(float textSize) { if (expandedSubtitleTextSize != textSize) { expandedSubtitleTextSize = textSize; recalculate(); } } public void setCollapsedSubtitleTextSize(float textSize) { if (collapsedSubtitleTextSize != textSize) { collapsedSubtitleTextSize = textSize; recalculate(); } } public void setCollapsedTitleTextColor(ColorStateList textColor) { if (collapsedTitleTextColor != textColor) { collapsedTitleTextColor = textColor; recalculate(); } } public void setExpandedTitleTextColor(ColorStateList textColor) { if (expandedTitleTextColor != textColor) { expandedTitleTextColor = textColor; recalculate(); } } public void setCollapsedSubtitleTextColor(ColorStateList textColor) { if (collapsedSubtitleTextColor != textColor) { collapsedSubtitleTextColor = textColor; recalculate(); } } public void setExpandedSubtitleTextColor(ColorStateList textColor) { if (expandedSubtitleTextColor != textColor) { expandedSubtitleTextColor = textColor; recalculate(); } } public void setExpandedBounds(int left, int top, int right, int bottom) { if (!rectEquals(expandedBounds, left, top, right, bottom)) { expandedBounds.set(left, top, right, bottom); boundsChanged = true; onBoundsChanged(); } } public void setExpandedBounds(@NonNull Rect bounds) { setExpandedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom); } public void setCollapsedBounds(int left, int top, int right, int bottom) { if (!rectEquals(collapsedBounds, left, top, right, bottom)) { collapsedBounds.set(left, top, right, bottom); boundsChanged = true; onBoundsChanged(); } } public void setCollapsedBounds(@NonNull Rect bounds) { setCollapsedBounds(bounds.left, bounds.top, bounds.right, bounds.bottom); } public void getCollapsedTitleTextActualBounds(@NonNull RectF bounds) { boolean isRtl = calculateIsRtl(title); bounds.left = !isRtl ? collapsedBounds.left : collapsedBounds.right - calculateCollapsedTitleTextWidth(); bounds.top = collapsedBounds.top; bounds.right = !isRtl ? bounds.left + calculateCollapsedTitleTextWidth() : collapsedBounds.right; bounds.bottom = collapsedBounds.top + getCollapsedTitleTextHeight(); } public float calculateCollapsedTitleTextWidth() { if (title == null) { return 0; } getTitleTextPaintCollapsed(titleTmpPaint); return titleTmpPaint.measureText(title, 0, title.length()); } public void getCollapsedSubtitleTextActualBounds(@NonNull RectF bounds) { boolean isRtl = calculateIsRtl(subtitle); bounds.left = !isRtl ? collapsedBounds.left : collapsedBounds.right - calculateCollapsedSubtitleTextWidth(); bounds.top = collapsedBounds.top; bounds.right = !isRtl ? bounds.left + calculateCollapsedSubtitleTextWidth() : collapsedBounds.right; bounds.bottom = collapsedBounds.top + getCollapsedSubtitleTextHeight(); } public float calculateCollapsedSubtitleTextWidth() { if (subtitle == null) { return 0; } getSubtitleTextPaintCollapsed(subtitleTmpPaint); return subtitleTmpPaint.measureText(subtitle, 0, subtitle.length()); } public float getExpandedTitleTextHeight() { getTitleTextPaintExpanded(titleTmpPaint); // Return expanded height measured from the baseline. return -titleTmpPaint.ascent(); } public float getCollapsedTitleTextHeight() { getTitleTextPaintCollapsed(titleTmpPaint); // Return collapsed height measured from the baseline. return -titleTmpPaint.ascent(); } public float getExpandedSubtitleTextHeight() { getSubtitleTextPaintExpanded(subtitleTmpPaint); // Return expanded height measured from the baseline. return -subtitleTmpPaint.ascent(); } public float getCollapsedSubtitleTextHeight() { getSubtitleTextPaintCollapsed(subtitleTmpPaint); // Return collapsed height measured from the baseline. return -subtitleTmpPaint.ascent(); } private void getTitleTextPaintExpanded(@NonNull TextPaint textPaint) { textPaint.setTextSize(expandedTitleTextSize); textPaint.setTypeface(expandedTitleTypeface); } private void getTitleTextPaintCollapsed(@NonNull TextPaint textPaint) { textPaint.setTextSize(collapsedTitleTextSize); textPaint.setTypeface(collapsedTitleTypeface); } private void getSubtitleTextPaintExpanded(@NonNull TextPaint textPaint) { textPaint.setTextSize(expandedSubtitleTextSize); textPaint.setTypeface(expandedSubtitleTypeface); } private void getSubtitleTextPaintCollapsed(@NonNull TextPaint textPaint) { textPaint.setTextSize(collapsedSubtitleTextSize); textPaint.setTypeface(collapsedSubtitleTypeface); } void onBoundsChanged() { drawTitle = collapsedBounds.width() > 0 && collapsedBounds.height() > 0 && expandedBounds.width() > 0 && expandedBounds.height() > 0; } public void setExpandedTextGravity(int gravity) { if (expandedTextGravity != gravity) { expandedTextGravity = gravity; recalculate(); } } public int getExpandedTextGravity() { return expandedTextGravity; } public void setCollapsedTextGravity(int gravity) { if (collapsedTextGravity != gravity) { collapsedTextGravity = gravity; recalculate(); } } public int getCollapsedTextGravity() { return collapsedTextGravity; } public void setCollapsedTitleTextAppearance(int resId) { TextAppearance textAppearance = new TextAppearance(view.getContext(), resId); if (textAppearance.getTextColor() != null) { collapsedTitleTextColor = textAppearance.getTextColor(); } if (textAppearance.getTextSize() != 0) { collapsedTitleTextSize = textAppearance.getTextSize(); } if (textAppearance.shadowColor != null) { collapsedTitleShadowColor = textAppearance.shadowColor; } collapsedTitleShadowDx = textAppearance.shadowDx; collapsedTitleShadowDy = textAppearance.shadowDy; collapsedTitleShadowRadius = textAppearance.shadowRadius; // Cancel pending async fetch, if any, and replace with a new one. if (collapsedTitleFontCallback != null) { collapsedTitleFontCallback.cancel(); } collapsedTitleFontCallback = new CancelableFontCallback(new CancelableFontCallback.ApplyFont() { @Override public void apply(Typeface font) { setCollapsedTitleTypeface(font); } }, textAppearance.getFallbackFont()); textAppearance.getFontAsync(view.getContext(), collapsedTitleFontCallback); recalculate(); } public void setExpandedTitleTextAppearance(int resId) { TextAppearance textAppearance = new TextAppearance(view.getContext(), resId); if (textAppearance.getTextColor() != null) { expandedTitleTextColor = textAppearance.getTextColor(); } if (textAppearance.getTextSize() != 0) { expandedTitleTextSize = textAppearance.getTextSize(); } if (textAppearance.shadowColor != null) { expandedTitleShadowColor = textAppearance.shadowColor; } expandedTitleShadowDx = textAppearance.shadowDx; expandedTitleShadowDy = textAppearance.shadowDy; expandedTitleShadowRadius = textAppearance.shadowRadius; // Cancel pending async fetch, if any, and replace with a new one. if (expandedTitleFontCallback != null) { expandedTitleFontCallback.cancel(); } expandedTitleFontCallback = new CancelableFontCallback(new CancelableFontCallback.ApplyFont() { @Override public void apply(Typeface font) { setExpandedTitleTypeface(font); } }, textAppearance.getFallbackFont()); textAppearance.getFontAsync(view.getContext(), expandedTitleFontCallback); recalculate(); } public void setCollapsedSubtitleTextAppearance(int resId) { TextAppearance textAppearance = new TextAppearance(view.getContext(), resId); if (textAppearance.getTextColor() != null) { collapsedSubtitleTextColor = textAppearance.getTextColor(); } if (textAppearance.getTextSize() != 0) { collapsedSubtitleTextSize = textAppearance.getTextSize(); } if (textAppearance.shadowColor != null) { collapsedSubtitleShadowColor = textAppearance.shadowColor; } collapsedSubtitleShadowDx = textAppearance.shadowDx; collapsedSubtitleShadowDy = textAppearance.shadowDy; collapsedSubtitleShadowRadius = textAppearance.shadowRadius; // Cancel pending async fetch, if any, and replace with a new one. if (collapsedSubtitleFontCallback != null) { collapsedSubtitleFontCallback.cancel(); } collapsedSubtitleFontCallback = new CancelableFontCallback(new CancelableFontCallback.ApplyFont() { @Override public void apply(Typeface font) { setCollapsedSubtitleTypeface(font); } }, textAppearance.getFallbackFont()); textAppearance.getFontAsync(view.getContext(), collapsedSubtitleFontCallback); recalculate(); } public void setExpandedSubtitleTextAppearance(int resId) { TextAppearance textAppearance = new TextAppearance(view.getContext(), resId); if (textAppearance.getTextColor() != null) { expandedSubtitleTextColor = textAppearance.getTextColor(); } if (textAppearance.getTextSize() != 0) { expandedSubtitleTextSize = textAppearance.getTextSize(); } if (textAppearance.shadowColor != null) { expandedSubtitleShadowColor = textAppearance.shadowColor; } expandedSubtitleShadowDx = textAppearance.shadowDx; expandedSubtitleShadowDy = textAppearance.shadowDy; expandedSubtitleShadowRadius = textAppearance.shadowRadius; // Cancel pending async fetch, if any, and replace with a new one. if (expandedSubtitleFontCallback != null) { expandedSubtitleFontCallback.cancel(); } expandedSubtitleFontCallback = new CancelableFontCallback(new CancelableFontCallback.ApplyFont() { @Override public void apply(Typeface font) { if (font != null) setExpandedSubtitleTypeface(font); } }, null); textAppearance.getFontAsync(view.getContext(), expandedSubtitleFontCallback); recalculate(); } public void setCollapsedTitleTypeface(Typeface typeface) { if (setCollapsedTitleTypefaceInternal(typeface)) { recalculate(); } } public void setExpandedTitleTypeface(Typeface typeface) { if (setExpandedTitleTypefaceInternal(typeface)) { recalculate(); } } public void setCollapsedSubtitleTypeface(Typeface typeface) { if (setCollapsedSubtitleTypefaceInternal(typeface)) { recalculate(); } } public void setExpandedSubtitleTypeface(Typeface typeface) { if (setExpandedSubtitleTypefaceInternal(typeface)) { recalculate(); } } public void setTitleTypefaces(Typeface typeface) { boolean collapsedFontChanged = setCollapsedTitleTypefaceInternal(typeface); boolean expandedFontChanged = setExpandedTitleTypefaceInternal(typeface); if (collapsedFontChanged || expandedFontChanged) { recalculate(); } } public void setSubtitleTypefaces(Typeface typeface) { boolean collapsedFontChanged = setCollapsedSubtitleTypefaceInternal(typeface); boolean expandedFontChanged = setExpandedSubtitleTypefaceInternal(typeface); if (collapsedFontChanged || expandedFontChanged) { recalculate(); } } @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView private boolean setCollapsedTitleTypefaceInternal(Typeface typeface) { // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding // already updated one when async op comes back after a while. if (collapsedTitleFontCallback != null) { collapsedTitleFontCallback.cancel(); } if (collapsedTitleTypeface != typeface) { collapsedTitleTypeface = typeface; return true; } return false; } @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView private boolean setExpandedTitleTypefaceInternal(Typeface typeface) { // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding // already updated one when async op comes back after a while. if (expandedTitleFontCallback != null) { expandedTitleFontCallback.cancel(); } if (expandedTitleTypeface != typeface) { expandedTitleTypeface = typeface; return true; } return false; } @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView private boolean setCollapsedSubtitleTypefaceInternal(Typeface typeface) { // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding // already updated one when async op comes back after a while. if (collapsedSubtitleFontCallback != null) { collapsedSubtitleFontCallback.cancel(); } if (collapsedSubtitleTypeface != typeface) { collapsedSubtitleTypeface = typeface; return true; } return false; } @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView private boolean setExpandedSubtitleTypefaceInternal(Typeface typeface) { // Explicit Typeface setting cancels pending async fetch, if any, to avoid old font overriding // already updated one when async op comes back after a while. if (expandedSubtitleFontCallback != null) { expandedSubtitleFontCallback.cancel(); } if (expandedSubtitleTypeface != typeface) { expandedSubtitleTypeface = typeface; return true; } return false; } public Typeface getCollapsedTitleTypeface() { return collapsedTitleTypeface != null ? collapsedTitleTypeface : Typeface.DEFAULT; } public Typeface getExpandedTitleTypeface() { return expandedTitleTypeface != null ? expandedTitleTypeface : Typeface.DEFAULT; } public Typeface getCollapsedSubtitleTypeface() { return collapsedSubtitleTypeface != null ? collapsedSubtitleTypeface : Typeface.DEFAULT; } public Typeface getExpandedSubtitleTypeface() { return expandedSubtitleTypeface != null ? expandedSubtitleTypeface : Typeface.DEFAULT; } /** * Set the value indicating the current scroll value. This decides how much of the background will * be displayed, as well as the title metrics/positioning. * *

A value of {@code 0.0} indicates that the layout is fully expanded. A value of {@code 1.0} * indicates that the layout is fully collapsed. */ public void setExpansionFraction(float fraction) { fraction = MathUtils.clamp(fraction, 0f, 1f); if (fraction != expandedFraction) { expandedFraction = fraction; calculateCurrentOffsets(); } } public final boolean setState(final int[] state) { this.state = state; if (isStateful()) { recalculate(); return true; } return false; } public final boolean isStateful() { return (collapsedTitleTextColor != null && collapsedTitleTextColor.isStateful()) || (expandedTitleTextColor != null && expandedTitleTextColor.isStateful()); } public float getExpansionFraction() { return expandedFraction; } public float getCollapsedTitleTextSize() { return collapsedTitleTextSize; } public float getExpandedTitleTextSize() { return expandedTitleTextSize; } public float getCollapsedSubtitleTextSize() { return collapsedSubtitleTextSize; } public float getExpandedSubtitleTextSize() { return expandedSubtitleTextSize; } public void setRtlTextDirectionHeuristicsEnabled(boolean rtlTextDirectionHeuristicsEnabled) { isRtlTextDirectionHeuristicsEnabled = rtlTextDirectionHeuristicsEnabled; } public boolean isRtlTextDirectionHeuristicsEnabled() { return isRtlTextDirectionHeuristicsEnabled; } private void calculateCurrentOffsets() { calculateOffsets(expandedFraction); } private void calculateOffsets(final float fraction) { interpolateBounds(fraction); currentTitleDrawX = lerp(expandedTitleDrawX, collapsedTitleDrawX, fraction, positionInterpolator); currentTitleDrawY = lerp(expandedTitleDrawY, collapsedTitleDrawY, fraction, positionInterpolator); currentSubtitleDrawX = lerp(expandedSubtitleDrawX, collapsedSubtitleDrawX, fraction, positionInterpolator); currentSubtitleDrawY = lerp(expandedSubtitleDrawY, collapsedSubtitleDrawY, fraction, positionInterpolator); setInterpolatedTitleTextSize(lerp(expandedTitleTextSize, collapsedTitleTextSize, fraction, textSizeInterpolator)); setInterpolatedSubtitleTextSize(lerp(expandedSubtitleTextSize, collapsedSubtitleTextSize, fraction, textSizeInterpolator)); if (collapsedTitleTextColor != expandedTitleTextColor) { // If the collapsed and expanded title colors are different, blend them based on the // fraction titleTextPaint.setColor(blendColors(getCurrentExpandedTitleTextColor(), getCurrentCollapsedTitleTextColor(), fraction)); } else { titleTextPaint.setColor(getCurrentCollapsedTitleTextColor()); } titleTextPaint.setShadowLayer( lerp(expandedTitleShadowRadius, collapsedTitleShadowRadius, fraction, null), lerp(expandedTitleShadowDx, collapsedTitleShadowDx, fraction, null), lerp(expandedTitleShadowDy, collapsedTitleShadowDy, fraction, null), blendColors(getCurrentColor(expandedTitleShadowColor), getCurrentColor(collapsedTitleShadowColor), fraction)); if (collapsedSubtitleTextColor != expandedSubtitleTextColor) { // If the collapsed and expanded title colors are different, blend them based on the // fraction subtitleTextPaint.setColor(blendColors(getCurrentExpandedSubtitleTextColor(), getCurrentCollapsedSubtitleTextColor(), fraction)); } else { subtitleTextPaint.setColor(getCurrentCollapsedSubtitleTextColor()); } subtitleTextPaint.setShadowLayer( lerp(expandedSubtitleShadowRadius, collapsedSubtitleShadowRadius, fraction, null), lerp(expandedSubtitleShadowDx, collapsedSubtitleShadowDx, fraction, null), lerp(expandedSubtitleShadowDy, collapsedSubtitleShadowDy, fraction, null), blendColors(getCurrentColor(expandedSubtitleShadowColor), getCurrentColor(collapsedSubtitleShadowColor), fraction)); ViewCompat.postInvalidateOnAnimation(view); } @ColorInt private int getCurrentExpandedTitleTextColor() { return getCurrentColor(expandedTitleTextColor); } @ColorInt private int getCurrentExpandedSubtitleTextColor() { return getCurrentColor(expandedSubtitleTextColor); } @ColorInt public int getCurrentCollapsedTitleTextColor() { return getCurrentColor(collapsedTitleTextColor); } @ColorInt public int getCurrentCollapsedSubtitleTextColor() { return getCurrentColor(collapsedSubtitleTextColor); } @ColorInt private int getCurrentColor(@Nullable ColorStateList colorStateList) { if (colorStateList == null) { return 0; } if (state != null) { return colorStateList.getColorForState(state, 0); } return colorStateList.getDefaultColor(); } private void calculateBaseOffsets() { final float currentTitleSize = this.currentTitleTextSize; final float currentSubtitleSize = this.currentSubtitleTextSize; final boolean isTitleOnly = TextUtils.isEmpty(subtitle); // We then calculate the collapsed title size, using the same logic calculateUsingTitleTextSize(collapsedTitleTextSize); calculateUsingSubtitleTextSize(collapsedSubtitleTextSize); float titleWidth = titleToDraw != null ? titleTextPaint.measureText(titleToDraw, 0, titleToDraw.length()) : 0; float subtitleWidth = subtitleToDraw != null ? subtitleTextPaint.measureText(subtitleToDraw, 0, subtitleToDraw.length()) : 0; final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity( collapsedTextGravity, isRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR); // reusable dimension float titleHeight = titleTextPaint.descent() - titleTextPaint.ascent(); float titleOffset = titleHeight / 2 - titleTextPaint.descent(); float subtitleHeight = subtitleTextPaint.descent() - subtitleTextPaint.ascent(); float subtitleOffset = subtitleHeight / 2 - subtitleTextPaint.descent(); if (isTitleOnly) { switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.BOTTOM: collapsedTitleDrawY = collapsedBounds.bottom; break; case Gravity.TOP: collapsedTitleDrawY = collapsedBounds.top - titleTextPaint.ascent(); break; case Gravity.CENTER_VERTICAL: default: float textHeight = titleTextPaint.descent() - titleTextPaint.ascent(); float textOffset = (textHeight / 2) - titleTextPaint.descent(); collapsedTitleDrawY = collapsedBounds.centerY() + textOffset; break; } } else { final float offset = (collapsedBounds.height() - (titleHeight + subtitleHeight)) / 3; collapsedTitleDrawY = collapsedBounds.top + offset - titleTextPaint.ascent(); collapsedSubtitleDrawY = collapsedBounds.top + offset * 2 + titleHeight - subtitleTextPaint.ascent(); } switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: collapsedTitleDrawX = collapsedBounds.centerX() - (titleWidth / 2); collapsedSubtitleDrawX = collapsedBounds.centerX() - (subtitleWidth / 2); break; case Gravity.RIGHT: collapsedTitleDrawX = collapsedBounds.right - titleWidth; collapsedSubtitleDrawX = collapsedBounds.right - subtitleWidth; break; case Gravity.LEFT: default: collapsedTitleDrawX = collapsedBounds.left; collapsedSubtitleDrawX = collapsedBounds.left; break; } calculateUsingTitleTextSize(expandedTitleTextSize); calculateUsingSubtitleTextSize(expandedSubtitleTextSize); titleWidth = titleToDraw != null ? titleTextPaint.measureText(titleToDraw, 0, titleToDraw.length()) : 0; subtitleWidth = subtitleToDraw != null ? subtitleTextPaint.measureText(subtitleToDraw, 0, subtitleToDraw.length()) : 0; // dimension modification titleHeight = titleTextPaint.descent() - titleTextPaint.ascent(); titleOffset = titleHeight / 2 - titleTextPaint.descent(); subtitleHeight = subtitleTextPaint.descent() - subtitleTextPaint.ascent(); subtitleOffset = subtitleHeight / 2 - subtitleTextPaint.descent(); final int expandedAbsGravity = GravityCompat.getAbsoluteGravity( expandedTextGravity, isRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR ); if (isTitleOnly) { switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.BOTTOM: expandedTitleDrawY = expandedBounds.bottom; break; case Gravity.TOP: expandedTitleDrawY = expandedBounds.top - titleTextPaint.ascent(); break; case Gravity.CENTER_VERTICAL: default: float textHeight = titleTextPaint.descent() - titleTextPaint.ascent(); float textOffset = (textHeight / 2) - titleTextPaint.descent(); expandedTitleDrawY = expandedBounds.centerY() + textOffset; break; } } else { switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.BOTTOM: expandedTitleDrawY = expandedBounds.bottom - subtitleHeight - titleOffset; expandedSubtitleDrawY = expandedBounds.bottom; break; case Gravity.TOP: expandedTitleDrawY = expandedBounds.top - titleTextPaint.ascent(); expandedSubtitleDrawY = expandedTitleDrawY + subtitleHeight + titleOffset; break; case Gravity.CENTER_VERTICAL: default: expandedTitleDrawY = expandedBounds.centerY() + titleOffset; expandedSubtitleDrawY = expandedTitleDrawY + subtitleHeight + titleOffset; break; } } switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: expandedTitleDrawX = expandedBounds.centerX() - (titleWidth / 2); expandedSubtitleDrawX = expandedBounds.centerX() - (subtitleWidth / 2); break; case Gravity.RIGHT: expandedTitleDrawX = expandedBounds.right - titleWidth; expandedSubtitleDrawX = expandedBounds.right - subtitleWidth; break; case Gravity.LEFT: default: expandedTitleDrawX = expandedBounds.left; expandedSubtitleDrawX = expandedBounds.left; break; } // The bounds have changed so we need to clear the texture clearTexture(); // Now reset the title size back to the original setInterpolatedTitleTextSize(currentTitleSize); setInterpolatedSubtitleTextSize(currentSubtitleSize); } private void interpolateBounds(float fraction) { currentBounds.left = lerp(expandedBounds.left, collapsedBounds.left, fraction, positionInterpolator); currentBounds.top = lerp(expandedTitleDrawY, collapsedTitleDrawY, fraction, positionInterpolator); currentBounds.right = lerp(expandedBounds.right, collapsedBounds.right, fraction, positionInterpolator); currentBounds.bottom = lerp(expandedBounds.bottom, collapsedBounds.bottom, fraction, positionInterpolator); } public void draw(@NonNull Canvas canvas) { final int saveCount = canvas.save(); if (drawTitle && titleToDraw != null) { float titleX = currentTitleDrawX; float titleY = currentTitleDrawY; float subtitleX = currentSubtitleDrawX; float subtitleY = currentSubtitleDrawY; final boolean drawTitleTexture = useTexture && expandedTitleTexture != null; final boolean drawSubtitleTexture = useTexture && expandedSubtitleTexture != null; final float titleAscent; final float titleDescent; if (drawTitleTexture) { titleAscent = titleTextureAscent * titleScale; titleDescent = titleTextureDescent * titleScale; } else { titleAscent = titleTextPaint.ascent() * titleScale; titleDescent = titleTextPaint.descent() * titleScale; } if (DEBUG_DRAW) { // Just a debug tool, which drawn a magenta rect in the text bounds canvas.drawRect(currentBounds.left, titleY + titleAscent, currentBounds.right, titleY + titleDescent, DEBUG_DRAW_PAINT); } if (drawTitleTexture) { titleY += titleAscent; } // additional canvas save for subtitle if (subtitleToDraw != null) { final int subtitleSaveCount = canvas.save(); if (subtitleScale != 1f) { canvas.scale(subtitleScale, subtitleScale, subtitleX, subtitleY); } if (drawSubtitleTexture) { // If we should use a texture, draw it instead of title canvas.drawBitmap(expandedSubtitleTexture, subtitleX, subtitleY, subtitleTexturePaint); } else { canvas.drawText(subtitleToDraw, 0, subtitleToDraw.length(), subtitleX, subtitleY, subtitleTextPaint); } canvas.restoreToCount(subtitleSaveCount); } if (titleScale != 1f) { canvas.scale(titleScale, titleScale, titleX, titleY); } if (drawTitleTexture) { // If we should use a texture, draw it instead of text canvas.drawBitmap(expandedTitleTexture, titleX, titleY, titleTexturePaint); } else { canvas.drawText(titleToDraw, 0, titleToDraw.length(), titleX, titleY, titleTextPaint); } } canvas.restoreToCount(saveCount); } private boolean calculateIsRtl(@NonNull CharSequence text) { final boolean defaultIsRtl = isDefaultIsRtl(); return isRtlTextDirectionHeuristicsEnabled ? isTextDirectionHeuristicsIsRtl(text, defaultIsRtl) : defaultIsRtl; } private boolean isDefaultIsRtl() { return ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL; } private boolean isTextDirectionHeuristicsIsRtl(@NonNull CharSequence text, boolean defaultIsRtl) { return (defaultIsRtl ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR) .isRtl(text, 0, text.length()); } private void setInterpolatedTitleTextSize(float textSize) { calculateUsingTitleTextSize(textSize); // Use our texture if the scale isn't 1.0 useTexture = USE_SCALING_TEXTURE && titleScale != 1f; if (useTexture) { // Make sure we have an expanded texture if needed ensureExpandedTitleTexture(); } ViewCompat.postInvalidateOnAnimation(view); } private void setInterpolatedSubtitleTextSize(float textSize) { calculateUsingSubtitleTextSize(textSize); // Use our texture if the scale isn't 1.0 useTexture = USE_SCALING_TEXTURE && subtitleScale != 1f; if (useTexture) { // Make sure we have an expanded texture if needed ensureExpandedSubtitleTexture(); } ViewCompat.postInvalidateOnAnimation(view); } @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView private void calculateUsingTitleTextSize(final float size) { if (title == null) { return; } final float collapsedWidth = collapsedBounds.width(); final float expandedWidth = expandedBounds.width(); final float availableWidth; final float newTextSize; boolean updateDrawText = false; if (isClose(size, collapsedTitleTextSize)) { newTextSize = collapsedTitleTextSize; titleScale = 1f; if (currentTitleTypeface != collapsedTitleTypeface) { currentTitleTypeface = collapsedTitleTypeface; updateDrawText = true; } availableWidth = collapsedWidth; } else { newTextSize = expandedTitleTextSize; if (currentTitleTypeface != expandedTitleTypeface) { currentTitleTypeface = expandedTitleTypeface; updateDrawText = true; } if (isClose(size, expandedTitleTextSize)) { // If we're close to the expanded title size, snap to it and use a scale of 1 titleScale = 1f; } else { // Else, we'll scale down from the expanded title size titleScale = size / expandedTitleTextSize; } final float textSizeRatio = collapsedTitleTextSize / expandedTitleTextSize; // This is the size of the expanded bounds when it is scaled to match the // collapsed title size final float scaledDownWidth = expandedWidth * textSizeRatio; if (scaledDownWidth > collapsedWidth) { // If the scaled down size is larger than the actual collapsed width, we need to // cap the available width so that when the expanded title scales down, it matches // the collapsed width availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth); } else { // Otherwise we'll just use the expanded width availableWidth = expandedWidth; } } if (availableWidth > 0) { updateDrawText = (currentTitleTextSize != newTextSize) || boundsChanged || updateDrawText; currentTitleTextSize = newTextSize; boundsChanged = false; } if (titleToDraw == null || updateDrawText) { titleTextPaint.setTextSize(currentTitleTextSize); titleTextPaint.setTypeface(currentTitleTypeface); // Use linear title scaling if we're scaling the canvas titleTextPaint.setLinearText(titleScale != 1f); // If we don't currently have title to draw, or the title size has changed, ellipsize... final CharSequence text = TextUtils .ellipsize(this.title, titleTextPaint, availableWidth, TextUtils.TruncateAt.END); if (!TextUtils.equals(text, titleToDraw)) { titleToDraw = text; isRtl = calculateIsRtl(titleToDraw); } } } @SuppressWarnings("ReferenceEquality") // Matches the Typeface comparison in TextView private void calculateUsingSubtitleTextSize(final float size) { if (subtitle == null) { return; } final float collapsedWidth = collapsedBounds.width(); final float expandedWidth = expandedBounds.width(); final float availableWidth; final float newTextSize; boolean updateDrawText = false; if (isClose(size, collapsedSubtitleTextSize)) { newTextSize = collapsedSubtitleTextSize; subtitleScale = 1f; if (currentSubtitleTypeface != collapsedSubtitleTypeface) { currentSubtitleTypeface = collapsedSubtitleTypeface; updateDrawText = true; } availableWidth = collapsedWidth; } else { newTextSize = expandedSubtitleTextSize; if (currentSubtitleTypeface != expandedSubtitleTypeface) { currentSubtitleTypeface = expandedSubtitleTypeface; updateDrawText = true; } if (isClose(size, expandedSubtitleTextSize)) { // If we're close to the expanded title size, snap to it and use a scale of 1 subtitleScale = 1f; } else { // Else, we'll scale down from the expanded title size subtitleScale = size / expandedSubtitleTextSize; } final float textSizeRatio = collapsedSubtitleTextSize / expandedSubtitleTextSize; // This is the size of the expanded bounds when it is scaled to match the // collapsed title size final float scaledDownWidth = expandedWidth * textSizeRatio; if (scaledDownWidth > collapsedWidth) { // If the scaled down size is larger than the actual collapsed width, we need to // cap the available width so that when the expanded title scales down, it matches // the collapsed width availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth); } else { // Otherwise we'll just use the expanded width availableWidth = expandedWidth; } } if (availableWidth > 0) { updateDrawText = (currentSubtitleTextSize != newTextSize) || boundsChanged || updateDrawText; currentSubtitleTextSize = newTextSize; boundsChanged = false; } if (subtitleToDraw == null || updateDrawText) { subtitleTextPaint.setTextSize(currentSubtitleTextSize); subtitleTextPaint.setTypeface(currentSubtitleTypeface); // Use linear title scaling if we're scaling the canvas subtitleTextPaint.setLinearText(subtitleScale != 1f); // If we don't currently have title to draw, or the title size has changed, ellipsize... final CharSequence text = TextUtils.ellipsize(this.subtitle, subtitleTextPaint, availableWidth, TextUtils.TruncateAt.END); if (!TextUtils.equals(text, subtitleToDraw)) { subtitleToDraw = text; isRtl = calculateIsRtl(subtitleToDraw); } } } private void ensureExpandedTitleTexture() { if (expandedTitleTexture != null || expandedBounds.isEmpty() || TextUtils.isEmpty(titleToDraw)) { return; } calculateOffsets(0f); titleTextureAscent = titleTextPaint.ascent(); titleTextureDescent = titleTextPaint.descent(); final int w = Math.round(titleTextPaint.measureText(titleToDraw, 0, titleToDraw.length())); final int h = Math.round(titleTextureDescent - titleTextureAscent); if (w <= 0 || h <= 0) { return; // If the width or height are 0, return } expandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(expandedTitleTexture); c.drawText(titleToDraw, 0, titleToDraw.length(), 0, h - titleTextPaint.descent(), titleTextPaint); if (titleTexturePaint == null) { // Make sure we have a paint titleTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); } } private void ensureExpandedSubtitleTexture() { if (expandedSubtitleTexture != null || expandedBounds.isEmpty() || TextUtils.isEmpty(subtitleToDraw)) { return; } calculateOffsets(0f); subtitleTextureAscent = subtitleTextPaint.ascent(); subtitleTextureDescent = subtitleTextPaint.descent(); final int w = Math.round(subtitleTextPaint.measureText(subtitleToDraw, 0, subtitleToDraw.length())); final int h = Math.round(subtitleTextureDescent - subtitleTextureAscent); if (w <= 0 || h <= 0) { return; // If the width or height are 0, return } expandedSubtitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(expandedSubtitleTexture); c.drawText(subtitleToDraw, 0, subtitleToDraw.length(), 0, h - subtitleTextPaint.descent(), subtitleTextPaint); if (subtitleTexturePaint == null) { // Make sure we have a paint subtitleTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); } } public void recalculate() { if (view.getHeight() > 0 && view.getWidth() > 0) { // If we've already been laid out, calculate everything now otherwise we'll wait // until a layout calculateBaseOffsets(); calculateCurrentOffsets(); } } /** * Set the title to display * * @param title */ public void setTitle(@Nullable CharSequence title) { if (title == null || !title.equals(this.title)) { this.title = title; titleToDraw = null; clearTexture(); recalculate(); } } @Nullable public CharSequence getTitle() { return title; } /** * Set the subtitle to display * * @param subtitle */ public void setSubtitle(@Nullable CharSequence subtitle) { if (subtitle == null || !subtitle.equals(this.subtitle)) { this.subtitle = subtitle; subtitleToDraw = null; clearTexture(); recalculate(); } } @Nullable public CharSequence getSubtitle() { return subtitle; } private void clearTexture() { if (expandedTitleTexture != null) { expandedTitleTexture.recycle(); expandedTitleTexture = null; } if (expandedSubtitleTexture != null) { expandedSubtitleTexture.recycle(); expandedSubtitleTexture = null; } } /** * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently * defined as it's difference being < 0.001. */ private static boolean isClose(float value, float targetValue) { return Math.abs(value - targetValue) < 0.001f; } public ColorStateList getExpandedTitleTextColor() { return expandedTitleTextColor; } public ColorStateList getExpandedSubtitleTextColor() { return expandedSubtitleTextColor; } public ColorStateList getCollapsedTitleTextColor() { return collapsedTitleTextColor; } public ColorStateList getCollapsedSubtitleTextColor() { return collapsedSubtitleTextColor; } /** * Blend {@code color1} and {@code color2} using the given ratio. * * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend, * 1.0 will return {@code color2}. */ private static int blendColors(int color1, int color2, float ratio) { final float inverseRatio = 1f - ratio; float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio); float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio); float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio); float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio); return Color.argb((int) a, (int) r, (int) g, (int) b); } private static float lerp( float startValue, float endValue, float fraction, @Nullable TimeInterpolator interpolator) { if (interpolator != null) { fraction = interpolator.getInterpolation(fraction); } return AnimationUtils.lerp(startValue, endValue, fraction); } private static boolean rectEquals(@NonNull Rect r, int left, int top, int right, int bottom) { return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/App.java ================================================ /* * 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 */ package org.lsposed.manager; import android.app.ActivityManager; import android.app.Application; import android.content.BroadcastReceiver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.Process; import android.provider.MediaStore; import android.provider.Settings; import android.system.Os; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; import androidx.preference.PreferenceManager; import org.lsposed.hiddenapibypass.HiddenApiBypass; import org.lsposed.manager.adapters.AppHelper; import org.lsposed.manager.receivers.LSPManagerServiceHolder; import org.lsposed.manager.repo.RepoLoader; import org.lsposed.manager.util.CloudflareDNS; import org.lsposed.manager.util.ModuleUtil; import org.lsposed.manager.util.ThemeUtil; import org.lsposed.manager.util.UpdateUtil; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.util.HashMap; import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import okhttp3.Cache; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import rikka.core.os.FileUtils; import rikka.material.app.LocaleDelegate; public class App extends Application { public static final int PER_USER_RANGE = 100000; public static final FutureTask HTML_TEMPLATE = new FutureTask<>(() -> readWebviewHTML("template.html")); public static final FutureTask HTML_TEMPLATE_DARK = new FutureTask<>(() -> readWebviewHTML("template_dark.html")); private static String readWebviewHTML(String name) { try { var input = App.getInstance().getAssets().open("webview/" + name); var result = new ByteArrayOutputStream(1024); FileUtils.copy(input, result); return result.toString(StandardCharsets.UTF_8.name()); } catch (IOException e) { Log.e(App.TAG, "read webview HTML", e); return "@body@"; } } static { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { HiddenApiBypass.addHiddenApiExemptions(""); } Looper.myQueue().addIdleHandler(() -> { if (App.getInstance() == null || App.getExecutorService() == null) return true; App.getExecutorService().submit(() -> { var list = AppHelper.getAppList(false); var pm = App.getInstance().getPackageManager(); list.parallelStream().forEach(i -> AppHelper.getAppLabel(i, pm)); AppHelper.getDenyList(false); ModuleUtil.getInstance(); RepoLoader.getInstance(); }); App.getExecutorService().submit(HTML_TEMPLATE); App.getExecutorService().submit(HTML_TEMPLATE_DARK); return false; }); } public static final String TAG = "LSPosedManager"; private static final String ACTION_USER_ADDED = "android.intent.action.USER_ADDED"; private static final String ACTION_USER_REMOVED = "android.intent.action.USER_REMOVED"; private static final String ACTION_USER_INFO_CHANGED = "android.intent.action.USER_INFO_CHANGED"; private static final String EXTRA_REMOVED_FOR_ALL_USERS = "android.intent.extra.REMOVED_FOR_ALL_USERS"; private static App instance = null; private static OkHttpClient okHttpClient; private static Cache okHttpCache; private SharedPreferences pref; private static final ExecutorService executorService = Executors.newCachedThreadPool(); private static final Handler MainHandler = new Handler(Looper.getMainLooper()); public static App getInstance() { return instance; } public static SharedPreferences getPreferences() { return instance.pref; } public static ExecutorService getExecutorService() { return executorService; } public static final boolean isParasitic = !Process.isApplicationUid(Process.myUid()); public static Handler getMainHandler() { return MainHandler; } @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); var map = new HashMap(1); map.put("isParasitic", String.valueOf(isParasitic)); var am = getSystemService(ActivityManager.class); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { map.clear(); var reasons = am.getHistoricalProcessExitReasons(null, 0, 1); if (reasons.size() == 1) { map.put("description", reasons.get(0).getDescription()); map.put("importance", String.valueOf(reasons.get(0).getImportance())); map.put("process", reasons.get(0).getProcessName()); map.put("reason", String.valueOf(reasons.get(0).getReason())); map.put("status", String.valueOf(reasons.get(0).getStatus())); } } } private void setCrashReport() { var handler = Thread.getDefaultUncaughtExceptionHandler(); Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { var time = OffsetDateTime.now(); var dir = new File(getCacheDir(), "crash"); //noinspection ResultOfMethodCallIgnored dir.mkdir(); var file = new File(dir, time.toEpochSecond() + ".log"); try (var pw = new PrintWriter(file)) { pw.println(BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")"); pw.println(time); pw.println("pid: " + Os.getpid() + " uid: " + Os.getuid()); throwable.printStackTrace(pw); } catch (IOException ignored) { } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { var table = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); var values = new ContentValues(); values.put(MediaStore.Downloads.DISPLAY_NAME, "LSPosed_crash_report" + time.toEpochSecond() + ".zip"); values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOCUMENTS); var cr = getContentResolver(); var uri = cr.insert(table, values); if (uri == null) return; try (var zipFd = cr.openFileDescriptor(uri, "wt")) { LSPManagerServiceHolder.getService().getLogs(zipFd); } catch (Exception ignored) { cr.delete(uri, null, null); } } if (handler != null) { handler.uncaughtException(thread, throwable); } }); } @Override public void onCreate() { super.onCreate(); instance = this; setCrashReport(); pref = PreferenceManager.getDefaultSharedPreferences(this); if (!pref.contains("doh")) { var name = "private_dns_mode"; if ("hostname".equals(Settings.Global.getString(getContentResolver(), name))) { pref.edit().putBoolean("doh", false).apply(); } else { pref.edit().putBoolean("doh", true).apply(); } } AppCompatDelegate.setDefaultNightMode(ThemeUtil.getDarkTheme()); LocaleDelegate.setDefaultLocale(getLocale()); var res = getResources(); var config = res.getConfiguration(); config.setLocale(LocaleDelegate.getDefaultLocale()); //noinspection deprecation res.updateConfiguration(config, res.getDisplayMetrics()); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction("org.lsposed.manager.NOTIFICATION"); registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent inIntent) { var intent = (Intent) inIntent.getParcelableExtra(Intent.EXTRA_INTENT); Log.d(TAG, "onReceive: " + intent); switch (intent.getAction()) { case Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_FULLY_REMOVED, Intent.ACTION_UID_REMOVED -> { var userId = intent.getIntExtra(Intent.EXTRA_USER, 0); var packageName = intent.getStringExtra("android.intent.extra.PACKAGES"); var packageRemovedForAllUsers = intent.getBooleanExtra(EXTRA_REMOVED_FOR_ALL_USERS, false); var isXposedModule = intent.getBooleanExtra("isXposedModule", false); if (packageName != null) { if (isXposedModule) ModuleUtil.getInstance().reloadSingleModule(packageName, userId, packageRemovedForAllUsers); else App.getExecutorService().submit(() -> AppHelper.getAppList(true)); } } case ACTION_USER_ADDED, ACTION_USER_REMOVED, ACTION_USER_INFO_CHANGED -> App.getExecutorService().submit(() -> ModuleUtil.getInstance().reloadInstalledModules()); } } }, intentFilter, Context.RECEIVER_NOT_EXPORTED); UpdateUtil.loadRemoteVersion(); } @NonNull public static OkHttpClient getOkHttpClient() { if (okHttpClient != null) return okHttpClient; var builder = new OkHttpClient.Builder() .cache(getOkHttpCache()) .dns(new CloudflareDNS()); if (BuildConfig.DEBUG) { var log = new HttpLoggingInterceptor(); log.setLevel(HttpLoggingInterceptor.Level.HEADERS); builder.addInterceptor(log); } okHttpClient = builder.build(); return okHttpClient; } @NonNull public static Cache getOkHttpCache() { if (okHttpCache != null) return okHttpCache; long size50MiB = 50 * 1024 * 1024; okHttpCache = new Cache(new File(instance.getCacheDir(), "http_cache"), size50MiB); return okHttpCache; } public static Locale getLocale(String tag) { if (TextUtils.isEmpty(tag) || "SYSTEM".equals(tag)) { return LocaleDelegate.getSystemLocale(); } return Locale.forLanguageTag(tag); } public static Locale getLocale() { String tag = getPreferences().getString("language", null); return getLocale(tag); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ConfigManager.java ================================================ /* * 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.manager; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import org.lsposed.lspd.ILSPManagerService; import org.lsposed.lspd.models.Application; import org.lsposed.lspd.models.UserInfo; import org.lsposed.manager.adapters.ScopeAdapter; import org.lsposed.manager.receivers.LSPManagerServiceHolder; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; public class ConfigManager { public static boolean isBinderAlive() { return LSPManagerServiceHolder.getService() != null; } public static int getXposedApiVersion() { try { return LSPManagerServiceHolder.getService().getXposedApiVersion(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return -1; } } public static String getXposedVersionName() { try { return LSPManagerServiceHolder.getService().getXposedVersionName(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return ""; } } public static int getXposedVersionCode() { try { return LSPManagerServiceHolder.getService().getXposedVersionCode(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return -1; } } public static List getInstalledPackagesFromAllUsers(int flags, boolean filterNoProcess) { List list = new ArrayList<>(); try { list.addAll(LSPManagerServiceHolder.getService().getInstalledPackagesFromAllUsers(flags, filterNoProcess).getList()); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); } return list; } public static String[] getEnabledModules() { try { return LSPManagerServiceHolder.getService().enabledModules(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return new String[0]; } } public static boolean setModuleEnabled(String packageName, boolean enable) { try { return enable ? LSPManagerServiceHolder.getService().enableModule(packageName) : LSPManagerServiceHolder.getService().disableModule(packageName); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static boolean setModuleScope(String packageName, boolean legacy, Set applications) { try { List list = new ArrayList<>(); applications.forEach(application -> { Application app = new Application(); app.userId = application.userId; app.packageName = application.packageName; list.add(app); }); if (legacy) { Application app = new Application(); app.userId = 0; app.packageName = packageName; list.add(app); } return LSPManagerServiceHolder.getService().setModuleScope(packageName, list); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static List getModuleScope(String packageName) { List list = new ArrayList<>(); try { var applications = LSPManagerServiceHolder.getService().getModuleScope(packageName); if (applications == null) { return list; } applications.forEach(application -> { if (!application.packageName.equals(packageName)) { list.add(new ScopeAdapter.ApplicationWithEquals(application)); } }); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); } return list; } public static boolean enableStatusNotification() { try { return LSPManagerServiceHolder.getService().enableStatusNotification(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static boolean setEnableStatusNotification(boolean enabled) { try { LSPManagerServiceHolder.getService().setEnableStatusNotification(enabled); return true; } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static boolean isVerboseLogEnabled() { try { return LSPManagerServiceHolder.getService().isVerboseLog(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static boolean setVerboseLogEnabled(boolean enabled) { try { LSPManagerServiceHolder.getService().setVerboseLog(enabled); return true; } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } 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(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return null; } } public static boolean clearLogs(boolean verbose) { try { return LSPManagerServiceHolder.getService().clearLogs(verbose); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static PackageInfo getPackageInfo(String packageName, int flags, int userId) throws PackageManager.NameNotFoundException { try { var info = LSPManagerServiceHolder.getService().getPackageInfo(packageName, flags, userId); if (info == null) throw new PackageManager.NameNotFoundException(); return info; } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); throw new PackageManager.NameNotFoundException(); } } public static boolean forceStopPackage(String packageName, int userId) { try { LSPManagerServiceHolder.getService().forceStopPackage(packageName, userId); return true; } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static boolean reboot() { try { LSPManagerServiceHolder.getService().reboot(); return true; } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static boolean uninstallPackage(String packageName, int userId) { try { return LSPManagerServiceHolder.getService().uninstallPackage(packageName, userId); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static boolean isSepolicyLoaded() { try { return LSPManagerServiceHolder.getService().isSepolicyLoaded(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static List getUsers() { try { return LSPManagerServiceHolder.getService().getUsers(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return null; } } public static boolean installExistingPackageAsUser(String packageName, int userId) { final int INSTALL_SUCCEEDED = 1; try { var ret = LSPManagerServiceHolder.getService().installExistingPackageAsUser(packageName, userId); return ret == INSTALL_SUCCEEDED; } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static boolean isMagiskInstalled() { var path = System.getenv("PATH"); if (path == null) return false; else return Arrays.stream(path.split(File.pathSeparator)) .anyMatch(str -> new File(str, "magisk").exists()); } public static boolean systemServerRequested() { try { return LSPManagerServiceHolder.getService().systemServerRequested(); } catch (RemoteException e) { return false; } } public static boolean dex2oatFlagsLoaded() { try { return LSPManagerServiceHolder.getService().dex2oatFlagsLoaded(); } catch (RemoteException e) { return false; } } public static int startActivityAsUserWithFeature(Intent intent, int userId) { try { return LSPManagerServiceHolder.getService().startActivityAsUserWithFeature(intent, userId); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return -1; } } public static List queryIntentActivitiesAsUser(Intent intent, int flags, int userId) { List list = new ArrayList<>(); try { list.addAll(LSPManagerServiceHolder.getService().queryIntentActivitiesAsUser(intent, flags, userId).getList()); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); } return list; } public static boolean setHiddenIcon(boolean hide) { try { LSPManagerServiceHolder.getService().setHiddenIcon(hide); return true; } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } 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 { list.addAll(LSPManagerServiceHolder.getService().getDenyListPackages()); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); } return list; } public static void flashZip(String zipPath, ParcelFileDescriptor outputStream) { try { LSPManagerServiceHolder.getService().flashZip(zipPath, outputStream); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); } } 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(); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return ILSPManagerService.DEX2OAT_CRASHED; } } public static boolean getAutoInclude(String packageName) { try { return LSPManagerServiceHolder.getService().getAutoInclude(packageName); } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } public static boolean setAutoInclude(String packageName, boolean enable) { try { LSPManagerServiceHolder.getService().setAutoInclude(packageName, enable); return true; } catch (RemoteException e) { Log.e(App.TAG, Log.getStackTraceString(e)); return false; } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/Constants.java ================================================ /* * 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 */ package org.lsposed.manager; import android.os.IBinder; import org.lsposed.manager.receivers.LSPManagerServiceHolder; public class Constants { public static boolean setBinder(IBinder binder) { LSPManagerServiceHolder.init(binder); return LSPManagerServiceHolder.getService().asBinder().isBinderAlive(); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/adapters/AppHelper.java ================================================ /* * 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 */ package org.lsposed.manager.adapters; import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Parcel; import android.view.MenuItem; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; public class AppHelper { public static final String SETTINGS_CATEGORY = "de.robv.android.xposed.category.MODULE_SETTINGS"; public static final int FLAG_SHOW_FOR_ALL_USERS = 0x0400; private static List denyList; private static List appList; private static final ConcurrentHashMap appLabel = new ConcurrentHashMap<>(); @SuppressLint("WrongConstant") public static Intent getSettingsIntent(String packageName, int userId) { Intent intentToResolve = new Intent(Intent.ACTION_MAIN); intentToResolve.addCategory(SETTINGS_CATEGORY); intentToResolve.setPackage(packageName); List ris = ConfigManager.queryIntentActivitiesAsUser(intentToResolve, 0, userId); if (ris.size() == 0) { return getLaunchIntentForPackage(packageName, userId); } Intent intent = new Intent(intentToResolve); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name); intent.putExtra("lsp_no_switch_to_user", (ris.get(0).activityInfo.flags & FLAG_SHOW_FOR_ALL_USERS) != 0); return intent; } @SuppressLint("WrongConstant") public static Intent getLaunchIntentForPackage(String packageName, int userId) { Intent intentToResolve = new Intent(Intent.ACTION_MAIN); intentToResolve.addCategory(Intent.CATEGORY_INFO); intentToResolve.setPackage(packageName); List ris = ConfigManager.queryIntentActivitiesAsUser(intentToResolve, 0, userId); if (ris.size() == 0) { intentToResolve.removeCategory(Intent.CATEGORY_INFO); intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER); intentToResolve.setPackage(packageName); ris = ConfigManager.queryIntentActivitiesAsUser(intentToResolve, 0, userId); } if (ris.size() == 0) { return null; } Intent intent = new Intent(intentToResolve); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name); intent.putExtra("lsp_no_switch_to_user", (ris.get(0).activityInfo.flags & FLAG_SHOW_FOR_ALL_USERS) != 0); return intent; } public static boolean onOptionsItemSelected(MenuItem item, SharedPreferences preferences) { int itemId = item.getItemId(); int i = preferences.getInt("list_sort", 0); if (itemId == R.id.item_sort_by_name) { i = (i % 2 == 0) ? 0 : 1; } else if (itemId == R.id.item_sort_by_package_name) { i = (i % 2 == 0) ? 2 : 3; } else if (itemId == R.id.item_sort_by_install_time) { i = (i % 2 == 0) ? 4 : 5; } else if (itemId == R.id.item_sort_by_update_time) { i = (i % 2 == 0) ? 6 : 7; } else if (itemId == R.id.reverse) { if (i % 2 == 0) i++; else i--; } else { return false; } preferences.edit().putInt("list_sort", i).apply(); if (item.isCheckable()) item.setChecked(!item.isChecked()); return true; } public static Comparator getAppListComparator(int sort, PackageManager pm) { ApplicationInfo.DisplayNameComparator displayNameComparator = new ApplicationInfo.DisplayNameComparator(pm); return switch (sort) { case 7 -> Collections.reverseOrder(Comparator.comparingLong((PackageInfo a) -> a.lastUpdateTime)); case 6 -> Comparator.comparingLong((PackageInfo a) -> a.lastUpdateTime); case 5 -> Collections.reverseOrder(Comparator.comparingLong((PackageInfo a) -> a.firstInstallTime)); case 4 -> Comparator.comparingLong((PackageInfo a) -> a.firstInstallTime); case 3 -> Collections.reverseOrder(Comparator.comparing(a -> a.packageName)); case 2 -> Comparator.comparing(a -> a.packageName); case 1 -> Collections.reverseOrder((PackageInfo a, PackageInfo b) -> displayNameComparator.compare(a.applicationInfo, b.applicationInfo)); default -> (PackageInfo a, PackageInfo b) -> displayNameComparator.compare(a.applicationInfo, b.applicationInfo); }; } synchronized public static List getAppList(boolean force) { if (appList == null || force) { appList = ConfigManager.getInstalledPackagesFromAllUsers(PackageManager.GET_META_DATA | PackageManager.MATCH_UNINSTALLED_PACKAGES, true); PackageInfo system = null; for (var app : appList) { if ("android".equals(app.packageName)) { var p = Parcel.obtain(); app.writeToParcel(p, 0); p.setDataPosition(0); system = PackageInfo.CREATOR.createFromParcel(p); system.packageName = "system"; system.applicationInfo.packageName = system.packageName; break; } } if (system != null) { appList.add(system); } } return appList; } synchronized public static List getDenyList(boolean force) { if (denyList == null || force) { denyList = ConfigManager.getDenyListPackages(); } return denyList; } public static CharSequence getAppLabel(PackageInfo info, PackageManager pm) { if (info == null || info.applicationInfo == null) return null; return appLabel.computeIfAbsent(info, i -> i.applicationInfo.loadLabel(pm)); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/adapters/ScopeAdapter.java ================================================ /* * 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 */ package org.lsposed.manager.adapters; import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS; import android.annotation.SuppressLint; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.SearchView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.google.android.material.checkbox.MaterialCheckBox; import org.lsposed.lspd.models.Application; import org.lsposed.manager.App; import org.lsposed.manager.BuildConfig; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import org.lsposed.manager.databinding.ItemMasterSwitchBinding; import org.lsposed.manager.databinding.ItemModuleBinding; import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder; import org.lsposed.manager.ui.fragment.AppListFragment; import org.lsposed.manager.ui.fragment.CompileDialogFragment; import org.lsposed.manager.ui.widget.EmptyStateRecyclerView; import org.lsposed.manager.util.GlideApp; import org.lsposed.manager.util.ModuleUtil; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import rikka.core.util.ResourceUtils; import rikka.material.app.LocaleDelegate; import rikka.widget.mainswitchbar.MainSwitchBar; import rikka.widget.mainswitchbar.OnMainSwitchChangeListener; public class ScopeAdapter extends EmptyStateRecyclerView.EmptyStateAdapter implements Filterable { private final Activity activity; private final AppListFragment fragment; private final PackageManager pm; private final SharedPreferences preferences; private final ModuleUtil moduleUtil; private final ModuleUtil.InstalledModule module; private Set recommendedList = new HashSet<>(); private Set checkedList = new HashSet<>(); private List searchList = new ArrayList<>(); private List showList = new ArrayList<>(); private List denyList = new ArrayList<>(); public RecyclerView.Adapter switchAdaptor = new RecyclerView.Adapter<>() { @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new RecyclerView.ViewHolder(ItemMasterSwitchBinding.inflate(activity.getLayoutInflater(), parent, false).masterSwitch) { }; } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { var mainSwitchBar = (MainSwitchBar) holder.itemView; mainSwitchBar.setChecked(enabled); mainSwitchBar.addOnSwitchChangeListener(switchBarOnCheckedChangeListener); } @Override public int getItemCount() { return 1; } }; private final OnMainSwitchChangeListener switchBarOnCheckedChangeListener = new OnMainSwitchChangeListener() { @Override public void onSwitchChanged(Switch view, boolean isChecked) { enabled = isChecked; if (!moduleUtil.setModuleEnabled(module.packageName, isChecked)) { view.setChecked(!isChecked); enabled = !isChecked; } var tmpChkList = new HashSet<>(checkedList); if (isChecked && !tmpChkList.isEmpty() && !ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList)) { view.setChecked(false); enabled = false; } fragment.runOnUiThread(ScopeAdapter.this::notifyDataSetChanged); } }; private ApplicationInfo selectedApplicationInfo; private boolean isLoaded = false; private boolean enabled = true; public ScopeAdapter(AppListFragment fragment, ModuleUtil.InstalledModule module) { this.fragment = fragment; this.activity = fragment.requireActivity(); this.module = module; moduleUtil = ModuleUtil.getInstance(); preferences = App.getPreferences(); pm = activity.getPackageManager(); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(ItemModuleBinding.inflate(activity.getLayoutInflater(), parent, false)); } private boolean shouldHideApp(PackageInfo info, ApplicationWithEquals app, HashSet tmpChkList) { if (info.packageName.equals("system")) { return false; } if (tmpChkList.contains(app)) { return false; } if (preferences.getBoolean("filter_denylist", false)) { if (denyList.contains(info.packageName)) { return true; } } if (preferences.getBoolean("filter_modules", true)) { if (ModuleUtil.getInstance().getModule(info.packageName, info.applicationInfo.uid / App.PER_USER_RANGE) != null) { return true; } } if (preferences.getBoolean("filter_games", true)) { if (info.applicationInfo.category == ApplicationInfo.CATEGORY_GAME) { return true; } //noinspection deprecation if ((info.applicationInfo.flags & ApplicationInfo.FLAG_IS_GAME) != 0) { return true; } } return preferences.getBoolean("filter_system_apps", true) && (info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; } private int sortApps(AppInfo x, AppInfo y) { Comparator comparator = AppHelper.getAppListComparator(preferences.getInt("list_sort", 0), pm); Comparator frameworkComparator = (a, b) -> { if (a.packageName.equals("system") == b.packageName.equals("system")) { return comparator.compare(a.packageInfo, b.packageInfo); } else if (a.packageName.equals("system")) { return -1; } else { return 1; } }; Comparator recommendedComparator = (a, b) -> { boolean aRecommended = !recommendedList.isEmpty() && recommendedList.contains(a.application); boolean bRecommended = !recommendedList.isEmpty() && recommendedList.contains(b.application); if (aRecommended == bRecommended) { return frameworkComparator.compare(a, b); } else if (aRecommended) { return -1; } else { return 1; } }; boolean aChecked = checkedList.contains(x.application); boolean bChecked = checkedList.contains(y.application); if (aChecked == bChecked) { return recommendedComparator.compare(x, y); } else if (aChecked) { return -1; } else { return 1; } } private void checkRecommended() { if (!enabled) { fragment.showHint(R.string.module_is_not_activated_yet, false); return; } fragment.runAsync(() -> { var tmpChkList = new HashSet<>(checkedList); tmpChkList.removeIf(i -> i.userId == module.userId); tmpChkList.addAll(recommendedList); ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList); checkedList = tmpChkList; fragment.runOnUiThread(this::notifyDataSetChanged); }); } @SuppressLint("NotifyDataSetChanged") private void setLoaded(List list, boolean loaded) { fragment.runOnUiThread(() -> { if (list != null) showList = list; isLoaded = loaded; notifyDataSetChanged(); }); } public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.use_recommended) { if (!checkedList.isEmpty()) { new BlurBehindDialogBuilder(activity, R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons) .setMessage(R.string.use_recommended_message) .setPositiveButton(android.R.string.ok, (dialog, which) -> checkRecommended()) .setNegativeButton(android.R.string.cancel, null) .show(); } else { checkRecommended(); } return true; } else if (itemId == R.id.item_filter_system) { item.setChecked(!item.isChecked()); preferences.edit().putBoolean("filter_system_apps", item.isChecked()).apply(); } else if (itemId == R.id.item_filter_games) { item.setChecked(!item.isChecked()); preferences.edit().putBoolean("filter_games", item.isChecked()).apply(); } else if (itemId == R.id.item_filter_modules) { item.setChecked(!item.isChecked()); preferences.edit().putBoolean("filter_modules", item.isChecked()).apply(); } else if (itemId == R.id.item_filter_denylist) { item.setChecked(!item.isChecked()); preferences.edit().putBoolean("filter_denylist", item.isChecked()).apply(); } else if (itemId == R.id.backup) { LocalDateTime now = LocalDateTime.now(); try { fragment.backupLauncher.launch(String.format(LocaleDelegate.getDefaultLocale(), "%s_%s.lsp", module.getAppName(), now.toString())); return true; } catch (ActivityNotFoundException e) { fragment.showHint(R.string.enable_documentui, true); return false; } } else if (itemId == R.id.restore) { try { fragment.restoreLauncher.launch(new String[]{"*/*"}); return true; } catch (ActivityNotFoundException e) { fragment.showHint(R.string.enable_documentui, true); return false; } } else if (itemId == R.id.select_all) { var tmpChkList = new HashSet(ConfigManager.getModuleScope(module.packageName)); for (AppInfo info : searchList) { if (info.packageName.equals("android")) { fragment.showHint(R.string.reboot_required, true, R.string.reboot, v -> ConfigManager.reboot()); } tmpChkList.add(info.application); } ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList); } else if (itemId == R.id.select_none) { var tmpChkList = new HashSet(ConfigManager.getModuleScope(module.packageName)); for (AppInfo info : searchList) { if (tmpChkList.remove(info.application) && info.packageName.equals("android")) { fragment.showHint(R.string.reboot_required, true, R.string.reboot, v -> ConfigManager.reboot()); } } ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList); } else if (itemId == R.id.auto_include) { item.setChecked(!item.isChecked()); ConfigManager.setAutoInclude(module.packageName, item.isChecked()); } else if (!AppHelper.onOptionsItemSelected(item, preferences)) { return false; } refresh(); return true; } public boolean onContextItemSelected(@NonNull MenuItem item) { var info = selectedApplicationInfo; if (info == null) { return false; } int itemId = item.getItemId(); if (itemId == R.id.menu_launch) { Intent launchIntent = AppHelper.getLaunchIntentForPackage(info.packageName, info.uid / App.PER_USER_RANGE); if (launchIntent != null) { ConfigManager.startActivityAsUserWithFeature(launchIntent, module.userId); } } else if (itemId == R.id.menu_compile_speed) { CompileDialogFragment.speed(fragment.getChildFragmentManager(), info); } else if (itemId == R.id.menu_other_app) { var intent = new Intent(Intent.ACTION_SHOW_APP_INFO); intent.putExtra(Intent.EXTRA_PACKAGE_NAME, module.packageName); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ConfigManager.startActivityAsUserWithFeature(intent, module.userId); } else if (itemId == R.id.menu_app_info) { ConfigManager.startActivityAsUserWithFeature(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", info.packageName, null)), module.userId); } else if (itemId == R.id.menu_force_stop) { if (info.packageName.equals("system")) { new BlurBehindDialogBuilder(activity, R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons) .setTitle(R.string.reboot) .setPositiveButton(android.R.string.ok, (dialog, which) -> ConfigManager.reboot()) .setNegativeButton(android.R.string.cancel, null) .show(); } else { new BlurBehindDialogBuilder(activity, R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons) .setTitle(R.string.force_stop_dlg_title) .setMessage(R.string.force_stop_dlg_text) .setPositiveButton(android.R.string.ok, (dialog, which) -> ConfigManager.forceStopPackage(info.packageName, info.uid / 100000)) .setNegativeButton(android.R.string.cancel, null) .show(); } } else { return false; } return true; } public void onPrepareOptionsMenu(@NonNull Menu menu) { List scopeList = module.getScopeList(); if (scopeList == null || scopeList.isEmpty()) { menu.removeItem(R.id.use_recommended); } menu.findItem(R.id.item_filter_system).setChecked(preferences.getBoolean("filter_system_apps", true)); menu.findItem(R.id.item_filter_games).setChecked(preferences.getBoolean("filter_games", true)); menu.findItem(R.id.item_filter_modules).setChecked(preferences.getBoolean("filter_modules", true)); menu.findItem(R.id.item_filter_denylist).setChecked(preferences.getBoolean("filter_denylist", false)); switch (preferences.getInt("list_sort", 0)) { case 7 -> { menu.findItem(R.id.item_sort_by_update_time).setChecked(true); menu.findItem(R.id.reverse).setChecked(true); } case 6 -> menu.findItem(R.id.item_sort_by_update_time).setChecked(true); case 5 -> { menu.findItem(R.id.item_sort_by_install_time).setChecked(true); menu.findItem(R.id.reverse).setChecked(true); } case 4 -> menu.findItem(R.id.item_sort_by_install_time).setChecked(true); case 3 -> { menu.findItem(R.id.item_sort_by_package_name).setChecked(true); menu.findItem(R.id.reverse).setChecked(true); } case 2 -> menu.findItem(R.id.item_sort_by_package_name).setChecked(true); case 1 -> { menu.findItem(R.id.item_sort_by_name).setChecked(true); menu.findItem(R.id.reverse).setChecked(true); } case 0 -> menu.findItem(R.id.item_sort_by_name).setChecked(true); } menu.findItem(R.id.auto_include).setChecked(ConfigManager.getAutoInclude(module.packageName)); } @Override public void onViewRecycled(@NonNull ViewHolder holder) { if (holder.checkbox != null) { holder.checkbox.setOnCheckedChangeListener(null); } super.onViewRecycled(holder); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { AppInfo appInfo = showList.get(position); boolean deny = denyList.contains(appInfo.packageName); holder.root.setAlpha(!deny && enabled ? 1.0f : .5f); boolean system = appInfo.packageName.equals("system"); CharSequence appName; int userId = appInfo.applicationInfo.uid / App.PER_USER_RANGE; appName = system ? activity.getString(R.string.android_framework) : appInfo.label; holder.appName.setText(appName); GlideApp.with(holder.appIcon).load(appInfo.packageInfo).into(new CustomTarget() { @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { holder.appIcon.setImageDrawable(resource); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { holder.appIcon.setImageDrawable(pm.getDefaultActivityIcon()); } }); if (system) { //noinspection SetTextI18n holder.appPackageName.setText("system"); holder.appVersionName.setVisibility(View.GONE); } else { holder.appVersionName.setVisibility(View.VISIBLE); holder.appPackageName.setText(appInfo.packageName); } holder.appPackageName.setVisibility(View.VISIBLE); holder.appVersionName.setText(activity.getString(R.string.app_version, appInfo.packageInfo.versionName)); var sb = new SpannableStringBuilder(); if (!recommendedList.isEmpty() && recommendedList.contains(appInfo.application)) { String recommended = activity.getString(R.string.requested_by_module); sb.append(recommended); final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(activity.getTheme(), com.google.android.material.R.attr.colorPrimary)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL)); sb.setSpan(typefaceSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } else { final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD); sb.setSpan(styleSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } sb.setSpan(foregroundColorSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } if (deny) { if (sb.length() != 0) sb.append("\n"); String denylist = activity.getString(R.string.deny_list_info); sb.append(denylist); final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(activity.getTheme(), com.google.android.material.R.attr.colorError)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL)); sb.setSpan(typefaceSpan, sb.length() - denylist.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } else { final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD); sb.setSpan(styleSpan, sb.length() - denylist.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } sb.setSpan(foregroundColorSpan, sb.length() - denylist.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } if (sb.length() == 0) { holder.hint.setVisibility(View.GONE); } else { holder.hint.setText(sb); holder.hint.setVisibility(View.VISIBLE); } holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { activity.getMenuInflater().inflate(R.menu.menu_app_item, menu); menu.setHeaderTitle(appName); Intent launchIntent = AppHelper.getLaunchIntentForPackage(appInfo.packageName, userId); if (launchIntent == null) { menu.removeItem(R.id.menu_launch); } if (system) { menu.findItem(R.id.menu_force_stop).setTitle(R.string.reboot); menu.removeItem(R.id.menu_compile_speed); menu.removeItem(R.id.menu_other_app); menu.removeItem(R.id.menu_app_info); } }); holder.checkbox.setChecked(checkedList.contains(appInfo.application)); holder.checkbox.setOnCheckedChangeListener((v, isChecked) -> onCheckedChange(v, isChecked, appInfo)); holder.itemView.setOnClickListener(v -> { if (enabled) holder.checkbox.toggle(); }); holder.itemView.setOnLongClickListener(v -> { fragment.searchView.clearFocus(); selectedApplicationInfo = appInfo.applicationInfo; return false; }); } @Override public long getItemId(int position) { PackageInfo info = showList.get(position).packageInfo; return (info.packageName + "!" + info.applicationInfo.uid / App.PER_USER_RANGE).hashCode(); } @Override public Filter getFilter() { return new ApplicationFilter(); } @Override public int getItemCount() { return showList.size(); } public void refresh() { refresh(false); } public void refresh(boolean force) { setLoaded(null, false); enabled = moduleUtil.isModuleEnabled(module.packageName); fragment.runAsync(() -> { List appList = AppHelper.getAppList(force); denyList = AppHelper.getDenyList(force); var tmpRecList = new HashSet(); var tmpChkList = new HashSet<>(ConfigManager.getModuleScope(module.packageName)); final var tmpList = new ArrayList(); final HashSet installedList = new HashSet<>(); List scopeList = module.getScopeList(); boolean emptyCheckedList = tmpChkList.isEmpty(); appList.parallelStream().forEach(info -> { int userId = info.applicationInfo.uid / App.PER_USER_RANGE; String packageName = info.packageName; if (packageName.equals("system") && userId != 0 || packageName.equals(module.packageName) || packageName.equals(BuildConfig.APPLICATION_ID)) { return; } ApplicationWithEquals application = new ApplicationWithEquals(packageName, userId); synchronized (installedList) { installedList.add(application); } if (userId != module.userId) { return; } if (scopeList != null && scopeList.contains(packageName)) { synchronized (tmpRecList) { tmpRecList.add(application); } } else if (shouldHideApp(info, application, tmpChkList)) { return; } AppInfo appInfo = new AppInfo(); appInfo.packageInfo = info; appInfo.label = AppHelper.getAppLabel(info, pm); appInfo.application = application; appInfo.packageName = info.packageName; appInfo.applicationInfo = info.applicationInfo; synchronized (tmpList) { tmpList.add(appInfo); } }); tmpChkList.retainAll(installedList); checkedList = tmpChkList; recommendedList = tmpRecList; searchList = tmpList.parallelStream().sorted(this::sortApps).collect(Collectors.toList()); String queryStr = fragment.searchView != null ? fragment.searchView.getQuery().toString() : ""; fragment.runOnUiThread(() -> getFilter().filter(queryStr)); }); } protected void onCheckedChange(CompoundButton buttonView, boolean isChecked, AppInfo appInfo) { var tmpChkList = new HashSet<>(checkedList); if (isChecked) { tmpChkList.add(appInfo.application); } else { tmpChkList.remove(appInfo.application); } if (!ConfigManager.setModuleScope(module.packageName, module.legacy, tmpChkList)) { fragment.showHint(R.string.failed_to_save_scope_list, true); if (!isChecked) { tmpChkList.add(appInfo.application); } else { tmpChkList.remove(appInfo.application); } buttonView.setChecked(!isChecked); } else if (appInfo.packageName.equals("system")) { fragment.showHint(R.string.reboot_required, true, R.string.reboot, v -> ConfigManager.reboot()); } else if (denyList.contains(appInfo.packageName)) { fragment.showHint(activity.getString(R.string.deny_list, appInfo.label), true); } checkedList = tmpChkList; } @Override public boolean isLoaded() { return isLoaded; } public static class ViewHolder extends RecyclerView.ViewHolder { ConstraintLayout root; ImageView appIcon; TextView appName; TextView appPackageName; TextView appVersionName; TextView hint; MaterialCheckBox checkbox; ViewHolder(ItemModuleBinding binding) { super(binding.getRoot()); root = binding.itemRoot; appIcon = binding.appIcon; appName = binding.appName; appPackageName = binding.appPackageName; appVersionName = binding.appVersionName; checkbox = binding.checkbox; hint = binding.hint; checkbox.setVisibility(View.VISIBLE); } } private class ApplicationFilter extends Filter { private boolean lowercaseContains(String s, String filter) { return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter); } @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults filterResults = new FilterResults(); List filtered = new ArrayList<>(); String filter = constraint.toString().toLowerCase(); for (AppInfo info : searchList) { if (lowercaseContains(info.label.toString(), filter) || lowercaseContains(info.packageName, filter)) { filtered.add(info); } } filterResults.values = filtered; filterResults.count = filtered.size(); return filterResults; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { //noinspection unchecked setLoaded((List) results.values, true); } } public SearchView.OnQueryTextListener getSearchListener() { return new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { getFilter().filter(query); return true; } @Override public boolean onQueryTextChange(String query) { getFilter().filter(query); return true; } }; } public void onBackPressed() { fragment.searchView.clearFocus(); if (isLoaded && enabled && checkedList.isEmpty()) { var builder = new BlurBehindDialogBuilder(activity, R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons); builder.setMessage(!recommendedList.isEmpty() ? R.string.no_scope_selected_has_recommended : R.string.no_scope_selected); if (!recommendedList.isEmpty()) { builder.setPositiveButton(android.R.string.ok, (dialog, which) -> checkRecommended()); } else { builder.setPositiveButton(android.R.string.cancel, null); } builder.setNegativeButton(!recommendedList.isEmpty() ? android.R.string.cancel : android.R.string.ok, (dialog, which) -> { moduleUtil.setModuleEnabled(module.packageName, false); Toast.makeText(activity, activity.getString(R.string.module_disabled_no_selection, module.getAppName()), Toast.LENGTH_LONG).show(); fragment.navigateUp(); }); builder.show(); } else { fragment.navigateUp(); } } public static class AppInfo { public PackageInfo packageInfo; public ApplicationWithEquals application; public ApplicationInfo applicationInfo; public String packageName; public CharSequence label = null; } public static class ApplicationWithEquals extends Application { public ApplicationWithEquals(String packageName, int userId) { this.packageName = packageName; this.userId = userId; } public ApplicationWithEquals(Application application) { packageName = application.packageName; userId = application.userId; } @Override public boolean equals(@Nullable Object obj) { if (!(obj instanceof Application)) { return false; } return packageName.equals(((Application) obj).packageName) && userId == ((Application) obj).userId; } @Override public int hashCode() { return Objects.hash(packageName, userId); } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/receivers/LSPManagerServiceHolder.java ================================================ /* * 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.manager.receivers; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.system.Os; import org.lsposed.lspd.ILSPManagerService; public class LSPManagerServiceHolder implements IBinder.DeathRecipient { private static LSPManagerServiceHolder holder = null; private static ILSPManagerService service = null; public static void init(IBinder binder) { if (holder == null) { holder = new LSPManagerServiceHolder(binder); } } public static ILSPManagerService getService() { return service; } private LSPManagerServiceHolder(IBinder binder) { linkToDeath(binder); service = ILSPManagerService.Stub.asInterface(binder); } private void linkToDeath(IBinder binder) { try { binder.linkToDeath(this, 0); } catch (RemoteException e) { binderDied(); } } @Override public void binderDied() { System.exit(0); Process.killProcess(Os.getpid()); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/repo/RepoLoader.java ================================================ /* * 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 */ package org.lsposed.manager.repo; import android.content.res.Resources; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.gson.Gson; import org.lsposed.manager.App; import org.lsposed.manager.R; import org.lsposed.manager.repo.model.OnlineModule; import org.lsposed.manager.repo.model.Release; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public class RepoLoader { private static RepoLoader instance = null; private Map onlineModules = new HashMap<>(); private Map latestVersion = new ConcurrentHashMap<>(); public static class ModuleVersion { public String versionName; public long versionCode; private ModuleVersion(long versionCode, String versionName) { this.versionName = versionName; this.versionCode = versionCode; } public boolean upgradable(long versionCode, String versionName) { return this.versionCode > versionCode || (this.versionCode == versionCode && !versionName.replace(' ', '_').equals(this.versionName)); } } private final Path repoFile = Paths.get(App.getInstance().getFilesDir().getAbsolutePath(), "repo.json"); private final Set listeners = ConcurrentHashMap.newKeySet(); private boolean repoLoaded = false; private static final String originRepoUrl = "https://modules.lsposed.org/"; private static final String backupRepoUrl = "https://modules-blogcdn.lsposed.org/"; private static final String secondBackupRepoUrl = "https://modules-cloudflare.lsposed.org/"; private static String repoUrl = originRepoUrl; private final Resources resources = App.getInstance().getResources(); private final String[] channels = resources.getStringArray(R.array.update_channel_values); public boolean isRepoLoaded() { return repoLoaded; } public static synchronized RepoLoader getInstance() { if (instance == null) { instance = new RepoLoader(); App.getExecutorService().submit(() -> instance.loadLocalData(true)); } return instance; } synchronized public void loadRemoteData() { repoLoaded = false; try { try (var response = App.getOkHttpClient().newCall(new Request.Builder().url(repoUrl + "modules.json").build()).execute()) { if (response.isSuccessful()) { ResponseBody body = response.body(); if (body != null) { try { String bodyString = body.string(); Files.write(repoFile, bodyString.getBytes(StandardCharsets.UTF_8)); loadLocalData(false); } catch (Throwable t) { Log.e(App.TAG, Log.getStackTraceString(t)); for (RepoListener listener : listeners) { listener.onThrowable(t); } } } } } } catch (Throwable e) { Log.e(App.TAG, "load remote data", e); for (RepoListener listener : listeners) { listener.onThrowable(e); } if (repoUrl.equals(originRepoUrl)) { repoUrl = backupRepoUrl; loadRemoteData(); } else if (repoUrl.equals(backupRepoUrl)) { repoUrl = secondBackupRepoUrl; loadRemoteData(); } } } synchronized public void loadLocalData(boolean updateRemoteRepo) { repoLoaded = false; try { if (Files.notExists(repoFile)) { loadRemoteData(); updateRemoteRepo = false; } byte[] encoded = Files.readAllBytes(repoFile); String bodyString = new String(encoded, StandardCharsets.UTF_8); Gson gson = new Gson(); Map modules = new HashMap<>(); OnlineModule[] repoModules = gson.fromJson(bodyString, OnlineModule[].class); Arrays.stream(repoModules).forEach(onlineModule -> modules.put(onlineModule.getName(), onlineModule)); var channel = App.getPreferences().getString("update_channel", channels[0]); updateLatestVersion(repoModules, channel); onlineModules = modules; } catch (Throwable t) { Log.e(App.TAG, Log.getStackTraceString(t)); for (RepoListener listener : listeners) { listener.onThrowable(t); } } finally { repoLoaded = true; for (RepoListener listener : listeners) { listener.onRepoLoaded(); } if (updateRemoteRepo) loadRemoteData(); } } synchronized private void updateLatestVersion(OnlineModule[] onlineModules, String channel) { repoLoaded = false; Map versions = new ConcurrentHashMap<>(); for (var module : onlineModules) { String release = module.getLatestRelease(); if (channel.equals(channels[1]) && module.getLatestBetaRelease() != null && !module.getLatestBetaRelease().isEmpty()) { release = module.getLatestBetaRelease(); } else if (channel.equals(channels[2])) { if (module.getLatestSnapshotRelease() != null && !module.getLatestSnapshotRelease().isEmpty()) release = module.getLatestSnapshotRelease(); else if (module.getLatestBetaRelease() != null && !module.getLatestBetaRelease().isEmpty()) release = module.getLatestBetaRelease(); } if (release == null || release.isEmpty()) continue; var splits = release.split("-", 2); if (splits.length < 2) continue; long verCode; String verName; try { verCode = Long.parseLong(splits[0]); verName = splits[1]; } catch (NumberFormatException ignored) { continue; } String pkgName = module.getName(); versions.put(pkgName, new ModuleVersion(verCode, verName)); } latestVersion = versions; repoLoaded = true; for (RepoListener listener : listeners) { listener.onRepoLoaded(); } } public void updateLatestVersion(String channel) { if (repoLoaded) updateLatestVersion(onlineModules.keySet().parallelStream().map(onlineModules::get).toArray(OnlineModule[]::new), channel); } @Nullable public ModuleVersion getModuleLatestVersion(String packageName) { return repoLoaded ? latestVersion.getOrDefault(packageName, null) : null; } @Nullable public List getReleases(String packageName) { var channel = App.getPreferences().getString("update_channel", channels[0]); List releases = new ArrayList<>(); if (repoLoaded) { var module = onlineModules.get(packageName); if (module != null) { releases = module.getReleases(); if (!module.releasesLoaded) { if (channel.equals(channels[1]) && !(module.getBetaReleases() != null && module.getBetaReleases().isEmpty())) { releases = module.getBetaReleases(); } else if (channel.equals(channels[2])) if (!(module.getSnapshotReleases() != null && module.getSnapshotReleases().isEmpty())) releases = module.getSnapshotReleases(); else if (!(module.getBetaReleases() != null && module.getBetaReleases().isEmpty())) releases = module.getBetaReleases(); } } } return releases; } @Nullable public String getLatestReleaseTime(String packageName, String channel) { String releaseTime = null; if (repoLoaded) { var module = onlineModules.get(packageName); if (module != null) { releaseTime = module.getLatestReleaseTime(); if (channel.equals(channels[1]) && module.getLatestBetaReleaseTime() != null) { releaseTime = module.getLatestBetaReleaseTime(); } else if (channel.equals(channels[2])) if (module.getLatestSnapshotReleaseTime() != null) releaseTime = module.getLatestSnapshotReleaseTime(); else if (module.getLatestBetaReleaseTime() != null) releaseTime = module.getLatestBetaReleaseTime(); } } return releaseTime; } public void loadRemoteReleases(String packageName) { App.getOkHttpClient().newCall(new Request.Builder().url(String.format(repoUrl + "module/%s.json", packageName)).build()).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.e(App.TAG, call.request().url() + e.getMessage()); if (repoUrl.equals(originRepoUrl)) { repoUrl = backupRepoUrl; loadRemoteReleases(packageName); } else if (repoUrl.equals(backupRepoUrl)) { repoUrl = secondBackupRepoUrl; loadRemoteReleases(packageName); } else { for (RepoListener listener : listeners) { listener.onThrowable(e); } } } @Override public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { ResponseBody body = response.body(); if (body != null) { try { String bodyString = body.string(); Gson gson = new Gson(); OnlineModule module = gson.fromJson(bodyString, OnlineModule.class); module.releasesLoaded = true; onlineModules.replace(packageName, module); for (RepoListener listener : listeners) { listener.onModuleReleasesLoaded(module); } } catch (Throwable t) { Log.e(App.TAG, Log.getStackTraceString(t)); for (RepoListener listener : listeners) { listener.onThrowable(t); } } } } } }); } public void addListener(RepoListener listener) { listeners.add(listener); } public void removeListener(RepoListener listener) { listeners.remove(listener); } @Nullable public OnlineModule getOnlineModule(String packageName) { return repoLoaded && packageName != null ? onlineModules.get(packageName) : null; } @Nullable public Collection getOnlineModules() { return repoLoaded ? onlineModules.values() : null; } public interface RepoListener { default void onRepoLoaded() { } default void onModuleReleasesLoaded(OnlineModule module) { } default void onThrowable(Throwable t) { Log.e(App.TAG, "load repo failed", t); } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/repo/model/Collaborator.java ================================================ /* * 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 */ package org.lsposed.manager.repo.model; import androidx.annotation.Nullable; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; public class Collaborator { @SerializedName("login") @Expose private String login; @SerializedName("name") @Expose private String name; @Nullable public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } @Nullable public String getName() { return name; } public void setName(String name) { this.name = name; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/repo/model/OnlineModule.java ================================================ /* * 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 */ package org.lsposed.manager.repo.model; import androidx.annotation.Nullable; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import java.util.List; public class OnlineModule { @SerializedName("name") @Expose private String name; @SerializedName("description") @Expose private String description; @SerializedName("url") @Expose private String url; @SerializedName("homepageUrl") @Expose private String homepageUrl; @SerializedName("collaborators") @Expose private List collaborators = new ArrayList<>(); @SerializedName("latestRelease") @Expose private String latestRelease; @SerializedName("latestReleaseTime") @Expose private String latestReleaseTime; @SerializedName("latestBetaRelease") @Expose private String latestBetaRelease; @SerializedName("latestBetaReleaseTime") @Expose private String latestBetaReleaseTime; @SerializedName("latestSnapshotRelease") @Expose private String latestSnapshotRelease; @SerializedName("latestSnapshotReleaseTime") @Expose private String latestSnapshotReleaseTime; @SerializedName("releases") @Expose private List releases = new ArrayList<>(); @SerializedName("betaReleases") @Expose private final List betaReleases = new ArrayList<>(); @SerializedName("snapshotReleases") @Expose private final List snapshotReleases = new ArrayList<>(); @SerializedName("readme") @Expose private String readme; @SerializedName("readmeHTML") @Expose private String readmeHTML; @SerializedName("summary") @Expose private String summary; @SerializedName("scope") @Expose private List scope = new ArrayList<>(); @SerializedName("sourceUrl") @Expose private String sourceUrl; @SerializedName("hide") @Expose private Boolean hide; @SerializedName("additionalAuthors") @Expose private List additionalAuthors = null; @SerializedName("updatedAt") @Expose private String updatedAt; @SerializedName("createdAt") @Expose private String createdAt; @SerializedName("stargazerCount") @Expose private Integer stargazerCount; public boolean releasesLoaded = false; @Nullable public String getName() { return name; } public void setName(String name) { this.name = name; } @Nullable public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Nullable public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } @Nullable public String getHomepageUrl() { return homepageUrl; } public void setHomepageUrl(String homepageUrl) { this.homepageUrl = homepageUrl; } @Nullable public List getCollaborators() { return collaborators; } public void setCollaborators(List collaborators) { this.collaborators = collaborators; } @Nullable public List getReleases() { return releases; } @Nullable public String getLatestReleaseTime() { return latestReleaseTime; } public void setReleases(List releases) { this.releases = releases; } @Nullable public String getReadme() { return readme; } public void setReadme(String readme) { this.readme = readme; } @Nullable public String getReadmeHTML() { return readmeHTML; } public void setReadmeHTML(String readmeHTML) { this.readmeHTML = readmeHTML; } @Nullable public String getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } @Nullable public List getScope() { return scope; } public void setScope(List scope) { this.scope = scope; } @Nullable public String getSourceUrl() { return sourceUrl; } public void setSourceUrl(String sourceUrl) { this.sourceUrl = sourceUrl; } public Boolean isHide() { return hide; } public void setHide(Boolean hide) { this.hide = hide; } @Nullable public List getAdditionalAuthors() { return additionalAuthors; } public void setAdditionalAuthors(List additionalAuthors) { this.additionalAuthors = additionalAuthors; } @Nullable public String getUpdatedAt() { return updatedAt; } public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } @Nullable public String getCreatedAt() { return createdAt; } public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } @Nullable public Integer getStargazerCount() { return stargazerCount; } public void setStargazerCount(Integer stargazerCount) { this.stargazerCount = stargazerCount; } @Nullable public String getLatestRelease() { return latestRelease; } public void setLatestRelease(String latestRelease) { this.latestRelease = latestRelease; } @Nullable public String getLatestBetaRelease() { return latestBetaRelease; } @Nullable public String getLatestBetaReleaseTime() { return latestBetaReleaseTime; } @Nullable public String getLatestSnapshotRelease() { return latestSnapshotRelease; } @Nullable public String getLatestSnapshotReleaseTime() { return latestSnapshotReleaseTime; } @Nullable public List getBetaReleases() { return betaReleases; } @Nullable public List getSnapshotReleases() { return snapshotReleases; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/repo/model/Release.java ================================================ /* * 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 */ package org.lsposed.manager.repo.model; import androidx.annotation.Nullable; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; import java.util.ArrayList; import java.util.List; public class Release { @SerializedName("name") @Expose private String name; @SerializedName("url") @Expose private String url; @SerializedName("description") @Expose private String description; @SerializedName("descriptionHTML") @Expose private String descriptionHTML; @SerializedName("createdAt") @Expose private String createdAt; @SerializedName("publishedAt") @Expose private String publishedAt; @SerializedName("updatedAt") @Expose private String updatedAt; @SerializedName("tagName") @Expose private String tagName; @SerializedName("isPrerelease") @Expose private Boolean isPrerelease; @SerializedName("releaseAssets") @Expose private List releaseAssets = new ArrayList<>(); @Nullable public String getName() { return name; } public void setName(String name) { this.name = name; } @Nullable public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } @Nullable public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } @Nullable public String getDescriptionHTML() { return descriptionHTML; } public void setDescriptionHTML(String descriptionHTML) { this.descriptionHTML = descriptionHTML; } @Nullable public String getCreatedAt() { return createdAt; } public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } @Nullable public String getPublishedAt() { return publishedAt; } public void setPublishedAt(String publishedAt) { this.publishedAt = publishedAt; } @Nullable public String getUpdatedAt() { return updatedAt; } public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } @Nullable public String getTagName() { return tagName; } public void setTagName(String tagName) { this.tagName = tagName; } @Nullable public Boolean getIsPrerelease() { return isPrerelease; } public void setIsPrerelease(Boolean isPrerelease) { this.isPrerelease = isPrerelease; } @Nullable public List getReleaseAssets() { return releaseAssets; } public void setReleaseAssets(List releaseAssets) { this.releaseAssets = releaseAssets; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/repo/model/ReleaseAsset.java ================================================ /* * 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 */ package org.lsposed.manager.repo.model; import androidx.annotation.Nullable; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; public class ReleaseAsset { @SerializedName("name") @Expose private String name; @SerializedName("contentType") @Expose private String contentType; @SerializedName("downloadUrl") @Expose private String downloadUrl; @SerializedName("downloadCount") @Expose private int downloadCount = 0; @SerializedName("size") @Expose private int size = 0; @Nullable public String getName() { return name; } public void setName(String name) { this.name = name; } @Nullable public String getContentType() { return contentType; } public void setContentType(String contentType) { this.contentType = contentType; } @Nullable public String getDownloadUrl() { return downloadUrl; } public void setDownloadUrl(String downloadUrl) { this.downloadUrl = downloadUrl; } public int getDownloadCount() { return downloadCount; } public void setDownloadCount(int downloadCount) { this.downloadCount = downloadCount; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/activity/MainActivity.java ================================================ /* * */ package org.lsposed.manager.ui.activity; import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.navigation.NavController; import androidx.navigation.NavOptions; import androidx.navigation.Navigation; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.NavigationUI; import com.google.android.material.navigation.NavigationBarView; import org.lsposed.manager.App; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import org.lsposed.manager.databinding.ActivityMainBinding; import org.lsposed.manager.repo.RepoLoader; import org.lsposed.manager.ui.activity.base.BaseActivity; import org.lsposed.manager.util.ModuleUtil; import org.lsposed.manager.util.ShortcutUtil; import org.lsposed.manager.util.UpdateUtil; import java.util.HashSet; import java.util.Objects; import rikka.core.util.ResourceUtils; public class MainActivity extends BaseActivity implements RepoLoader.RepoListener, ModuleUtil.ModuleListener { private static final String KEY_PREFIX = MainActivity.class.getName() + '.'; private static final String EXTRA_SAVED_INSTANCE_STATE = KEY_PREFIX + "SAVED_INSTANCE_STATE"; private static final RepoLoader repoLoader = RepoLoader.getInstance(); private static final ModuleUtil moduleUtil = ModuleUtil.getInstance(); private boolean restarting; private ActivityMainBinding binding; @NonNull public static Intent newIntent(@NonNull Context context) { return new Intent(context, MainActivity.class); } @NonNull private static Intent newIntent(@NonNull Bundle savedInstanceState, @NonNull Context context) { return newIntent(context) .putExtra(EXTRA_SAVED_INSTANCE_STATE, savedInstanceState); } @Override public void onCreate(Bundle savedInstanceState) { if (savedInstanceState == null) { savedInstanceState = getIntent().getBundleExtra(EXTRA_SAVED_INSTANCE_STATE); } super.onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); repoLoader.addListener(this); moduleUtil.addListener(this); onModulesReloaded(); NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment); if (navHostFragment == null) { return; } NavController navController = navHostFragment.getNavController(); var nav = (NavigationBarView) binding.nav; NavigationUI.setupWithNavController(nav, navController); handleIntent(getIntent()); } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); handleIntent(intent); } private void handleIntent(Intent intent) { if (intent == null) { return; } NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment); if (navHostFragment == null) { return; } NavController navController = navHostFragment.getNavController(); var nav = (NavigationBarView) binding.nav; if (intent.getAction() != null && intent.getAction().equals("android.intent.action.APPLICATION_PREFERENCES")) { nav.setSelectedItemId(R.id.settings_fragment); } else if (ConfigManager.isBinderAlive()) { if (!TextUtils.isEmpty(intent.getDataString())) { switch (intent.getDataString()) { case "modules" -> nav.setSelectedItemId(R.id.modules_nav); case "logs" -> nav.setSelectedItemId(R.id.logs_fragment); case "repo" -> { if (ConfigManager.isMagiskInstalled()) { nav.setSelectedItemId(R.id.repo_nav); } } case "settings" -> nav.setSelectedItemId(R.id.settings_fragment); default -> { var data = intent.getData(); if (data != null && Objects.equals(data.getScheme(), "module")) { navController.navigate( new Uri.Builder().scheme("lsposed").authority("module").appendQueryParameter("modulePackageName", data.getHost()).appendQueryParameter("moduleUserId", String.valueOf(data.getPort())).build(), new NavOptions.Builder().setEnterAnim(R.anim.fragment_enter).setExitAnim(R.anim.fragment_exit).setPopEnterAnim(R.anim.fragment_enter_pop).setPopExitAnim(R.anim.fragment_exit_pop).setLaunchSingleTop(true).setPopUpTo(navController.getGraph().getStartDestinationId(), false, true).build()); } } } } } } @Override public boolean onSupportNavigateUp() { NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment); return navController.navigateUp() || super.onSupportNavigateUp(); } public void restart() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || App.isParasitic) { recreate(); } else { try { Bundle savedInstanceState = new Bundle(); onSaveInstanceState(savedInstanceState); finish(); startActivity(newIntent(savedInstanceState, this)); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); restarting = true; } catch (Throwable e) { recreate(); } } } @Override public boolean dispatchKeyEvent(@NonNull KeyEvent event) { return restarting || super.dispatchKeyEvent(event); } @SuppressLint("RestrictedApi") @Override public boolean dispatchKeyShortcutEvent(@NonNull KeyEvent event) { return restarting || super.dispatchKeyShortcutEvent(event); } @Override public boolean dispatchTouchEvent(@NonNull MotionEvent event) { return restarting || super.dispatchTouchEvent(event); } @Override public boolean dispatchTrackballEvent(@NonNull MotionEvent event) { return restarting || super.dispatchTrackballEvent(event); } @Override public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) { return restarting || super.dispatchGenericMotionEvent(event); } @Override public void onRepoLoaded() { final int[] count = new int[]{0}; HashSet processedModules = new HashSet<>(); var modules = moduleUtil.getModules(); if (modules == null) return; modules.forEach((k, v) -> { if (!processedModules.contains(k.first)) { var ver = repoLoader.getModuleLatestVersion(k.first); if (ver != null && ver.upgradable(v.versionCode, v.versionName)) { ++count[0]; } processedModules.add(k.first); } } ); runOnUiThread(() -> { if (count[0] > 0 && binding != null) { var nav = (NavigationBarView) binding.nav; var badge = nav.getOrCreateBadge(R.id.repo_nav); badge.setVisible(true); badge.setNumber(count[0]); } else { onThrowable(null); } }); } @Override public void onThrowable(Throwable t) { runOnUiThread(() -> { if (binding != null) { var nav = (NavigationBarView) binding.nav; var badge = nav.getOrCreateBadge(R.id.repo_nav); badge.setVisible(false); } }); } @Override public void onModulesReloaded() { onRepoLoaded(); setModulesSummary(moduleUtil.getEnabledModulesCount()); } @Override public void onResume() { super.onResume(); if (ConfigManager.isBinderAlive()) { setModulesSummary(moduleUtil.getEnabledModulesCount()); } else setModulesSummary(0); if (binding != null) { var nav = (NavigationBarView) binding.nav; if (UpdateUtil.needUpdate()) { var badge = nav.getOrCreateBadge(R.id.main_fragment); badge.setVisible(true); } if (!ConfigManager.isBinderAlive()) { nav.getMenu().removeItem(R.id.logs_fragment); nav.getMenu().removeItem(R.id.modules_nav); if (!ConfigManager.isMagiskInstalled()) { nav.getMenu().removeItem(R.id.repo_nav); } } } if (App.isParasitic) { var updateShortcut = ShortcutUtil.updateShortcut(); Log.d(App.TAG, "update shortcut success = " + updateShortcut); } } private void setModulesSummary(int moduleCount) { runOnUiThread(() -> { if (binding != null) { var nav = (NavigationBarView) binding.nav; var badge = nav.getOrCreateBadge(R.id.modules_nav); badge.setBackgroundColor(ResourceUtils.resolveColor(getTheme(), com.google.android.material.R.attr.colorPrimary)); badge.setBadgeTextColor(ResourceUtils.resolveColor(getTheme(), com.google.android.material.R.attr.colorOnPrimary)); if (moduleCount > 0) { badge.setVisible(true); badge.setNumber(moduleCount); } else { badge.setVisible(false); } } }); } @Override protected void onDestroy() { super.onDestroy(); repoLoader.removeListener(this); moduleUtil.removeListener(this); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/activity/base/BaseActivity.java ================================================ /* * 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 */ package org.lsposed.manager.ui.activity.base; import android.app.ActivityManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.os.Bundle; import android.view.Window; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.lsposed.manager.App; import org.lsposed.manager.R; import org.lsposed.manager.util.ThemeUtil; import rikka.material.app.MaterialActivity; public class BaseActivity extends MaterialActivity { private static Bitmap icon = null; @Override public void onCreate(@Nullable Bundle savedInstanceState) { setTheme(R.style.AppTheme); super.onCreate(savedInstanceState); } @Override protected void onStart() { super.onStart(); if (!App.isParasitic) return; for (var task : getSystemService(ActivityManager.class).getAppTasks()) { task.setExcludeFromRecents(false); } if (icon == null) { var drawable = getApplicationInfo().loadIcon(getPackageManager()); if (drawable instanceof BitmapDrawable) { icon = ((BitmapDrawable) drawable).getBitmap(); } else if (drawable instanceof AdaptiveIconDrawable) { icon = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(icon); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); } } setTaskDescription(new ActivityManager.TaskDescription(getTitle().toString(), icon, getColor(R.color.ic_launcher_background))); } @Override public void onApplyUserThemeResource(@NonNull Resources.Theme theme, boolean isDecorView) { if (!ThemeUtil.isSystemAccent()) { theme.applyStyle(ThemeUtil.getColorThemeStyleRes(), true); } theme.applyStyle(ThemeUtil.getNightThemeStyleRes(this), true); theme.applyStyle(rikka.material.preference.R.style.ThemeOverlay_Rikka_Material3_Preference, true); } @Override public String computeUserThemeKey() { return ThemeUtil.getColorTheme() + ThemeUtil.getNightTheme(this); } @Override public void onApplyTranslucentSystemBars() { super.onApplyTranslucentSystemBars(); Window window = getWindow(); window.setStatusBarColor(Color.TRANSPARENT); window.setNavigationBarColor(Color.TRANSPARENT); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/dialog/BlurBehindDialogBuilder.java ================================================ /* * 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.manager.ui.dialog; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; import android.util.Log; import android.view.SurfaceControl; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.view.animation.DecelerateInterpolator; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.lsposed.manager.App; import java.lang.reflect.Method; import java.util.function.Consumer; @SuppressWarnings({"JavaReflectionMemberAccess", "ConstantConditions"}) public class BlurBehindDialogBuilder extends MaterialAlertDialogBuilder { private static final boolean supportBlur = getSystemProperty("ro.surface_flinger.supports_background_blur", false) && !getSystemProperty("persist.sys.sf.disable_blurs", false); public BlurBehindDialogBuilder(@NonNull Context context) { super(context); } public BlurBehindDialogBuilder(@NonNull Context context, int overrideThemeResId) { super(context, overrideThemeResId); } @NonNull @Override public AlertDialog create() { AlertDialog dialog = super.create(); setupWindowBlurListener(dialog); return dialog; } private void setupWindowBlurListener(AlertDialog dialog) { var window = dialog.getWindow(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); Consumer windowBlurEnabledListener = enabled -> updateWindowForBlurs(window, enabled); window.getDecorView().addOnAttachStateChangeListener( new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(@NonNull View v) { window.getWindowManager().addCrossWindowBlurEnabledListener( windowBlurEnabledListener); } @Override public void onViewDetachedFromWindow(@NonNull View v) { window.getWindowManager().removeCrossWindowBlurEnabledListener( windowBlurEnabledListener); } }); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { dialog.setOnShowListener(d -> updateWindowForBlurs(window, supportBlur)); } } private void updateWindowForBlurs(Window window, boolean blursEnabled) { float mDimAmountWithBlur = 0.1f; float mDimAmountNoBlur = 0.32f; window.setDimAmount(blursEnabled ? mDimAmountWithBlur : mDimAmountNoBlur); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { window.getAttributes().setBlurBehindRadius(20); window.setAttributes(window.getAttributes()); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { if (blursEnabled) { View view = window.getDecorView(); ValueAnimator animator = ValueAnimator.ofInt(1, 53); animator.setInterpolator(new DecelerateInterpolator()); try { Object viewRootImpl = view.getClass().getMethod("getViewRootImpl").invoke(view); if (viewRootImpl == null) { return; } SurfaceControl surfaceControl = (SurfaceControl) viewRootImpl.getClass().getMethod("getSurfaceControl").invoke(viewRootImpl); @SuppressLint("BlockedPrivateApi") Method setBackgroundBlurRadius = SurfaceControl.Transaction.class.getDeclaredMethod("setBackgroundBlurRadius", SurfaceControl.class, int.class); animator.addUpdateListener(animation -> { try { SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); var animatedValue = animation.getAnimatedValue(); if (animatedValue != null) { setBackgroundBlurRadius.invoke(transaction, surfaceControl, (int) animatedValue); } transaction.apply(); } catch (Throwable t) { Log.e(App.TAG, "Blur behind dialog builder", t); } }); } catch (Throwable t) { Log.e(App.TAG, "Blur behind dialog builder", t); } view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(@NonNull View v) { } @Override public void onViewDetachedFromWindow(@NonNull View v) { animator.cancel(); } }); animator.start(); } } } public static boolean getSystemProperty(String key, boolean defaultValue) { boolean value = defaultValue; try { Class c = Class.forName("android.os.SystemProperties"); Method get = c.getMethod("getBoolean", String.class, boolean.class); value = (boolean) get.invoke(c, key, defaultValue); } catch (Exception e) { Log.e(App.TAG, "Blur behind dialog builder get system property", e); } return value; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/dialog/FlashDialogBuilder.java ================================================ /* * 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.manager.ui.dialog; import static org.lsposed.manager.App.TAG; import android.content.Context; import android.content.DialogInterface; import android.graphics.Typeface; import android.os.ParcelFileDescriptor; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import com.google.android.material.textview.MaterialTextView; import org.lsposed.manager.App; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import org.lsposed.manager.databinding.DialogTitleBinding; import org.lsposed.manager.databinding.ScrollableDialogBinding; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import rikka.widget.borderview.BorderNestedScrollView; public class FlashDialogBuilder extends BlurBehindDialogBuilder { private final String zipPath; private final TextView textView; private final BorderNestedScrollView rootView; public FlashDialogBuilder(@NonNull Context context, DialogInterface.OnClickListener cancel) { super(context, R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons); var pref = App.getPreferences(); var notes = pref.getString("release_notes", ""); this.zipPath = pref.getString("zip_file", null); LayoutInflater inflater = LayoutInflater.from(context); var title = DialogTitleBinding.inflate(inflater).getRoot(); title.setText(R.string.update_lsposed); setCustomTitle(title); textView = new MaterialTextView(context); var text = notes + "\n\n\n" + context.getString(R.string.update_lsposed_msg) + "\n\n"; textView.setText(text); textView.setMovementMethod(LinkMovementMethod.getInstance()); textView.setTextIsSelectable(true); var binding = ScrollableDialogBinding.inflate(inflater, null, false); binding.dialogContainer.addView(textView); rootView = binding.getRoot(); setView(rootView); title.setOnClickListener(v -> rootView.smoothScrollTo(0, 0)); setNegativeButton(android.R.string.cancel, cancel); setPositiveButton(R.string.install, null); setCancelable(false); } @Override public AlertDialog show() { var dialog = super.show(); var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); rootView.setBorderVisibilityChangedListener((t, ot, b, ob) -> button.setEnabled(!b)); button.setOnClickListener((v) -> { rootView.setBorderVisibilityChangedListener(null); setFlashView(v, dialog); }); return dialog; } private void setFlashView(View view, AlertDialog dialog) { var positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); var negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); positiveButton.setEnabled(false); positiveButton.setText(android.R.string.ok); positiveButton.setOnClickListener((v) -> dialog.dismiss()); negativeButton.setVisibility(View.GONE); textView.setText(""); textView.setTypeface(Typeface.MONOSPACE); App.getExecutorService().submit(() -> flash(view, positiveButton)); } private void flash(View view, Button button) { try { var pipe = ParcelFileDescriptor.createReliablePipe(); var readSide = pipe[0]; var writeSide = pipe[1]; ConfigManager.flashZip(zipPath, writeSide); writeSide.close(); var inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readSide); var reader = new BufferedReader(new InputStreamReader(inputStream)); for (var line = ""; line != null; line = reader.readLine()) { if (line.length() > 0) { var showLine = line + "\n"; view.post(() -> { textView.append(showLine); rootView.fullScroll(View.FOCUS_DOWN); }); } } reader.close(); } catch (IOException e) { Log.e(TAG, "flash", e); view.post(() -> textView.append("\n\n" + e.getMessage())); rootView.fullScroll(View.FOCUS_DOWN); } view.post(() -> button.setEnabled(true)); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/dialog/WelcomeDialog.java ================================================ /* * 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.manager.ui.dialog; import android.app.Dialog; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentManager; import org.lsposed.manager.App; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import org.lsposed.manager.ui.fragment.BaseFragment; import org.lsposed.manager.util.ShortcutUtil; public class WelcomeDialog extends DialogFragment { private static boolean shown = false; private Dialog parasiticDialog(BlurBehindDialogBuilder builder) { var shortcutSupported = ShortcutUtil.isRequestPinShortcutSupported(requireContext()); builder .setTitle(R.string.parasitic_welcome) .setMessage(shortcutSupported ? R.string.parasitic_welcome_summary : R.string.parasitic_welcome_summary_no_shortcut_support) .setNegativeButton(R.string.never_show, (dialog, which) -> App.getPreferences().edit().putBoolean("never_show_welcome", true).apply()) .setPositiveButton(android.R.string.ok, null) .setNeutralButton(R.string.create_shortcut, (dialog, which) -> { var home = (BaseFragment) getParentFragment(); if (!ShortcutUtil.requestPinLaunchShortcut(() -> { App.getPreferences().edit().putBoolean("never_show_welcome", true).apply(); if (home != null) { home.showHint(R.string.settings_shortcut_pinned_hint, false); } })) { if (home != null) { home.showHint(R.string.settings_unsupported_pin_shortcut_summary, false); } } }); return builder.create(); } private Dialog appDialog(BlurBehindDialogBuilder builder) { return builder .setTitle(R.string.app_welcome) .setMessage(R.string.app_welcome_summary) .setNegativeButton(R.string.never_show, (d, w) -> App.getPreferences().edit().putBoolean("never_show_welcome", true).apply()) .setPositiveButton(android.R.string.ok, null) .create(); } @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { var builder = new BlurBehindDialogBuilder(requireContext(), R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons); if (App.isParasitic) { return parasiticDialog(builder); } else { return appDialog(builder); } } public static void showIfNeed(FragmentManager fm) { if (shown) return; if (!ConfigManager.isBinderAlive() || App.getPreferences().getBoolean("never_show_welcome", false) || (App.isParasitic && ShortcutUtil.isLaunchShortcutPinned())) { shown = true; return; } new WelcomeDialog().show(fm, "welcome"); shown = true; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/AppListFragment.java ================================================ /* * */ package org.lsposed.manager.ui.fragment; import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.activity.OnBackPressedCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.SearchView; import androidx.core.view.MenuProvider; import androidx.recyclerview.widget.ConcatAdapter; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import org.lsposed.manager.App; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import org.lsposed.manager.adapters.AppHelper; import org.lsposed.manager.adapters.ScopeAdapter; import org.lsposed.manager.databinding.FragmentAppListBinding; import org.lsposed.manager.util.BackupUtils; import org.lsposed.manager.util.ModuleUtil; import rikka.material.app.LocaleDelegate; import rikka.recyclerview.RecyclerViewKt; public class AppListFragment extends BaseFragment implements MenuProvider { public SearchView searchView; private ScopeAdapter scopeAdapter; private ModuleUtil.InstalledModule module; private SearchView.OnQueryTextListener searchListener; public FragmentAppListBinding binding; public ActivityResultLauncher backupLauncher; public ActivityResultLauncher restoreLauncher; private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { if (binding != null && scopeAdapter != null) { binding.swipeRefreshLayout.setRefreshing(!scopeAdapter.isLoaded()); } } }; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentAppListBinding.inflate(getLayoutInflater(), container, false); if (module == null) { return binding.getRoot(); } binding.appBar.setLiftable(true); String title; if (module.userId != 0) { title = String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", module.getAppName(), module.userId); } else { title = module.getAppName(); } binding.toolbar.setSubtitle(module.packageName); scopeAdapter = new ScopeAdapter(this, module); scopeAdapter.setHasStableIds(true); scopeAdapter.registerAdapterDataObserver(observer); var concatAdapter = new ConcatAdapter(); concatAdapter.addAdapter(scopeAdapter.switchAdaptor); concatAdapter.addAdapter(scopeAdapter); binding.recyclerView.setAdapter(concatAdapter); binding.recyclerView.setHasFixedSize(true); binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity())); binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top)); RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true); binding.swipeRefreshLayout.setOnRefreshListener(() -> scopeAdapter.refresh(true)); binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset()); Intent intent = AppHelper.getSettingsIntent(module.packageName, module.userId); if (intent == null) { binding.fab.setVisibility(View.GONE); } else { binding.fab.setVisibility(View.VISIBLE); binding.fab.setOnClickListener(v -> ConfigManager.startActivityAsUserWithFeature(intent, module.userId)); } searchListener = scopeAdapter.getSearchListener(); setupToolbar(binding.toolbar, binding.clickView, title, R.menu.menu_app_list, view -> requireActivity().getOnBackPressedDispatcher().onBackPressed()); View.OnClickListener l = v -> { if (searchView.isIconified()) { binding.recyclerView.smoothScrollToPosition(0); binding.appBar.setExpanded(true, true); } }; binding.toolbar.setOnClickListener(l); binding.clickView.setOnClickListener(l); return binding.getRoot(); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (module == null) { if (!safeNavigate(R.id.action_app_list_fragment_to_modules_fragment)) { safeNavigate(R.id.modules_nav); } } } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); AppListFragmentArgs args = AppListFragmentArgs.fromBundle(getArguments()); String modulePackageName = args.getModulePackageName(); int moduleUserId = args.getModuleUserId(); module = ModuleUtil.getInstance().getModule(modulePackageName, moduleUserId); if (module == null) { if (!safeNavigate(R.id.action_app_list_fragment_to_modules_fragment)) { safeNavigate(R.id.modules_nav); } } backupLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("application/gzip"), uri -> { if (uri == null) return; runAsync(() -> { try { BackupUtils.backup(uri, modulePackageName); } catch (Exception e) { var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage()); showHint(text, false); } }); }); restoreLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), uri -> { if (uri == null) return; runAsync(() -> { try { BackupUtils.restore(uri, modulePackageName); } catch (Exception e) { var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage()); showHint(text, false); } }); }); requireActivity().getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { scopeAdapter.onBackPressed(); } }); } @Override public void onResume() { super.onResume(); if (scopeAdapter != null) scopeAdapter.refresh(); } @Override public void onDestroyView() { super.onDestroyView(); if (scopeAdapter != null) scopeAdapter.unregisterAdapterDataObserver(observer); binding = null; } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { return scopeAdapter.onOptionsItemSelected(item); } @Override public void onPrepareMenu(@NonNull Menu menu) { searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView(); searchView.setOnQueryTextListener(searchListener); searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View arg0) { binding.appBar.setExpanded(false, true); binding.recyclerView.setNestedScrollingEnabled(false); } @Override public void onViewDetachedFromWindow(View v) { binding.recyclerView.setNestedScrollingEnabled(true); } }); searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT); scopeAdapter.onPrepareOptionsMenu(menu); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { } @Override public boolean onContextItemSelected(@NonNull MenuItem item) { if (scopeAdapter.onContextItemSelected(item)) { return true; } return super.onContextItemSelected(item); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/BaseFragment.java ================================================ /* * */ package org.lsposed.manager.ui.fragment; import android.view.View; import android.widget.Toast; import androidx.annotation.IdRes; import androidx.annotation.StringRes; import androidx.appcompat.widget.Toolbar; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.NavOptions; import androidx.navigation.fragment.NavHostFragment; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; import org.lsposed.manager.App; import org.lsposed.manager.R; import org.lsposed.manager.util.AccessibilityUtils; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; public abstract class BaseFragment extends Fragment { public void navigateUp() { getNavController().navigateUp(); } public NavController getNavController() { return NavHostFragment.findNavController(this); } public boolean safeNavigate(@IdRes int resId) { try { if (!AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver())) { var clearedNavOptions = new NavOptions.Builder().build(); getNavController().navigate(resId, clearedNavOptions); } else { getNavController().navigate(resId); } return true; } catch (IllegalArgumentException ignored) { return false; } } public boolean safeNavigate(NavDirections direction) { try { if (!AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver())) { var clearedNavOptions = new NavOptions.Builder().build(); getNavController().navigate(direction, clearedNavOptions); } else { getNavController().navigate(direction); } return true; } catch (IllegalArgumentException ignored) { return false; } } public void setupToolbar(Toolbar toolbar, View tipsView, int title) { setupToolbar(toolbar, tipsView, getString(title), -1); } public void setupToolbar(Toolbar toolbar, View tipsView, int title, int menu) { setupToolbar(toolbar, tipsView, getString(title), menu, null); } public void setupToolbar(Toolbar toolbar, View tipsView, String title, int menu) { setupToolbar(toolbar, tipsView, title, menu, null); } public void setupToolbar(Toolbar toolbar, View tipsView, String title, int menu, View.OnClickListener navigationOnClickListener) { toolbar.setNavigationOnClickListener(navigationOnClickListener == null ? (v -> navigateUp()) : navigationOnClickListener); toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24); toolbar.setTitle(title); toolbar.setTooltipText(title); if (tipsView != null) tipsView.setTooltipText(title); if (menu != -1) { toolbar.inflateMenu(menu); if (this instanceof MenuProvider self) { toolbar.setOnMenuItemClickListener(self::onMenuItemSelected); self.onPrepareMenu(toolbar.getMenu()); } } } public void runAsync(Runnable runnable) { App.getExecutorService().submit(runnable); } public Future runAsync(Callable callable) { return App.getExecutorService().submit(callable); } public void runOnUiThread(Runnable runnable) { App.getMainHandler().post(runnable); } public Future runOnUiThread(Callable callable) { var task = new FutureTask<>(callable); runOnUiThread(task); return task; } public void showHint(@StringRes int res, boolean lengthShort, @StringRes int actionRes, View.OnClickListener action) { showHint(App.getInstance().getString(res), lengthShort, App.getInstance().getString(actionRes), action); } public void showHint(@StringRes int res, boolean lengthShort) { showHint(App.getInstance().getString(res), lengthShort, null, null); } public void showHint(CharSequence str, boolean lengthShort) { showHint(str, lengthShort, null, null); } public void showHint(CharSequence str, boolean lengthShort, CharSequence actionStr, View.OnClickListener action) { var container = getView(); if (isResumed() && container != null) { var snackbar = Snackbar.make(container, str, lengthShort ? Snackbar.LENGTH_SHORT : Snackbar.LENGTH_LONG); var fab = container.findViewById(R.id.fab); if (fab instanceof FloatingActionButton && ((FloatingActionButton) fab).isOrWillBeShown()) snackbar.setAnchorView(fab); if (actionStr != null && action != null) snackbar.setAction(actionStr, action); snackbar.show(); return; } runOnUiThread(() -> { try { Toast.makeText(App.getInstance(), str, lengthShort ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show(); } catch (Throwable ignored) { } }); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/CompileDialogFragment.java ================================================ /* * 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 */ package org.lsposed.manager.ui.fragment; import android.app.Dialog; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Bundle; import android.view.LayoutInflater; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDialogFragment; import androidx.fragment.app.FragmentManager; import org.lsposed.manager.App; import org.lsposed.manager.R; import org.lsposed.manager.databinding.FragmentCompileDialogBinding; import org.lsposed.manager.receivers.LSPManagerServiceHolder; import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder; import java.lang.ref.WeakReference; @SuppressWarnings("deprecation") public class CompileDialogFragment extends AppCompatDialogFragment { public static void speed(FragmentManager fragmentManager, ApplicationInfo info) { CompileDialogFragment fragment = new CompileDialogFragment(); fragment.setCancelable(false); var bundle = new Bundle(); bundle.putParcelable("appInfo", info); fragment.setArguments(bundle); fragment.show(fragmentManager, "compile_dialog"); } @Override @NonNull public Dialog onCreateDialog(Bundle savedInstanceState) { var arguments = getArguments(); ApplicationInfo appInfo = arguments != null ? arguments.getParcelable("appInfo") : null; if (appInfo == null) { throw new IllegalStateException("appInfo should not be null."); } FragmentCompileDialogBinding binding = FragmentCompileDialogBinding.inflate(LayoutInflater.from(requireActivity()), null, false); final PackageManager pm = requireContext().getPackageManager(); var builder = new BlurBehindDialogBuilder(requireActivity()) .setIcon(appInfo.loadIcon(pm)) .setTitle(appInfo.loadLabel(pm)) .setView(binding.getRoot()); var alertDialog = builder.create(); new CompileTask(this).executeOnExecutor(App.getExecutorService(), appInfo.packageName); return alertDialog; } private static class CompileTask extends AsyncTask { WeakReference outerRef; CompileTask(CompileDialogFragment fragment) { outerRef = new WeakReference<>(fragment); } @Override protected Throwable doInBackground(String... commands) { try { LSPManagerServiceHolder.getService().clearApplicationProfileData(commands[0]); if (LSPManagerServiceHolder.getService().performDexOptMode(commands[0])) { return null; } else { return new UnknownError(); } } catch (Throwable e) { return e; } } @Override protected void onPostExecute(Throwable result) { Context context = App.getInstance(); String text; if (result != null) { if (result instanceof UnknownError) { text = context.getString(R.string.compile_failed); } else { text = context.getString(R.string.compile_failed_with_info) + result; } } else { text = context.getString(R.string.compile_done); } try { CompileDialogFragment fragment = outerRef.get(); if (fragment != null) { fragment.dismissAllowingStateLoss(); var parent = fragment.getParentFragment(); if (parent instanceof BaseFragment) { ((BaseFragment) parent).showHint(text, true); } } } catch (IllegalStateException ignored) { } } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/HomeFragment.java ================================================ /* * */ package org.lsposed.manager.ui.fragment; import android.app.Activity; import android.app.Dialog; import android.os.Build; import android.os.Bundle; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.text.HtmlCompat; import androidx.core.view.MenuProvider; import androidx.fragment.app.DialogFragment; import org.lsposed.lspd.ILSPManagerService; import org.lsposed.manager.BuildConfig; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import org.lsposed.manager.databinding.DialogAboutBinding; import org.lsposed.manager.databinding.FragmentHomeBinding; import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder; import org.lsposed.manager.ui.dialog.FlashDialogBuilder; import org.lsposed.manager.ui.dialog.WelcomeDialog; import org.lsposed.manager.util.NavUtil; import org.lsposed.manager.util.UpdateUtil; import org.lsposed.manager.util.chrome.LinkTransformationMethod; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.HashMap; import java.util.concurrent.atomic.AtomicBoolean; import rikka.core.util.ClipboardUtils; import rikka.material.app.LocaleDelegate; public class HomeFragment extends BaseFragment implements MenuProvider { private FragmentHomeBinding binding; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); WelcomeDialog.showIfNeed(getChildFragmentManager()); } @Override public void onPrepareMenu(Menu menu) { menu.findItem(R.id.menu_about).setOnMenuItemClickListener(v -> { showAbout(); return true; }); menu.findItem(R.id.menu_issue).setOnMenuItemClickListener(v -> { NavUtil.startURL(requireActivity(), "https://github.com/JingMatrix/LSPosed/issues/new/choose"); return true; }); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { } @Override public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { return false; } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FragmentHomeBinding.inflate(inflater, container, false); setupToolbar(binding.toolbar, binding.clickView, R.string.app_name, R.menu.menu_home); binding.toolbar.setNavigationIcon(null); binding.toolbar.setOnClickListener(v -> showAbout()); binding.clickView.setOnClickListener(v -> showAbout()); binding.appBar.setLiftable(true); binding.nestedScrollView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top)); updateStates(requireActivity(), ConfigManager.isBinderAlive(), UpdateUtil.needUpdate()); return binding.getRoot(); } private void updateStates(Activity activity, boolean binderAlive, boolean needUpdate) { if (binderAlive) { if (needUpdate) { binding.updateTitle.setText(R.string.need_update); binding.updateSummary.setText(getString(R.string.please_update_summary)); binding.statusIcon.setImageResource(R.drawable.ic_round_update_24); binding.updateBtn.setOnClickListener(v -> { if (UpdateUtil.canInstall()) { new FlashDialogBuilder(activity, null).show(); } else { NavUtil.startURL(activity, getString(R.string.latest_url)); } }); binding.updateCard.setVisibility(View.VISIBLE); } else { binding.updateCard.setVisibility(View.GONE); } boolean dex2oatAbnormal = ConfigManager.getDex2OatWrapperCompatibility() != ILSPManagerService.DEX2OAT_OK && !ConfigManager.dex2oatFlagsLoaded(); var sepolicyAbnormal = !ConfigManager.isSepolicyLoaded(); var systemServerAbnormal = !ConfigManager.systemServerRequested(); if (sepolicyAbnormal || systemServerAbnormal || dex2oatAbnormal) { binding.statusTitle.setText(R.string.partial_activated); binding.statusIcon.setImageResource(R.drawable.ic_round_warning_24); binding.warningCard.setVisibility(View.VISIBLE); if (sepolicyAbnormal) { binding.warningTitle.setText(R.string.selinux_policy_not_loaded_summary); binding.warningSummary.setText(HtmlCompat.fromHtml(getString(R.string.selinux_policy_not_loaded), HtmlCompat.FROM_HTML_MODE_LEGACY)); } if (systemServerAbnormal) { binding.warningTitle.setText(R.string.system_inject_fail_summary); binding.warningSummary.setText(HtmlCompat.fromHtml(getString(R.string.system_inject_fail), HtmlCompat.FROM_HTML_MODE_LEGACY)); } if (dex2oatAbnormal) { binding.warningTitle.setText(R.string.system_prop_incorrect_summary); binding.warningSummary.setText(HtmlCompat.fromHtml(getString(R.string.system_prop_incorrect), HtmlCompat.FROM_HTML_MODE_LEGACY)); } } else { binding.warningCard.setVisibility(View.GONE); 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.developerWarningCard.setVisibility(isDeveloper() ? View.VISIBLE : View.GONE); } else { boolean isMagiskInstalled = ConfigManager.isMagiskInstalled(); if (isMagiskInstalled) { binding.updateTitle.setText(R.string.install); binding.updateSummary.setText(R.string.install_summary); binding.statusIcon.setImageResource(R.drawable.ic_round_error_outline_24); binding.updateBtn.setOnClickListener(v -> { if (UpdateUtil.canInstall()) { new FlashDialogBuilder(activity, null).show(); } else { NavUtil.startURL(activity, getString(R.string.install_url)); } }); binding.updateCard.setVisibility(View.VISIBLE); } else { binding.updateCard.setVisibility(View.GONE); } binding.warningCard.setVisibility(View.GONE); binding.statusTitle.setText(R.string.not_installed); binding.statusSummary.setText(R.string.not_install_summary); } 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) { binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.android_version_unsatisfied))); } else switch (ConfigManager.getDex2OatWrapperCompatibility()) { case ILSPManagerService.DEX2OAT_OK -> binding.dex2oatWrapper.setText(R.string.supported); case ILSPManagerService.DEX2OAT_CRASHED -> binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.crashed))); case ILSPManagerService.DEX2OAT_MOUNT_FAILED -> binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.mount_failed))); case ILSPManagerService.DEX2OAT_SELINUX_PERMISSIVE -> binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.selinux_permissive))); case ILSPManagerService.DEX2OAT_SEPOLICY_INCORRECT -> binding.dex2oatWrapper.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%s)", getString(R.string.unsupported), getString(R.string.sepolicy_incorrect))); } } 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()); } if (Build.VERSION.PREVIEW_SDK_INT != 0) { binding.systemVersion.setText(String.format(LocaleDelegate.getDefaultLocale(), "%1$s Preview (API %2$d)", Build.VERSION.CODENAME, Build.VERSION.SDK_INT)); } else { binding.systemVersion.setText(String.format(LocaleDelegate.getDefaultLocale(), "%1$s (API %2$d)", Build.VERSION.RELEASE, Build.VERSION.SDK_INT)); } binding.device.setText(getDevice()); binding.systemAbi.setText(Build.SUPPORTED_ABIS[0]); String info = activity.getString(R.string.info_api_version) + "\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() + "\n\n" + activity.getString(R.string.info_framework_version) + "\n" + binding.frameworkVersion.getText() + "\n\n" + activity.getString(R.string.info_manager_package_name) + "\n" + binding.managerPackageName.getText() + "\n\n" + activity.getString(R.string.info_system_version) + "\n" + binding.systemVersion.getText() + "\n\n" + activity.getString(R.string.info_device) + "\n" + binding.device.getText() + "\n\n" + activity.getString(R.string.info_system_abi) + "\n" + 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 -> { ClipboardUtils.put(activity, info); showHint(R.string.info_copied, false); }); } private String getDevice() { String manufacturer = Character.toUpperCase(Build.MANUFACTURER.charAt(0)) + Build.MANUFACTURER.substring(1); if (!Build.BRAND.equals(Build.MANUFACTURER)) { manufacturer += " " + Character.toUpperCase(Build.BRAND.charAt(0)) + Build.BRAND.substring(1); } manufacturer += " " + Build.MODEL + " "; return manufacturer; } private boolean isDeveloper() { var developer = new AtomicBoolean(false); var pids = Paths.get("/data/local/tmp/.studio/ipids"); try (var dir = Files.list(pids)) { dir.findFirst().ifPresent(name -> { var pid = Integer.parseInt(name.getFileName().toString()); try { Os.kill(pid, 0); developer.set(true); } catch (ErrnoException e) { if (e.errno == OsConstants.ESRCH) { try { Files.delete(name); } catch (IOException ignored) { } } else { developer.set(true); } } }); } catch (IOException e) { return false; } return developer.get(); } public static class AboutDialog extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { DialogAboutBinding binding = DialogAboutBinding.inflate(getLayoutInflater(), null, false); binding.designAboutTitle.setText(R.string.app_name); binding.designAboutInfo.setMovementMethod(LinkMovementMethod.getInstance()); binding.designAboutInfo.setTransformationMethod(new LinkTransformationMethod(requireActivity())); binding.designAboutInfo.setText(HtmlCompat.fromHtml(getString( R.string.about_view_source_code, "GitHub", "Telegram"), HtmlCompat.FROM_HTML_MODE_LEGACY)); binding.designAboutVersion.setText(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)); return new BlurBehindDialogBuilder(requireContext()) .setView(binding.getRoot()).create(); } } private void showAbout() { new AboutDialog().show(getChildFragmentManager(), "about"); } @Override public void onDestroyView() { super.onDestroyView(); binding = null; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/LogsFragment.java ================================================ /* * 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.manager.ui.fragment; import android.annotation.SuppressLint; import android.content.ActivityNotFoundException; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.HorizontalScrollView; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.FragmentStateAdapter; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import com.google.android.material.textview.MaterialTextView; import org.lsposed.manager.App; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import org.lsposed.manager.databinding.FragmentPagerBinding; import org.lsposed.manager.databinding.ItemLogTextviewBinding; import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding; import org.lsposed.manager.receivers.LSPManagerServiceHolder; import org.lsposed.manager.ui.widget.EmptyStateRecyclerView; import org.lsposed.manager.util.AccessibilityUtils; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.InputStreamReader; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; import rikka.material.app.LocaleDelegate; import rikka.recyclerview.RecyclerViewKt; public class LogsFragment extends BaseFragment implements MenuProvider { private FragmentPagerBinding binding; private LogPageAdapter adapter; private MenuItem wordWrap; interface OptionsItemSelectListener { boolean onOptionsItemSelected(@NonNull MenuItem item); } private OptionsItemSelectListener optionsItemSelectListener; private final ActivityResultLauncher saveLogsLauncher = registerForActivityResult( new ActivityResultContracts.CreateDocument("application/zip"), uri -> { if (uri == null) return; runAsync(() -> { var context = requireContext(); var cr = context.getContentResolver(); try (var zipFd = cr.openFileDescriptor(uri, "wt")) { showHint(context.getString(R.string.logs_saving), false); LSPManagerServiceHolder.getService().getLogs(zipFd); showHint(context.getString(R.string.logs_saved), true); } catch (Throwable e) { var cause = e.getCause(); var message = cause == null ? e.getMessage() : cause.getMessage(); var text = context.getString(R.string.logs_save_failed2, message); showHint(text, false); Log.w(App.TAG, "save log", e); } }); }); @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentPagerBinding.inflate(inflater, container, false); binding.appBar.setLiftable(true); setupToolbar(binding.toolbar, binding.clickView, R.string.Logs, R.menu.menu_logs); binding.toolbar.setNavigationIcon(null); binding.toolbar.setSubtitle(ConfigManager.isVerboseLogEnabled() ? R.string.enabled_verbose_log : R.string.disabled_verbose_log); adapter = new LogPageAdapter(this); binding.viewPager.setAdapter(adapter); var isAnimationEnabled = AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver()); new TabLayoutMediator( binding.tabLayout, binding.viewPager, // `autoRefresh = true` by default. Update the tabs automatically when the data set of the view pager's // adapter changes. true, isAnimationEnabled, (tab, position) -> tab.setText((int) adapter.getItemId(position)) ).attach(); binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0); int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum(); if (tabLayoutWidth <= binding.getRoot().getWidth()) { binding.tabLayout.setTabMode(TabLayout.MODE_FIXED); binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); } }); return binding.getRoot(); } public void setOptionsItemSelectListener(OptionsItemSelectListener optionsItemSelectListener) { this.optionsItemSelectListener = optionsItemSelectListener; } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { var itemId = item.getItemId(); if (itemId == R.id.menu_save) { save(); return true; } else if (itemId == R.id.menu_word_wrap) { item.setChecked(!item.isChecked()); App.getPreferences().edit().putBoolean("enable_word_wrap", item.isChecked()).apply(); binding.viewPager.setUserInputEnabled(item.isChecked()); adapter.refresh(); return true; } if (optionsItemSelectListener != null) { return optionsItemSelectListener.onOptionsItemSelected(item); } return false; } @Override public void onPrepareMenu(@NonNull Menu menu) { wordWrap = menu.findItem(R.id.menu_word_wrap); wordWrap.setChecked(App.getPreferences().getBoolean("enable_word_wrap", false)); binding.viewPager.setUserInputEnabled(wordWrap.isChecked()); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { } @Override public void onDestroyView() { super.onDestroyView(); binding = null; } private void save() { LocalDateTime now = LocalDateTime.now(); String filename = String.format(LocaleDelegate.getDefaultLocale(), "LSPosed_%s.zip", now.toString()); try { saveLogsLauncher.launch(filename); } catch (ActivityNotFoundException e) { showHint(R.string.enable_documentui, true); } } public static class LogFragment extends BaseFragment { public static final int SCROLL_THRESHOLD = 500; protected boolean verbose; protected SwiperefreshRecyclerviewBinding binding; protected LogAdaptor adaptor; protected LinearLayoutManager layoutManager; class LogAdaptor extends EmptyStateRecyclerView.EmptyStateAdapter { private List log = Collections.emptyList(); private boolean isLoaded = false; @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(ItemLogTextviewBinding.inflate(getLayoutInflater(), parent, false)); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.item.setText(log.get(position)); } @Override public int getItemCount() { return log.size(); } @SuppressLint("NotifyDataSetChanged") void refresh(List log) { runOnUiThread(() -> { isLoaded = true; this.log = log; notifyDataSetChanged(); }); } void fullRefresh() { runAsync(() -> { isLoaded = false; List tmp; try (var parcelFileDescriptor = ConfigManager.getLog(verbose); var br = new BufferedReader(new InputStreamReader(new FileInputStream(parcelFileDescriptor != null ? parcelFileDescriptor.getFileDescriptor() : null)))) { tmp = br.lines().parallel().collect(Collectors.toList()); } catch (Throwable e) { tmp = Arrays.asList(Log.getStackTraceString(e).split("\n")); } refresh(tmp); }); } @Override public boolean isLoaded() { return isLoaded; } class ViewHolder extends RecyclerView.ViewHolder { final MaterialTextView item; public ViewHolder(ItemLogTextviewBinding binding) { super(binding.getRoot()); item = binding.logItem; } } } protected LogAdaptor createAdaptor() { return new LogAdaptor(); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = SwiperefreshRecyclerviewBinding.inflate(getLayoutInflater(), container, false); var arguments = getArguments(); if (arguments == null) return null; verbose = arguments.getBoolean("verbose"); adaptor = createAdaptor(); binding.recyclerView.setAdapter(adaptor); layoutManager = new LinearLayoutManager(requireActivity()); binding.recyclerView.setLayoutManager(layoutManager); // ltr even for rtl languages because of log format binding.recyclerView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset()); RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true); binding.swipeRefreshLayout.setOnRefreshListener(adaptor::fullRefresh); adaptor.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { binding.swipeRefreshLayout.setRefreshing(!adaptor.isLoaded()); } }); adaptor.fullRefresh(); return binding.getRoot(); } public void scrollToTop(LogsFragment logsFragment) { logsFragment.binding.appBar.setExpanded(true, true); if (layoutManager.findFirstVisibleItemPosition() > SCROLL_THRESHOLD) { binding.recyclerView.scrollToPosition(0); } else { binding.recyclerView.smoothScrollToPosition(0); } } public void scrollToBottom(LogsFragment logsFragment) { logsFragment.binding.appBar.setExpanded(false, true); var end = Math.max(adaptor.getItemCount() - 1, 0); if (adaptor.getItemCount() - layoutManager.findLastVisibleItemPosition() > SCROLL_THRESHOLD) { binding.recyclerView.scrollToPosition(end); } else { binding.recyclerView.smoothScrollToPosition(end); } } void attachListeners() { var parent = getParentFragment(); if (parent instanceof LogsFragment logsFragment) { logsFragment.binding.appBar.setLifted(!binding.recyclerView.getBorderViewDelegate().isShowingTopBorder()); binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> logsFragment.binding.appBar.setLifted(!top)); logsFragment.setOptionsItemSelectListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.menu_scroll_top) { scrollToTop(logsFragment); } else if (itemId == R.id.menu_scroll_down) { scrollToBottom(logsFragment); } else if (itemId == R.id.menu_clear) { if (ConfigManager.clearLogs(verbose)) { logsFragment.showHint(R.string.logs_cleared, true); adaptor.fullRefresh(); } else { logsFragment.showHint(R.string.logs_clear_failed_2, true); } return true; } return false; }); View.OnClickListener l = v -> scrollToTop(logsFragment); logsFragment.binding.clickView.setOnClickListener(l); logsFragment.binding.toolbar.setOnClickListener(l); } } void detachListeners() { binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener(null); } @Override public void onStart() { super.onStart(); attachListeners(); } @Override public void onResume() { super.onResume(); attachListeners(); } @Override public void onPause() { super.onPause(); detachListeners(); } @Override public void onStop() { super.onStop(); detachListeners(); } } public static class UnwrapLogFragment extends LogFragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { var root = super.onCreateView(inflater, container, savedInstanceState); binding.swipeRefreshLayout.removeView(binding.recyclerView); HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext()); horizontalScrollView.setFillViewport(true); horizontalScrollView.setHorizontalScrollBarEnabled(false); horizontalScrollView.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); if (!AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver())) { horizontalScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); } binding.swipeRefreshLayout.addView(horizontalScrollView); horizontalScrollView.addView(binding.recyclerView); binding.recyclerView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; return root; } @Override protected LogAdaptor createAdaptor() { return new LogAdaptor() { @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { super.onBindViewHolder(holder, position); var view = holder.item; view.measure(0, 0); int desiredWidth = view.getMeasuredWidth(); ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); layoutParams.width = desiredWidth; if (binding.recyclerView.getWidth() < desiredWidth) { binding.recyclerView.requestLayout(); } } }; } } class LogPageAdapter extends FragmentStateAdapter { public LogPageAdapter(@NonNull Fragment fragment) { super(fragment); } @NonNull @Override public Fragment createFragment(int position) { var bundle = new Bundle(); bundle.putBoolean("verbose", verbose(position)); var f = getItemViewType(position) == 0 ? new LogFragment() : new UnwrapLogFragment(); f.setArguments(bundle); return f; } @Override public int getItemCount() { return 2; } @Override public long getItemId(int position) { return verbose(position) ? R.string.nav_item_logs_verbose : R.string.nav_item_logs_module; } @Override public boolean containsItem(long itemId) { return itemId == R.string.nav_item_logs_verbose || itemId == R.string.nav_item_logs_module; } public boolean verbose(int position) { return position != 0; } @Override public int getItemViewType(int position) { return wordWrap.isChecked() ? 0 : 1; } public void refresh() { runOnUiThread(this::notifyDataSetChanged); } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/ModulesFragment.java ================================================ /* * */ package org.lsposed.manager.ui.fragment; import static android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS; import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Filter; import android.widget.Filterable; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.SearchView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.core.view.MenuProvider; import androidx.fragment.app.Fragment; import androidx.navigation.NavOptions; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.google.android.material.behavior.HideBottomViewOnScrollBehavior; import com.google.android.material.checkbox.MaterialCheckBox; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import org.lsposed.lspd.models.UserInfo; import org.lsposed.manager.App; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import org.lsposed.manager.adapters.AppHelper; import org.lsposed.manager.databinding.FragmentPagerBinding; import org.lsposed.manager.databinding.ItemModuleBinding; import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding; import org.lsposed.manager.repo.RepoLoader; import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder; import org.lsposed.manager.ui.widget.EmptyStateRecyclerView; import org.lsposed.manager.util.GlideApp; import org.lsposed.manager.util.ModuleUtil; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.function.Consumer; import java.util.stream.IntStream; import rikka.core.util.ResourceUtils; import rikka.material.app.LocaleDelegate; import rikka.recyclerview.RecyclerViewKt; public class ModulesFragment extends BaseFragment implements ModuleUtil.ModuleListener, RepoLoader.RepoListener, MenuProvider { private static final PackageManager pm = App.getInstance().getPackageManager(); private static final ModuleUtil moduleUtil = ModuleUtil.getInstance(); private static final RepoLoader repoLoader = RepoLoader.getInstance(); protected FragmentPagerBinding binding; protected SearchView searchView; private SearchView.OnQueryTextListener searchListener; SparseArray adapters = new SparseArray<>(); PagerAdapter pagerAdapter = null; private ModuleUtil.InstalledModule selectedModule; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); searchListener = new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { forEachAdaptor(adapter -> adapter.getFilter().filter(query)); return false; } @Override public boolean onQueryTextChange(String query) { forEachAdaptor(adapter -> adapter.getFilter().filter(query)); return false; } }; } private void forEachAdaptor(Consumer action) { var snapshot = adapters; for (var i = 0; i < snapshot.size(); ++i) { action.accept(snapshot.valueAt(i)); } } private void showFab() { var layoutParams = binding.fab.getLayoutParams(); if (layoutParams instanceof CoordinatorLayout.LayoutParams) { var coordinatorLayoutBehavior = ((CoordinatorLayout.LayoutParams) layoutParams).getBehavior(); if (coordinatorLayoutBehavior instanceof HideBottomViewOnScrollBehavior) { //noinspection unchecked ((HideBottomViewOnScrollBehavior) coordinatorLayoutBehavior).slideUp(binding.fab); } } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentPagerBinding.inflate(inflater, container, false); binding.appBar.setLiftable(true); setupToolbar(binding.toolbar, binding.clickView, R.string.Modules, R.menu.menu_modules); binding.toolbar.setNavigationIcon(null); pagerAdapter = new PagerAdapter(this); binding.viewPager.setAdapter(pagerAdapter); binding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { showFab(); } }); new TabLayoutMediator(binding.tabLayout, binding.viewPager, (tab, position) -> { if (position < adapters.size()) { tab.setText(adapters.valueAt(position).getUser().name); } }).attach(); binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0); int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum(); if (tabLayoutWidth <= binding.getRoot().getWidth()) { binding.tabLayout.setTabMode(TabLayout.MODE_FIXED); binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); } }); binding.fab.setOnClickListener(v -> { var bundle = new Bundle(); var user = adapters.valueAt(binding.viewPager.getCurrentItem()).getUser(); bundle.putParcelable("userInfo", user); var f = new RecyclerViewDialogFragment(); f.setArguments(bundle); f.show(getChildFragmentManager(), "install_to_user" + user.id); }); moduleUtil.addListener(this); repoLoader.addListener(this); onModulesReloaded(); return binding.getRoot(); } @Override public void onPrepareMenu(Menu menu) { searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView(); if (searchView != null) { searchView.setOnQueryTextListener(searchListener); searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(@NonNull View arg0) { binding.appBar.setExpanded(false, true); } @Override public void onViewDetachedFromWindow(@NonNull View v) { } }); searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT); } } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { } @Override public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { return false; } @Override public void onResume() { super.onResume(); forEachAdaptor(ModuleAdapter::refresh); } @Override public void onSingleModuleReloaded(ModuleUtil.InstalledModule module) { forEachAdaptor(ModuleAdapter::refresh); } @Override public void onModulesReloaded() { var users = moduleUtil.getUsers(); if (users == null) return; if (users.size() != 1) { binding.viewPager.setUserInputEnabled(true); binding.tabLayout.setVisibility(View.VISIBLE); binding.fab.show(); } else { binding.viewPager.setUserInputEnabled(false); binding.tabLayout.setVisibility(View.GONE); } var tmp = new SparseArray(users.size()); var snapshot = adapters; for (var user : users) { if (snapshot.indexOfKey(user.id) >= 0) { tmp.put(user.id, snapshot.get(user.id)); } else { var adapter = new ModuleAdapter(user); adapter.setHasStableIds(true); tmp.put(user.id, adapter); } } adapters = tmp; forEachAdaptor(ModuleAdapter::refresh); runOnUiThread(pagerAdapter::notifyDataSetChanged); updateModuleSummary(); } @Override public void onRepoLoaded() { forEachAdaptor(ModuleAdapter::refresh); } private void updateModuleSummary() { var moduleCount = moduleUtil.getEnabledModulesCount(); runOnUiThread(() -> { if (binding != null) { binding.toolbar.setSubtitle(moduleCount == -1 ? getString(R.string.loading) : getResources().getQuantityString(R.plurals.modules_enabled_count, moduleCount, moduleCount)); binding.toolbarLayout.setSubtitle(binding.toolbar.getSubtitle()); } }); } void installModuleToUser(ModuleUtil.InstalledModule module, UserInfo user) { new BlurBehindDialogBuilder(requireActivity(), R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons) .setTitle(getString(R.string.install_to_user, user.name)) .setMessage(getString(R.string.install_to_user_message, module.getAppName(), user.name)) .setPositiveButton(android.R.string.ok, (dialog, which) -> runAsync(() -> { var success = ConfigManager.installExistingPackageAsUser(module.packageName, user.id); String text = success ? getString(R.string.module_installed, module.getAppName(), user.name) : getString(R.string.module_install_failed); showHint(text, false); if (success) moduleUtil.reloadSingleModule(module.packageName, user.id); })) .setNegativeButton(android.R.string.cancel, null) .show(); } @SuppressLint("WrongConstant") @Override public boolean onContextItemSelected(@NonNull MenuItem item) { if (selectedModule == null) { return false; } int itemId = item.getItemId(); if (itemId == R.id.menu_launch) { String packageName = selectedModule.packageName; if (packageName == null) { return false; } Intent intent = AppHelper.getSettingsIntent(packageName, selectedModule.userId); if (intent != null) { ConfigManager.startActivityAsUserWithFeature(intent, selectedModule.userId); } return true; } else if (itemId == R.id.menu_other_app) { var intent = new Intent(Intent.ACTION_SHOW_APP_INFO); intent.putExtra(Intent.EXTRA_PACKAGE_NAME, selectedModule.packageName); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ConfigManager.startActivityAsUserWithFeature(intent, selectedModule.userId); return true; } else if (itemId == R.id.menu_app_info) { ConfigManager.startActivityAsUserWithFeature(new Intent(ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", selectedModule.packageName, null)), selectedModule.userId); return true; } else if (itemId == R.id.menu_uninstall) { new BlurBehindDialogBuilder(requireActivity(), R.style.ThemeOverlay_MaterialAlertDialog_FullWidthButtons) .setIcon(selectedModule.app.loadIcon(pm)) .setTitle(selectedModule.getAppName()) .setMessage(R.string.module_uninstall_message) .setPositiveButton(android.R.string.ok, (dialog, which) -> runAsync(() -> { boolean success = ConfigManager.uninstallPackage(selectedModule.packageName, selectedModule.userId); String text = success ? getString(R.string.module_uninstalled, selectedModule.getAppName()) : getString(R.string.module_uninstall_failed); showHint(text, false); if (success) moduleUtil.reloadSingleModule(selectedModule.packageName, selectedModule.userId); })) .setNegativeButton(android.R.string.cancel, null) .show(); return true; } else if (itemId == R.id.menu_repo) { var navController = getNavController(); navController.navigate( new Uri.Builder().scheme("lsposed").authority("repo").appendQueryParameter("modulePackageName", selectedModule.packageName).build(), new NavOptions.Builder().setEnterAnim(R.anim.fragment_enter).setExitAnim(R.anim.fragment_exit).setPopEnterAnim(R.anim.fragment_enter_pop).setPopExitAnim(R.anim.fragment_exit_pop).setLaunchSingleTop(true).setPopUpTo(getNavController().getGraph().getStartDestinationId(), false, true).build()); return true; } else if (itemId == R.id.menu_compile_speed) { CompileDialogFragment.speed(getChildFragmentManager(), selectedModule.pkg.applicationInfo); } return super.onContextItemSelected(item); } @Override public void onDestroyView() { super.onDestroyView(); moduleUtil.removeListener(this); repoLoader.removeListener(this); binding = null; } public static class ModuleListFragment extends Fragment { public SwiperefreshRecyclerviewBinding binding; private ModuleAdapter adapter; private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { binding.swipeRefreshLayout.setRefreshing(!adapter.isLoaded()); } }; private final View.OnAttachStateChangeListener searchViewLocker = new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(@NonNull View v) { binding.recyclerView.setNestedScrollingEnabled(false); } @Override public void onViewDetachedFromWindow(@NonNull View v) { binding.recyclerView.setNestedScrollingEnabled(true); } }; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { ModulesFragment fragment = (ModulesFragment) getParentFragment(); Bundle arguments = getArguments(); if (fragment == null || arguments == null) { return null; } int userId = arguments.getInt("user_id"); binding = SwiperefreshRecyclerviewBinding.inflate(getLayoutInflater(), container, false); adapter = fragment.adapters.get(userId); binding.recyclerView.setAdapter(adapter); binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity())); binding.swipeRefreshLayout.setOnRefreshListener(adapter::fullRefresh); binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset()); RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true); adapter.registerAdapterDataObserver(observer); return binding.getRoot(); } void attachListeners() { var parent = getParentFragment(); if (parent instanceof ModulesFragment moduleFragment) { binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> moduleFragment.binding.appBar.setLifted(!top)); moduleFragment.binding.appBar.setLifted(!binding.recyclerView.getBorderViewDelegate().isShowingTopBorder()); moduleFragment.searchView.addOnAttachStateChangeListener(searchViewLocker); binding.recyclerView.setNestedScrollingEnabled(moduleFragment.searchView.isIconified()); View.OnClickListener l = v -> { if (moduleFragment.searchView.isIconified()) { binding.recyclerView.smoothScrollToPosition(0); moduleFragment.binding.appBar.setExpanded(true, true); } }; moduleFragment.binding.clickView.setOnClickListener(l); moduleFragment.binding.toolbar.setOnClickListener(l); } } void detachListeners() { binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener(null); var parent = getParentFragment(); if (parent instanceof ModulesFragment moduleFragment) { moduleFragment.searchView.removeOnAttachStateChangeListener(searchViewLocker); binding.recyclerView.setNestedScrollingEnabled(true); } } @Override public void onStart() { super.onStart(); attachListeners(); } @Override public void onResume() { super.onResume(); attachListeners(); } @Override public void onDestroyView() { adapter.unregisterAdapterDataObserver(observer); super.onDestroyView(); } @Override public void onPause() { super.onPause(); detachListeners(); } @Override public void onStop() { super.onStop(); detachListeners(); } } private class PagerAdapter extends FragmentStateAdapter { public PagerAdapter(@NonNull Fragment fragment) { super(fragment); } @NonNull @Override public Fragment createFragment(int position) { Bundle bundle = new Bundle(); bundle.putInt("user_id", adapters.keyAt(position)); Fragment fragment = new ModuleListFragment(); fragment.setArguments(bundle); return fragment; } @Override public int getItemCount() { return adapters.size(); } @Override public long getItemId(int position) { return adapters.keyAt(position); } @Override public boolean containsItem(long itemId) { return adapters.indexOfKey((int) itemId) >= 0; } } ModuleAdapter createPickModuleAdapter(UserInfo userInfo) { return new ModuleAdapter(userInfo, true); } class ModuleAdapter extends EmptyStateRecyclerView.EmptyStateAdapter implements Filterable { private List searchList = new ArrayList<>(); private List showList = new ArrayList<>(); private final UserInfo user; private final boolean isPick; private boolean isLoaded; private View.OnClickListener onPickListener; ModuleAdapter(UserInfo user) { this(user, false); } ModuleAdapter(UserInfo user, boolean isPick) { this.user = user; this.isPick = isPick; } public UserInfo getUser() { return user; } @NonNull @Override public ModuleAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(ItemModuleBinding.inflate(getLayoutInflater(), parent, false)); } public boolean isPick() { return isPick; } @Override public void onBindViewHolder(@NonNull ModuleAdapter.ViewHolder holder, int position) { ModuleUtil.InstalledModule item = showList.get(position); String appName; if (item.userId != 0) { appName = String.format(LocaleDelegate.getDefaultLocale(), "%s (%d)", item.getAppName(), item.userId); } else { appName = item.getAppName(); } holder.appName.setText(appName); GlideApp.with(holder.appIcon) .load(item.getPackageInfo()) .into(new CustomTarget() { @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { holder.appIcon.setImageDrawable(resource); } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); SpannableStringBuilder sb = new SpannableStringBuilder(); if (!item.getDescription().isEmpty()) { sb.append(item.getDescription()); } else { sb.append(getString(R.string.module_empty_description)); } holder.appDescription.setText(sb); holder.appDescription.setVisibility(View.VISIBLE); sb = new SpannableStringBuilder(); int installXposedVersion = ConfigManager.getXposedApiVersion(); String warningText = null; if (item.minVersion == 0) { warningText = getString(R.string.no_min_version_specified); } else if (installXposedVersion > 0 && item.minVersion > installXposedVersion) { warningText = getString(R.string.warning_xposed_min_version, item.minVersion); } else if (item.targetVersion > installXposedVersion) { warningText = getString(R.string.warning_target_version_higher, item.targetVersion); } else if (item.minVersion < ModuleUtil.MIN_MODULE_VERSION) { warningText = getString(R.string.warning_min_version_too_low, item.minVersion, ModuleUtil.MIN_MODULE_VERSION); } else if (item.isInstalledOnExternalStorage()) { warningText = getString(R.string.warning_installed_on_external_storage); } if (warningText != null) { sb.append(warningText); final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), com.google.android.material.R.attr.colorError)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL)); sb.setSpan(typefaceSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } else { final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD); sb.setSpan(styleSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } sb.setSpan(foregroundColorSpan, sb.length() - warningText.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } var ver = repoLoader.getModuleLatestVersion(item.packageName); if (ver != null && ver.upgradable(item.versionCode, item.versionName)) { if (warningText != null) sb.append("\n"); String recommended = getString(R.string.update_available, ver.versionName); sb.append(recommended); final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), androidx.appcompat.R.attr.colorPrimary)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL)); sb.setSpan(typefaceSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } else { final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD); sb.setSpan(styleSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } sb.setSpan(foregroundColorSpan, sb.length() - recommended.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } if (sb.length() == 0) { holder.hint.setVisibility(View.GONE); } else { holder.hint.setVisibility(View.VISIBLE); holder.hint.setText(sb); } if (!isPick) { holder.root.setAlpha(moduleUtil.isModuleEnabled(item.packageName) ? 1.0f : .5f); holder.itemView.setOnClickListener(v -> { searchView.clearFocus(); if (isLoaded()) { safeNavigate(ModulesFragmentDirections.actionModulesFragmentToAppListFragment(item.packageName, item.userId)); } }); holder.itemView.setOnLongClickListener(v -> { searchView.clearFocus(); selectedModule = item; return false; }); holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { requireActivity().getMenuInflater().inflate(R.menu.context_menu_modules, menu); menu.setHeaderTitle(item.getAppName()); Intent intent = AppHelper.getSettingsIntent(item.packageName, item.userId); if (intent == null) { menu.removeItem(R.id.menu_launch); } if (repoLoader.getOnlineModule(item.packageName) == null) { menu.removeItem(R.id.menu_repo); } if (item.userId == 0) { var users = ConfigManager.getUsers(); if (users != null) { for (var user : users) { if (moduleUtil.getModule(item.packageName, user.id) == null) { menu.add(1, user.id, 0, getString(R.string.install_to_user, user.name)).setOnMenuItemClickListener(i -> { installModuleToUser(selectedModule, user); return true; }); } } } } }); holder.appVersion.setVisibility(View.VISIBLE); holder.appVersion.setText(item.versionName); holder.appVersion.setSelected(true); } else { holder.itemView.setTag(item); holder.itemView.setOnClickListener(v -> { if (onPickListener != null) onPickListener.onClick(v); }); } } @Override public void onViewRecycled(@NonNull ViewHolder holder) { holder.itemView.setTag(null); super.onViewRecycled(holder); } @Override public int getItemCount() { return showList.size(); } @Override public long getItemId(int position) { var module = showList.get(position); return (module.packageName + "!" + module.userId).hashCode(); } @Override public Filter getFilter() { return new ModuleAdapter.ApplicationFilter(); } public void setOnPickListener(View.OnClickListener onPickListener) { this.onPickListener = onPickListener; } public void refresh() { runAsync(reloadModules); } public void fullRefresh() { runAsync(() -> { setLoaded(null, false); moduleUtil.reloadInstalledModules(); refresh(); }); } private final Runnable reloadModules = () -> { var modules = moduleUtil.getModules(); if (modules == null) return; Comparator cmp = AppHelper.getAppListComparator(0, pm); setLoaded(null, false); var tmpList = new ArrayList(); modules.values().parallelStream() .sorted((a, b) -> { boolean aChecked = moduleUtil.isModuleEnabled(a.packageName); boolean bChecked = moduleUtil.isModuleEnabled(b.packageName); if (aChecked == bChecked) { var c = cmp.compare(a.pkg, b.pkg); if (c == 0) { if (a.userId == getUser().id) return -1; if (b.userId == getUser().id) return 1; else return Integer.compare(a.userId, b.userId); } return c; } else if (aChecked) { return -1; } else { return 1; } }).forEachOrdered(new Consumer<>() { private final HashSet uniquer = new HashSet<>(); @Override public void accept(ModuleUtil.InstalledModule module) { if (isPick()) { if (!uniquer.contains(module.packageName)) { uniquer.add(module.packageName); if (module.userId != getUser().id) tmpList.add(module); } } else if (module.userId == getUser().id) { tmpList.add(module); } } }); String queryStr = searchView != null ? searchView.getQuery().toString() : ""; searchList = tmpList; runOnUiThread(() -> getFilter().filter(queryStr)); }; @SuppressLint("NotifyDataSetChanged") private void setLoaded(List list, boolean loaded) { runOnUiThread(() -> { if (list != null) showList = list; isLoaded = loaded; notifyDataSetChanged(); }); } @Override public boolean isLoaded() { return isLoaded && moduleUtil.isModulesLoaded(); } static class ViewHolder extends RecyclerView.ViewHolder { ConstraintLayout root; ImageView appIcon; TextView appName; TextView appDescription; TextView appVersion; TextView hint; MaterialCheckBox checkBox; ViewHolder(ItemModuleBinding binding) { super(binding.getRoot()); root = binding.itemRoot; appIcon = binding.appIcon; appName = binding.appName; appDescription = binding.description; appVersion = binding.versionName; hint = binding.hint; checkBox = binding.checkbox; } } class ApplicationFilter extends Filter { private boolean lowercaseContains(String s, String filter) { return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter); } @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults filterResults = new FilterResults(); List filtered = new ArrayList<>(); String filter = constraint.toString().toLowerCase(); for (ModuleUtil.InstalledModule info : searchList) { if (lowercaseContains(info.getAppName(), filter) || lowercaseContains(info.packageName, filter) || lowercaseContains(info.getDescription(), filter)) { filtered.add(info); } } filterResults.values = filtered; filterResults.count = filtered.size(); return filterResults; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { //noinspection unchecked setLoaded((List) results.values, true); } } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/RecyclerViewDialogFragment.java ================================================ /* * 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.manager.ui.fragment; import android.app.Dialog; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDialogFragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import org.lsposed.lspd.models.UserInfo; import org.lsposed.manager.R; import org.lsposed.manager.databinding.DialogTitleBinding; import org.lsposed.manager.databinding.SwiperefreshRecyclerviewBinding; import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder; import org.lsposed.manager.util.ModuleUtil; public class RecyclerViewDialogFragment extends AppCompatDialogFragment { @Override @NonNull public Dialog onCreateDialog(Bundle savedInstanceState) { var parent = getParentFragment(); var arguments = getArguments(); if (!(parent instanceof ModulesFragment) || arguments == null) { throw new IllegalStateException(); } var modulesFragment = (ModulesFragment) parent; var user = (UserInfo) arguments.getParcelable("userInfo"); var pickAdaptor = modulesFragment.createPickModuleAdapter(user); var binding = SwiperefreshRecyclerviewBinding.inflate(LayoutInflater.from(requireActivity()), null, false); binding.recyclerView.setAdapter(pickAdaptor); binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity())); pickAdaptor.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { binding.swipeRefreshLayout.setRefreshing(!pickAdaptor.isLoaded()); } }); binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset()); binding.swipeRefreshLayout.setOnRefreshListener(pickAdaptor::fullRefresh); pickAdaptor.refresh(); var title = DialogTitleBinding.inflate(getLayoutInflater()).getRoot(); title.setText(getString(R.string.install_to_user, user.name)); var dialog = new BlurBehindDialogBuilder(requireActivity(), R.style.ThemeOverlay_MaterialAlertDialog_FullWidthButtons) .setCustomTitle(title) .setView(binding.getRoot()) .setNegativeButton(android.R.string.cancel, null) .create(); title.setOnClickListener(s -> binding.recyclerView.smoothScrollToPosition(0)); pickAdaptor.setOnPickListener(picked -> { var module = (ModuleUtil.InstalledModule) picked.getTag(); modulesFragment.installModuleToUser(module, user); dialog.dismiss(); }); onViewCreated(binding.getRoot(), savedInstanceState); return dialog; } // prevent from overriding public final void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/RepoFragment.java ================================================ /* * */ package org.lsposed.manager.ui.fragment; import android.annotation.SuppressLint; import android.content.res.Resources; import android.graphics.Typeface; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.webkit.WebView; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.SearchView; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.view.MenuProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import org.lsposed.manager.App; import org.lsposed.manager.R; import org.lsposed.manager.databinding.FragmentRepoBinding; import org.lsposed.manager.databinding.ItemOnlinemoduleBinding; import org.lsposed.manager.repo.RepoLoader; import org.lsposed.manager.repo.model.OnlineModule; import org.lsposed.manager.ui.widget.EmptyStateRecyclerView; import org.lsposed.manager.util.ModuleUtil; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import rikka.core.util.LabelComparator; import rikka.core.util.ResourceUtils; import rikka.recyclerview.RecyclerViewKt; public class RepoFragment extends BaseFragment implements RepoLoader.RepoListener, ModuleUtil.ModuleListener, MenuProvider { protected FragmentRepoBinding binding; protected SearchView searchView; private SearchView.OnQueryTextListener mSearchListener; private final Handler mHandler = new Handler(Looper.getMainLooper()); private boolean preLoadWebview = true; private final RepoLoader repoLoader = RepoLoader.getInstance(); private final ModuleUtil moduleUtil = ModuleUtil.getInstance(); private RepoAdapter adapter; private final RecyclerView.AdapterDataObserver observer = new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { binding.swipeRefreshLayout.setRefreshing(!adapter.isLoaded()); } }; @Override public void onCreate(@Nullable Bundle savedInstanceState) { mSearchListener = new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { adapter.getFilter().filter(query); return false; } @Override public boolean onQueryTextChange(String newText) { adapter.getFilter().filter(newText); return false; } }; super.onCreate(savedInstanceState); } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentRepoBinding.inflate(getLayoutInflater(), container, false); binding.appBar.setLiftable(true); binding.recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> binding.appBar.setLifted(!top)); setupToolbar(binding.toolbar, binding.clickView, R.string.module_repo, R.menu.menu_repo); binding.toolbar.setNavigationIcon(null); adapter = new RepoAdapter(); adapter.setHasStableIds(true); adapter.registerAdapterDataObserver(observer); binding.recyclerView.setAdapter(adapter); binding.recyclerView.setHasFixedSize(true); binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity())); RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true); binding.swipeRefreshLayout.setOnRefreshListener(adapter::fullRefresh); binding.swipeRefreshLayout.setProgressViewEndTarget(true, binding.swipeRefreshLayout.getProgressViewEndOffset()); View.OnClickListener l = v -> { if (searchView.isIconified()) { binding.recyclerView.smoothScrollToPosition(0); binding.appBar.setExpanded(true, true); } }; binding.toolbar.setOnClickListener(l); binding.clickView.setOnClickListener(l); repoLoader.addListener(this); moduleUtil.addListener(this); onRepoLoaded(); return binding.getRoot(); } private void updateRepoSummary() { final int[] count = new int[]{0}; HashSet processedModules = new HashSet<>(); var modules = moduleUtil.getModules(); if (modules != null && repoLoader.isRepoLoaded()) { modules.forEach((k, v) -> { if (!processedModules.contains(k.first)) { var ver = repoLoader.getModuleLatestVersion(k.first); if (ver != null && ver.upgradable(v.versionCode, v.versionName)) { ++count[0]; } processedModules.add(k.first); } } ); } else { count[0] = -1; } runOnUiThread(() -> { if (binding != null) { if (count[0] > 0) { binding.toolbar.setSubtitle(getResources().getQuantityString(R.plurals.module_repo_upgradable, count[0], count[0])); } else if (count[0] == 0) { binding.toolbar.setSubtitle(getResources().getString(R.string.module_repo_up_to_date)); } else { binding.toolbar.setSubtitle(getResources().getString(R.string.loading)); } binding.toolbarLayout.setSubtitle(binding.toolbar.getSubtitle()); } }); } @Override public void onPrepareMenu(Menu menu) { searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView(); if (searchView != null) { searchView.setOnQueryTextListener(mSearchListener); searchView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(@NonNull View arg0) { binding.appBar.setExpanded(false, true); binding.recyclerView.setNestedScrollingEnabled(false); } @Override public void onViewDetachedFromWindow(@NonNull View v) { binding.recyclerView.setNestedScrollingEnabled(true); } }); searchView.findViewById(androidx.appcompat.R.id.search_edit_frame).setLayoutDirection(View.LAYOUT_DIRECTION_INHERIT); } int sort = App.getPreferences().getInt("repo_sort", 0); if (sort == 0) { menu.findItem(R.id.item_sort_by_name).setChecked(true); } else if (sort == 1) { menu.findItem(R.id.item_sort_by_update_time).setChecked(true); } menu.findItem(R.id.item_upgradable_first).setChecked(App.getPreferences().getBoolean("upgradable_first", true)); } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { } @Override public void onDestroyView() { super.onDestroyView(); mHandler.removeCallbacksAndMessages(null); repoLoader.removeListener(this); moduleUtil.removeListener(this); adapter.unregisterAdapterDataObserver(observer); binding = null; } @Override public void onResume() { super.onResume(); adapter.refresh(); if (preLoadWebview) { mHandler.postDelayed(() -> new WebView(requireContext()), 500); preLoadWebview = false; } } @Override public void onRepoLoaded() { if (adapter != null) { adapter.refresh(); } updateRepoSummary(); } @Override public void onThrowable(Throwable t) { showHint(getString(R.string.repo_load_failed, t.getLocalizedMessage()), true); updateRepoSummary(); } @Override public void onModulesReloaded() { updateRepoSummary(); } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.item_sort_by_name) { item.setChecked(true); App.getPreferences().edit().putInt("repo_sort", 0).apply(); adapter.refresh(); } else if (itemId == R.id.item_sort_by_update_time) { item.setChecked(true); App.getPreferences().edit().putInt("repo_sort", 1).apply(); adapter.refresh(); } else if (itemId == R.id.item_upgradable_first) { item.setChecked(!item.isChecked()); App.getPreferences().edit().putBoolean("upgradable_first", item.isChecked()).apply(); adapter.refresh(); } else { return false; } return true; } private class RepoAdapter extends EmptyStateRecyclerView.EmptyStateAdapter implements Filterable { private List fullList, showList; private final LabelComparator labelComparator = new LabelComparator(); private boolean isLoaded = false; private final Resources resources = App.getInstance().getResources(); private final String[] channels = resources.getStringArray(R.array.update_channel_values); private String channel; private final RepoLoader repoLoader = RepoLoader.getInstance(); RepoAdapter() { fullList = showList = Collections.emptyList(); } @NonNull @Override public RepoAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(ItemOnlinemoduleBinding.inflate(getLayoutInflater(), parent, false)); } RepoLoader.ModuleVersion getUpgradableVer(OnlineModule module) { ModuleUtil.InstalledModule installedModule = moduleUtil.getModule(module.getName()); if (installedModule != null) { var ver = repoLoader.getModuleLatestVersion(installedModule.packageName); if (ver != null && ver.upgradable(installedModule.versionCode, installedModule.versionName)) return ver; } return null; } @Override public void onBindViewHolder(@NonNull RepoAdapter.ViewHolder holder, int position) { OnlineModule module = showList.get(position); holder.appName.setText(module.getDescription()); holder.appPackageName.setText(module.getName()); Instant instant; channel = App.getPreferences().getString("update_channel", channels[0]); var latestReleaseTime = repoLoader.getLatestReleaseTime(module.getName(), channel); instant = Instant.parse(latestReleaseTime != null ? latestReleaseTime : module.getLatestReleaseTime()); var formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) .withLocale(App.getLocale()).withZone(ZoneId.systemDefault()); holder.publishedTime.setText(String.format(getString(R.string.module_repo_updated_time), formatter.format(instant))); SpannableStringBuilder sb = new SpannableStringBuilder(); String summary = module.getSummary(); if (summary != null) { sb.append(summary); } holder.appDescription.setVisibility(View.VISIBLE); holder.appDescription.setText(sb); sb = new SpannableStringBuilder(); var upgradableVer = getUpgradableVer(module); if (upgradableVer != null) { String hint = getString(R.string.update_available, upgradableVer.versionName); sb.append(hint); final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), com.google.android.material.R.attr.colorPrimary)); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { final TypefaceSpan typefaceSpan = new TypefaceSpan(Typeface.create("sans-serif-medium", Typeface.NORMAL)); sb.setSpan(typefaceSpan, sb.length() - hint.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } else { final StyleSpan styleSpan = new StyleSpan(Typeface.BOLD); sb.setSpan(styleSpan, sb.length() - hint.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } sb.setSpan(foregroundColorSpan, sb.length() - hint.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } else if (moduleUtil.getModule(module.getName()) != null) { String installed = getString(R.string.installed); sb.append(installed); final StyleSpan styleSpan = new StyleSpan(Typeface.ITALIC); sb.setSpan(styleSpan, sb.length() - installed.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(requireActivity().getTheme(), com.google.android.material.R.attr.colorSecondary)); sb.setSpan(foregroundColorSpan, sb.length() - installed.length(), sb.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE); } if (sb.length() > 0) { holder.hint.setVisibility(View.VISIBLE); holder.hint.setText(sb); } else { holder.hint.setVisibility(View.GONE); } holder.itemView.setOnClickListener(v -> { searchView.clearFocus(); safeNavigate(RepoFragmentDirections.actionRepoFragmentToRepoItemFragment(module.getName())); }); holder.itemView.setTooltipText(module.getDescription()); } @Override public int getItemCount() { return showList.size(); } @SuppressLint("NotifyDataSetChanged") private void setLoaded(List list, boolean isLoaded) { runOnUiThread(() -> { if (list != null) showList = list; this.isLoaded = isLoaded; notifyDataSetChanged(); }); } public void setData(Collection modules) { if (modules == null) return; setLoaded(null, false); channel = App.getPreferences().getString("update_channel", channels[0]); int sort = App.getPreferences().getInt("repo_sort", 0); boolean upgradableFirst = App.getPreferences().getBoolean("upgradable_first", true); ConcurrentHashMap upgradable = new ConcurrentHashMap<>(); fullList = modules.parallelStream().filter((onlineModule -> !onlineModule.isHide() && !(repoLoader.getReleases(onlineModule.getName()) != null && repoLoader.getReleases(onlineModule.getName()).isEmpty()))) .sorted((a, b) -> { if (upgradableFirst) { var aUpgrade = upgradable.computeIfAbsent(a.getName(), n -> getUpgradableVer(a) != null); var bUpgrade = upgradable.computeIfAbsent(b.getName(), n -> getUpgradableVer(b) != null); if (aUpgrade && !bUpgrade) return -1; else if (!aUpgrade && bUpgrade) return 1; } if (sort == 0) { return labelComparator.compare(a.getDescription(), b.getDescription()); } else { return Instant.parse(repoLoader.getLatestReleaseTime(b.getName(), channel)).compareTo(Instant.parse(repoLoader.getLatestReleaseTime(a.getName(), channel))); } }).collect(Collectors.toList()); String queryStr = searchView != null ? searchView.getQuery().toString() : ""; runOnUiThread(() -> getFilter().filter(queryStr)); } public void fullRefresh() { runAsync(() -> { setLoaded(null, false); repoLoader.loadRemoteData(); refresh(); }); } public void refresh() { runAsync(() -> adapter.setData(repoLoader.getOnlineModules())); } @Override public long getItemId(int position) { return showList.get(position).getName().hashCode(); } @Override public Filter getFilter() { return new RepoAdapter.ModuleFilter(); } @Override public boolean isLoaded() { return isLoaded && repoLoader.isRepoLoaded(); } static class ViewHolder extends RecyclerView.ViewHolder { ConstraintLayout root; TextView appName; TextView appPackageName; TextView appDescription; TextView hint; TextView publishedTime; ViewHolder(ItemOnlinemoduleBinding binding) { super(binding.getRoot()); root = binding.itemRoot; appName = binding.appName; appPackageName = binding.appPackageName; appDescription = binding.description; hint = binding.hint; publishedTime = binding.publishedTime; } } class ModuleFilter extends Filter { private boolean lowercaseContains(String s, String filter) { return !TextUtils.isEmpty(s) && s.toLowerCase().contains(filter); } @Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults filterResults = new FilterResults(); ArrayList filtered = new ArrayList<>(); String filter = constraint.toString().toLowerCase(); for (OnlineModule info : fullList) { if (lowercaseContains(info.getDescription(), filter) || lowercaseContains(info.getName(), filter) || lowercaseContains(info.getSummary(), filter)) { filtered.add(info); } } filterResults.values = filtered; filterResults.count = filtered.size(); return filterResults; } @Override protected void publishResults(CharSequence constraint, FilterResults results) { //noinspection unchecked setLoaded((List) results.values, true); } } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/RepoItemFragment.java ================================================ /* * */ package org.lsposed.manager.ui.fragment; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.content.res.Resources; import android.graphics.Color; import android.os.Bundle; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.Formatter; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ArrayAdapter; import android.widget.ScrollView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.MenuProvider; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.FragmentStateAdapter; import com.google.android.material.button.MaterialButton; import com.google.android.material.progressindicator.CircularProgressIndicator; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import org.lsposed.manager.App; import org.lsposed.manager.R; import org.lsposed.manager.databinding.FragmentPagerBinding; import org.lsposed.manager.databinding.ItemRepoLoadmoreBinding; import org.lsposed.manager.databinding.ItemRepoReadmeBinding; import org.lsposed.manager.databinding.ItemRepoRecyclerviewBinding; import org.lsposed.manager.databinding.ItemRepoReleaseBinding; import org.lsposed.manager.databinding.ItemRepoTitleDescriptionBinding; import org.lsposed.manager.repo.RepoLoader; import org.lsposed.manager.repo.model.Collaborator; import org.lsposed.manager.repo.model.OnlineModule; import org.lsposed.manager.repo.model.Release; import org.lsposed.manager.repo.model.ReleaseAsset; import org.lsposed.manager.ui.dialog.BlurBehindDialogBuilder; import org.lsposed.manager.ui.widget.EmptyStateRecyclerView; import org.lsposed.manager.ui.widget.LinkifyTextView; import org.lsposed.manager.util.AccessibilityUtils; import org.lsposed.manager.util.NavUtil; import org.lsposed.manager.util.SimpleStatefulAdaptor; import org.lsposed.manager.util.chrome.CustomTabsURLSpan; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import java.util.stream.Collectors; import java.util.stream.IntStream; import okhttp3.Headers; import okhttp3.Request; import okhttp3.Response; import rikka.core.util.ResourceUtils; import rikka.material.app.LocaleDelegate; import rikka.recyclerview.RecyclerViewKt; import rikka.widget.borderview.BorderView; public class RepoItemFragment extends BaseFragment implements RepoLoader.RepoListener, MenuProvider { FragmentPagerBinding binding; OnlineModule module; private ReleaseAdapter releaseAdapter; private InformationAdapter informationAdapter; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentPagerBinding.inflate(getLayoutInflater(), container, false); if (module == null) return binding.getRoot(); String modulePackageName = module.getName(); String moduleName = module.getDescription(); binding.appBar.setLiftable(true); setupToolbar(binding.toolbar, binding.clickView, moduleName, R.menu.menu_repo_item); binding.clickView.setTooltipText(moduleName); binding.toolbar.setSubtitle(modulePackageName); binding.viewPager.setAdapter(new PagerAdapter(this)); int[] titles = new int[]{R.string.module_readme, R.string.module_releases, R.string.module_information}; var isAnimationEnabled = AccessibilityUtils.isAnimationEnabled(requireContext().getContentResolver()); new TabLayoutMediator( binding.tabLayout, binding.viewPager, // `autoRefresh = true` by default. Update the tabs automatically when the data set of the view pager's // adapter changes. true, isAnimationEnabled, (tab, position) -> tab.setText(titles[position]) ).attach(); binding.tabLayout.addOnLayoutChangeListener((view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { ViewGroup vg = (ViewGroup) binding.tabLayout.getChildAt(0); int tabLayoutWidth = IntStream.range(0, binding.tabLayout.getTabCount()).map(i -> vg.getChildAt(i).getWidth()).sum(); if (tabLayoutWidth <= binding.getRoot().getWidth()) { binding.tabLayout.setTabMode(TabLayout.MODE_FIXED); binding.tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); } }); binding.toolbar.setOnClickListener(v -> binding.appBar.setExpanded(true, true)); releaseAdapter = new ReleaseAdapter(); informationAdapter = new InformationAdapter(); RepoLoader.getInstance().addListener(this); return binding.getRoot(); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { RepoLoader.getInstance().addListener(this); super.onCreate(savedInstanceState); String modulePackageName = getArguments() == null ? null : getArguments().getString("modulePackageName"); module = RepoLoader.getInstance().getOnlineModule(modulePackageName); if (module == null) { if (!safeNavigate(R.id.action_repo_item_fragment_to_repo_fragment)) { safeNavigate(R.id.repo_nav); } } } private void renderGithubMarkdown(WebView view, @Nullable String text) { try { view.setBackgroundColor(Color.TRANSPARENT); var setting = view.getSettings(); setting.setOffscreenPreRaster(true); setting.setDomStorageEnabled(true); setting.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); setting.setAllowContentAccess(false); setting.setAllowFileAccessFromFileURLs(true); setting.setAllowFileAccess(false); setting.setGeolocationEnabled(false); setting.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); setting.setTextZoom(80); String body; String direction; if (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { direction = "rtl"; } else { direction = "ltr"; } if (text == null) { text = "
" + App.getInstance().getString(R.string.list_empty) + "
"; } if (ResourceUtils.isNightMode(getResources().getConfiguration())) { body = App.HTML_TEMPLATE_DARK.get().replace("@dir@", direction).replace("@body@", text); } else { body = App.HTML_TEMPLATE.get().replace("@dir@", direction).replace("@body@", text); } view.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { NavUtil.startURL(requireActivity(), request.getUrl()); return true; } @Nullable @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { if (!request.getUrl().getScheme().startsWith("http")) return null; var client = App.getOkHttpClient(); var call = client.newCall( new Request.Builder() .url(request.getUrl().toString()) .method(request.getMethod(), null) .headers(Headers.of(request.getRequestHeaders())) .build()); try { Response reply = call.execute(); var header = reply.header("content-type", "image/*;charset=utf-8"); String[] contentTypes = new String[0]; if (header != null) { contentTypes = header.split(";\\s*"); } var mimeType = contentTypes.length > 0 ? contentTypes[0] : "image/*"; var charset = contentTypes.length > 1 ? contentTypes[1].split("=\\s*")[1] : "utf-8"; var body = reply.body(); if (body == null) return null; return new WebResourceResponse( mimeType, charset, body.byteStream() ); } catch (Throwable e) { return new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream(Log.getStackTraceString(e).getBytes(StandardCharsets.UTF_8))); } } }); view.loadDataWithBaseURL("https://github.com", body, "text/html", StandardCharsets.UTF_8.name(), null); } catch (Throwable e) { Log.e(App.TAG, "render readme", e); } } @Override public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { } @Override public boolean onMenuItemSelected(@NonNull MenuItem item) { int id = item.getItemId(); if (id == R.id.menu_open_in_browser) { NavUtil.startURL(requireActivity(), "https://modules.lsposed.org/module/" + module.getName()); return true; } return false; } @Override public void onDestroyView() { super.onDestroyView(); RepoLoader.getInstance().removeListener(this); binding = null; } @Override public void onModuleReleasesLoaded(OnlineModule module) { this.module = module; var repoLoader = RepoLoader.getInstance(); if (releaseAdapter != null) { runAsync(releaseAdapter::loadItems); } if ((repoLoader.getReleases(module.getName()) != null ? repoLoader.getReleases(module.getName()).size() : 1) == 1) { showHint(R.string.module_release_no_more, true); } } @Override public void onThrowable(Throwable t) { if (releaseAdapter != null) { runAsync(releaseAdapter::loadItems); } showHint(getString(R.string.repo_load_failed, t.getLocalizedMessage()), true); } private class InformationAdapter extends SimpleStatefulAdaptor { private int rowCount = 0; private int homepageRow = -1; private int collaboratorsRow = -1; private int sourceUrlRow = -1; public InformationAdapter() { if (!TextUtils.isEmpty(module.getHomepageUrl())) { homepageRow = rowCount++; } if (module.getCollaborators() != null && !module.getCollaborators().isEmpty()) { collaboratorsRow = rowCount++; } if (!TextUtils.isEmpty(module.getSourceUrl())) { sourceUrlRow = rowCount++; } } @NonNull @Override public InformationAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(ItemRepoTitleDescriptionBinding.inflate(getLayoutInflater(), parent, false)); } @Override public void onBindViewHolder(@NonNull InformationAdapter.ViewHolder holder, int position) { if (position == homepageRow) { holder.title.setText(R.string.module_information_homepage); holder.description.setText(module.getHomepageUrl()); } else if (position == collaboratorsRow) { List collaborators = module.getCollaborators(); if (collaborators == null) return; holder.title.setText(R.string.module_information_collaborators); SpannableStringBuilder sb = new SpannableStringBuilder(); ListIterator iterator = collaborators.listIterator(); while (iterator.hasNext()) { Collaborator collaborator = iterator.next(); var collaboratorLogin = collaborator.getLogin(); if (collaboratorLogin == null) continue; String name = collaborator.getName() == null ? collaboratorLogin : collaborator.getName(); sb.append(name); CustomTabsURLSpan span = new CustomTabsURLSpan(requireActivity(), String.format("https://github.com/%s", collaborator.getLogin())); sb.setSpan(span, sb.length() - name.length(), sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); if (iterator.hasNext()) { sb.append(", "); } } holder.description.setText(sb); } else if (position == sourceUrlRow) { holder.title.setText(R.string.module_information_source_url); holder.description.setText(module.getSourceUrl()); } holder.itemView.setOnClickListener(v -> { if (position == homepageRow) { NavUtil.startURL(requireActivity(), module.getHomepageUrl()); } else if (position == collaboratorsRow) { ClickableSpan span = holder.description.getCurrentSpan(); holder.description.clearCurrentSpan(); if (span instanceof CustomTabsURLSpan) { span.onClick(v); } } else if (position == sourceUrlRow) { NavUtil.startURL(requireActivity(), module.getSourceUrl()); } }); } @Override public int getItemCount() { return rowCount; } class ViewHolder extends RecyclerView.ViewHolder { TextView title; LinkifyTextView description; public ViewHolder(ItemRepoTitleDescriptionBinding binding) { super(binding.getRoot()); title = binding.title; description = binding.description; } } } public static class DownloadDialog extends DialogFragment { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { var args = getArguments(); if (args == null) throw new IllegalArgumentException(); return new BlurBehindDialogBuilder(requireActivity(), R.style.ThemeOverlay_MaterialAlertDialog_Centered_FullWidthButtons) .setTitle(R.string.module_release_view_assets) .setPositiveButton(android.R.string.cancel, null) .setAdapter(new ArrayAdapter<>(requireActivity(), R.layout.dialog_item, args.getCharSequenceArray("names")), (dialog, which) -> NavUtil.startURL(requireActivity(), args.getStringArrayList("urls").get(which))) .create(); } static void create(Activity activity, FragmentManager fm, List assets) { var f = new DownloadDialog(); var bundle = new Bundle(); var displayNames = new CharSequence[assets.size()]; for (int i = 0; i < assets.size(); i++) { var sb = new SpannableStringBuilder(assets.get(i).getName()); var count = assets.get(i).getDownloadCount(); var countStr = activity.getResources().getQuantityString(R.plurals.module_release_assets_download_count, count, count); var sizeStr = Formatter.formatShortFileSize(activity, assets.get(i).getSize()); sb.append('\n').append(sizeStr).append('/').append(countStr); final ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(ResourceUtils.resolveColor(activity.getTheme(), android.R.attr.textColorSecondary)); final RelativeSizeSpan relativeSizeSpan = new RelativeSizeSpan(0.8f); sb.setSpan(foregroundColorSpan, sb.length() - sizeStr.length() - countStr.length() - 1, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); sb.setSpan(relativeSizeSpan, sb.length() - sizeStr.length() - countStr.length() - 1, sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); displayNames[i] = sb; } bundle.putCharSequenceArray("names", displayNames); bundle.putStringArrayList("urls", assets.stream().map(ReleaseAsset::getDownloadUrl).collect(Collectors.toCollection(ArrayList::new))); f.setArguments(bundle); f.show(fm, "download"); } } private class ReleaseAdapter extends EmptyStateRecyclerView.EmptyStateAdapter { private List items = new ArrayList<>(); private final Resources resources = App.getInstance().getResources(); public ReleaseAdapter() { runAsync(this::loadItems); } @SuppressLint("NotifyDataSetChanged") public void loadItems() { var channels = resources.getStringArray(R.array.update_channel_values); var channel = App.getPreferences().getString("update_channel", channels[0]); var releases = RepoLoader.getInstance().getReleases(module.getName()); if (releases == null) releases = module.getReleases(); List tmpList; if (channel.equals(channels[0])) { tmpList = releases != null ? releases.parallelStream().filter(t -> { if (Boolean.TRUE.equals(t.getIsPrerelease())) return false; var name = t.getName() != null ? t.getName().toLowerCase(LocaleDelegate.getDefaultLocale()) : null; return !(name != null && name.startsWith("snapshot")) && !(name != null && name.startsWith("nightly")); }).collect(Collectors.toList()) : null; } else if (channel.equals(channels[1])) { tmpList = releases != null ? releases.parallelStream().filter(t -> { var name = t.getName() != null ? t.getName().toLowerCase(LocaleDelegate.getDefaultLocale()) : null; return !(name != null && name.startsWith("snapshot")) && !(name != null && name.startsWith("nightly")); }).collect(Collectors.toList()) : null; } else tmpList = releases; runOnUiThread(() -> { items = tmpList; notifyDataSetChanged(); }); } @NonNull @Override public ReleaseAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { if (viewType == 0) { return new ReleaseViewHolder(ItemRepoReleaseBinding.inflate(getLayoutInflater(), parent, false)); } else { return new LoadmoreViewHolder(ItemRepoLoadmoreBinding.inflate(getLayoutInflater(), parent, false)); } } @Override public void onBindViewHolder(@NonNull ReleaseAdapter.ViewHolder holder, int position) { if (holder.getItemViewType() == 1) { holder.progress.setVisibility(View.GONE); holder.title.setVisibility(View.VISIBLE); holder.itemView.setOnClickListener(v -> { if (holder.progress.getVisibility() == View.GONE) { holder.title.setVisibility(View.GONE); holder.progress.show(); RepoLoader.getInstance().loadRemoteReleases(module.getName()); } }); } else { Release release = items.get(position); holder.title.setText(release.getName()); var instant = Instant.parse(release.getPublishedAt()); var formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) .withLocale(App.getLocale()).withZone(ZoneId.systemDefault()); holder.publishedTime.setText(String.format(getString(R.string.module_repo_published_time), formatter.format(instant))); renderGithubMarkdown(holder.description, release.getDescriptionHTML()); holder.openInBrowser.setOnClickListener(v -> NavUtil.startURL(requireActivity(), release.getUrl())); List assets = release.getReleaseAssets(); if (assets != null && !assets.isEmpty()) { holder.viewAssets.setOnClickListener(v -> DownloadDialog.create(requireActivity(), getParentFragmentManager(), assets)); } else { holder.viewAssets.setVisibility(View.GONE); } } } @Override public int getItemCount() { return items.size() + (module.releasesLoaded ? 0 : 1); } @Override public int getItemViewType(int position) { return !module.releasesLoaded && position == getItemCount() - 1 ? 1 : 0; } @Override public boolean isLoaded() { return module.releasesLoaded; } class ViewHolder extends RecyclerView.ViewHolder { TextView title; TextView publishedTime; WebView description; MaterialButton openInBrowser; MaterialButton viewAssets; CircularProgressIndicator progress; public ViewHolder(@NonNull View itemView) { super(itemView); } } class ReleaseViewHolder extends ReleaseAdapter.ViewHolder { public ReleaseViewHolder(ItemRepoReleaseBinding binding) { super(binding.getRoot()); title = binding.title; publishedTime = binding.publishedTime; description = binding.description; openInBrowser = binding.openInBrowser; viewAssets = binding.viewAssets; } } class LoadmoreViewHolder extends ReleaseAdapter.ViewHolder { public LoadmoreViewHolder(ItemRepoLoadmoreBinding binding) { super(binding.getRoot()); title = binding.title; progress = binding.progress; } } } private static class PagerAdapter extends FragmentStateAdapter { public PagerAdapter(@NonNull Fragment fragment) { super(fragment); } @NonNull @Override public Fragment createFragment(int position) { Bundle bundle = new Bundle(); bundle.putInt("position", position); Fragment f; if (position == 0) { f = new ReadmeFragment(); } else if (position == 1) { f = new RecyclerviewFragment(); } else { f = new RecyclerviewFragment(); } f.setArguments(bundle); return f; } @Override public int getItemCount() { return 3; } @Override public int getItemViewType(int position) { return position == 0 ? 0 : 1; } @Override public long getItemId(int position) { return position; } } public static abstract class BorderFragment extends BaseFragment { BorderView borderView; void attachListeners() { var parent = getParentFragment(); if (parent instanceof RepoItemFragment) { var repoItemFragment = (RepoItemFragment) parent; borderView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> repoItemFragment.binding.appBar.setLifted(!top)); repoItemFragment.binding.appBar.setLifted(!borderView.getBorderViewDelegate().isShowingTopBorder()); repoItemFragment.binding.toolbar.setOnClickListener(v -> { repoItemFragment.binding.appBar.setExpanded(true, true); scrollToTop(); }); } } abstract void scrollToTop(); void detachListeners() { borderView.getBorderViewDelegate().setBorderVisibilityChangedListener(null); } @Override public void onResume() { super.onResume(); attachListeners(); } @Override public void onStart() { super.onStart(); attachListeners(); } @Override public void onStop() { super.onStop(); detachListeners(); } @Override public void onPause() { super.onPause(); detachListeners(); } } public static class ReadmeFragment extends BorderFragment { ItemRepoReadmeBinding binding; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { var parent = getParentFragment(); if (!(parent instanceof RepoItemFragment)) { if (!safeNavigate(R.id.action_repo_item_fragment_to_repo_fragment)) { safeNavigate(R.id.repo_nav); } return null; } var repoItemFragment = (RepoItemFragment) parent; binding = ItemRepoReadmeBinding.inflate(getLayoutInflater(), container, false); repoItemFragment.renderGithubMarkdown(binding.readme, repoItemFragment.module.getReadmeHTML()); borderView = binding.scrollView; return binding.getRoot(); } @Override void scrollToTop() { binding.scrollView.fullScroll(ScrollView.FOCUS_UP); } } public static class RecyclerviewFragment extends BorderFragment { ItemRepoRecyclerviewBinding binding; RecyclerView.Adapter adapter; @Override void scrollToTop() { binding.recyclerView.smoothScrollToPosition(0); } public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { var arguments = getArguments(); var parent = getParentFragment(); if (arguments == null || !(parent instanceof RepoItemFragment)) { if (!safeNavigate(R.id.action_repo_item_fragment_to_repo_fragment)) { safeNavigate(R.id.repo_nav); } return null; } var repoItemFragment = (RepoItemFragment) parent; var position = arguments.getInt("position", 0); if (position == 1) adapter = repoItemFragment.releaseAdapter; else if (position == 2) adapter = repoItemFragment.informationAdapter; else return null; binding = ItemRepoRecyclerviewBinding.inflate(getLayoutInflater(), container, false); binding.recyclerView.setAdapter(adapter); binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireActivity())); RecyclerViewKt.fixEdgeEffect(binding.recyclerView, false, true); borderView = binding.recyclerView; return binding.getRoot(); } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/fragment/SettingsFragment.java ================================================ /* * */ package org.lsposed.manager.ui.fragment; import android.content.ActivityNotFoundException; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.text.HtmlCompat; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.color.DynamicColors; import org.lsposed.manager.App; import org.lsposed.manager.BuildConfig; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.R; import org.lsposed.manager.databinding.FragmentSettingsBinding; import org.lsposed.manager.repo.RepoLoader; import org.lsposed.manager.ui.activity.MainActivity; import org.lsposed.manager.util.BackupUtils; import org.lsposed.manager.util.CloudflareDNS; import org.lsposed.manager.util.LangList; import org.lsposed.manager.util.NavUtil; import org.lsposed.manager.util.ShortcutUtil; import org.lsposed.manager.util.ThemeUtil; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Locale; import rikka.core.util.ResourceUtils; import rikka.material.app.LocaleDelegate; import rikka.material.preference.MaterialSwitchPreference; import rikka.preference.SimpleMenuPreference; import rikka.recyclerview.RecyclerViewKt; import rikka.widget.borderview.BorderRecyclerView; public class SettingsFragment extends BaseFragment { FragmentSettingsBinding binding; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = FragmentSettingsBinding.inflate(inflater, container, false); binding.appBar.setLiftable(true); setupToolbar(binding.toolbar, binding.clickView, R.string.Settings); binding.toolbar.setNavigationIcon(null); if (savedInstanceState == null) { 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())); } else { binding.toolbar.setSubtitle(String.format(LocaleDelegate.getDefaultLocale(), "%s (%d) - %s", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, getString(R.string.not_installed))); } return binding.getRoot(); } @Override public void onDestroyView() { super.onDestroyView(); binding = null; } public static class PreferenceFragment extends PreferenceFragmentCompat { private SettingsFragment parentFragment; ActivityResultLauncher backupLauncher = registerForActivityResult(new ActivityResultContracts.CreateDocument("application/gzip"), uri -> { if (uri == null || parentFragment == null) return; parentFragment.runAsync(() -> { try { BackupUtils.backup(uri); } catch (Exception e) { var text = App.getInstance().getString(R.string.settings_backup_failed2, e.getMessage()); parentFragment.showHint(text, false); } }); }); ActivityResultLauncher restoreLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), uri -> { if (uri == null || parentFragment == null) return; parentFragment.runAsync(() -> { try { BackupUtils.restore(uri); } catch (Exception e) { var text = App.getInstance().getString(R.string.settings_restore_failed2, e.getMessage()); parentFragment.showHint(text, false); } }); }); @Override public void onAttach(@NonNull Context context) { super.onAttach(context); parentFragment = (SettingsFragment) requireParentFragment(); } @Override public void onDetach() { super.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"; addPreferencesFromResource(R.xml.prefs); boolean installed = ConfigManager.isBinderAlive(); MaterialSwitchPreference prefVerboseLogs = findPreference("disable_verbose_log"); if (prefVerboseLogs != null) { prefVerboseLogs.setEnabled(!BuildConfig.DEBUG && installed); if (BuildConfig.DEBUG) ConfigManager.setVerboseLogEnabled(false); prefVerboseLogs.setChecked(!installed || !ConfigManager.isVerboseLogEnabled()); 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.setOnPreferenceChangeListener((p, v) -> { var succeeded = ConfigManager.setEnableStatusNotification((boolean) v); if ((boolean) v && App.isParasitic && !ShortcutUtil.isLaunchShortcutPinned()) { setNotificationPreferenceEnabled(notificationPreference, false); } return succeeded; }); } Preference shortcut = findPreference("add_shortcut"); if (shortcut != null) { shortcut.setVisible(App.isParasitic); if (!ShortcutUtil.isRequestPinShortcutSupported(requireContext())) { shortcut.setEnabled(false); shortcut.setSummary(R.string.settings_unsupported_pin_shortcut_summary); } 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); })) { parentFragment.showHint(R.string.settings_unsupported_pin_shortcut_summary, true); } return true; }); } Preference backup = findPreference("backup"); if (backup != null) { backup.setEnabled(installed); backup.setOnPreferenceClickListener(preference -> { LocalDateTime now = LocalDateTime.now(); try { backupLauncher.launch(String.format(LocaleDelegate.getDefaultLocale(), "LSPosed_%s.lsp", now.toString())); return true; } catch (ActivityNotFoundException e) { parentFragment.showHint(R.string.enable_documentui, true); return false; } }); } Preference restore = findPreference("restore"); if (restore != null) { restore.setEnabled(installed); restore.setOnPreferenceClickListener(preference -> { try { restoreLauncher.launch(new String[]{"*/*"}); return true; } catch (ActivityNotFoundException e) { parentFragment.showHint(R.string.enable_documentui, true); return false; } }); } Preference theme = findPreference("dark_theme"); if (theme != null) { theme.setOnPreferenceChangeListener((preference, newValue) -> { if (!App.getPreferences().getString("dark_theme", ThemeUtil.MODE_NIGHT_FOLLOW_SYSTEM).equals(newValue)) { AppCompatDelegate.setDefaultNightMode(ThemeUtil.getDarkTheme((String) newValue)); } return true; }); } Preference black_dark_theme = findPreference("black_dark_theme"); if (black_dark_theme != null) { black_dark_theme.setOnPreferenceChangeListener((preference, newValue) -> { MainActivity activity = (MainActivity) getActivity(); if (activity != null && ResourceUtils.isNightMode(getResources().getConfiguration())) { activity.restart(); } return true; }); } Preference primary_color = findPreference("theme_color"); if (primary_color != null) { primary_color.setOnPreferenceChangeListener((preference, newValue) -> { MainActivity activity = (MainActivity) getActivity(); if (activity != null) { activity.restart(); } return true; }); } MaterialSwitchPreference prefShowHiddenIcons = findPreference("show_hidden_icon_apps_enabled"); if (prefShowHiddenIcons != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (ConfigManager.isBinderAlive()) { prefShowHiddenIcons.setEnabled(true); prefShowHiddenIcons.setOnPreferenceChangeListener((preference, newValue) -> ConfigManager.setHiddenIcon(!(boolean) newValue)); } prefShowHiddenIcons.setChecked(Settings.Global.getInt(requireActivity().getContentResolver(), "show_hidden_icon_apps_enabled", 1) != 0); } MaterialSwitchPreference prefFollowSystemAccent = findPreference("follow_system_accent"); if (prefFollowSystemAccent != null && DynamicColors.isDynamicColorAvailable()) { if (primary_color != null) { primary_color.setVisible(!prefFollowSystemAccent.isChecked()); } prefFollowSystemAccent.setVisible(true); prefFollowSystemAccent.setOnPreferenceChangeListener((preference, newValue) -> { MainActivity activity = (MainActivity) getActivity(); if (activity != null) { activity.restart(); } return true; }); } MaterialSwitchPreference prefDoH = findPreference("doh"); if (prefDoH != null) { var dns = (CloudflareDNS) App.getOkHttpClient().dns(); if (!dns.noProxy) { prefDoH.setEnabled(false); prefDoH.setVisible(false); var group = prefDoH.getParent(); assert group != null; group.setVisible(false); } prefDoH.setOnPreferenceChangeListener((p, v) -> { dns.DoH = (boolean) v; return true; }); } SimpleMenuPreference language = findPreference("language"); if (language != null) { var tag = language.getValue(); var userLocale = App.getLocale(); var entries = new ArrayList(); var lstLang = LangList.LOCALES; for (var lang : lstLang) { if (lang.equals(SYSTEM)) { entries.add(getString(rikka.core.R.string.follow_system)); continue; } var locale = Locale.forLanguageTag(lang); entries.add(HtmlCompat.fromHtml(locale.getDisplayName(locale), HtmlCompat.FROM_HTML_MODE_LEGACY)); } language.setEntries(entries.toArray(new CharSequence[0])); language.setEntryValues(lstLang); if (TextUtils.isEmpty(tag) || SYSTEM.equals(tag)) { language.setSummary(getString(rikka.core.R.string.follow_system)); } else { var locale = Locale.forLanguageTag(tag); language.setSummary(!TextUtils.isEmpty(locale.getScript()) ? locale.getDisplayScript(userLocale) : locale.getDisplayName(userLocale)); } language.setOnPreferenceChangeListener((preference, newValue) -> { var app = App.getInstance(); var locale = App.getLocale((String) newValue); var res = app.getResources(); var config = res.getConfiguration(); config.setLocale(locale); LocaleDelegate.setDefaultLocale(locale); //noinspection deprecation res.updateConfiguration(config, res.getDisplayMetrics()); MainActivity activity = (MainActivity) getActivity(); if (activity != null) { activity.restart(); } return true; }); } Preference translation = findPreference("translation"); if (translation != null) { translation.setOnPreferenceClickListener(preference -> { NavUtil.startURL(requireActivity(), "https://crowdin.com/project/lsposed_jingmatrix"); return true; }); translation.setSummary(getString(R.string.settings_translation_summary, getString(R.string.app_name))); } Preference translation_contributors = findPreference("translation_contributors"); if (translation_contributors != null) { var translators = HtmlCompat.fromHtml(getString(R.string.translators), HtmlCompat.FROM_HTML_MODE_LEGACY); if (translators.toString().equals("null")) { translation_contributors.setVisible(false); } else { translation_contributors.setSummary(translators); } } SimpleMenuPreference channel = findPreference("update_channel"); if (channel != null) { channel.setOnPreferenceChangeListener((preference, newValue) -> { var repoLoader = RepoLoader.getInstance(); repoLoader.updateLatestVersion(String.valueOf(newValue)); return true; }); } } @NonNull @Override public RecyclerView onCreateRecyclerView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent, Bundle savedInstanceState) { BorderRecyclerView recyclerView = (BorderRecyclerView) super.onCreateRecyclerView(inflater, parent, savedInstanceState); RecyclerViewKt.fixEdgeEffect(recyclerView, false, true); recyclerView.getBorderViewDelegate().setBorderVisibilityChangedListener((top, oldTop, bottom, oldBottom) -> parentFragment.binding.appBar.setLifted(!top)); var fragment = getParentFragment(); if (fragment instanceof SettingsFragment settingsFragment) { View.OnClickListener l = v -> { settingsFragment.binding.appBar.setExpanded(true, true); recyclerView.smoothScrollToPosition(0); }; settingsFragment.binding.toolbar.setOnClickListener(l); settingsFragment.binding.clickView.setOnClickListener(l); } return recyclerView; } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/widget/EmptyStateRecyclerView.java ================================================ /* * */ package org.lsposed.manager.ui.widget; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.util.DisplayMetrics; import androidx.annotation.Nullable; import androidx.recyclerview.widget.ConcatAdapter; import org.lsposed.manager.R; import org.lsposed.manager.util.SimpleStatefulAdaptor; import rikka.core.util.ResourceUtils; public class EmptyStateRecyclerView extends StatefulRecyclerView { private final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); private final String emptyText; public EmptyStateRecyclerView(Context context) { this(context, null); } public EmptyStateRecyclerView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public EmptyStateRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); DisplayMetrics dm = context.getResources().getDisplayMetrics(); paint.setColor(ResourceUtils.resolveColor(context.getTheme(), android.R.attr.textColorSecondary)); paint.setTextSize(16f * dm.scaledDensity); emptyText = context.getString(R.string.list_empty); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); var adapter = getAdapter(); if (adapter instanceof ConcatAdapter) { for (var a : ((ConcatAdapter) adapter).getAdapters()) { if (a instanceof EmptyStateAdapter) { adapter = a; break; } } } if (adapter instanceof EmptyStateAdapter && ((EmptyStateAdapter) adapter).isLoaded() && adapter.getItemCount() == 0) { final int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); final int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); var textLayout = new StaticLayout(emptyText, paint, width, Layout.Alignment.ALIGN_CENTER, 1.0f, 0.0f, false); canvas.save(); canvas.translate(getPaddingLeft(), (height >> 1) + getPaddingTop() - (textLayout.getHeight() >> 1)); textLayout.draw(canvas); canvas.restore(); } } public abstract static class EmptyStateAdapter extends SimpleStatefulAdaptor { abstract public boolean isLoaded(); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/widget/ExpandableTextView.java ================================================ /* * */ package org.lsposed.manager.ui.widget; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Typeface; import android.os.Bundle; import android.os.Parcelable; import android.text.Layout; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.transition.TransitionManager; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import com.google.android.material.textview.MaterialTextView; import org.lsposed.manager.R; public class ExpandableTextView extends MaterialTextView { private CharSequence text = null; private int nextLines = 0; private final int maxLines; private final SpannableString collapse; private final SpannableString expand; private final SpannableStringBuilder sb = new SpannableStringBuilder(); private int lineCount = 0; public ExpandableTextView(Context context) { this(context, null); } public ExpandableTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ExpandableTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); maxLines = getMaxLines(); collapse = new SpannableString(context.getString(R.string.collapse)); ClickableSpan span = new ClickableSpan() { @Override public void onClick(@NonNull View widget) { TransitionManager.beginDelayedTransition((ViewGroup) getParent()); setMaxLines(nextLines); ExpandableTextView.super.setText(text); } @Override public void updateDrawState(@NonNull TextPaint ds) { ds.setTypeface(Typeface.DEFAULT_BOLD); } }; collapse.setSpan(span, 0, collapse.length(), 0); expand = new SpannableString(context.getString(R.string.expand)); expand.setSpan(span, 0, expand.length(), 0); setMovementMethod(LinkMovementMethod.getInstance()); } @Override public void setText(CharSequence text, BufferType type) { this.text = text; super.setText(text, type); } @Override public boolean onPreDraw() { this.getViewTreeObserver().removeOnPreDrawListener(this); if (lineCount == 0) { lineCount = getLayout().getLineCount(); } if (lineCount > maxLines) { int hintTextOffsetEnd; if (maxLines == getMaxLines()) { nextLines = lineCount + 1; hintTextOffsetEnd = getLayout().getLineStart(getMaxLines() - 1); setTextWithSpan(text, hintTextOffsetEnd - 1, expand); } else if (nextLines == getMaxLines()) { nextLines = maxLines; hintTextOffsetEnd = getLayout().getLineStart(getMaxLines() - 1); setTextWithSpan(text, hintTextOffsetEnd, collapse); } } return super.onPreDraw(); } private void setTextWithSpan(CharSequence text, int textOffsetEnd, SpannableString sbStr) { sb.clearSpans(); sb.clear(); sb.append(text, 0, textOffsetEnd); sb.append("\n"); sb.append(sbStr); super.setText(sb, BufferType.NORMAL); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (getLayout() != null) { lineCount = getLayout().getLineCount(); } } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(@NonNull MotionEvent event) { Layout layout = this.getLayout(); if (layout != null) { int line = layout.getLineForVertical((int) event.getY()); int offset = layout.getOffsetForHorizontal(line, event.getX()); if (getText() instanceof Spanned) { Spanned spanned = (Spanned) getText(); ClickableSpan[] links = spanned.getSpans(offset, offset, ClickableSpan.class); if (links.length == 0) { return false; } else { return super.onTouchEvent(event); } } } return false; } @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable("superState", super.onSaveInstanceState()); bundle.putInt("maxLines", getMaxLines()); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; setMaxLines(bundle.getInt("maxLines")); state = bundle.getParcelable("superState"); } super.onRestoreInstanceState(state); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/widget/LinkifyTextView.java ================================================ /* * 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 */ package org.lsposed.manager.ui.widget; import android.annotation.SuppressLint; import android.content.Context; import android.text.Layout; import android.text.Spanned; import android.text.style.ClickableSpan; import android.util.AttributeSet; import android.view.MotionEvent; import androidx.annotation.NonNull; public class LinkifyTextView extends androidx.appcompat.widget.AppCompatTextView { private ClickableSpan mCurrentSpan; public LinkifyTextView(Context context) { super(context); } public LinkifyTextView(Context context, AttributeSet attrs) { super(context, attrs); } public LinkifyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public ClickableSpan getCurrentSpan() { return mCurrentSpan; } public void clearCurrentSpan() { mCurrentSpan = null; } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(@NonNull MotionEvent event) { // Let the parent or grandparent of TextView to handles click action. // Otherwise click effect like ripple will not work, and if touch area // do not contain a url, the TextView will still get MotionEvent. // onTouchEven must be called with MotionEvent.ACTION_DOWN for each touch // action on it, so we analyze touched url here. if (event.getAction() == MotionEvent.ACTION_DOWN) { mCurrentSpan = null; if (getText() instanceof Spanned) { // Get this code from android.text.method.LinkMovementMethod. // Work fine ! int x = (int) event.getX(); int y = (int) event.getY(); x -= getTotalPaddingLeft(); y -= getTotalPaddingTop(); x += getScrollX(); y += getScrollY(); Layout layout = getLayout(); if (null != layout) { int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); ClickableSpan[] spans = ((Spanned) getText()).getSpans(off, off, ClickableSpan.class); if (spans.length > 0) { mCurrentSpan = spans[0]; } } } } return super.onTouchEvent(event); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/widget/ScrollWebView.java ================================================ /* * 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.manager.ui.widget; import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewParent; import android.webkit.WebView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import rikka.widget.borderview.BorderRecyclerView; public class ScrollWebView extends WebView { public ScrollWebView(@NonNull Context context) { super(context); } public ScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public ScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public ScrollWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { var viewParent = findViewParentIfNeeds(this); if (viewParent != null) viewParent.requestDisallowInterceptTouchEvent(true); } return super.onTouchEvent(event); } @Override protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { if (clampedX) { var viewParent = findViewParentIfNeeds(this); if (viewParent != null) viewParent.requestDisallowInterceptTouchEvent(false); } super.onOverScrolled(scrollX, scrollY, clampedX, clampedY); } private static ViewParent findViewParentIfNeeds(View v) { var parent = v.getParent(); if (parent == null) return null; if (parent instanceof RecyclerView && !(parent instanceof BorderRecyclerView)) { return parent; } else if (parent instanceof View) { return findViewParentIfNeeds((View) parent); } else { return parent; } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/ui/widget/StatefulRecyclerView.java ================================================ /* * 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.manager.ui.widget; import android.content.Context; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.viewpager2.adapter.StatefulAdapter; import rikka.widget.borderview.BorderRecyclerView; public class StatefulRecyclerView extends BorderRecyclerView { public StatefulRecyclerView(@NonNull Context context) { super(context); } public StatefulRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public StatefulRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable("superState", super.onSaveInstanceState()); var adapter = getAdapter(); if (adapter instanceof StatefulAdapter) { bundle.putParcelable("adaptor", ((StatefulAdapter) adapter).saveState()); } return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; super.onRestoreInstanceState(bundle.getParcelable("superState")); var adapter = getAdapter(); if (adapter instanceof StatefulAdapter) { ((StatefulAdapter) adapter).restoreState(bundle.getParcelable("adaptor")); } } else { super.onRestoreInstanceState(state); } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/AccessibilityUtils.java ================================================ package org.lsposed.manager.util; import android.content.ContentResolver; import android.provider.Settings; public class AccessibilityUtils { public static boolean isAnimationEnabled(ContentResolver cr) { return !(Settings.Global.getFloat(cr, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0.0f && Settings.Global.getFloat(cr, Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f) == 0.0f && Settings.Global.getFloat(cr, Settings.Global.WINDOW_ANIMATION_SCALE, 1.0f) == 0.0f); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/AppIconModelLoader.java ================================================ /* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.lsposed.manager.util; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.graphics.Bitmap; import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.signature.ObjectKey; import org.lsposed.manager.App; import me.zhanghai.android.appiconloader.AppIconLoader; public class AppIconModelLoader implements ModelLoader { @NonNull private final AppIconLoader mLoader; @NonNull private final Context mContext; private AppIconModelLoader(@Px int iconSize, boolean shrinkNonAdaptiveIcons, @NonNull Context context) { mLoader = new AppIconLoader(iconSize, shrinkNonAdaptiveIcons, context); mContext = context; } @Override public boolean handles(@NonNull PackageInfo model) { return true; } @Nullable @Override public LoadData buildLoadData(@NonNull PackageInfo model, int width, int height, @NonNull Options options) { var warpApplicationInfo = new ApplicationInfo(model.applicationInfo); warpApplicationInfo.uid = warpApplicationInfo.uid % App.PER_USER_RANGE; var warpPackageInfo = new PackageInfo(); warpPackageInfo.applicationInfo = warpApplicationInfo; warpPackageInfo.versionCode = model.versionCode; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { warpPackageInfo.setLongVersionCode(model.getLongVersionCode()); } return new LoadData<>(new ObjectKey(AppIconLoader.getIconKey(warpPackageInfo, mContext)), new Fetcher(mLoader, warpApplicationInfo)); } private static class Fetcher implements DataFetcher { @NonNull private final AppIconLoader mLoader; @NonNull private final ApplicationInfo mApplicationInfo; public Fetcher(@NonNull AppIconLoader loader, @NonNull ApplicationInfo applicationInfo) { mLoader = loader; mApplicationInfo = applicationInfo; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { try { Bitmap icon = mLoader.loadIcon(mApplicationInfo); callback.onDataReady(icon); } catch (Exception e) { callback.onLoadFailed(e); } } @Override public void cleanup() { } @Override public void cancel() { } @NonNull @Override public Class getDataClass() { return Bitmap.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } public static class Factory implements ModelLoaderFactory { @Px private final int mIconSize; private final boolean mShrinkNonAdaptiveIcons; @NonNull private final Context mContext; public Factory(@Px int iconSize, boolean shrinkNonAdaptiveIcons, @NonNull Context context) { mIconSize = iconSize; mShrinkNonAdaptiveIcons = shrinkNonAdaptiveIcons; mContext = context.getApplicationContext(); } @NonNull @Override public ModelLoader build( @NonNull MultiModelLoaderFactory multiFactory) { return new AppIconModelLoader(mIconSize, mShrinkNonAdaptiveIcons, mContext); } @Override public void teardown() { } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/AppModule.java ================================================ /* * 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 */ package org.lsposed.manager.util; import android.content.Context; import android.content.pm.PackageInfo; import android.graphics.Bitmap; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; import org.lsposed.manager.R; @GlideModule public class AppModule extends AppGlideModule { @Override public boolean isManifestParsingEnabled() { return false; } @Override public void registerComponents(Context context, @NonNull Glide glide, Registry registry) { int iconSize = context.getResources().getDimensionPixelSize(R.dimen.app_icon_size); var factory = new AppIconModelLoader.Factory(iconSize, false, context); registry.prepend(PackageInfo.class, Bitmap.class, factory); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/BackupUtils.java ================================================ /* * 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 */ package org.lsposed.manager.util; import android.net.Uri; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.lsposed.manager.App; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.adapters.ScopeAdapter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import rikka.core.os.FileUtils; public class BackupUtils { private static final int VERSION = 2; public static void backup(Uri uri) throws JSONException, IOException { backup(uri, null); } public static void backup(Uri uri, String packageName) throws IOException, JSONException { JSONObject rootObject = new JSONObject(); rootObject.put("version", VERSION); JSONArray modulesArray = new JSONArray(); var modules = ModuleUtil.getInstance().getModules(); if (modules == null) return; for (ModuleUtil.InstalledModule module : modules.values()) { if (packageName != null && !module.packageName.equals(packageName)) { continue; } JSONObject moduleObject = new JSONObject(); moduleObject.put("enable", ModuleUtil.getInstance().isModuleEnabled(module.packageName)); moduleObject.put("package", module.packageName); List scope = ConfigManager.getModuleScope(module.packageName); JSONArray scopeArray = new JSONArray(); for (ScopeAdapter.ApplicationWithEquals s : scope) { JSONObject app = new JSONObject(); app.put("package", s.packageName); app.put("userId", s.userId); scopeArray.put(app); } moduleObject.put("scope", scopeArray); modulesArray.put(moduleObject); } rootObject.put("modules", modulesArray); try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(App.getInstance().getContentResolver().openOutputStream(uri))) { gzipOutputStream.write(rootObject.toString().getBytes()); } } public static void restore(Uri uri) throws JSONException, IOException { restore(uri, null); } public static void restore(Uri uri, String packageName) throws IOException, JSONException { try (GZIPInputStream gzipInputStream = new GZIPInputStream(App.getInstance().getContentResolver().openInputStream(uri), 32)) { StringBuilder string = new StringBuilder(); try (var os = new ByteArrayOutputStream()) { FileUtils.copy(gzipInputStream, os); string.append(os); } gzipInputStream.close(); JSONObject rootObject = new JSONObject(string.toString()); int version = rootObject.getInt("version"); if (version == VERSION || version == 1) { JSONArray modules = rootObject.getJSONArray("modules"); for (int i = 0; i < modules.length(); i++) { JSONObject moduleObject = modules.getJSONObject(i); String name = moduleObject.getString("package"); if (packageName != null && !name.equals(packageName)) { continue; } ModuleUtil.InstalledModule module = ModuleUtil.getInstance().getModule(name); if (module != null) { var enabled = moduleObject.getBoolean("enable"); ModuleUtil.getInstance().setModuleEnabled(name, enabled); if (!enabled) continue; JSONArray scopeArray = moduleObject.getJSONArray("scope"); HashSet scope = new HashSet<>(); for (int j = 0; j < scopeArray.length(); j++) { if (version == VERSION) { JSONObject app = scopeArray.getJSONObject(j); scope.add(new ScopeAdapter.ApplicationWithEquals(app.getString("package"), app.getInt("userId"))); } else { scope.add(new ScopeAdapter.ApplicationWithEquals(scopeArray.getString(j), 0)); } } ConfigManager.setModuleScope(name, module.legacy, scope); } } } else { throw new IllegalArgumentException("Unknown backup file version"); } } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/CloudflareDNS.java ================================================ package org.lsposed.manager.util; import android.os.Build; import androidx.annotation.NonNull; import org.lsposed.manager.App; import java.net.InetAddress; import java.net.Proxy; import java.net.ProxySelector; import java.net.UnknownHostException; import java.util.List; import okhttp3.ConnectionSpec; import okhttp3.Dns; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.dnsoverhttps.DnsOverHttps; import okhttp3.internal.platform.Platform; public final class CloudflareDNS implements Dns { private static final HttpUrl url = HttpUrl.get("https://cloudflare-dns.com/dns-query"); public boolean DoH = App.getPreferences().getBoolean("doh", false); public boolean noProxy = ProxySelector.getDefault().select(url.uri()).get(0) == Proxy.NO_PROXY; private final Dns cloudflare; public CloudflareDNS() { var trustManager = Platform.get().platformTrustManager(); var tls = ConnectionSpec.RESTRICTED_TLS; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { //noinspection deprecation tls = new ConnectionSpec.Builder(tls) .supportsTlsExtensions(false) .build(); } var builder = new DnsOverHttps.Builder() .resolvePrivateAddresses(true) .url(HttpUrl.get("https://cloudflare-dns.com/dns-query")) .client(new OkHttpClient.Builder() .cache(App.getOkHttpCache()) .sslSocketFactory(new NoSniFactory(), trustManager) .connectionSpecs(List.of(tls)) .build()); try { builder.bootstrapDnsHosts(List.of( InetAddress.getByName("1.1.1.1"), InetAddress.getByName("1.0.0.1"), InetAddress.getByName("2606:4700:4700::1111"), InetAddress.getByName("2606:4700:4700::1001"))); } catch (UnknownHostException ignored) { } cloudflare = builder.build(); } @NonNull @Override public List lookup(@NonNull String hostname) throws UnknownHostException { if (DoH && noProxy) { return cloudflare.lookup(hostname); } else { return SYSTEM.lookup(hostname); } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/EmptyAccessibilityDelegate.java ================================================ /* * 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.manager.util; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeProvider; public class EmptyAccessibilityDelegate extends View.AccessibilityDelegate { @Override public void sendAccessibilityEvent(View host, int eventType) { } @Override public boolean performAccessibilityAction(View host, int action, Bundle args) { return true; } @Override public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { } @Override public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { return true; } @Override public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { } @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { } @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { } @Override public void addExtraDataToAccessibilityNodeInfo(View host, AccessibilityNodeInfo info, String extraDataKey, Bundle arguments) { } @Override public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) { return true; } @Override public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) { return null; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/ModuleUtil.java ================================================ /* * 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 */ package org.lsposed.manager.util; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import org.lsposed.lspd.models.UserInfo; import org.lsposed.manager.App; import org.lsposed.manager.ConfigManager; import org.lsposed.manager.repo.RepoLoader; import org.lsposed.manager.repo.model.OnlineModule; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.zip.ZipFile; public final class ModuleUtil { // xposedminversion below this public static int MIN_MODULE_VERSION = 2; // reject modules with private static ModuleUtil instance = null; private final PackageManager pm; private final Set listeners = ConcurrentHashMap.newKeySet(); private HashSet enabledModules = new HashSet<>(); private List users = new ArrayList<>(); private Map, InstalledModule> installedModules = new HashMap<>(); private boolean modulesLoaded = false; 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; private ModuleUtil() { pm = App.getInstance().getPackageManager(); } public boolean isModulesLoaded() { return modulesLoaded; } public static synchronized ModuleUtil getInstance() { if (instance == null) { instance = new ModuleUtil(); App.getExecutorService().submit(instance::reloadInstalledModules); } return instance; } public static int extractIntPart(String str) { int result = 0, length = str.length(); for (int offset = 0; offset < length; offset++) { char c = str.charAt(offset); if ('0' <= c && c <= '9') result = result * 10 + (c - '0'); else break; } return result; } public static ZipFile getModernModuleApk(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}; ZipFile zip = null; for (var apk : apks) { try { zip = new ZipFile(apk); if (zip.getEntry("META-INF/xposed/java_init.list") != null) { return zip; } zip.close(); zip = null; } catch (IOException ignored) { } } return zip; } public static boolean isLegacyModule(ApplicationInfo info) { return info.metaData != null && info.metaData.containsKey("xposedminversion"); } synchronized public void reloadInstalledModules() { modulesLoaded = false; if (!ConfigManager.isBinderAlive()) { modulesLoaded = true; return; } Map, InstalledModule> modules = new HashMap<>(); var users = ConfigManager.getUsers(); for (PackageInfo pkg : ConfigManager.getInstalledPackagesFromAllUsers(PackageManager.GET_META_DATA | MATCH_ALL_FLAGS, false)) { ApplicationInfo app = pkg.applicationInfo; var modernApk = getModernModuleApk(app); if (modernApk != null || isLegacyModule(app)) { modules.computeIfAbsent(Pair.create(pkg.packageName, app.uid / App.PER_USER_RANGE), k -> new InstalledModule(pkg, modernApk)); } } installedModules = modules; this.users = users; enabledModules = new HashSet<>(Arrays.asList(ConfigManager.getEnabledModules())); modulesLoaded = true; listeners.forEach(ModuleListener::onModulesReloaded); } @Nullable public List getUsers() { return modulesLoaded ? users : null; } public InstalledModule reloadSingleModule(String packageName, int userId) { return reloadSingleModule(packageName, userId, false); } public InstalledModule reloadSingleModule(String packageName, int userId, boolean packageFullyRemoved) { if (packageFullyRemoved && isModuleEnabled(packageName)) { enabledModules.remove(packageName); listeners.forEach(ModuleListener::onModulesReloaded); } PackageInfo pkg; try { pkg = ConfigManager.getPackageInfo(packageName, PackageManager.GET_META_DATA, userId); } catch (NameNotFoundException e) { InstalledModule old = installedModules.remove(Pair.create(packageName, userId)); if (old != null) listeners.forEach(i -> i.onSingleModuleReloaded(old)); return null; } ApplicationInfo app = pkg.applicationInfo; var modernApk = getModernModuleApk(app); if (modernApk != null || isLegacyModule(app)) { InstalledModule module = new InstalledModule(pkg, modernApk); installedModules.put(Pair.create(packageName, userId), module); listeners.forEach(i -> i.onSingleModuleReloaded(module)); return module; } else { InstalledModule old = installedModules.remove(Pair.create(packageName, userId)); if (old != null) listeners.forEach(i -> i.onSingleModuleReloaded(old)); return null; } } @Nullable public InstalledModule getModule(String packageName, int userId) { return modulesLoaded ? installedModules.get(Pair.create(packageName, userId)) : null; } @Nullable public InstalledModule getModule(String packageName) { return getModule(packageName, 0); } @Nullable synchronized public Map, InstalledModule> getModules() { return modulesLoaded ? installedModules : null; } public boolean setModuleEnabled(String packageName, boolean enabled) { if (!ConfigManager.setModuleEnabled(packageName, enabled)) { return false; } if (enabled) { enabledModules.add(packageName); } else { enabledModules.remove(packageName); } return true; } public boolean isModuleEnabled(String packageName) { return enabledModules.contains(packageName); } public int getEnabledModulesCount() { return modulesLoaded ? enabledModules.size() : -1; } public void addListener(ModuleListener listener) { listeners.add(listener); } public void removeListener(ModuleListener listener) { listeners.remove(listener); } public interface ModuleListener { /** * Called whenever one (previously or now) installed module has been * reloaded */ default void onSingleModuleReloaded(InstalledModule module) { } default void onModulesReloaded() { } } public class InstalledModule { //private static final int FLAG_FORWARD_LOCK = 1 << 29; public final int userId; public final String packageName; public final String versionName; public final long versionCode; public final boolean legacy; public final int minVersion; public final int targetVersion; public final boolean staticScope; public final long installTime; public final long updateTime; public final ApplicationInfo app; public final PackageInfo pkg; private String appName; // loaded lazily private String description; // loaded lazily private List scopeList; // loaded lazily private InstalledModule(PackageInfo pkg, ZipFile modernModuleApk) { app = pkg.applicationInfo; this.pkg = pkg; userId = pkg.applicationInfo.uid / App.PER_USER_RANGE; packageName = pkg.packageName; versionName = pkg.versionName; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { versionCode = pkg.versionCode; } else { versionCode = pkg.getLongVersionCode(); } installTime = pkg.firstInstallTime; updateTime = pkg.lastUpdateTime; legacy = modernModuleApk == null; if (legacy) { Object minVersionRaw = app.metaData.get("xposedminversion"); if (minVersionRaw instanceof Integer) { minVersion = (Integer) minVersionRaw; } else if (minVersionRaw instanceof String) { minVersion = extractIntPart((String) minVersionRaw); } else { minVersion = 0; } targetVersion = minVersion; // legacy modules don't have a target version staticScope = false; } else { int minVersion = 100; int targetVersion = 100; boolean staticScope = false; try (modernModuleApk) { var propEntry = modernModuleApk.getEntry("META-INF/xposed/module.prop"); if (propEntry != null) { var prop = new Properties(); prop.load(modernModuleApk.getInputStream(propEntry)); minVersion = extractIntPart(prop.getProperty("minApiVersion")); targetVersion = extractIntPart(prop.getProperty("targetApiVersion")); staticScope = TextUtils.equals(prop.getProperty("staticScope"), "true"); } var scopeEntry = modernModuleApk.getEntry("META-INF/xposed/scope.list"); if (scopeEntry != null) { try (var reader = new BufferedReader(new InputStreamReader(modernModuleApk.getInputStream(scopeEntry)))) { scopeList = reader.lines().collect(Collectors.toList()); } } else { scopeList = Collections.emptyList(); } } catch (IOException | OutOfMemoryError e) { Log.e(App.TAG, "Error while closing modern module APK", e); } this.minVersion = minVersion; this.targetVersion = targetVersion; this.staticScope = staticScope; } } public boolean isInstalledOnExternalStorage() { return (app.flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; } public String getAppName() { if (appName == null) appName = app.loadLabel(pm).toString(); return appName; } public String getDescription() { if (this.description != null) return this.description; String descriptionTmp = ""; if (legacy) { Object descriptionRaw = app.metaData.get("xposeddescription"); if (descriptionRaw instanceof String) { descriptionTmp = ((String) descriptionRaw).trim(); } else if (descriptionRaw instanceof Integer) { try { int resId = (Integer) descriptionRaw; if (resId != 0) descriptionTmp = pm.getResourcesForApplication(app).getString(resId).trim(); } catch (Exception ignored) { } } } else { var des = app.loadDescription(pm); if (des != null) descriptionTmp = des.toString(); } this.description = descriptionTmp; return this.description; } public List getScopeList() { if (scopeList != null) return scopeList; List list = null; try { int scopeListResourceId = app.metaData.getInt("xposedscope"); if (scopeListResourceId != 0) { list = Arrays.asList(pm.getResourcesForApplication(app).getStringArray(scopeListResourceId)); } else { String scopeListString = app.metaData.getString("xposedscope"); if (scopeListString != null) list = Arrays.asList(scopeListString.split(";")); } } catch (Exception ignored) { } if (list == null) { OnlineModule module = RepoLoader.getInstance().getOnlineModule(packageName); if (module != null && module.getScope() != null) { list = module.getScope(); } } if (list != null) { //For historical reasons, legacy modules use the opposite name. //https://github.com/rovo89/XposedBridge/commit/6b49688c929a7768f3113b4c65b429c7a7032afa list.replaceAll(s -> switch (s) { case "android" -> "system"; case "system" -> "android"; default -> s; } ); scopeList = list; } return scopeList; } public PackageInfo getPackageInfo() { return pkg; } @NonNull @Override public String toString() { return getAppName(); } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/NavUtil.java ================================================ /* * 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 */ package org.lsposed.manager.util; import android.app.Activity; import android.content.ActivityNotFoundException; import android.net.Uri; import android.widget.Toast; import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabsIntent; import rikka.core.util.ResourceUtils; public final class NavUtil { public static void startURL(Activity activity, Uri uri) { CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder(); customTabsIntent.setShowTitle(true); CustomTabColorSchemeParams params = new CustomTabColorSchemeParams.Builder() .setToolbarColor(ResourceUtils.resolveColor(activity.getTheme(), android.R.attr.colorBackground)) .setNavigationBarColor(ResourceUtils.resolveColor(activity.getTheme(), android.R.attr.navigationBarColor)) .setNavigationBarDividerColor(0) .build(); customTabsIntent.setDefaultColorSchemeParams(params); boolean night = ResourceUtils.isNightMode(activity.getResources().getConfiguration()); customTabsIntent.setColorScheme(night ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT); try { customTabsIntent.build().launchUrl(activity, uri); } catch (ActivityNotFoundException ignored) { Toast.makeText(activity, uri.toString(), Toast.LENGTH_SHORT).show(); } } public static void startURL(Activity activity, String url) { startURL(activity, Uri.parse(url)); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/NoSniFactory.java ================================================ package org.lsposed.manager.util; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import javax.net.ssl.SSLSocketFactory; public final class NoSniFactory extends SSLSocketFactory { private static final SSLSocketFactory defaultFactory = (SSLSocketFactory) getDefault(); @SuppressWarnings("deprecation") private static final android.net.SSLCertificateSocketFactory openSSLSocket = (android.net.SSLCertificateSocketFactory) android.net.SSLCertificateSocketFactory .getDefault(1000); @Override public String[] getDefaultCipherSuites() { return defaultFactory.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return defaultFactory.getSupportedCipherSuites(); } @Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { return config(defaultFactory.createSocket(s, host, port, autoClose)); } @Override public Socket createSocket(String host, int port) throws IOException { return config(defaultFactory.createSocket(host, port)); } @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { return config(defaultFactory.createSocket(host, port, localHost, localPort)); } @Override public Socket createSocket(InetAddress host, int port) throws IOException { return config(defaultFactory.createSocket(host, port)); } @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return config(defaultFactory.createSocket(address, port, localAddress, localPort)); } private Socket config(Socket socket) { try { openSSLSocket.setHostname(socket, null); openSSLSocket.setUseSessionTickets(socket, true); } catch (IllegalArgumentException ignored) { } return socket; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/ShortcutUtil.java ================================================ /* * 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.manager.util; import android.annotation.SuppressLint; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; 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.os.Build; import org.lsposed.manager.App; import org.lsposed.manager.R; import java.util.ArrayList; import java.util.List; import java.util.UUID; public class ShortcutUtil { private static final String SHORTCUT_ID = "org.lsposed.manager.shortcut"; private static Bitmap getBitmap(Context context, int id) { var r = context.getResources(); var res = r.getDrawable(id, context.getTheme()); 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 Intent getLaunchIntent(Context context) { var pm = context.getPackageManager(); var pkg = context.getPackageName(); var intent = pm.getLaunchIntentForPackage(pkg); if (intent == null) { try { var pkgInfo = pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES); if (pkgInfo.activities != null) { for (var activityInfo : pkgInfo.activities) { if (activityInfo.processName.equals(activityInfo.packageName)) { intent = new Intent(Intent.ACTION_MAIN); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setComponent(new ComponentName(pkg, activityInfo.name)); break; } } } } catch (PackageManager.NameNotFoundException ignored) { } } if (intent != null) { var categories = intent.getCategories(); if (categories != null) { categories.clear(); } intent.addCategory("org.lsposed.manager.LAUNCH_MANAGER"); intent.setPackage(pkg); } return intent; } @SuppressLint("InlinedApi") private static IntentSender registerReceiver(Context context, Runnable task) { if (task == null) return null; var uuid = UUID.randomUUID().toString(); var filter = new IntentFilter(uuid); var permission = "android.permission.CREATE_USERS"; var receiver = new BroadcastReceiver() { @Override public void onReceive(Context c, Intent intent) { if (!uuid.equals(intent.getAction())) return; context.unregisterReceiver(this); task.run(); } }; context.registerReceiver(receiver, filter, permission, null/* main thread */, Context.RECEIVER_EXPORTED); var intent = new Intent(uuid); int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; return PendingIntent.getBroadcast(context, 0, intent, flags).getIntentSender(); } private static ShortcutInfo.Builder getShortcutBuilder(Context context) { var builder = new ShortcutInfo.Builder(context, SHORTCUT_ID) .setShortLabel(context.getString(R.string.app_name)) .setIntent(getLaunchIntent(context)) .setIcon(Icon.createWithAdaptiveBitmap(getBitmap(context, R.drawable.ic_launcher))); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { var activity = new ComponentName(context.getPackageName(), "android.app.AppDetailsActivity"); builder.setActivity(activity); } return builder; } public static boolean isRequestPinShortcutSupported(Context context) throws RuntimeException { var sm = context.getSystemService(ShortcutManager.class); return sm.isRequestPinShortcutSupported(); } public static boolean requestPinLaunchShortcut(Runnable afterPinned) { if (!App.isParasitic) throw new RuntimeException(); var context = App.getInstance(); var sm = context.getSystemService(ShortcutManager.class); if (!sm.isRequestPinShortcutSupported()) return false; return sm.requestPinShortcut(getShortcutBuilder(context).build(), registerReceiver(context, afterPinned)); } public static boolean updateShortcut() { if (!isLaunchShortcutPinned()) return false; var context = App.getInstance(); var sm = context.getSystemService(ShortcutManager.class); List shortcutInfoList = new ArrayList<>(); shortcutInfoList.add(getShortcutBuilder(context).build()); return sm.updateShortcuts(shortcutInfoList); } public static boolean isLaunchShortcutPinned() { var context = App.getInstance(); var sm = context.getSystemService(ShortcutManager.class); for (var info : sm.getPinnedShortcuts()) { if (SHORTCUT_ID.equals(info.getId())) { return true; } } return false; } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/SimpleStatefulAdaptor.java ================================================ /* * 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.manager.util; import android.os.Bundle; import android.os.Parcelable; import android.util.SparseArray; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.adapter.StatefulAdapter; import java.util.HashMap; import java.util.List; public abstract class SimpleStatefulAdaptor extends RecyclerView.Adapter implements StatefulAdapter { HashMap> states = new HashMap<>(); protected RecyclerView rv = null; public SimpleStatefulAdaptor() { setStateRestorationPolicy(StateRestorationPolicy.PREVENT_WHEN_EMPTY); } @Override @CallSuper public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { rv = recyclerView; super.onAttachedToRecyclerView(recyclerView); } @Override public void onViewRecycled(@NonNull T holder) { saveStateOf(holder); super.onViewRecycled(holder); } @CallSuper @Override public final void onBindViewHolder(@NonNull T holder, int position, @NonNull List payloads) { var state = states.remove(holder.getItemId()); if (state != null) { holder.itemView.restoreHierarchyState(state); } onBindViewHolder(holder, position); } private void saveStateOf(@NonNull RecyclerView.ViewHolder holder) { var state = new SparseArray(); holder.itemView.saveHierarchyState(state); states.put(holder.getItemId(), state); } @NonNull public Parcelable saveState() { for (int childCount = rv.getChildCount(), i = 0; i < childCount; ++i) { saveStateOf(rv.getChildViewHolder(rv.getChildAt(i))); } var out = new Bundle(); for (var state : states.entrySet()) { var item = new Bundle(); for (int i = 0; i < state.getValue().size(); ++i) { item.putParcelable(String.valueOf(state.getValue().keyAt(i)), state.getValue().valueAt(i)); } out.putParcelable(String.valueOf(state.getKey()), item); } return out; } @Override public void restoreState(@NonNull Parcelable savedState) { if (savedState instanceof Bundle) { for (var stateKey : ((Bundle) savedState).keySet()) { var array = new SparseArray(); var state = ((Bundle) savedState).getParcelable(stateKey); if (state instanceof Bundle) { for (var itemKey : ((Bundle) state).keySet()) { var item = ((Bundle) state).getParcelable(itemKey); array.put(Integer.parseInt(itemKey), item); } } states.put(Long.parseLong(stateKey), array); } } } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/ThemeUtil.java ================================================ /* * 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.manager.util; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.StyleRes; import androidx.appcompat.app.AppCompatDelegate; import com.google.android.material.color.DynamicColors; import org.lsposed.manager.App; import org.lsposed.manager.R; import java.util.HashMap; import java.util.Map; import rikka.core.util.ResourceUtils; public class ThemeUtil { private static final Map colorThemeMap = new HashMap<>(); private static final SharedPreferences preferences; public static final String MODE_NIGHT_FOLLOW_SYSTEM = "MODE_NIGHT_FOLLOW_SYSTEM"; public static final String MODE_NIGHT_NO = "MODE_NIGHT_NO"; public static final String MODE_NIGHT_YES = "MODE_NIGHT_YES"; static { preferences = App.getPreferences(); colorThemeMap.put("SAKURA", R.style.ThemeOverlay_MaterialSakura); colorThemeMap.put("MATERIAL_RED", R.style.ThemeOverlay_MaterialRed); colorThemeMap.put("MATERIAL_PINK", R.style.ThemeOverlay_MaterialPink); colorThemeMap.put("MATERIAL_PURPLE", R.style.ThemeOverlay_MaterialPurple); colorThemeMap.put("MATERIAL_DEEP_PURPLE", R.style.ThemeOverlay_MaterialDeepPurple); colorThemeMap.put("MATERIAL_INDIGO", R.style.ThemeOverlay_MaterialIndigo); colorThemeMap.put("MATERIAL_BLUE", R.style.ThemeOverlay_MaterialBlue); colorThemeMap.put("MATERIAL_LIGHT_BLUE", R.style.ThemeOverlay_MaterialLightBlue); colorThemeMap.put("MATERIAL_CYAN", R.style.ThemeOverlay_MaterialCyan); colorThemeMap.put("MATERIAL_TEAL", R.style.ThemeOverlay_MaterialTeal); colorThemeMap.put("MATERIAL_GREEN", R.style.ThemeOverlay_MaterialGreen); colorThemeMap.put("MATERIAL_LIGHT_GREEN", R.style.ThemeOverlay_MaterialLightGreen); colorThemeMap.put("MATERIAL_LIME", R.style.ThemeOverlay_MaterialLime); colorThemeMap.put("MATERIAL_YELLOW", R.style.ThemeOverlay_MaterialYellow); colorThemeMap.put("MATERIAL_AMBER", R.style.ThemeOverlay_MaterialAmber); colorThemeMap.put("MATERIAL_ORANGE", R.style.ThemeOverlay_MaterialOrange); colorThemeMap.put("MATERIAL_DEEP_ORANGE", R.style.ThemeOverlay_MaterialDeepOrange); colorThemeMap.put("MATERIAL_BROWN", R.style.ThemeOverlay_MaterialBrown); colorThemeMap.put("MATERIAL_BLUE_GREY", R.style.ThemeOverlay_MaterialBlueGrey); } private static final String THEME_DEFAULT = "DEFAULT"; private static final String THEME_BLACK = "BLACK"; private static boolean isBlackNightTheme() { return preferences.getBoolean("black_dark_theme", false); } public static boolean isSystemAccent() { return DynamicColors.isDynamicColorAvailable() && preferences.getBoolean("follow_system_accent", true); } public static String getNightTheme(Context context) { if (isBlackNightTheme() && ResourceUtils.isNightMode(context.getResources().getConfiguration())) return THEME_BLACK; return THEME_DEFAULT; } @StyleRes public static int getNightThemeStyleRes(Context context) { switch (getNightTheme(context)) { case THEME_BLACK: return R.style.ThemeOverlay_Black; case THEME_DEFAULT: default: return R.style.ThemeOverlay; } } public static String getColorTheme() { if (isSystemAccent()) { return "SYSTEM"; } return preferences.getString("theme_color", "COLOR_BLUE"); } @StyleRes public static int getColorThemeStyleRes() { Integer theme = colorThemeMap.get(getColorTheme()); if (theme == null) { return R.style.ThemeOverlay_MaterialBlue; } return theme; } public static int getDarkTheme(String mode) { switch (mode) { case MODE_NIGHT_FOLLOW_SYSTEM: default: return AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; case MODE_NIGHT_YES: return AppCompatDelegate.MODE_NIGHT_YES; case MODE_NIGHT_NO: return AppCompatDelegate.MODE_NIGHT_NO; } } public static int getDarkTheme() { return getDarkTheme(preferences.getString("dark_theme", MODE_NIGHT_FOLLOW_SYSTEM)); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/UpdateUtil.java ================================================ /* * 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.manager.util; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.lsposed.manager.App; import org.lsposed.manager.BuildConfig; import org.lsposed.manager.ConfigManager; import java.io.File; import java.io.IOException; import java.time.Instant; import java.time.ZoneOffset; import java.util.Locale; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Request; import okhttp3.Response; import okio.Okio; public class UpdateUtil { public static void loadRemoteVersion() { var request = new Request.Builder() .url("https://api.github.com/repos/JingMatrix/LSPosed/releases/latest") .addHeader("Accept", "application/vnd.github.v3+json") .build(); var callback = new Callback() { @Override 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)); } } catch (Throwable t) { Log.e(App.TAG, t.getMessage(), t); } } @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.e(App.TAG, "loadRemoteVersion: " + e.getMessage()); var pref = App.getPreferences(); if (pref.getBoolean("checked", false)) return; pref.edit().putBoolean("checked", true).apply(); } }; App.getOkHttpClient().newCall(request).enqueue(callback); } private static void checkAssets(JsonObject assets, String releaseNotes, String api) { 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()) .putString("release_notes", releaseNotes) .putString("zip_file", null) .putBoolean("checked", true) .apply(); var updatedAt = Instant.parse(assets.get("updated_at").getAsString()); var downloadUrl = assets.get("browser_download_url").getAsString(); var zipTime = pref.getLong("zip_time", 0); if (!updatedAt.equals(Instant.ofEpochSecond(zipTime))) { var zip = downloadNewZipSync(downloadUrl, name); var size = assets.get("size").getAsLong(); if (zip != null && zip.length() == size) { pref.edit() .putLong("zip_time", updatedAt.getEpochSecond()) .putString("zip_file", zip.getAbsolutePath()) .apply(); } } } public static boolean needUpdate() { var pref = App.getPreferences(); if (!pref.getBoolean("checked", false)) return false; var now = Instant.now(); var buildTime = Instant.ofEpochSecond(BuildConfig.BUILD_TIME); var check = pref.getLong("latest_check", 0); if (check > 0) { var checkTime = Instant.ofEpochSecond(check); if (checkTime.atOffset(ZoneOffset.UTC).plusDays(30).toInstant().isBefore(now)) return true; var code = pref.getInt("latest_version", 0); return code > BuildConfig.VERSION_CODE; } return buildTime.atOffset(ZoneOffset.UTC).plusDays(30).toInstant().isBefore(now); } @Nullable private static File downloadNewZipSync(String url, String name) { var request = new Request.Builder().url(url).build(); var zip = new File(App.getInstance().getCacheDir(), name); try (Response response = App.getOkHttpClient().newCall(request).execute()) { var body = response.body(); if (!response.isSuccessful() || body == null) return null; try (var source = body.source(); var sink = Okio.buffer(Okio.sink(zip))) { sink.writeAll(source); } } catch (IOException e) { Log.e(App.TAG, "downloadNewZipSync: " + e.getMessage()); return null; } return zip; } public static boolean canInstall() { if (!ConfigManager.isBinderAlive()) return false; var pref = App.getPreferences(); var zip = pref.getString("zip_file", null); return zip != null && new File(zip).isFile(); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/chrome/CustomTabsURLSpan.java ================================================ /* * 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 */ package org.lsposed.manager.util.chrome; import android.app.Activity; import android.text.style.URLSpan; import android.view.View; import org.lsposed.manager.util.NavUtil; public class CustomTabsURLSpan extends URLSpan { private final Activity activity; public CustomTabsURLSpan(Activity activity, String url) { super(url); this.activity = activity; } @Override public void onClick(View widget) { String url = getURL(); NavUtil.startURL(activity, url); } } ================================================ FILE: app/src/main/java/org/lsposed/manager/util/chrome/LinkTransformationMethod.java ================================================ /* * 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 */ package org.lsposed.manager.util.chrome; import android.app.Activity; import android.graphics.Rect; import android.text.Spannable; import android.text.Spanned; import android.text.method.TransformationMethod; import android.text.style.URLSpan; import android.view.View; import android.widget.TextView; public class LinkTransformationMethod implements TransformationMethod { private final Activity activity; public LinkTransformationMethod(Activity activity) { this.activity = activity; } @Override public CharSequence getTransformation(CharSequence source, View view) { if (view instanceof TextView) { TextView textView = (TextView) view; if (textView.getText() == null || !(textView.getText() instanceof Spannable)) { return source; } Spannable text = (Spannable) textView.getText(); URLSpan[] spans = text.getSpans(0, textView.length(), URLSpan.class); for (int i = spans.length - 1; i >= 0; i--) { URLSpan oldSpan = spans[i]; int start = text.getSpanStart(oldSpan); int end = text.getSpanEnd(oldSpan); String url = oldSpan.getURL(); text.removeSpan(oldSpan); text.setSpan(new CustomTabsURLSpan(activity, url), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } return text; } return source; } @Override public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, Rect previouslyFocusedRect) { } } ================================================ FILE: app/src/main/res/anim/fragment_enter.xml ================================================ ================================================ FILE: app/src/main/res/anim/fragment_enter_pop.xml ================================================ ================================================ FILE: app/src/main/res/anim/fragment_exit.xml ================================================ ================================================ FILE: app/src/main/res/anim/fragment_exit_pop.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_assignment_checkable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_attach_file.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_add_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_arrow_back_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_assignment_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_chat_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_extension_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_get_app_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_home_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_info_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_search_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_settings_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_settings_backup_restore_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_extension_checkable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_get_app_checkable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_home_checkable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_keyboard_arrow_down.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_open_in_browser.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_android_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_app_shortcut_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_assignment_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_dark_mode_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_dns_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_extension_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_format_color_fill_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_get_app_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_groups_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_home_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_invert_colors_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_language_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_merge_type_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_palette_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_restore_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_settings_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_shield_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_speaker_notes_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_outline_translate_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_bug_report_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_check_circle_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_error_outline_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_settings_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_update_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_warning_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_save.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_settings_checkable.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shortcut_ic_logs.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shortcut_ic_modules.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shortcut_ic_repo.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shortcut_ic_settings.xml ================================================ ================================================ FILE: app/src/main/res/drawable/simple_menu_background.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_about.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/dialog_title.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_app_list.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_compile_dialog.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_home.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_pager.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_repo.xml ================================================ ================================================ FILE: app/src/main/res/layout/fragment_settings.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_log_textview.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_master_switch.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_module.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_onlinemodule.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_repo_loadmore.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_repo_readme.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_repo_recyclerview.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_repo_release.xml ================================================ ================================================ FILE: app/src/main/res/layout/item_repo_title_description.xml ================================================ ================================================ FILE: app/src/main/res/layout/preference_recyclerview.xml ================================================ ================================================ FILE: app/src/main/res/layout/scrollable_dialog.xml ================================================ ================================================ FILE: app/src/main/res/layout/swiperefresh_recyclerview.xml ================================================ ================================================ FILE: app/src/main/res/layout-sw600dp/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/menu/context_menu_modules.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_app_item.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_app_list.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_home.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_logs.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_modules.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_repo.xml ================================================ ================================================ FILE: app/src/main/res/menu/menu_repo_item.xml ================================================ ================================================ FILE: app/src/main/res/menu/navigation_menu.xml ================================================ ================================================ FILE: app/src/main/res/menu-sw600dp/navigation_menu.xml ================================================ ================================================ FILE: app/src/main/res/navigation/main_nav.xml ================================================ ================================================ FILE: app/src/main/res/navigation/modules_nav.xml ================================================ ================================================ FILE: app/src/main/res/navigation/repo_nav.xml ================================================ ================================================ FILE: app/src/main/res/values/arrays.xml ================================================ @string/dark_theme_off @string/dark_theme_on @string/dark_theme_follow_system MODE_NIGHT_NO MODE_NIGHT_YES MODE_NIGHT_FOLLOW_SYSTEM SAKURA MATERIAL_RED MATERIAL_PINK MATERIAL_PURPLE MATERIAL_DEEP_PURPLE MATERIAL_INDIGO MATERIAL_BLUE MATERIAL_LIGHT_BLUE MATERIAL_CYAN MATERIAL_TEAL MATERIAL_GREEN MATERIAL_LIGHT_GREEN MATERIAL_LIME MATERIAL_YELLOW MATERIAL_AMBER MATERIAL_ORANGE MATERIAL_DEEP_ORANGE MATERIAL_BROWN MATERIAL_BLUE_GREY @string/color_sakura @string/color_red @string/color_pink @string/color_purple @string/color_deep_purple @string/color_indigo @string/color_blue @string/color_light_blue @string/color_cyan @string/color_teal @string/color_green @string/color_light_green @string/color_lime @string/color_yellow @string/color_amber @string/color_orange @string/color_deep_orange @string/color_brown @string/color_blue_grey @string/update_channel_stable @string/update_channel_bate @string/update_channel_nightly CHANNEL_STABLE CHANNEL_BETA CHANNEL_NIGHTLY ================================================ FILE: app/src/main/res/values/attrs.xml ================================================ ================================================ FILE: app/src/main/res/values/colors.xml ================================================ @color/abc_primary_text_material_dark @color/abc_primary_text_material_light #FFFFFF #F48FB1 ================================================ FILE: app/src/main/res/values/dimens.xml ================================================ 48dp 48dp 6dp ================================================ FILE: app/src/main/res/values/integer.xml ================================================ 0x00800007 0x30 0 ================================================ FILE: app/src/main/res/values/settings.xml ================================================ false ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Overview Modules %d module enabled %d modules enabled Logs Settings Feedback or suggestion About Report issue Repository All modules up to date Published at %s Updated at %s %d module upgradable %d modules upgradable Join our %2$s channel]]> null Install Tap to install LSPosed Not installed LSPosed is not Installed Activated Partially activated SEPolicy is not loaded properly Please report this to Magisk developer.]]> System Framework injection failed Magisk or some low-quality Magisk modules.
Please try to disable Magisk modules other than Riru and LSPosed or submit full log to developers.]]>
System prop incorrect Modules may invalidate occasionally.]]> Need to update Please install the latest version of LSPosed Tips for module developer Please disable deploy optimizations on Android Studio, or use `gradlew installDebug` command to install. Otherwise the module apk will not be updated. API version Framework version Manager package name System version Device System ABI Dex Optimizer Wrapper Enabled Not enabled Supported Unsupported Android version unsatisfied Crashed Mount failed SELinux is permissive SELinux policy is incorrect Update LSPosed Confirm to update LSPosed? This device will reboot after update completion Copied to clipboard Welcome to LSPosed You are using the parasitic manager, which can create shortcut or still open from notification. You are using the parasitic manager, which can open from notification. Create shortcut Never show Parasitic Manager Recommended LSPosed now supports system parasitization to avoid detection, you can open parasitic manager from notification. It is recommended to uninstall the current application. Save Verbose Logs Modules Logs Saving log, please wait Logs saved Failed to save:\n%s Clear log now Log successfully cleared. Scroll to top Loading… Scroll to bottom Reload Failed to clear the log Word Wrap Verbose log enabled Verbose log disabled (no description provided) This module requires a newer Xposed version (%d) and thus cannot be activated This module is designed for a newer Xposed version (%d) and thus some functionalities may not work This module does not specify the Xposed version it needs. This module was created for Xposed version %1$d, but due to incompatible changes in version %2$d, it has been disabled This module cannot be loaded because it\'s installed on the SD card, please move it to internal storage Uninstall Module settings View in Repo Do you want to uninstall this module? Uninstalled %1$s Uninstall unsuccessful Add module to user Added %1$s to user %2$s Adding module failed Install to user %s Want to install %1$s to user %2$s? It is recommended to install manually, forcing installation via LSPosed may cause problems. expand collapse Re-optimize Optimizing… Optimization complete Launch it Optimization failed: return value is empty Optimization failed: Application name Package name Install time Update time Reverse System apps Sorting Enable module You did not select any app. Continue? Games Modules Denylist Failed to save scope list Version: %1$s Select Recommended You did not select any app. Select recommended apps? Select recommended apps? All None Auto-Include Xposed module is not activated yet Recommended Update available: %1$s Module %s has been disabled since no app selected. System Framework Backup Backup Restore Force stop Force stop? If you force stop an app, it may misbehave. Reboot is required for this change to apply Reboot Hide %s is on denylist. It may not take effect. On denylist View in other app App info ¯\\_(ツ)_\/¯\nNothing here 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 Backup and restore Backup module list and scope lists. Restore module list and scope lists. Backup Failed to backup:\n%s Please enable DocumentUI Restore Failed to restore:\n%s Network DNS over HTTPS Workaround DNS poisoning in some nations Theme color System theme color Force apps to show launcher icons After Android 10, apps are not allowed to hide their launcher icons. Turn off the toggle to disable this system feature. System Language Translation contributors Participate in translation Help us translate %s into your language Create a shortcut that can open parasitic manager Shortcut pinned 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 Info Homepage Source code Collaborators Assets Open in browser Show older versions No more release Failed to load module repo: %s Upgradable first Installed %d download %d downloads Sakura Red Pink Purple Deep purple Indigo Blue Light blue Cyan Teal Green Light green Lime Yellow Amber Orange Deep orange Brown Blue grey
================================================ FILE: app/src/main/res/values/strings_untranslatable.xml ================================================ LSPosed https://github.com/JingMatrix/LSPosed#install https://github.com/JingMatrix/LSPosed/releases/latest @string/module_repo ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: app/src/main/res/values/themes.xml ================================================ ================================================ FILE: app/src/main/res/values/themes_custom.xml ================================================ ================================================ FILE: app/src/main/res/values/themes_override.xml ================================================ ================================================ FILE: app/src/main/res/values-af/strings.xml ================================================ Oorsig Modules %d module enabled %d module enabled Logs Settings Terugvoer of voorstel Oor Rapporteer probleem Bewaarplek Alle modules op datum Published at %s Opgedateer op %s %d module opgradeerbaar %d modules opgradeerbaar Sluit aan by ons %2$s -kanaal]]> null Installeer2 Tik om LSPosed te installeer Not installed1 LSPosed is nie geïnstalleer nie Activated Partially activated SEPolicy is nie behoorlik gelaai nie Rapporteer dit asseblief aan Magisk ontwikkelaar.]]> Stelselraamwerk-inspuiting het misluk Magisk of sommige Magisk-modules van lae gehalte.
Probeer asseblief om Magisk-modules anders as Riru en LSPosed te deaktiveer of dien volledige log aan ontwikkelaars in.]]>
Stelselstut is verkeerd Modules kan soms ongeldig word.]]> Need to update Please install the latest version of LSPosed API version Framework version Bestuurder pakket naam Stelsel weergawe Toestel Stelsel ABI Dex Optimizer Wrapper Geaktiveer Nie geaktiveer nie Ondersteun Ongesteun Android-weergawe nie tevrede nie Het neergestort Montering het misluk SELinux is permissief SELinux-beleid is verkeerd Dateer LSPosed op Bevestig om LSPosed op te dateer? Hierdie toestel sal herlaai nadat die opdatering voltooi is Gekopieer na knipbord Welkom by LSPosed Jy gebruik die parasitiese bestuurder, wat kortpad kan skep of steeds oopmaak vanaf kennisgewing. Jy gebruik die parasitiese bestuurder, wat kan oopmaak vanaf kennisgewing. Skep kortpad Moet nooit wys nie Parasitiese Bestuurder Aanbeveel LSPosed ondersteun nou stelselparasitering om opsporing te vermy, jy kan parasitiese bestuurder oopmaak vanaf kennisgewing. Dit word aanbeveel om die huidige toepassing te verwyder. Stoor Uitgebreide logs Modules Stoor tans logboek, wag asseblief Logs gestoor Kon nie stoor nie:\n%s Vee logboek nou uit Log is suksesvol uitgevee. Blaai na bo Laai… Blaai na onder Herlaai Kon nie die logboek skoonmaak nie Woordomhulsel Uitgebreide logboek geaktiveer Uitgebreide logboek gedeaktiveer (geen beskrywing verskaf nie) Hierdie module vereis \'n nuwer Xposed weergawe (%d) en kan dus nie geaktiveer word nie This module is designed for a newer Xposed version (%d) and thus some functionalities may not work Hierdie module spesifiseer nie die Xposed-weergawe wat dit benodig nie. Hierdie module is geskep vir Xposed weergawe %1$d, maar as gevolg van onversoenbare veranderinge in weergawe %2$d, is dit gedeaktiveer Hierdie module kan nie gelaai word nie omdat dit op die SD-kaart geïnstalleer is, skuif dit asseblief na interne berging Deïnstalleer Module enabled Kyk in Repo Wil jy hierdie module deïnstalleer? Deïnstalleer %1$s Deïnstalleer onsuksesvol %d module enabled Het %1$s by gebruiker %2$sgevoeg %d module enabled Installeer op gebruiker %s Wil jy %1$s op gebruiker %2$sinstalleer? Dit word aanbeveel om met die hand te installeer, om installasie via LSPosed te dwing, kan probleme veroorsaak. uitbrei inval Heroptimaliseer Optimaliseer… Optimering voltooi Begin dit Optimalisering het misluk: terugkeerwaarde is leeg Optimalisering het misluk: Aansoeknaam Pakketnaam Installeer tyd Dateer tyd op Omgekeerde Stelseltoepassings Sorteer Aktiveer module Jy het geen toepassing gekies nie. Aanhou? Speletjies Modules Ontkenlys Kon nie omvanglys stoor nie weergawe: %1$s Aanbeveel Jy het geen toepassing gekies nie. Kies aanbevole programme? Kies aanbevole programme? Xposed-module is nog nie geaktiveer nie Aanbeveel Opdatering beskikbaar: %1$s Module %s is gedeaktiveer aangesien geen toepassing gekies is nie. Stelselraamwerk Ondersteuning Ondersteuning Herstel Forseer om te stop Forseer om te stop? As jy \'n program dwing om te stop, kan dit dalk wangedra. Herselflaai word vereis vir hierdie verandering om van toepassing te wees Herlaai Versteek %s is op ontkenlys. Dit mag dalk nie in werking tree nie. Op ontkenlys Bekyk in \'n ander toepassing App inligting ¯\\\\_(ツ)_\/¯\nNiks hier nie Raamwerk Deaktiveer verbose logs Rapporteer kwessies versoek om verbose logs in te sluit Swart donker tema Gebruik die suiwer swart tema as donker tema geaktiveer is Tema Friends en herstel Rugsteunmodulelys en omvanglyste. Herstel modulelys en omvanglyste. Ondersteuning Kon nie rugsteun nie:\n%s Aktiveer asseblief DocumentUI Herstel Kon nie herstel nie:\n%s Netwerk DNS oor HTTPS Oplossing DNS-vergiftiging in sommige lande Tema kleur Stelsel tema kleur Dwing programme om lanseerder-ikone te wys Ná Android 10 word programme nie toegelaat om hul lanseerder-ikone te versteek nie. Skakel die skakelaar af om hierdie stelselkenmerk te deaktiveer. Stelsel Taal Vertaling bydraers Neem deel aan vertaling Help ons om %s in jou taal te vertaal Skep \'n kortpad wat parasitiese bestuurder kan oopmaak Shortcut pinned 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 Info Tuisblad Bronkode Medewerkers Bates Maak oop in blaaier Wys ouer weergawes Geen vrystelling meer nie Kon nie module repo laai nie: %s Eers opgradeerbaar Geïnstalleer %d aflaai %d aflaaie Sakura Rooi Pienk Pers Diep pers Indigo Blou Ligblou Siaan Blauwgroen Groen Ligte groen Lemmetjie Geel Amber Oranje Diep oranje Bruin Blou grys
================================================ FILE: app/src/main/res/values-ar/strings.xml ================================================ ملخص الوحدات %d وحدة مفعلة %d وحدة مفعلة %d وحدة مفعلة %d وحدات مفعلة %d وحدة مفعلة %d وحدة مفعلة السجلات الإعدادات ملاحظات أو اقتراحات عنّ الإبلاغ عن مشكلة المُستودع جميع الوحدات محدثة تم نشرها في %s تَم تحديثها في %s %d وحدة قابلة للترقية %d وحدة قابلة للترقية %d وحدة قابلة للترقية %d وحدات قابلة للترقية %d وحدة قابلة للترقية %d وحدة قابلة للترقية انضم إلى قناتنا %2$s]]> Mahmoud Abd El-Hamed (7TM) تثبيت انقر لتثبيت LSPosed غير مثبت LSPosed غير مثبت مُفعّل مفعل جزئياً لم يتم تحميل SEPolicy بشكل صحيح الرجاء الإبلاغ عن هذا إلى ماجيسك مطور النظام.]]> فشل حقن إطار عمل النظام Magisk أو بعض وحدات Magisk منخفضة الجودة.
الرجاء محاولة تعطيل وحدات Magisk خلاف Riru وLSPosed أو إرسال سجل كامل للمطورين.]]>
نظام prop غير صحيح قد تبطل الوحدات أحيانا.]]> تحتاج إلى التحديث يرجى تثبيت أحدث إصدار من LSPosed نصائح لمطور الوحدة يرجى تعطيل تحسينات النشر على Android Studio، أو استخدام الأمر `gradlew installDebug` للتثبيت. وإلا فلن يتم تحديث ملف Apk للوحدة. إصدار API إصدار إطار العمل Lاسم حزمة المديرo إصدار النظام الجهاز نظام ABI غلاف Dex المحسّن مفعل غير مفعل مدعوم غير متوافق نسخة أندرويد غير راضية تعطّل فشل التحميل SELinux متساهل سياسة SELinux غير صحيحة تحديث LSPosed تأكيد تحديث LSPosed؟ سيتم إعـادة تشغيل هذا الجهاز بعد اكتمال التحديث تم النسخ إلى الحافظة مرحباً بك في LSPosed أنت تستخدم مدير الطفيليات، الذي يمكنه إنشاء اختصار أو لا يزال مفتوحا من الإشعارات. أنت تستخدم المدير الطفيلي، الذي يمكن فتحه من الإشعار. إنشاء إختصار لا تظهر أبداً مدير طفيلي موصي به يدعم LSPosed الآن تطهير النظام لتجنب الكشف، يمكنك فتح مدير الطفيليات من الإشعار. من المستحسن إلغاء تثبيت التطبيق الحالي. احفظ سجلات مفصّلة سجلات الوحدات حفظ السجل ، يرجى الانتظار تم حفظ السجلات فشل الحفظ:\n%s مسح السجل الآن تم مسح السجل بنجاح. التمرير لأعلى جارٍ التحميل… التمرير لأسفل إعادة التحميل فشل مسح السجل التفاف الكلمات تم تفعيل السجل المفصّل تم تعطيل السجل المفصّل (لم يتم تقديم وصف) هذه الوحدة تتطلب إصدار Xposed الأحدث (%d) لذا لا يمكن تفعيلها تم تصميم هذه الوحدة لإصدار Xposed أحدث (%d) وبالتالي قد لا تعمل بعض الوظائف لا تحدد هذه الوحدة إصدار Xposed الذي تحتاجه. تم إنشاء هذه الوحدة لإصدار Xposed %1$d ، ولكن بسبب التغييرات غير المتوافقة في الإصدار %2$d، فقد تم تعطيلها لا يمكن تحميل هذه الوحدة لأنها مثبتة على بطاقة الذاكرة، يرجى نقلها إلى مساحة التخزين الداخلية إلغاء التثبيت إعدادات الوحدة عرض في المستودع هل تريد إلغاء تثبيت هذه الوحدة؟ تم إلغاء تثبيت %1$s فشل إلغاء التثبيت إضافة وحدة للمستخدم تم إضافة %1$s للمستخدم %2$s فشل إضافة الوحدة تثبيت للمستخدم %s هل ترغب في تثبيت %1$s للمستخدم %2$s؟ ينصح بالتثبيت يدوياً، قد يسبب إجبار التثبيت عبر LSPosed مشكلات. توسّع انهيار إعادة تحسين تحسين… اكتمل التحسين تشغيله فشل التحسين: قيمة الإرجاع فارغة فشل التحسين: أسم التطبيق أسم الحُزْمَة وقت التثبيت وقت التحديث عكسي تطبيقات النظام ترتيب تفعيل الوحدة أنت لم تحدد أي تطبيق. المتابعة؟ ألعاب وحدات قائمة الرفض فشل في حفظ قائمة النطاق الإصدار: %1$s مُوصى به أنت لم تحدد أي تطبيق. تحديد التطبيقات الموصى بها؟ تحديد التطبيقات الموصى بها؟ وحدة Xposed لم يتم تفعيلها بعد مُوصى به تحديث متاح: %1$s الوحدة %s تم تعطيلها لعدم تحديد أي تطبيق. إطار النظام نسخ احتياطي نسخ احتياطي استعادة إيقاف إجباري إيقاف إجباري؟ إذا أغلقت التطبيق إجبارياً، قد يتصرف بشكل خاطئ. إعادة التشغيل مطلوبة لتطبيق هذا التغيير إعادة تشغيل إخفاء %s على قائمة الرفض. قد لا يكون ساري المفعول. في قائمة الرفض عرض في تطبيق آخر معلومات التطبيق ¯\\\\_(ツ)_\/¯\n لا شيء هنا إطار العمل تعطيل السجلات المفصّلة الإبلاغ عن مشاكل طلب لتضمين السجلات المفصولة تمكين مراقبة السجل يقوم مراقب السجل الخاص بـ LSPosed بتعديل خصائص النظام، والتي يمكن استغلالها للكشف عن LSPosed السمة السوداء المظلمة استخدام السمة السوداء الخالصة إذا تم تمكين السمة المظلمة السمة النسخ الاحتياطي والاستعادة نسخ احتياطي لقائمة الوحدات وقوائم النطاق. استعادة قائمة الوحدات وقوائم النطاق. نسخ احتياطي فشل في النسخ الاحتياطي:\n%s الرجاء تمكين DocumentUI استعادة فشل في الاستعادة:\n%s شبكة DNS عبر HTTPS حل بديل لتسمم DNS في بعض الدول لون السمة لون سمة النظام إجبار التطبيقات على إظهار أيقونات المشغل بعد أندرويد 10، لا يسمح للتطبيقات بإخفاء أيقونات المشغل. قم بإيقاف تشغيل التبديل لتعطيل مِيزة النظام هذه. نظام اللغة المساهمون بالترجمة المشاركة في الترجمة ساعدنا في ترجمة %s إلى لغتك إنشاء اختصار يمكنه فتح مدير الطفيليات تم تثبيت الاختصار المشغل الافتراضي الحالي لا يدعم اختصارات الدبوس إشعارات الحالة إظهار إشعار يمكنه فتح مدير الطفيليات لا يوجد اختصار، لا يمكن تعطيل الإشعار قناة التحديث مستقر تجريبي البناء الليلي حماية استدعاء Xposed API حظر رمز الوحدة الذي يتم تحميله ديناميكيًا لاستخدام Xposed API ، قد يؤدي ذلك إلى كسر بعض الوحدات ولكنه يفيد الأمان اقرأني إصدارات معلومات الصفحة الرئيسية كود المصدر المتعاونين الأصول فتح في المتصفح إظهار الإصدارات الأقدم لا مزيد من الإصدار فشل تحميل مستودع الوحدة: %s قابل للترقية أولاً المثبتة %d التنزيلات %d تنزيل %d التنزيلات %d التنزيلات %d التنزيلات %d التنزيلات لون ساكورا أحمر وردي بنفسجي بنفسجي عميق نيلي أزرق أزرق فاتح سماوي أزرق مخضرّ أخضر أخضر فاتح ليموني أصفر كهرماني برتقالي برتقالي عميق بني أزرق رمادي
================================================ FILE: app/src/main/res/values-bg/strings.xml ================================================ نظرة عامة الوحدات %d модулът е активиран %d включени модули Дневници Настройки Обратна връзка или предложение За нас Докладване на проблем Хранилище Всички модули са актуализирани Публикувано в %s Акттуализиран на %s %d модула има актуализация %d модули с актуализации Присъединете се към нашия %2$s канал]]> невалидно Инсталиране на Натиснете, за да инсталирате LSPosed Не е инсталиран LSPosed не е инсталиран Активиран Частично активиран SEPolicy не е заредена правилно Моля, докладвайте за това на Magisk разработчик.]]> Неуспешно инжектиране на системната рамка Magisk или някои нискокачествени модули на Magisk.
Моля, опитайте се да деактивирате модулите на Magisk, различни от Riru и LSPosed, или изпратете пълен журнал на разработчиците.]]>
Неправилна стойност на системата Понякога модулите могат да се обезсилват.]]> Необходимо е да се актуализира Моля, инсталирайте най-новата версия на LSPosed Версия на API Версия на рамката Име на пакета на мениджъра Версия на системата Устройство ABI на системата Обвивка на Dex Optimizer Разрешено Не е разрешено Поддържан Неподдържан Версията за Android е неудовлетворена Счупен Монтирането е неуспешно SELinux е разрешаващ Политиката на SELinux е неправилна Актуализиране на LSPosed Потвърждаване на актуализацията на LSPosed? Това устройство ще се рестартира след завършване на актуализацията Копиране в клипборда Добре дошли в LSPosed Използвате паразитния мениджър, който може да създаде пряк път или все още да се отваря от известието. Използвате паразитния мениджър, който може да се отвори от известие. Създаване на пряк път Никога не показвайте Препоръчва се паразитен мениджър LSPosed вече поддържа паразитиране на системата, за да се избегне откриването, можете да отворите мениджъра на паразити от известието. Препоръчва се да деинсталирате текущото приложение. Запазете Условни дневници Дневници на модулите Запазване на дневника, моля изчакайте Запазени дневници Не успяхте да запазите:\n%s Изчистване на дневника сега Дневникът е изчистен успешно. Превъртете към началото Зареждане… Превъртете към дъното Презареждане Неуспешно изчистване на дневника Обвиване на думата Разрешен е вербален дневник Деактивиран е вербалният дневник (не е предоставено описание) Този модул изисква по-нова версия на Xposed (%d) и поради това не може да бъде активиран Този модул е предназначен за по-нова версия на Xposed (%d) и поради това някои функционалности може да не работят Този модул не посочва версията на Xposed, която му е необходима. Този модул е създаден за Xposed версия %1$d, но поради несъвместими промени във версия %2$d, той е деактивиран Този модул не може да бъде зареден, защото е инсталиран на SD картата, моля, преместете го във вътрешната памет Деинсталиране на Настройки на модула Преглед в Repo Искате ли да деинсталирате този модул? Деинсталиран %1$s Деинсталирането е неуспешно Добавяне на модул към потребителя Добавяне на %1$s към потребител %2$s Добавянето на модул е неуспешно Инсталиране на потребител %s Искате да инсталирате %1$s на потребител %2$s? Препоръчително е да се инсталира ръчно, принудителното инсталиране чрез LSPosed може да доведе до проблеми. разширяване на срив Оптимизиране на Оптимизиране на… Оптимизацията е завършена Стартирайте го Оптимизацията е неуспешна: върнатата стойност е празна Оптимизацията е неуспешна: Име на приложението Име на пакета Време за инсталиране Време за актуализация Обратен Системни приложения Сортиране Включване на модула Не сте избрали нито едно приложение. Продължете? Игри Модули Denylist Неуспешно запазване на списъка с обхвата Версия: %1$s Препоръчителен Не сте избрали нито едно приложение. Избрахте препоръчани приложения? Изберете препоръчани приложения? Модулът Xposed все още не е активиран Препоръчителен Налична е актуализация: %1$s Модулът %s е деактивиран, тъй като не е избрано приложение. Рамка на системата Резервно копие Резервно копие Възстановяване на Спиране на силата Насилствено спиране? Ако спрете приложение принудително, то може да се държи неправилно. Необходимо е рестартиране, за да се приложи тази промяна. Рестартиране на Скрий %s е в denylist. Възможно е той да не влезе в сила. На denylist Преглед в друго приложение Информация за приложението ¯\\\\_(ツ)_\/¯\nНищо тук Рамка Деактивиране на вербалните дневници Искане за включване на вербални дневници Черна тъмна тема Използвайте чисто черната тема, ако е активирана тъмна тема Тема Архивиране и възстановяване Списък с резервни копия на модули и списъци на обхвата. Възстановяване на списъците с модули и области. Резервно копие Неуспешно архивиране:\n%s Моля, разрешете DocumentUI Възстановяване на Неуспешно възстановяване:\n%s Мрежа DNS през HTTPS Заобикаляне на отравянето на DNS в някои държави Цвят на темата Цвят на темата на системата Принуждаване на приложенията да показват икони на стартирането След Android 10 на приложенията не е позволено да скриват иконите си за стартиране. Изключете превключвателя, за да деактивирате тази системна функция. Система Език Преводачи, които допринасят за превода Участие в превод Помогнете ни да преведем %s на вашия език Създаване на пряк път, който може да отваря паразитен мениджър Кратък път, закачен Текущият стартер по подразбиране не поддържа преки пътища Известие за състоянието Показване на известие, което може да отвори паразитен мениджър Няма пряк път, не мога да деактивирам известието Актуализиране на канала Стабилен Бета Нощно изграждане Защита на повикванията на Xposed API Блокиране на динамично зареждания код на модула, за да се използва Xposed API, това може да наруши някои модули, но е от полза за сигурността Readme Освобождава Информация Начална страница Изходен код Сътрудници Активи Отваряне в браузъра Показване на по-стари версии Няма повече освобождаване Неуспешно зареждане на модул repo: %s Може да се надгражда първо Инсталиран %d изтегляне %d Изтегляния Сакура Червено Розов Лилаво Наситено лилаво Indigo Синьо Светлосиньо Cyan Teal Зелен Светлозелено Lime Жълт Амбър Orange Наситено оранжево Кафяв Синьо сиво
================================================ FILE: app/src/main/res/values-bn/strings.xml ================================================ সার সংক্ষেপ মডিউল %d মডিউল সক্ষম %d মডিউল সক্রিয় তথ্য সার-সংক্ষেপ বিন্যাস প্রতিক্রিয়া বা পরামর্শ সম্পর্কিত সমস্যা প্রতিবেদন ভান্ডার সব মডিউল আপ টু ডেট %sএ প্রকাশিত %sএ আপডেট করা হয়েছে %d মডিউল আপগ্রেডযোগ্য %d মডিউল আপগ্রেডযোগ্য এ সোর্স কোড দেখুন আমাদের %2$s চ্যানেলে যোগ দিন]]> bdtipsntricks ইনস্টল করুন LSPosed ইনস্টল করতে আলতো চাপুন ইনস্টল করা না LSPosed ইনস্টল করা হয় না সক্রিয় আংশিক সক্রিয় এসইপলিসি সঠিকভাবে লোড করা হয় না অনুগ্রহ করে এটি Magisk বিকাশকারীকে রিপোর্ট করুন।]]> সিস্টেম ফ্রেমওয়ার্ক ইনজেকশন ব্যর্থ হয়েছে Magisk বা কিছু নিম্নমানের Magisk মডিউলের কারণে হতে পারে।
অনুগ্রহ করে Riru এবং LSPosed ব্যতীত Magisk মডিউলগুলি নিষ্ক্রিয় করার চেষ্টা করুন বা বিকাশকারীদের কাছে সম্পূর্ণ লগ জমা দিন।]]>
সিস্টেম প্রপ ভুল মডিউল মাঝে মাঝে অবৈধ হতে পারে।]]> আপডেট করতে হবে অনুগ্রহ করে LSPosed এর সর্বশেষ সংস্করণটি ইনস্টল করুন API সংস্করণ ফ্রেমওয়ার্ক সংস্করণ ম্যানেজার প্যাকেজের নাম সিস্টেম সংস্করণ যন্ত্র সিস্টেম ABI ডেক্স অপ্টিমাইজার মোড়ক সক্রিয় সক্রিয় না সমর্থিত অসমর্থিত অ্যান্ড্রয়েড সংস্করণ অসন্তুষ্ট বিধ্বস্ত মাউন্ট ব্যর্থ হয়েছে SELinux অনুমোদিত SELinux নীতি ভুল আপডেট LSPosed LSPosed আপডেট করার জন্য নিশ্চিত? আপডেট সম্পূর্ণ হওয়ার পরে এই ডিভাইসটি রিবুট হবে ক্লিপবোর্ডে কপি করা হয়েছে LSPosed স্বাগতম আপনি পরজীবী ম্যানেজার ব্যবহার করছেন, যা শর্টকাট তৈরি করতে পারে বা এখনও বিজ্ঞপ্তি থেকে খুলতে পারে। আপনি পরজীবী ম্যানেজার ব্যবহার করছেন, যা বিজ্ঞপ্তি থেকে খুলতে পারে। শর্টকাট তৈরি করুন কখনও দেখাবে না পরজীবী ব্যবস্থাপক প্রস্তাবিত LSPosed এখন সনাক্তকরণ এড়াতে সিস্টেম প্যারাসাইটাইজেশন সমর্থন করে, আপনি বিজ্ঞপ্তি থেকে পরজীবী ম্যানেজার খুলতে পারেন। বর্তমান অ্যাপ্লিকেশনটি আনইনস্টল করার পরামর্শ দেওয়া হচ্ছে। সংরক্ষণ ভার্বোস লগ মডিউল লগ লগ সংরক্ষণ করা হচ্ছে, অনুগ্রহ করে অপেক্ষা করুন লগ সংরক্ষিত সংরক্ষণ করতে ব্যর্থ হয়েছে:\n%s এখন লগ সাফ করুন লগ সফলভাবে সাফ করা হয়েছে৷ উপরে যান লোড হচ্ছে… নীচে স্ক্রোল করুন পুনরায় লোড করুন লগ সাফ করতে ব্যর্থ হয়েছে শব্দ মোড়ানো ভার্বোস লগ সক্ষম ভার্বোস লগ নিষ্ক্রিয় (কোন বর্ণনা দেওয়া হয়নি) এই মডিউলটির একটি নতুন Xposed সংস্করণ প্রয়োজন (%d) এবং এইভাবে সক্রিয় করা যাবে না এই মডিউলটি একটি নতুন Xposed সংস্করণ (%d) এর জন্য ডিজাইন করা হয়েছে এবং এইভাবে কিছু কার্যকারিতা কাজ নাও করতে পারে এই মডিউলটি তার প্রয়োজনীয় Xposed সংস্করণটি নির্দিষ্ট করে না। এই মডিউলটি Xposed সংস্করণ %1$d-এর জন্য তৈরি করা হয়েছিল, কিন্তু সংস্করণ %2$d-এ অসামঞ্জস্যপূর্ণ পরিবর্তনের কারণে, এটি নিষ্ক্রিয় করা হয়েছে। এই মডিউলটি লোড করা যাবে না কারণ এটি SD কার্ডে ইনস্টল করা আছে, দয়া করে এটিকে অভ্যন্তরীণ সঞ্চয়স্থানে নিয়ে যান৷ আনইনস্টল করুন মডিউল সেটিংস রেপোতে দেখুন আপনি এই মডিউল আনইনস্টল করতে চান? আনইনস্টল %1$s আনইনস্টল করা যায়নি ব্যবহারকারীর জন্য মডিউল যোগ করুন ব্যবহারকারী %2$sএ %1$s যোগ করা হয়েছে মডিউল যোগ করা ব্যর্থ হয়েছে৷ ব্যবহারকারী %sএ ইনস্টল করুন ব্যবহারকারী %2$sথেকে %1$s ইনস্টল করতে চান? এটি ম্যানুয়ালি ইনস্টল করার সুপারিশ করা হয়, LSPosed এর মাধ্যমে জোর করে ইনস্টল করার ফলে সমস্যা হতে পারে। বিস্তৃত করা পতন পুনরায় অপ্টিমাইজ করুন অপ্টিমাইজ করা… অপ্টিমাইজেশান সম্পূর্ণ এটি চালু করুন অপ্টিমাইজেশান ব্যর্থ হয়েছে: রিটার্ন মান খালি অপ্টিমাইজেশান ব্যর্থ হয়েছে: আবেদনের নাম প্যাকেজের নাম ইন্সটল করার সময় আপডেটের সময় বিপরীত সিস্টেম অ্যাপস শ্রেণীবিভাজন মডিউল সক্ষম করুন আপনি কোনো অ্যাপ নির্বাচন করেননি। চালিয়ে যান? গেমস মডিউল অস্বীকারকারী সুযোগ তালিকা সংরক্ষণ করতে ব্যর্থ হয়েছে সংস্করণ: %1$s প্রস্তাবিত আপনি কোনো অ্যাপ নির্বাচন করেননি। প্রস্তাবিত অ্যাপ নির্বাচন করবেন? প্রস্তাবিত অ্যাপ নির্বাচন করবেন? Xposed মডিউল এখনও সক্রিয় করা হয় নি প্রস্তাবিত আপডেট উপলব্ধ: %1$s মডিউল %s অক্ষম করা হয়েছে যেহেতু কোনো অ্যাপ নির্বাচন করা হয়নি৷ সিস্টেম ফ্রেমওয়ার্ক ব্যাকআপ ব্যাকআপ পুনরুদ্ধার করুন জোরপুর্বক থামা জোরপুর্বক থামা? আপনি যদি একটি অ্যাপকে জোর করে বন্ধ করেন, তাহলে সেটি খারাপ আচরণ করতে পারে। এই পরিবর্তনটি প্রয়োগ করার জন্য রিবুট প্রয়োজন রিবুট করুন লুকান %s ডিনালিস্টে রয়েছে। এটি কার্যকর নাও হতে পারে। ডিনালিস্টে অন্য অ্যাপে দেখুন অ্যাপের তথ্য ¯\\\\_(ツ)_\/¯\nএখানে কিছুই নেই ফ্রেমওয়ার্ক ভার্বোস লগ অক্ষম করুন ভার্বোস লগগুলি অন্তর্ভুক্ত করার জন্য সমস্যার প্রতিবেদন করুন কালো অন্ধকার থিম অন্ধকার থিম সক্ষম থাকলে খাঁটি কালো থিম ব্যবহার করুন থিম ব্যাকআপ এবং পুনঃস্থাপন ব্যাকআপ মডিউল তালিকা এবং সুযোগ তালিকা. মডিউল তালিকা এবং সুযোগ তালিকা পুনরুদ্ধার করুন। ব্যাকআপ ব্যাকআপ করতে ব্যর্থ হয়েছে:\n%s অনুগ্রহ করে ডকুমেন্টইউআই সক্ষম করুন পুনরুদ্ধার করুন পুনরুদ্ধার করতে ব্যর্থ হয়েছে:\n%s অন্তর্জাল HTTPS এর উপর DNS কিছু দেশে ডিএনএস বিষক্রিয়ার সমাধান থিম রঙ সিস্টেম থিম রঙ অ্যাপ্লিকেশানগুলিকে লঞ্চার আইকনগুলি দেখাতে বাধ্য করুন৷ অ্যান্ড্রয়েড 10 এর পরে, অ্যাপগুলিকে তাদের লঞ্চার আইকনগুলি লুকানোর অনুমতি দেওয়া হয় না। এই সিস্টেম বৈশিষ্ট্যটি নিষ্ক্রিয় করতে টগলটি বন্ধ করুন৷ পদ্ধতি ভাষা অনুবাদ অবদানকারী অনুবাদে অংশগ্রহণ করুন আপনার ভাষায় %s অনুবাদ করতে আমাদের সাহায্য করুন একটি শর্টকাট তৈরি করুন যা পরজীবী ম্যানেজার খুলতে পারে শর্টকাট পিন করা হয়েছে বর্তমান ডিফল্ট লঞ্চার পিন শর্টকাট সমর্থন করে না স্থিতি বিজ্ঞপ্তি পরজীবী ম্যানেজার খুলতে পারে এমন একটি বিজ্ঞপ্তি দেখান কোন শর্টকাট, বিজ্ঞপ্তি নিষ্ক্রিয় করতে পারবেন না চ্যানেল আপডেট করুন স্থিতিশীল বেটা রাতারাতি নির্মাণ Xposed API কল সুরক্ষা এক্সপোজড এপিআই ব্যবহার করতে গতিশীলভাবে লোড করা মডিউল কোড ব্লক করুন, এটি কিছু মডিউল ভেঙে ফেলতে পারে তবে নিরাপত্তার সুবিধা পাবে রিডমি মুক্তি দেয় তথ্য হোমপেজ সোর্স কোড সহযোগীরা সম্পদ ব্রাউজারে খোলা পুরোনো সংস্করণ দেখান আর মুক্তি নেই মডিউল রেপো লোড করতে ব্যর্থ হয়েছে: %s প্রথমে আপগ্রেডযোগ্য ইনস্টল করা হয়েছে %d ডাউনলোড %d ডাউনলোড সাকুরা লাল গোলাপী বেগুনি গভীর বেগুনি নীল নীল হালকা নীল সায়ান টিল সবুজ হালকা সবুজ চুন হলুদ অ্যাম্বার কমলা গভীর কমলা বাদামী নীল ধূসর
================================================ FILE: app/src/main/res/values-ca/strings.xml ================================================ Visió general Mòduls %d Mòdul activat %d Mòduls activats Logs Configuració Comentari o suggeriment Sobre Reportar problema Repositori Tots els mòduls actualitzats Published at %s Actualitzat a les %s %d mòdul actualitzable %d mòduls actualitzables Uneix-te al nostre %2$s canal]]> Frederic Blay, Ghost Face, Yannick Kamin Instalar Toca per instal·lar LSPosed No instalat LSPosed no està instal·lat Activat Parcialment activat SEPolicy no s\'ha carregat correctament Informeu-ho al desenvolupador Magisk.]]> La injecció del marc del sistema ha fallat Magisk o algún Mòdul de baixa qualitat
Si us plau, prova a desactivar els demés mòduls de magisk excepte Riru i LSPosed o envia un informe complet als desarrolladors.]]>
Prop del sistema incorrecte Els mòduls poden invalidar-se ocasionalment.]]> Cal actualitzar Instal·leu la darrera versió de LSPosed Versió de l\'API Versió del marc Nom del paquet del gestor Versió del sistema Dispositiu Sistema ABI Embolcall de Dex Optimizer Habilitat No habilitat Admet Sense suport La versió d\'Android no està satisfeta Estavellat El muntatge ha fallat SELinux és permissiu La política de SELinux és incorrecta Actualització LSPosed Confirmeu per actualitzar LSPosed? Aquest dispositiu es reiniciarà un cop finalitzada l\'actualització S\'ha copiat al porta-retalls Benvingut a LSPosed Esteu utilitzant el gestor de paràsits, que pot crear dreceres o encara obrir-se des de la notificació. Esteu utilitzant el gestor de paràsits, que es pot obrir des de la notificació. Crear accès directe No mostris mai Administrador de paràsits recomanat LSPosed ara admet la parasitització del sistema per evitar la detecció, podeu obrir el gestor de paràsits des de la notificació. Es recomana desinstal·lar l\'aplicació actual. Desa Registres detallats Registres de mòduls S\'està desant el registre, espereu Registres guardats No s\'ha pogut desar:\n%s Esborra el registre ara El registre s\'ha esborrat correctament. Desplaceu-vos cap a dalt Carregant… Desplaceu-vos cap avall Recarregar No s\'ha pogut esborrar el registre L\'ajust de línia Registre detallat activat Registre detallat desactivat (no s\'ofereix cap descripció) Aquest mòdul requereix una versió més nova de Xposed (%d) i per tant no es pot activar Aquest mòdul està dissenyat per a una versió més nova de Xposed (%d) i per tant algunes funcionalitats poden no funcionar Aquest mòdul no especifica la versió de Xposed que necessita. Aquest mòdul es va crear per a Xposed versió %1$d, però a causa de canvis incompatibles a la versió %2$d, s\'ha desactivat Aquest mòdul no es pot carregar perquè està instal·lat a la targeta SD, moveu-lo a l\'emmagatzematge intern Desinstal·la Configuració del mòdul Veure a Repo Voleu desinstal·lar aquest mòdul? Desinstal·lat %1$s La desinstal·lació no s\'ha realitzat correctament Afegeix un mòdul a l\'usuari S\'ha afegit %1$s a l\'usuari %2$s S\'ha produït un error en afegir el mòdul Instal·lar a l\'usuari %s Voleu instal·lar %1$s a l\'usuari %2$s? Es recomana instal·lar manualment, forçar la instal·lació mitjançant LSPosed pot causar problemes. expandir col·lapse Torna a optimitzar Optimització… Optimització completa Llança\'l L\'optimització ha fallat: el valor de retorn és buit L\'optimització ha fallat: Nom de l\'aplicació Nom del paquet Temps d\'instal·lació Hora d\'actualització Revés Aplicacions del sistema Classificació Activa el mòdul No heu seleccionat cap aplicació. Continuar? Jocs Mòduls Llista de denegació No s\'ha pogut desar la llista d\'àmbits Versió: %1$s Recomanat No heu seleccionat cap aplicació. Seleccioneu aplicacions recomanades? Vols seleccionar aplicacions recomanades? El mòdul Xposed encara no està activat Recomanat Actualització disponible: %1$s El mòdul %s s\'ha desactivat perquè no s\'ha seleccionat cap aplicació. Marc del sistema Còpia de seguretat Còpia de seguretat Restaurar Parada forçada Parada forçada? Si forços l\'aturada d\'una aplicació, és possible que es comporti malament. Cal reiniciar perquè s\'apliqui aquest canvi Reinicieu Amaga %s és a la llista denegada. És possible que no tingui efecte. A la llista denegada Veure en una altra aplicació Informació de l\'aplicació ¯\\\\_(ツ)_\/¯\nAquí no hi ha res Marc Desactiva els registres detallats Sol·licitud d\'informes de problemes per incloure registres detallats Tema negre fosc Utilitzeu el tema negre pur si el tema fosc està habilitat Tema Còpia de seguretat i restaurar Llista de mòduls de còpia de seguretat i llistes d\'abast. Restaura la llista de mòduls i les llistes d\'abast. Còpia de seguretat No s\'ha pogut fer la còpia de seguretat:\n%s Si us plau, activeu DocumentUI Restaurar No s\'ha pogut restaurar:\n%s Xarxa DNS sobre HTTPS Solució alternativa a l\'enverinament per DNS en algunes nacions Color del tema Color del tema del sistema Força les aplicacions a mostrar icones del llançador Després d\'Android 10, les aplicacions no poden amagar les icones del llançador. Desactiveu el commutador per desactivar aquesta funció del sistema. Sistema Llenguatge Col·laboradors de traducció Participar en la traducció Ajuda\'ns a traduir %s al teu idioma Creeu una drecera que pugui obrir el gestor de paràsits Drecera fixada 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 Informació Pàgina d\'inici Codi font Col·laboradors Actius Oberta al navegador Mostra les versions anteriors No més llançament No s\'ha pogut carregar el dipòsit del mòdul: %s Actualitzable primer Instal·lat %d descàrrega %d descàrregues Sakura Vermell Rosa Porpra Lila fosc Indigo Blau Blau clar Cian Teal verd Verd clar Lima groc Ambre taronja Taronja profund marró Gris blau
================================================ FILE: app/src/main/res/values-cs/strings.xml ================================================ Přehled Moduly %d modul aktivován %d modul povolen %d Modul aktivován %d Modul aktivován Protokoly Nastavení Zpětná vazba nebo návrh O aplikaci Nahlásit problém Repozitář Všechny moduly jsou aktuální Publikováno v %s Aktualizováno v %s %d modul je možné aktualizovat %d moduly je možné aktualizovat %d modolů je možné aktualizovat %d modulů je možné aktualizovat Připojte se k našemu kanálu %2$s]]> https://www.instagram.com/kasi33/ Instalovat Klepnutím nainstalujete LSPosed Není nainstalováno LSPosed není nainstalován Aktivováno Částečně aktivováno SEPolicy není správně načten Nahlaste to prosím vývojáři Magisk.]]> Načtení System Framework se nezdařilo Magiskem nebo některými málo kvalitními moduly Magisku.
Zkuste vypnout moduly Magisk jiné než Riru a LSPosed nebo odešlete kompletní log vývojářům.]]>
Nesprávné systémové prop Moduly mohou být někdy neplatné a tedy nefunkční.]]> Je třeba aktualizovat Nainstalujte si prosím nejnovější verzi LSPosed Verze API Verze frameworku Název balíčku správce Verze systému Zařízení Systémové ABI (architektura) Dex Optimizer Wrapper Povoleno Není povoleno Podporováno Nepodporováno Verze Androidu není spokojena Havaroval Připojení se nezdařilo SELinux je permisivní Zásady SELinuxu jsou nesprávné Aktualizovat LSPosed Potvrdit aktualizaci LSPosed? Toto zařízení se restartuje po dokončení aktualizace Zkopírováno do schránky Vítejte v LSPosed Používáte parazitního správce, který může vytvořit zástupce nebo se stále otevírat z oznámení. Používáte parazitního správce, který se může otevřít z oznámení. Vytvořit zástupce Nikdy nezobrazovat Doporučený parazitický manažer LSPosed nyní podporuje parazitování systému. K zabránění detekce můžete správce parazitů otevřít z oznámení. Doporučuje se odinstalovat aktuální aplikaci. Uložit Podrobné protokoly Protokoly modulů Ukládání protokolu, čekejte prosím Protokoly uloženy Nepodařilo se uložit:\n%s Vymazat log Log byl úspěšně vymazán. Přejít na začátek Načítání… Přejít na začátek Znovu načíst Nepodařilo se vymazat protokol Zalamování řádků Podrobný záznam povolen Podrobný záznam zakázán (žádný popis) Tento modul vyžaduje novější Xposed verzi (%d) a proto nemůže být aktivován Tento modul je určen pro novější verzi Xposed (%d), a proto některé funkce nemusí fungovat Tento modul nespecifikuje potřebnou Xposed verzi. Tento modul byl vytvořen pro Xposed verzi %1$d, a tak z důvodu nekompatibilních změn ve verzi %2$dbyl zakázán Tento modul nelze načíst, protože je nainstalován na SD kartě, přesuňte jej na interní úložiště Odinstalovat Nastavení modulů Zobrazit v repozitáři Chcete odinstalovat tento modul? %1$s odinstalován Odinstalace nebyla úspěšná Přidat modul k uživateli Modul %1$s přidán k uživateli %2$s Přidání modulu se nezdařilo Instalovat uživateli %s Chcete nainstalovat %1$s uživateli %2$s.? Je doporučeno instalovat ručně, vynucení instalace přes LSPosed může způsobit problémy. rozbalit sbalit Znovu optimalizovat Optimalizace… Optimalizace dokončena Spustit Optimalizace selhala: návratová hodnota je prázdná Optimalizace selhala: Název aplikace Název balíčku Doba instalace Čas aktualizace Obrátit pořadí řazení Systémové aplikace Řazení Povolit modul Nevybrali jste žádnou aplikaci. Pokračovat? Hry Moduly Seznam zakázaných Nepodařilo se uložit seznam Verze: %1$s Zvolit doporučené Nevybrali jste žádnou aplikaci. Vybrat doporučené aplikace? Vybrat doporučené aplikace? Xposed modul ještě není aktivován Doporučené Je k dispozici aktualizace: %1$s Modul %s byl zakázán, protože nebyla vybrána žádná aplikace. Systémový Framework Zálohování Zálohovat Obnovení Vynutit zastavení Vynutit zastavení? Pokud vynutíte zastavení aplikace, může dojít k chybnému chování. Pro aplikaci této změny je vyžadován restart Restartovat Skrýt %s je na seznamu zakázaných. Nemusí se projevit. Na seznamu zakázaných Zobrazit v jiné aplikaci Informace o aplikaci <unk> \\\\_(<unk> )_\/ <unk>\nTady nic není Framework Zakázat podrobné protokoly Nahlásit požadavek na problémy a zahrnout detailní záznamy Čistě černý motiv Použít čistý černý motiv, pokud je tmavý motiv povolen Vzhled Zálohování a obnovení Zálohovat seznam modulů a nastavení. Obnovit seznam modulů a nastavení. Zálohování Zálohování se nezdařilo:\n%s Prosím povolte DocumentUI Obnovení Nepodařilo se obnovit:\n%s Síť DNS over HTTPS Řešení DNS oprav v některých zemích Barva motivu Barva systémového motivu Vynutit aplikace k zobrazení ikon spouštěče Od Androidu 10 není aplikacím povoleno skrývat ikony spouštěče. Vypněte přepínač pro vypnutí této systémové funkce. Systém Jazyk Přispěvatelé překladu Účast na překladu Pomozte nám přeložit %s do vašeho jazyka Vytvoření zástupce, který může otevřít parazitního správce Připnutí zástupci 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í Informace Domovská stránka Zdrojový kód Spolupracovníci Assets Otevřít v prohlížeči Zobrazit starší verze Žádné další vydání Nepodařilo se načíst repozitář modulu: %s Nejprve aktualizovatelný Instalováno %d stažení %d ke stažení %d ke stažení %d ke stažení Sakura Červená Růžová Fialová Tmavě fialová Indigo Modrá Světle modrá Azurová Modrozelená Zelená Světle zelená Limetková Žlutá Jantarová Oranžová Tmavě oranžová Hnědá Modro-šedivá
================================================ FILE: app/src/main/res/values-da/strings.xml ================================================ Oversigt Moduler %d modul aktiveret %d moduler aktiveret Logfiler Indstillinger Feedback eller forslag Om Anmeld problem Lagre Alle moduler opdateret Udgivet på %s Opdateret på %s %d modul opgraderbar %d moduler opgraderbare Tilmeld dig vores %2$s kanal]]> null Installér Tryk for at installere LSPosed Ikke installeret LSPosed er ikke installeret Aktiveret Delvist aktiveret SEPolicy er ikke indlæst korrekt Du bedes rapportere dette til Magisk udvikleren.]]> System Framework injektion mislykkedes Magisk eller nogle lavkvalitets Magisk moduler.
Prøv at deaktivere andre Magisk moduler end Riru og LSPosed eller indsende fuld log til udviklere.]]>
System prop forkert Moduler kan ugyldiggøre lejlighedsvis.]]> Skal opdateres Installér venligst den seneste version af LSPosed API version Rammer version Navn på manager-pakke System version Enhed System ABI Dex Optimizer Wrapper Aktiveret Ikke aktiveret Understøttet Ikke understøttet Android-version utilfreds Nedstyrtet Montering mislykkedes SELinux er tilladende SELinux-politik er forkert Opdater LSPosed Bekræft opdatering af LSPosed? Denne enhed vil genstarte efter opdateringsfuldførelse Kopieret til udklipsholderen Velkommen til LSPosed Du bruger den parasitære manager, som kan oprette genvej eller stadig åbne fra meddelelsen. Du bruger den parasitære manager, som kan åbnes fra notifikationen. Opret genvej Vis aldrig Parasitic Manager Anbefalet LSPosed understøtter nu systemparasitering for at undgå registrering, du kan åbne parasitmanager fra meddelelsen. Det anbefales at afinstallere det aktuelle program. Gem Verbose Logs Moduler Logs Gemmer log, vent venligst Gemte logfiler Mislykkedes at gemme:\n%s Ryd log nu Loggen blev ryddet. Rul til toppen Indlæser… Rul til bunden Reload Kunne ikke rydde loggen Tekstombrydning Verbose log aktiveret Verbose log deaktiveret (ingen beskrivelse angivet) Dette modul kræver en nyere Xposed version (%d) og kan derfor ikke aktiveres Dette modul er designet til en nyere Xposed-version (%d), og derfor fungerer nogle funktioner muligvis ikke Dette modul angiver ikke den Xposed version det behøver. Dette modul blev oprettet til Xposed version %1$d, men på grund af inkompatible ændringer i version %2$d, er det blevet deaktiveret Dette modul kan ikke indlæses, da det er installeret på SD-kortet, flyt det til intern lagerplads Afinstaller Modul indstillinger Se i Repo Vil du afinstallere dette modul? Afinstalleret %1$s Afinstallation mislykkedes Tilføj modul til bruger Tilføjede %1$s til bruger %2$s Tilføjelse af modul mislykkedes Installér til bruger %s Vil du installere %1$s til bruger %2$s? Det anbefales at installere manuelt, tvinger installation via LSPosed kan forårsage problemer. udvid kollaps Genoptimér Optimerer… Optimering fuldført Start det Optimering mislykkedes: returværdi er tom Optimering mislykkedes: Applikations navn Pakke navn Installér tid Opdater tid Omvendt System apps Sortering Aktiver modul Du valgte ikke nogen app. Fortsæt? Spil Moduler Denylist Kunne ikke gemme scope-liste Version: %1$s Anbefalet Du valgte ikke nogen app. Vælg anbefalede apps? Vælg anbefalede apps? Xposed modul er endnu ikke aktiveret Anbefalet Opdatering tilgængelig: %1$s Modul %s er blevet deaktiveret siden ingen app er valgt. System Framework Sikkerhedskopi Sikkerhedskopi Gendan Gennemtving stop Gennemtving stop? Hvis du tvinger til at stoppe en app, kan den virke forkert. Genstart er påkrævet for at denne ændring kan anvendes Reboot Skjul %s er på denylist. Det kan ikke træde i kraft. På benægtelsesliste Se i anden app Oplysninger om appen Spredning \\\\_(Ι)_\/ Ι\nIntet her Framework Deaktivere udførlige logfiler Anmodning om at medtage verbose logs i rapporten om problemer Sort mørkt tema Brug det rene sorte tema, hvis mørkt tema er aktiveret Tema Sikkerhedskopiér og gendan Backup modul liste og scope-lister. Gendan modulliste og scope-lister. Sikkerhedskopi Sikkerhedskopiering mislykkedes:\n%s Aktiver venligst DocumentUI Gendan Kunne ikke gendanne:\n%s Netværk DNS over HTTPS Workaround DNS forgiftning i nogle nationer Tema farve Farve på systemtema Tving apps til at vise launcher-ikoner Efter Android 10 må apps ikke skjule deres launcher-ikoner. Slå toggle fra for at deaktivere denne systemfunktion. System Sprog Oversættelsesbidragsydere Deltag i oversættelse Hjælp os med at oversætte %s til dit sprog Opret en genvej, der kan åbne parasitic manager Genvej fastgjort 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 Info Hjemmeside Kilde kode Samarbejdspartnere Aktiver Åbn i browser Vis ældre versioner Ikke mere udgivelse Kunne ikke indlæse modul repo: %s Opgradérbar først Installeret %d download %d downloads Sakura Rød Lyserød Lilla Dyb lilla Indigo Blå Lyseblå Cyan Grønblåt Grøn Lysegrøn Limegrøn Gul Ravgul Orange Dyb orange Brun Blå grå
================================================ FILE: app/src/main/res/values-de/strings.xml ================================================ Übersicht Module %d Modul aktiviert %d Module aktiviert Protokolle Einstellungen Feedback oder Vorschlag Über Fehler melden Modularchiv Alle Module sind auf dem neusten Stand Veröffentlicht am %s Aktualisiert am %s %d Modul Update verfügbar %d Modul Updates verfügbar Folge unserem %2$s-Kanal]]> mshinni80, JJ108 Installieren Tippen um LSPosed zu installieren Nicht installiert LSPosed ist nicht installiert Aktiviert Teilweise aktiviert SEPolicy wird nicht korrekt geladen Bitte melde es dem Magisk Entwickler.]]> System-Framework-Injektion fehlgeschlagen Magisk oder einige minderwertige Magisk Module verursacht werden.
Bitte deaktiviere alle Magisk-Module bis auf Riru und LSPosed, oder sende eine vollständige Fehlermeldung an die Entwickler.]]>
System-Prop falsch Module können gelegentlich außer Kraft gesetzt werden.]]> Aktualisierung erforderlich Bitte installieren Sie die neueste Version von LSPosed Tipps für Modulentwickler Bitte deaktiviere Deploy-Optimierungen in Android Studio oder benutze den `gradlew installDebug` Befehl zum Installieren. Andernfalls wird die Modul-Apk nicht aktualisiert. API Version Framework Version Name des Managerpakets System Version Gerät System ABI Dex Optimizer Wrapper Aktiviert Deaktiviert Unterstützt Nicht unterstützt Android-Version passt nicht Abgestürzt Mounten fehlgeschlagen SELinux ist permissiv SELinux-Richtlinie ist falsch Bitte LSPosed aktualisieren Soll LSPosed aktualisiert werden? Dieses Gerät wird nach dem Update neu gestartet In die Zwischenablage kopiert Willkommen bei LSPosed Du verwendest den parasitären Manager, der eine Verknüpfung erstellen oder über eine Benachrichtigung noch geöffnet werden kann. Du verwendest den parasitären Manager, der von der Benachrichtigung geöffnet werden kann. Verknüpfung erstellen Niemals anzeigen Parasitärer Manager empfohlen LSPosed unterstützt nun Systemparasitisierung, um eine Erkennung zu vermeiden. Du kannst den parasitären Manager über die Benachrichtigung öffnen. Es wird empfohlen, die aktuelle App zu deinstallieren. Speichern Ausführliche Protokolle Modul-Protokolle Protokoll wird gespeichert, bitte warten Gespeicherte Protokolle Speichern fehlgeschlagen:\n%s Protokoll jetzt löschen Protokoll erfolgreich gelöscht. Hochscrollen Laden… Runterscrollen Erneut laden Protokoll löschen fehlgeschlagen Wortumbruch Ausführliches Protokoll aktiviert Ausführliches Protokoll deaktiviert (keine Beschreibung angegeben) Dieses Modul erfordert eine neuere Version von LSPosed (%d) und kann daher nicht aktiviert werden Dieses Modul wurde für eine neuere Xposed-Version (%d) entwickelt und daher funktionieren einige Funktionen möglicherweise nicht Dieses Modul gibt nicht die benötigte LSPosed-Version an. Dieses Modul wurde für die LSPosed-Version %1$d erstellt, wurde jedoch aufgrund inkompatibler Änderungen in der Version %2$d deaktiviert Dieses Modul kann nicht geladen werden, da es auf der SD-Karte installiert ist, bitte in den internen Speicher verschieben Deinstallieren Moduleinstellungen In Repo anzeigen Möchtest du dieses Modul deinstallieren? %1$s deinstalliert Deinstallation fehlgeschlagen Modul zum Benutzer hinzufügen %1$s zu Benutzer %2$s hinzugefügt Modul hinzufügen fehlgeschlagen Auf Benutzer %s installieren Möchtest du %1$s auf Benutzer %2$s installieren? Es wird empfohlen manuell zu installieren, das Erzwingen der Installation über LSPosed kann Probleme verursachen. ausklappen einklappen Erneut optimieren Optimieren … Optimierung abgeschlossen. Starten Optimierung fehlgeschlagen: Rückgabewert ist leer Optimierung fehlgeschlagen: App-Name Paketname Installationszeit Aktualisierungszeit Umkehren System-Apps Sortieren Modul aktivieren Du hast keine App ausgewählt. Weiter? Spiele Module Verweigerungsliste Scope-Liste speichern fehlgeschlagen Version: %1$s Auswählen Empfohlen Du hast keine App ausgewählt. Empfohlene Apps auswählen? Empfohlene Apps auswählen? Alle Auswählen Keine Auswahl Automatisch einbinden Das LSPosed-Modul wurde noch nicht aktiviert Empfohlen Aktualisierung verfügbar: %1$s Modul %s wurde deaktiviert, da keine App ausgewählt wurde. System-Framework Sichern Sichern Wiederherstellen Stopp erzwingen Stopp erzwingen? Wenn du einen App-Stopp erzwingst, können Probleme entstehen. Neustart erforderlich, um diese Änderung zu übernehmen Neustart Ausblenden %s ist auf Verweigerungsliste. Dies könnte ohne Auswirkung bleiben. Auf Verweigerungsliste In anderer App anzeigen App-Information ¯\\\\_(ツ)_\/¯\nNichts hier 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 Sichern und Wiederherstellen Modul- und Scope-Listen sichern. Modul- und Scope-Listen wiederherstellen Sichern Sicherung fehlgeschlagen:\n%s Bitte DocumentUI aktivieren Wiederherstellen Wiederherstellung fehlgeschlagen:\n%s Netzwerk DNS über HTTPS Problemumgehung für DNS-Vergiftungen in einigen Ländern Designfarbe System Themenfarbe Apps erzwingen Launcher-Symbole anzuzeigen Ab Android 10 dürfen Apps ihre Launcher-Symbole nicht ausblenden. Schalte den Schalter aus, um diese System-Funktion zu deaktivieren. System Sprache Übersetzer Beim Übersetzen mitmachen Helfe uns, %s in deine Sprache zu übersetzen Eine Verknüpfung zum Öffnen des parasitären Managers erstellen Verknüpfung angeheftet 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 Info Webseite Quellcode Mitarbeiter Ressourcen Im Browser öffnen Ältere Versionen anzeigen Keine Veröffentlichung mehr Modularchiv laden fehlgeschlagen: %s Aktualisierbare zuerst Eingerichtet %d Download %d Downloads Sakura Rot Pink Lila Dunkellila Indigoblau Blau Hellblau Türkis Blaugrün Grün Hellgrün Limette Gelb Bernstein Orange Dunkelorange Braun Blaugrau
================================================ FILE: app/src/main/res/values-el/strings.xml ================================================ Επισκόπηση Πρόσθετα %d πρόσθετο ενεργοποιημένο %d ενθέματα ενεργοποιήθηκαν Αρχεία καταγραφής Ρυθμίσεις Σχόλια ή πρόταση Πληροφορίες Αναφορά προβλήματος Αποθετήριο Όλα τα πρόσθετα είναι ενημερωμένα Δημοσιεύθηκε στο %s Ενημερώθηκε στο %s %d ένθεμα αναβαθμίσιμο %d πρόσθετα μπορούν να ενημερωθούν Εγγραφείτε στο %2$s κανάλι μας]]> Aristeidis Alexopoulos Εγκατάσταση Πατήστε για εγκατάσταση του LSPosed Μη εγκατεστημένο Το LSPosed δεν είναι εγκατεστημένο Ενεργοποιημένο Μερικώς ενεργοποιημένο Το SEPolicy δεν φορτώθηκε σωστά Παρακαλούμε αναφέρετε το γεγονός αυτό στον προγραμματιστή Magisk .]]> Η έγχυση στο πλαίσιο συστήματος απέτυχε Magisk ή από κάποια χαμηλής ποιότητας πρόσθετα τουMagisk.
Παρακαλώ προσπαθήστε να απενεργοποιήσετε όλα τα πρόσθετα του Magisk εκτός από το Riru και το LSPosed ή να υποβάλετε το πλήρες αρχείο καταγραφής στους προγραμματιστές.]]>
Λανθασμένο στήριγμα συστήματος Τα πρόσθετα μπορεί να ακυρωθούν περιστασιακά.]]> Απαιτείται ενημέρωση Παρακαλώ εγκαταστήστε την τελευταία έκδοση του LSPosed Έκδοση API Έκδοση πλαισίου Όνομα πακέτου διαχειριστή Έκδοση συστήματος Συσκευή Σύστημα ABI Dex Optimizer Wrapper Ενεργό Μη ενεργό Υποστηριζόμενο Μη υποστηριζόμενο Ανικανοποίητη έκδοση Android Συνετρίβη Mount απέτυχε Το SELinux είναι επιτρεπτικό Η πολιτική SELinux είναι λανθασμένη Ενημέρωση LSPosed Επιβεβαίωση ενημέρωσης του LSPosed? Αυτή η συσκευή θα επανεκκινηθεί μετά την ολοκλήρωση της ενημέρωσης Αντιγραφή στο πρόχειρο Καλωσορίσατε στο LSPosed Χρησιμοποιείτε τον παρασιτικό διαχειριστή, ο οποίος μπορεί να δημιουργήσει συντόμευση ή να παραμένει ανοικτός από την ειδοποίηση. Χρησιμοποιείτε τον παρασιτικό διαχειριστή, ο οποίος μπορεί να δημιουργήσει συντόμευση ή να παραμένει ανοικτός από την ειδοποίηση. Δημιουργία συντόμευσης Να μην εμφανίζεται ποτέ Προτεινόμενος Παρασιτικός Διαχειριστής Το LSPosed υποστηρίζει τώρα την παρασιτοποίηση του συστήματος για να αποφύγετε την ανίχνευση, μπορείτε να ανοίξετε τον παρασιτικό διαχειριστή από την ειδοποίηση. Συνιστάται να απεγκαταστήσετε την τρέχουσα εφαρμογή. Αποθήκευση Λεπτομερείς Καταγραφές Αρχείο Καταγραφής Πρόσθετων Αποθήκευση αρχείου καταγραφής, παρακαλώ περιμένετε Αποθηκευμένα αρχεία καταγραφής Αποτυχία αποθήκευσης:\n%s Καθαρισμός αρχείου καταγραφής τώρα Το αρχείο καταγραφής εκκαθαρίστηκε επιτυχώς. Κύλιση στην κορυφή Φόρτωση… Κύλιση προς τα κάτω Φόρτωση ξανά Αποτυχία εκκαθάρισης του αρχείου καταγραφής Αναδίπλωση Λέξεων Ενεργοποίηση λεπτομερούς καταγραφής Λεπτομερής καταγραφή απενεργοποιημένη (δεν παρέχεται περιγραφή) Αυτό το ένθεμα απαιτεί μια νεότερη έκδοση του Xposed (%d) και επομένως δεν μπορεί να ενεργοποιηθεί Αυτή η ενότητα έχει σχεδιαστεί για μια νεότερη έκδοση Xposed (%d) και ως εκ τούτου ορισμένες λειτουργίες ενδέχεται να μην λειτουργούν. Αυτό το ένθεμα δεν καθορίζει την έκδοση Xposed που χρειάζεται. Αυτό το ένθεμα δημιουργήθηκε για την έκδοση Xposed %1$d, αλλά λόγω μη συμβατών αλλαγών στην έκδοση %2$d, έχει απενεργοποιηθεί Αυτό το πρόσθετο δεν μπορεί να φορτωθεί επειδή είναι εγκατεστημένο στην κάρτα SD, παρακαλώ μετακινήστε το στον εσωτερικό αποθηκευτικό χώρο Απεγκατάσταση Ρυθμίσεις πρόσθετου Προβολή στο Repo Θέλετε να απεγκαταστήσετε αυτό το πρόσθετο? Απεγκαταστάθηκε %1$s Απεγκατάσταση ανεπιτυχής Προσθήκη module στο χρήστη Προστέθηκε %1$s στον χρήστη %2$s Η προσθήκη module απέτυχε Εγκατάσταση σε χρήστη %s Θέλετε να εγκαταστήσετε %1$s στο χρήστη %2$s? Συνιστάται η χειροκίνητη εγκατάσταση, αναγκάζοντας την εγκατάσταση μέσω LSPosed μπορεί να προκαλέσει προβλήματα. επέκταση σύμπτυξη Επαναβελτιστοποίηση Βελτιστοποίηση… Η βελτιστοποίηση ολοκληρώθηκε Εκκίνηση Η βελτιστοποίηση απέτυχε: η τιμή επιστροφής είναι κενή Η βελτιστοποίηση απέτυχε: Όνομα εφαρμογής Όνομα πακέτου Χρόνος εγκατάστασης Χρόνος ενημέρωσης Αντίστροφη Εφαρμογές συστήματος Ταξινόμηση Ενεργοποίηση module Δεν έχετε επιλέξει καμία εφαρμογή. Συνέχεια? Παιχνίδια Πρόσθετα Denylist Αποτυχία αποθήκευσης της λίστας πεδίου Έκδοση: %1$s Προτεινόμενο Δεν έχετε επιλέξει καμία εφαρμογή. Επιλέξτε τις προτεινόμενες εφαρμογές? Επιλέξτε προτεινόμενες εφαρμογές? Το Xposed πρόσθετο δεν έχει ενεργοποιηθεί ακόμα Προτεινόμενο Διαθέσιμη ενημέρωση: %1$s Το ένθεμα %s έχει απενεργοποιηθεί δεδομένου ότι δεν έχει επιλεγεί εφαρμογή. Πλαίσιο Συστήματος Αντίγραφα Ασφαλείας Αντίγραφα Ασφαλείας Επαναφορά Αναγκαστική διακοπή Αναγκαστική διακοπή? Αν επιβάλετε τη διακοπή μιας εφαρμογής, ενδέχεται να μην λειτουργήσει σωστά. Απαιτείται επανεκκίνηση για να εφαρμοστεί αυτή η αλλαγή Reboot Απόκρυψη %s είναι σε denylist. Μπορεί να μην τεθεί σε ισχύ. Κατά την denylist Προβολή σε άλλη εφαρμογή Πληροφορίες εφαρμογής ◆ \\\\_(\")_\/ \"\nΤίποτα εδώ Framework Απενεργοποιήστε αναλυτικά στοιχεία καταγραφής Αναφορά προβλημάτων ζητάει να συμπεριλαμβάνεται τα αναλυτικά στοιχεία καταγραφής Μαύρο σκούρο θέμα Χρησιμοποιήστε το καθαρό μαύρο θέμα αν το σκούρο θέμα είναι ενεργοποιημένο Θέμα Αντίγραφα ασφαλείας και επαναφορά Λίστα module αντιγράφων ασφαλείας και λίστες εμβέλειας. Επαναφορά λίστας ενθεμάτων και πεδίου εφαρμογής. Αντίγραφα Ασφαλείας Αποτυχία δημιουργίας αντιγράφων ασφαλείας:\n%s Ενεργοποιήστε το DocumentUI Επαναφορά Αποτυχία επαναφοράς:\n%s Δίκτυο DNS μέσω HTTPS Εργαστείτε γύρω από τη δηλητηρίαση DNS σε ορισμένες χώρες Χρώμα θέματος Χρώμα θέματος συστήματος Εξαναγκασμός των εφαρμογών να εμφανίζουν εικονίδια εκκίνησης Μετά το Android 10, οι εφαρμογές δεν επιτρέπεται να αποκρύψουν τα εικονίδια εκτοξευτή τους. Απενεργοποιήστε την εναλλαγή για να απενεργοποιήσετε αυτήν τη λειτουργία συστήματος. Σύστημα Γλώσσα Συντελεστές μετάφρασης Συμμετοχή στη μετάφραση Βοηθήστε μας να μεταφράσουμε το %s στη γλώσσα σας Δημιουργήστε μια συντόμευση που μπορεί να ανοίξει τον παρασιτικό διαχειριστή Συντόμευση καρφιτσωμένη Ο τρέχων προεπιλεγμένος εκτοξευτής δεν υποστηρίζει συντομεύσεις καρφίτσας Ειδοποίηση καταστάσεως Εμφάνιση μιας ειδοποίησης που μπορεί να ανοίξει τον διαχειριστή παρασιτικής Δεν υπάρχει συντόμευση, δεν είναι δυνατή η απενεργοποίηση της ειδοποίησης Ενημέρωση καναλιού Σταθερό Βήτα Νυχτερινή κατασκευή Προστασία κλήσεων Xposed API Αποκλείστε τον δυναμικά φορτωμένο κώδικα μονάδας για να χρησιμοποιήσετε το Xposed API, αυτό μπορεί να σπάσει ορισμένες μονάδες αλλά να ωφελήσει την ασφάλεια Έτοιμο Εκδόσεις Πληροφορίες Αρχική Πηγαίος κώδικας Συνεργάτες Ενεργητικό Άνοιγμα σε πρόγραμμα περιήγησης Εμφάνιση παλαιότερων εκδόσεων Δεν υπάρχει πλέον έκδοση Αποτυχία φόρτωσης πρόσθετου repo: %s Αναβαθμίσιμο πρώτα Εγκατεστημένο %d λήψη %d downloads Sakura Κόκκινο Ροζ Μωβ Βαθύ μωβ Indigo Μπλε Ανοιχτό μπλε Κυανό Τιρκουάζ Πράσινο Ανοιχτό πράσινο Άσβεστος Κίτρινο Κεχριμπάρι Πορτοκαλί Βαθύ πορτοκαλί Καφέ Μπλε γκρι
================================================ FILE: app/src/main/res/values-es/strings.xml ================================================ Resumen Módulos %d módulo activado %d Módulos activados Trozas Configuración Comentarios o sugerencia Acerca de Reportar problema Repositorio Todos los modulos actualizados Publicado el %s Actualizado el %s %d módulo actualizable %d módulos repositorio actualizables Únete a nuestro %2$s canal]]> squaredDot Instalar Pulsa para instalar LSPosed No instalado LSPosed no está instalado Activado Parcialmente activado Selinux policy no está cargado correctamente Informa de ello al desarrollador de Magisk .]]> System Framework injection failed Magisk or some low-quality Magisk modules.
Please try to disable Magisk modules other than Riru and LSPosed or submit full log to developers.]]>
System prop incorrect Modules may invalidate occasionally.]]> Need to update Por favor instale la última versión de LSPosed Sugerencias para desarrolladores de módulos Por favor, desactiva las optimizaciones de implementación en Android Studio, o ejecuta el comando `gradlew installDebug` para instalar el módulo. De lo contrario, el apk del módulo no se actualizará. Versión de la API Versión del framework Nombre del paquete de gestión Versión del sistema Dispositivo ABI del Sistema Envoltura del optimizador Dex Activado No habilitado Apoyado No soportado Versión Android insatisfecha Se estrelló El montaje falló SELinux es permisivo La política de SELinux es incorrecta Actualizar LSPosed ¿Confirmar para actualizar LSPose? Este dispositivo se reiniciará después de completar la actualización Información copiada al portapapeles Bienvenido a LSPosed Usted está utilizando el gestor de parásitos, que puede crear acceso directo o todavía abierta de notificación. Estás usando el gestor de parásitos, que se puede abrir desde la notificación. Crear acceso directo Nunca mostrar Se recomienda Parasitic Manager LSPosed ahora soporta la parasitación del sistema para evitar la detección, puede abrir el gestor de parásitos desde la notificación. Se recomienda desinstalar la aplicación actual. Guardar Registros detallados Logs de los módulos Guardando registro, por favor espere Registros guardados No se pudo guardar:\n%s Limpiar los registros Registros limpiados satisfactoriamente. Desplazar hasta el inicio Cargando… Desplazar hasta el final Recargar Fallo al limpiar los registros Ajuste de palabras Registro detallado habilitado Registro detallado desactivado (sin descripción) Este módulo requiere una versión más nueva de Xposed (%d), por lo que no puede ser activado Este módulo está diseñado para una versión más reciente Xposed (%d) y por lo tanto algunas funcionalidades pueden no funcionar Este módulo no especifica la versión de Xposed que necesita. Este módulo fue creado para la versión de Xposed %1$d, pero, debido a cambios incompatibles en la versión %2$d, ha sido desactivado Este módulo no puede ser cargado porque está instalado en la tarjeta SD. Por favor, muévelo al almacenamiento interno Desinstalar Configuración del módulo Ver en el repositorio ¿Quieres desinstalar este módulo? Desinstalado %1$s Fallo en la desinstalación Añadir módulo al usuario %1$s instalado %2$s Fallo en la instalación Instalar al usuario %s ¿Quieres instalar %1$s al usuario %2$s? Se recomienda que lo instales manualmente; forzar la instalación a través de LSPosed puede causar problemas. expandir contraer Optimizar de nuevo Optimizando… Optimización completada. Abrir La optimización falló o devolvió un valor vacío. Fallo en la optimización: Filtrar por nombre de aplicación Filtrar por nombre de paquete Filtrar por fecha de instalación Filtrar por fecha de actualización Invertir Aplicaciones del sistema Filtrando Activar módulo No seleccionaste ninguna aplicación. ¿Quieres continuar? Juegos Módulos Lista de denegación Fallo al guardar la lista de scopes Versión: %1$s Seleccionar Recomendado No seleccionaste ninguna aplicación. ¿Quieres seleccionar las aplicaciones recomendadas? ¿Quieres seleccionar las aplicaciones recomendadas? Todo Ninguno Auto-Incluir El módulo Xposed no está activado aún Recomendado Actualización disponible: %1$s El módulo %s ha sido desactivado ya que no se ha seleccionado ninguna aplicación. Framework del sistema Respaldo Hacer un respaldo Restaurar Forzar la detención ¿Quieres forzar la detención? Si fuerzas la detención de una aplicación puede que esta se comporte de manera indefinida. Necesitas reiniciar la aplicación para aplicar este cambio Reiniciar Ocultar %s está en la lista de negación. Puede no tener efecto. En lista de denegación Ver en otra aplicación Información de la aplicación ¯\\\\_(ツ)_\/¯\nNo hay nada por aquí 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 Respaldo y restauración Hacer un respaldo de la lista de módulos y scopes. Hacer una restauración de la lista de módulos y scopes. Hacer un respaldo Error al realizar la copia de seguridad:\n%s Por favor, activa DocumentUI Restaurar Error al restaurar:\n%s Red DNS sobre HTTPS Solución alternativa al ataque de DNS en algunos países Color del tema Color de acentuación del sistema Forzar a las aplicaciones a mostrar los íconos del ejecutable En versiones posteriores a Android 10 no se permite a las aplicaciones (especialmente los módulos de Xposed) a ocultar el logo de su ejecutable. Desactiva la opción para desactivar esta característica. Sistema Idioma Colaboradores de traducción Participar en la traducción Ayúdanos a traducir %s a tu idioma Crear un acceso directo que pueda abrir el gestor de parásitos Acceso directo anclado 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 Información Página principal Código fuente Colaboradores Archivos Abrir en el navegador Mostrar versiones anteriores No hay más versiones Fallo al cargar el módulo de repositorio: %s Actualizables Instalado %d descargar %d descargas Sakura Rojo Rosa Morado Morado profundo Indigo Azul Azul claro Cian Teal Verde Verde claro Lima Amarillo Ámbar Naranja Naranja oscuro Marrón Gris azul
================================================ FILE: app/src/main/res/values-et/strings.xml ================================================ Kodu Moodulid %d moodul on lubatud %d moodulit on lubatud Logid Seaded Tagasiside või ettepanek Kohta Teata probleemist Repo Kõik moodulid on ajakohased Avaldatud %s Uuendatud %s %d moodul on täiendatav %d moodulit täiendatavad Liitu meie %2$s \'i kanaliga]]> Subaru Pan Installi Puudutage LSPosedi installimiseks Puudub LSPosed ei ole installitud Aktiveeritud Osaliselt aktiveeritud SEPolicy ei ole korralikult laetud Palun teatage sellest Magisk arendajale.]]> System Frameworki süstimine ebaõnnestus Magisk või mõned madala kvaliteediga Magisk-moodulid.
Palun proovige lülitada välja Magisk\'i moodulid peale Riru ja LSPosed või esitage täielik logi arendajatele.]]>
Süsteemi tugi vale Moodulid võivad aeg-ajalt kehtetuks muutuda.]]> Vaja uuendada Palun installige LSPosedi uusim versioon API\'i versioon Raamistiku versioon Manager paketi nimi Süsteemi versioon Seade Süsteemi ABI Dex Optimizer Wrapper Lubatud Pole lubatud Toetatud Toetamata Androidi versioon ei ole toetatud Kokku jooksnud Kinnitus ebaõnnestus SELinux on lubav SELinux policy on vale LSPosedi uuendamine Kas kinnitada LSPosed uuendamine? See seade taaskäivitub pärast uuendamise lõpetamist Kopeeritud lõikelauale Tere tulemast LSPosedisse Sa kasutad parasiitide haldurit, mis võib luua otsetee või ikka avada teateid. Kasutate parasiitide haldurit, mida saab avada teatisest. Loo otsetee Mitte kunagi ei näidata Parasiitide haldur Soovitatav LSPosed toetab nüüd süsteemi parasiitide tuvastamise vältimiseks, saate avada parasiitide haldaja teatest. Praegune rakendus on soovitatav eemaldada. Salvesta Põhjalikud logid Moodulite logid Logi salvestamine, palun oodake Logi salvestatud Salvestamine ebaõnnestus:\n%s Kustuta logi kohe Logi edukalt kustutatud. Kerige üles Laadimine… Kerige alla Laadi uuesti Logi tühjendamine ebaõnnestus Word Wrap Paljusõnaline logi on lubatud Paljusõnaline logi on välja lülitatud (kirjeldus puudub) See moodul nõuab uuemat Xposed versiooni (%d) ja seega ei saa seda aktiveerida. See moodul on mõeldud uuemale Xposedi versioonile (%d) ja seetõttu ei pruugi mõned funktsioonid töötada See moodul ei täpsusta Xposedi versiooni, mida ta vajab. See moodul loodi Xposedi versiooni %1$d jaoks, kuid versioonis %2$d tehtud ühildumatute muudatuste tõttu on see välja lülitatud Seda moodulit ei saa laadida, sest see on paigaldatud SD-kaardile, palun viige see sisemällu. Eemalda Mooduli seaded Vaata Repos Kas soovite selle mooduli eemaldada? Eemaldatud %1$s Eemaldamine ebaõnnestus Lisa kasutajale moodul Lisatud %1$s kasutajale %2$s Mooduli lisamine ebaõnnestus Installi kasutajale %s Tahad paigaldada %1$s kasutajale %2$s? Soovitatav on paigaldada käsitsi, LSPosed\'i kaudu sunniviisiline paigaldamine võib põhjustada probleeme. laienda kollaps Optimeeri uuesti Optimeerimine… Optimeeritud Ava Optimeerimine ebaõnnestus: tagastusväärtus on tühi Optimeerimine ebaõnnestus: Rakenduse nimi Paketi nimi Installimise aeg Uuendamise aeg Tagasipööra Süsteemirakendused Sortimisalus Luba moodul Te ei valinud ühtegi rakendust. Jätka? Mängud Moodulid Denylist Ei õnnestunud salvestada reguleerimisala nimekirja Versioon: %1$s Soovitatav Te ei valinud ühtegi rakendust. Valige soovitatud rakendused? Valige soovitatavad rakendused? Xposed moodul ei ole aktiveeritud Soovitatav Uuendus on saadaval: %1$s Moodul %s on välja lülitatud, kuna ühtegi rakendust ei ole valitud. Süsteemi raamistik Varukoopia Varukoopia Taasta Sundpeata Sundpeata? Kui te peatate rakenduse sunniviisiliselt, võib see halvasti käituda. Selle muudatuse kohaldamiseks on vajalik taaskäivitamine Taaskäivitus Peida %s on denylistis. See ei pruugi jõustuda. Denylistil Vaadake teises rakenduses Rakenduse teave ¯\\\\_(ツ)_\/¯\nSiin pole midagi. Raamistik Lülita sõnalised logid välja Aruande probleemid taotluse lisada sõnalogid Must tume teema Kasutage puhast musta teemat, kui tume teema on lubatud. Teema Varundamine ja taastamine Moodulite varukoopiate nimekiri ja ulatusloendid. Taastab moodulite loendi ja ulatusloendite loendi. Varukoopia Varundamine ebaõnnestus:\n%s Palun lubage DocumentUI Taasta Ei õnnestunud taastada:\n%s Võrk DNS üle HTTPS Workaround DNS mürgistus mõnedes riikides Teema värv Süsteemi teema värv Rakenduste sundimine käivitaja ikoonide kuvamiseks Pärast Android 10 ei ole rakendustel lubatud oma käivitaja ikoonid ära peita. Selle süsteemifunktsiooni väljalülitamiseks lülitage lüliti välja. Süsteem Keel Tõlkimise toetajad Osalege tõlkimises Aita meil tõlkida %s sinu keelde Loo otsetee, mis võib avada parasiitide halduri Otsetee kinnitatud 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 Teave Koduleht Lähtekood Koostööpartnerid Varad Ava brauseris Kuva vanemad versioonid Enam ei ole väljaannet Ebaõnnestus mooduli repo laadimine: %s Esimesena uuendatav Paigaldatud allalaaditud on %d kord allalaaditud on %d korda Sakura Punane Roosa Lilla Sügavlilla Indigo Sinine Helesinine Tsüaansinine Sinakasroheline Roheline Heleroheline Laimiroheline Kollane Amber Oranž Sügavoranž Pruun Sinine hall
================================================ FILE: app/src/main/res/values-fa/strings.xml ================================================ نمای کلی ماژول‌ها %d ماژول فعال %d ماژول فعال لاگ ها تنظیمات بازخورد یا پیشنهاد درباره گزارش مشکل مخزن همه ماژول ها بروز هستند منتشر شده در %s بروزرسانی شده در %s %d ماژول قابل بروزرسانی %d ماژول قابل بروزرسانی عضو کانال %2$s شوید]]> null نصب برای نصب LSPosed لمس کنید نصب نشده LSPosed نصب نشده فعال شده نیمه فعال سیاست SELinux به درستی بارگذاری نشده لطفاً این موضوع را به توسعه دهنده Magisk گزارش دهید.]]> تزریق به چارچوب سیستم ناموفق بود Magisk یا برخی ماژول های بی کیفیت Magisk باشد.
لطفاً ماژول های Magisk به جز Riru و LSPosed را غیرفعال کنید یا لاگ کامل را برای توسعه دهندگان بفرستید.]]>
ویژگی های سیستم نادرست است ماژول ها ممکن است گاهی کار نکنند.]]> نیاز به بروزرسانی LSPosedLSPosed نکات برای توسعه دهنده ماژول لطفاً بهینه‌سازی‌های استقرار را در اندروید استودیو خاموش کنید یا از دستور `gradlew installDebug` استفاده کنید. در غیر این صورت APK ماژول آپدیت نمی‌شود. نسخه API نسخه چارچوب نام پکیج مدیر نسخه سیستم دستگاه ABI سیستم قالب بهینه‌سازی Dex فعال غیرفعال پشتیبانی شده پشتیبانی نمی شود نسخه اندروید پشتیبانی نمی شود کرش کرد مونت ناموفق بود SELinux در حالت Permissive است سیاست SELinux نادرست است بروزرسانی LSPosed آیا بروزرسانی LSPosed را تأیید می کنید؟ بعد از پایان، دستگاه ری استارت می شود کپی شد به LSPosed خوش آمدید شما از مدیر Parasitic استفاده می کنید که می تواند شورتکات بسازد یا از نوتیفیکیشن باز شود. شما از مدیر Parasitic استفاده می کنید که فقط از نوتیفیکیشن باز می شود. ساخت شورتکات هرگز نمایش نده مدیر Parasitic توصیه شده LSPosed حالا از سیستم Parasitic پشتیبانی می کند تا شناسایی نشود، می توانید از نوتیفیکیشن مدیر Parasitic را باز کنید. بهتر است برنامه فعلی را حذف کنید. ذخیره لاگ های مفصل لاگ های ماژول در حال ذخیره لاگ، لطفاً صبر کنید لاگ ها ذخیره شدند ذخیره موفق نبود:\n%s همین الان لاگ ها را پاک کن لاگ ها با موفقیت پاک شدند. برگشت به بالا در حال بارگذاری… رفتن به پایین بارگذاری مجدد پاک کردن لاگ ناموفق بود شکستن خودکار خطوط لاگ مفصل فعال شد لاگ مفصل غیرفعال شد (توضیحی داده نشده) این ماژول نیاز به نسخه جدیدتر Xposed (%d) دارد و نمی تواند فعال شود این ماژول برای نسخه جدیدتر Xposed (%d) ساخته شده، پس ممکن است برخی امکانات کار نکنند این ماژول نسخه Xposed مورد نیازش را مشخص نکرده. این ماژول برای نسخه %1$d ساخته شده، اما به دلیل تغییرات ناسازگار در نسخه %2$d غیرفعال شده این ماژول نمی تواند بارگذاری شود چون روی کارت حافظه نصب شده، لطفاً به حافظه داخلی منتقل کنید حذف نصب تنظیمات ماژول مشاهده در مخزن می خواهید این ماژول را حذف کنید؟ حذف شد %1$s حذف موفق نبود اضافه کردن ماژول به کاربر اضافه شد %1$s به کاربر %2$s اضافه کردن ماژول موفق نبود نصب برای کاربر %s می خواهید %1$s را برای کاربر %2$s نصب کنید؟ توصیه می شود دستی نصب کنید، نصب با LSPosed ممکن است مشکل ایجاد کند. باز کن ببند بهینه‌سازی مجدد در حال بهینه‌سازی… بهینه‌سازی تمام شد باز کن بهینه‌سازی شکست خورد: خروجی خالی است بهینه‌سازی شکست خورد: نام برنامه نام پکیج زمان نصب زمان بروزرسانی معکوس برنامه های سیستمی مرتب‌سازی فعال کردن ماژول برنامه ای انتخاب نکردی، ادامه میدی؟ بازی ها ماژول ها لیست مسدود ذخیره لیست ناموفق بود نسخه: %1$s انتخاب توصیه شده برنامه ای انتخاب نکردی. برنامه های توصیه شده را انتخاب کنم؟ می خوای برنامه های توصیه شده رو انتخاب کنی؟ همه هیچی شامل خودکار ماژول Xposed هنوز فعال نشده توصیه شده بروزرسانی موجود: %1$s ماژول %s به خاطر انتخاب نکردن برنامه غیرفعال شده. چارچوب سیستم پشتیبان گیری پشتیبان گیری بازیابی توقف اجباری توقف اجباری؟ اگر برنامه را به زور متوقف کنی، ممکن است درست کار نکند. برای اعمال تغییر باید ری استارت کنی ری استارت مخفی کن %s در لیست مسدود است. ممکن است کار نکند. در لیست مسدود مشاهده در برنامه دیگر اطلاعات برنامه ¯\_(ツ)_/¯\nاینجا چیزی نیست چارچوب غیرفعال کردن لاگ مفصل لاگ مفصل برای گزارش مشکل لازم است فعال کردن نظارت لاگ نظارت لاگ LSPosed ویژگی های سیستم را تغییر می دهد و ممکن است برای شناسایی LSPosed استفاده شود تم سیاه کامل اگر تم تاریک فعال است از تم کاملا سیاه استفاده کن تم پشتیبان گیری و بازیابی پشتیبان گیری از لیست ماژول ها و برنامه ها. بازیابی لیست ماژول ها و برنامه ها. پشتیبان گیری پشتیبان گیری ناموفق بود:\n%s لطفاً DocumentUI را فعال کنید بازیابی بازیابی ناموفق بود:\n%s شبکه DNS روی HTTPS حل مشکل مسمومیت DNS در بعضی کشورها رنگ تم رنگ تم سیستم نمایش آیکون های لانچر برنامه ها از اندروید ۱۰ به بعد، برنامه ها نمی توانند آیکون لانچر را مخفی کنند. این گزینه را خاموش کن تا این ویژگی غیرفعال شود. سیستم زبان مشارکت کنندگان ترجمه مشارکت در ترجمه کمک کن %s را به زبان خودت ترجمه کنیم شورتکاتی بساز که مدیر Parasitic را باز کند شورتکات پین شد لانچر پیش فرض فعلی شورتکات های پین شده را پشتیبانی نمی کند نمایش اعلان وضعیت نمایش اعلانی که مدیر Parasitic را باز کند شورتکات نیست، نمی توان اعلان را غیرفعال کرد کانال بروزرسانی پایدار بتا نسخه شبانه حفاظت از تماس API Xposed جلوگیری از استفاده کد ماژول های بارگذاری شده به صورت داینامیک از API های Xposed، ممکن است بعضی ماژول ها کار نکنند ولی امنیت بهتر می شود راهنما نسخه ها اطلاعات صفحه اصلی سورس کد همکاران دارایی ها باز کردن در مرورگر نمایش نسخه های قدیمی تر نسخه ای بیشتر نیست بارگذاری مخزن ماژول شکست خورد: %s اول ماژول های قابل بروزرسانی نصب شده %d دانلود %d دانلود ساکورا قرمز صورتی بنفش بنفش تیره نیلی آبی آبی روشن آبی فیروزه ای آبی خاکستری سبز سبز روشن لیمویی زرد کهربایی نارنجی نارنجی تیره قهوه ای آبی خاکستری
================================================ FILE: app/src/main/res/values-fi/strings.xml ================================================ Yleiskatsaus Moduulit %d moduuli käytössä %d moduulia käytössä Lokit Asetukset Palaute tai ehdotus Tietoja Ilmoita ongelmasta Versiovarasto Kaikki moduulit ajan tasalla Julkaistu osoitteessa %s Päivitetty osoitteessa %s %d moduuli päivitettävissä %d moduulia päivitettävissä Liity kanavaamme %2$s]]> null Asenna Napauta asentaaksesi LSPosed Ei asennettu LSPosed ei ole asennettu Aktivoitu Osittain aktivoitu SEPolicy ei ole ladattu oikein Ilmoita tästä Magisk kehittäjälle.]]> Järjestelmän kehysinjektointi epäonnistui Magisk tai joitakin heikkolaatuisia Magisk moduuleja.
Yritä poistaa käytöstä muut Magisk moduulit kuin Riru ja LSPosed tai lähettää täysi loki kehittäjille.]]>
Järjestelmän prop virheellinen Moduulit voivat mitätöidä satunnaisesti.]]> Täytyy päivittää Asenna LSPosedin uusin versio API versio Kehyksen versio Manager-paketin nimi Järjestelmän versio Laite Järjestelmä ABI Dex Optimizer Wrapper Käytössä Ei käytössä Tuettu Ei tuettu Android-versio tyytymätön Crashed Kiinnitys epäonnistui SELinux on salliva SELinux-käytäntö on virheellinen Päivitys LSPostettu Vahvista LSPost-päivitys? Tämä laite käynnistyy uudelleen päivityksen jälkeen Kopioitu leikepöydälle Tervetuloa LSPosed Käytät loishallintaohjelmaa, joka voi luoda pikakuvakkeen tai silti avata ilmoituksen. Käytät loishallintaohjelmaa, joka voidaan avata ilmoituksesta. Luo pikakuvake Älä näytä koskaan Parasitic Manager Suositellaan LSPosed tukee nyt järjestelmän loisimista havaitsemisen välttämiseksi, voit avata loishallinnan ilmoituksesta. On suositeltavaa poistaa nykyinen sovellus. Tallenna Verbose Lokit Moduulien Lokit Lokin tallentaminen, odota Tallennetut lokit Tallennus epäonnistui:\n%s Tyhjennä loki nyt Loki tyhjennetty. Vieritä ylös Ladataan… Siirry alareunaan Reload Lokin tyhjentäminen epäonnistui Sanan Rivitys Verbose loki käytössä Verbose loki pois käytöstä (ei kuvausta annettu) Tämä moduuli vaatii uudemman Xposed version (%d) eikä sitä näin ollen voi aktivoida Tämä moduuli on suunniteltu uudemmalle Xposed-versiolle (%d), joten jotkin toiminnot eivät välttämättä toimi. Tämä moduuli ei määrittele tarvitsemaansa Xposed versiota. Tämä moduuli on luotu Xposed versiolle %1$d, mutta koska versiossa %2$don tehty yhteensopimattomia muutoksia, se on poistettu käytöstä Tätä moduulia ei voi ladata, koska se on asennettu SD-kortille, siirrä se sisäiseen tallennustilaan Poista Moduulin asetukset Näytä repossa Haluatko poistaa tämän moduulin? Poista %1$s Poisto epäonnistui Lisää moduuli käyttäjälle Lisätty %1$s käyttäjälle %2$s Moduulin lisääminen epäonnistui Asenna käyttäjälle %s Haluatko asentaa %1$s käyttäjälle %2$s? On suositeltavaa asentaa manuaalisesti, pakottaa asennus LSPosedin kautta voi aiheuttaa ongelmia. laajenna pienennä Uudelleenoptimoi Optimoidaan… Optimointi valmis Käynnistä se Optimointi epäonnistui: palautusarvo on tyhjä Optimointi epäonnistui: Sovelluksen nimi Paketin nimi Asenna aika Päivityksen aika Käänteinen Järjestelmäsovellukset Lajittelu Ota moduuli käyttöön Et valinnut yhtään sovellusta. Jatketaanko? Pelit Moduulit Denylist Valmistelulistan tallentaminen epäonnistui Versio: %1$s Suositeltu Et valinnut yhtään sovellusta. Valitse suositellut sovellukset? Valitse suositellut sovellukset? Xposed moduuli ei ole vielä aktivoitu Suositeltu Päivitys saatavilla: %1$s Moduuli %s on poistettu käytöstä koska sovellusta ei ole valittu. Järjestelmän Puitteet Varmuuskopio Varmuuskopio Palauta Pakota lopetus Pakotetaanko lopetus? Jos pakotat sovelluksen pysähtymään, se saattaa käyttäytyä väärin. Uudelleenkäynnistys vaaditaan tämän muutoksen käyttöönottamiseksi Reboot Piilota %s on denylistissä. Se ei ehkä tule voimaan. On denylist Näytä toisessa sovelluksessa Sovelluksen tiedot ¶ \\\\_(konferenssissa)_\/ ¶\nEi mitään tässä Framework Sanallisten lokien poistaminen käytöstä Raportti pyytää sisällyttämään sanalliset lokit Musta tumma teema Käytä puhdas musta teema, jos tumma teema on käytössä Teema Varmuuskopioi ja palauta Varmuuskopioi moduulien listat ja sisällysluettelot. Palauta moduulien luettelo ja sisällysluettelot. Varmuuskopio Varmuuskopiointi epäonnistui:\n%s Ota DocumentUI käyttöön Palauta Palautus epäonnistui:\n%s Verkko DNS yli HTTPS Workaround DNS myrkytys joissakin kansoissa Teeman väri Järjestelmän teeman väri Pakota sovellukset näyttämään käynnistimen kuvakkeet Android 10:n jälkeen sovellukset eivät saa piilottaa niiden käynnistyskuvakkeita. Poista valinta käytöstä poistaaksesi järjestelmän ominaisuuden. Järjestelmä Kieli Käännöksen osallistujat Osallistu käännökseen Auta meitä kääntämään %s kielellesi Luo pikakuvake, joka voi avata loishallintaohjelman. Pikakuvake kiinnitetty 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 Tiedot Kotisivu Lähdekoodi Yhteistyökumppanit Laitteet Avaa selaimessa Näytä vanhemmat versiot Ei enää versiota Ei voitu ladata moduulia repo: %s Päivitettävissä ensin Asennettu %d lataa %d lataukset Sakura Punainen Pinkki Violetti Syvä violetti Indigo Sininen Vaalea sininen Syaani Sinappi Vihreä Vaalea vihreä Limea Keltainen Meripihka Oranssi Syvä oranssi Ruskea Sininen harmaa
================================================ FILE: app/src/main/res/values-fr/strings.xml ================================================ Aperçu Modules %d module actif %d modules actifs Journaux Réglages Réaction ou suggestion À propos Signaler un problème Dépôt Tous les modules sont à jour Publié le %s Mise à jour le %s %d modules évolutifs %d modules évolutifs Rejoindre notre canal %2$s]]> https://github.com/xerta555 https://github.com/tclement0922 JingMatrix Installer Appuyer pour installer LSPosed Non installé LSPosed n\'est pas installé Activé Partiellement activé SEPolicy n\’est pas chargé correctement Merci de ne pas remonter celà vers le développeur Magisk.]]> Échec de l\’injection du sous système Magisk ou certains modules Magisk de basse qualité.
Essayez de désactiver les modules Magisk autres que Riru et LSPosed ou envoyez le journal complet aux développeurs.]]>
Propriétés système incorrectes Des modules peuvent s\'invalider occasionnellement.]]> Mise à jour nécessaire Merci d\’installer la dernière version de LSPosed Conseils pour les développeurs de modules Veuillez désactiver les optimisations de déploiement sur Android Studio, ou utilisez la commande `gradlew installDebug` pour installer. Sinon, l\'APK du module ne sera pas mis à jour. Version de l\’API Version du framework Nom de paquet du gestionnaire Version du système Périphérique Architecture du système Enveloppeur Dex Optimizer Activé Non actif Supporté Non supporté Version d\'Android non satisfaisante Planté Échec du montage SELinux est permissif La politique SELinux est incorrecte Mettre à jour LSPosed Vous confirmez la mise à jour LSPosed ? Ce périphérique redémarrera après la mise à jour effectuée Copié dans le presse-papier Bienvenue dans LSPosed Vous utilisez le gestionnaire parasité, qui ne peut pas créer de raccourcis ou même être ouvert à partir d\'une notification. Vous utilisez le gestionnaire parasité, qui peut être ouvert depuis la notification. Créer le raccourci Ne jamais afficher Gestionnaire parasité recommandé LSPosed supporte maintenant la parasitage du système afin d\'éviter les détection, vous pouvez l\'ouvrir depuis la notification. Il est recommandé de désinstaller l\'application actuelle. Sauvegarder Journaux détaillés Journaux des modules Enregistrement du journal, veuillez patienter Journaux enregistrés Échec de la sauvegarde :\n%s Effacer le journal maintenant Journal effacé avec succès. Haut de page Chargement… Pied de page Recharger Échec de l\'effacement du journal Retour à la ligne Journaux détaillés activés Journaux détaillés désactivés (aucune description fournie) Ce module requière une nouvelle version d\'Xposed (%d) et n\'a donc pas pu être activé Ce module a été conçu pour une nouvelle version d\’Xposed (%d) et certaines fonctionnalités pourraient ne pas fonctionner Ce module ne spécifie pas la version d\'Xposed nécessaire. Ce module à été créé pour la version Xposed %1$d, mais due à des changements incompatibles dans la version %2$d, il à été désactivé Ce module ne peut pas être chargé car il est installé sur la carte SD, merci de le déplacer sur le stockage interne Désinstaller Réglages du module Afficher dans le dépôt Voulez-vous désinstaller ce module ? Désinstallation de %1$s Échec de la désinstallation Ajouter le module à l\’utilisateur %1$s ajouté à l’utilisateur %2$s Échec de l\’ajout du module Installer dans l\'utilisateur %s Vous voulez installer %1$s dans l\'utilisateur %2$s ? Il est recommandé de l\'installer manuellement, forcer l\'installation via LSPosed pourrait causer des problèmes. développer réduire Ré-optimiser Optimisation… Optimisation terminée Démarrer Échec de l\’optimisation : la valeur renvoyée est vide Échec de l\’optimisation : Trier par nom d\’application Trier par nom de paquet Trier par date d\’installation Trier par heure de mise à jour Inversé Applications système Trier Activer le module Vous n\'avez sélectionné aucune application. Continuer ? Jeux Modules Liste de refus Échec de l\'enregistrement de la liste des périmètres d\'applications Version : %1$s Choisir Recommandé Vous n\'avez sélectionné aucune application. Sélectionner les applications recommandées ? Sélectionner les applications recommandées ? Toutes Aucune Inclus auto Le module Xposed n\’est pas encore activé Recommandé Mise à jour disponible : %1$s Le module %s a été désactivé étant donné qu\’aucune application n\’ai été sélectionné. Cadre du sous-système Sauvegarde Sauvegarder Restaurer Forcer l\’arrêt Forcer l\'arrêt ? Si vous forcez l\'arrêt d\'une application, celle-ci pourrait mal fonctionner. Un redémarrage est requis pour appliquer les changements Redémarrer Masquage %s est sur la liste de refus. Cela pourrait ne pas avoir d\'effet. Sur liste de refus Afficher dans une autre application Informations d\’application ¯\\\\_(ツ)_\/¯\nIl n\’y a rien ici 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 Sauvegarder et restaurer Sauvegarder la liste des modules ainsi que leurs champs d\'applications. Restaurer la liste des modules ainsi que leurs champs d\'applications. Sauvegarder Échec de la sauvegarde :\n%s Merci d\'activer le gestionnaire de fichiers Restaurer Échec de la restauration :\n%s Réseau DNS sur HTTPS Contourner la censure DNS dans certains pays Couleur du thème Couleur d\'accentuation du système Forcer les applications à afficher leurs icônes dans le lanceur Après Android 10, les applications ne sont pas autorisées à masquer leurs icônes dans le lanceur. Désactiver ce commutateur pour désactiver cette fonctionnalité du système. Système Langage Contributeurs de traduction Participer à la traduction Aidez-nous à traduire %s dans votre langue Créer un raccourci qui peut ouvrir le gestionnaire de parasites Raccourci épinglé 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 Infos Page d\’accueil Code source Collaborateurs Actifs Ouvrir dans le navigateur Afficher les anciennes versions Pas d\’autres versions Échec de chargement du dépôt des modules : %s Évolutifs en premier installée %d téléchargé %d téléchargés Sakura Rouge Rose Violet Violet foncé Indigo Bleu Bleu clair Cyan Turquoise Vert Vert clair Vert citron Jaune Ambre Orange Orange foncé Marron Bleu grisâtre
================================================ FILE: app/src/main/res/values-hi/strings.xml ================================================ ओवरव्यू मॉड्यूल्स %d मॉड्यूल एनेबल किए गए %d मॉड्यूल्स एनेबल किए गए लॉग्स सेटिंग्स फीडबैक या सजेशन इसके बारे में इशू को रिपोर्ट करें रिपोज़िटरी सभी मॉड्यूल अप टू डेट %s. पर प्रकाशित %s. पर अपडेट किया गया %d मॉड्यूल अपग्रेड करने योग्य %d मॉड्यूल अपग्रेड करने योग्य पर सोर्स कोड देखें हमारे %2$s चैनल से जुड़ें]]> Ahmad Shaikh स्थापित करना LSPosed स्थापित करने के लिए टैप करें स्थापित नहीं हे LSPosed स्थापित नहीं है सक्रिय आंशिक रूप से सक्रिय SEPolicy ठीक से लोड नहीं है कृपया इसकी सूचना मैजिक डेवलपर को दें।]]> सिस्टम फ्रेमवर्क इंजेक्शन विफल Magisk या कुछ निम्न-गुणवत्ता वाले Magisk मॉड्यूल के कारण हो सकता है।
कृपया Riru और LSPosed के अलावा अन्य Magisk मॉड्यूल को अक्षम करने का प्रयास करें या डेवलपर्स को पूर्ण लॉग सबमिट करें।]]>
सिस्टम प्रोप गलत मॉड्यूल कभी-कभी अमान्य हो सकते हैं।]]> अद्यतन करने की आवश्यकता है कृपया LSPosed का नवीनतम संस्करण स्थापित करें एपीआई संस्करण फ्रेमवर्क संस्करण प्रबंधक पैकेज का नाम सिस्टम संस्करण उपकरण सिस्टम एबीआई डेक्स ऑप्टिमाइज़र रैपर सक्रिय निष्क्रिय समर्थित असमर्थित Android संस्करण असंतुष्ट दुर्घटनाग्रस्त माउंट विफल SELinux अनुमेय है SELinux नीति गलत है अद्यतन LSPosed LSPosed को अपडेट करने की पुष्टि करें? अपडेट पूरा होने के बाद यह डिवाइस रीबूट हो जाएगा क्लिपबोर्ड पर नकल LSPosed में आपका स्वागत है आप परजीवी प्रबंधक का उपयोग कर रहे हैं, जो शॉर्टकट बना सकता है या सूचना से अभी भी खुला हो सकता है। आप परजीवी प्रबंधक का उपयोग कर रहे हैं, जो सूचना से खुल सकता है। शॉर्टकट बनाएं कभी भी न दिखाओ परजीवी प्रबंधक की सिफारिश की LSPosed अब पता लगाने से बचने के लिए सिस्टम परजीवीकरण का समर्थन करता है, आप अधिसूचना से परजीवी प्रबंधक खोल सकते हैं। वर्तमान एप्लिकेशन को अनइंस्टॉल करने की अनुशंसा की जाती है। बचाना वर्बोज़ लॉग्स मॉड्यूल लॉग लॉग सहेजा जा रहा है, कृपया प्रतीक्षा करें लॉग सेव हो गए सहेजने में विफल:\n%s अभी लॉग साफ़ करें लॉग सफलतापूर्वक साफ़ किया गया। शीर्ष तक स्क्रॉल करें लोड हो रहा है… नीचे स्क्रॉल करें पुनः लोड करें लॉग साफ़ करने में विफल वर्ड रैप वर्बोज़ लॉग सक्षम वर्बोज़ लॉग अक्षम (कोई विवरण नहीं दिया गया) इस मॉड्यूल को एक नए Xposed संस्करण (%d) की आवश्यकता है और इस प्रकार इसे सक्रिय नहीं किया जा सकता है यह मॉड्यूल एक नए Xposed संस्करण (%d) के लिए डिज़ाइन किया गया है और इस प्रकार कुछ कार्यात्मकताएँ काम नहीं कर सकती हैं यह मॉड्यूल Xposed संस्करण को निर्दिष्ट नहीं करता है जिसकी उसे आवश्यकता है। यह मॉड्यूल Xposed संस्करण %1$dके लिए बनाया गया था, लेकिन संस्करण %2$dमें असंगत परिवर्तनों के कारण, इसे अक्षम कर दिया गया है यह मॉड्यूल लोड नहीं किया जा सकता क्योंकि यह एसडी कार्ड पर स्थापित है, कृपया इसे आंतरिक भंडारण में ले जाएं स्थापना रद्द करें मॉड्यूल सेटिंग्स रेपो में देखें क्या आप इस मॉड्यूल को अनइंस्टॉल करना चाहते हैं? अनइंस्टॉल किया गया %1$s अनइंस्टॉल असफल उपयोगकर्ता में मॉड्यूल जोड़ें उपयोगकर्ता %2$sमें %1$s जोड़ा गया मॉड्यूल जोड़ना विफल उपयोगकर्ता को स्थापित करें %s उपयोगकर्ता %2$sपर %1$s स्थापित करना चाहते हैं? मैन्युअल रूप से स्थापित करने की अनुशंसा की जाती है, LSPosed के माध्यम से स्थापना को मजबूर करने से समस्या हो सकती है। विस्तार ढहना पुन: अनुकूलित अनुकूलन… अनुकूलन पूर्ण इसे लॉन्च करें अनुकूलन विफल: वापसी मूल्य खाली है अनुकूलन विफल: आवेदन का नाम पैकेज का नाम समय स्थापित करें समय सुधारें उल्टा सिस्टम ऐप्स छंटाई मॉड्यूल सक्षम करें आपने कोई ऐप नहीं चुना है। जारी रखें? खेल मॉड्यूल इनकार करनेवाला कार्यक्षेत्र सूची सहेजने में विफल संस्करण: %1$s अनुशंसित आपने कोई ऐप नहीं चुना है। अनुशंसित ऐप्स चुनें? अनुशंसित ऐप्स चुनें? एक्सपोज़ड मॉड्यूल अभी तक सक्रिय नहीं है अनुशंसित अपडेट उपलब्ध: %1$s मॉड्यूल %s को अक्षम कर दिया गया है क्योंकि कोई ऐप नहीं चुना गया है। सिस्टम फ्रेमवर्क बैकअप बैकअप पुनर्स्थापित करना जबर्दस्ती बंद करें जबर्दस्ती बंद करें? यदि आप किसी ऐप को जबरदस्ती बंद करते हैं, तो वह गलत व्यवहार कर सकता है। इस परिवर्तन को लागू करने के लिए रीबूट की आवश्यकता है रीबूट छिपाना %s अस्वीकृत सूची में है। यह प्रभावी नहीं हो सकता है। अस्वीकृत सूची पर अन्य ऐप में देखें अनुप्रयोग की जानकारी ¯\\\\_(ツ)_\/¯\nयहाँ कुछ भी नहीं रूपरेखा वर्बोज़ लॉग अक्षम करें रिपोर्ट वर्बोज़ लॉग शामिल करने का अनुरोध जारी करती है ब्लैक डार्क थीम यदि डार्क थीम सक्षम है तो शुद्ध काली थीम का उपयोग करें थीम बैकअप और पुनर्स्थापना बैकअप मॉड्यूल सूची और कार्यक्षेत्र सूचियाँ। मॉड्यूल सूची और कार्यक्षेत्र सूचियों को पुनर्स्थापित करें। बैकअप बैकअप में विफल:\n%s कृपया DocumentUI सक्षम करें पुनर्स्थापित करना पुनर्स्थापित करने में विफल:\n%s नेटवर्क एचटीटीपीएस पर डीएनएस कुछ देशों में DNS विषाक्तता का समाधान थीम रंग सिस्टम थीम रंग लॉन्चर आइकन दिखाने के लिए ऐप्स को बाध्य करें Android 10 के बाद, ऐप्स को अपने लॉन्चर आइकन छिपाने की अनुमति नहीं है। इस सिस्टम सुविधा को अक्षम करने के लिए टॉगल बंद करें। प्रणाली भाषा अनुवाद योगदानकर्ता अनुवाद में भाग लें %s को अपनी भाषा में अनुवाद करने में हमारी सहायता करें एक शॉर्टकट बनाएं जो परजीवी प्रबंधक खोल सके शॉर्टकट पिन किया गया वर्तमान डिफ़ॉल्ट लांचर पिन शॉर्टकट का समर्थन नहीं करता स्थिति अधिसूचना एक अधिसूचना दिखाएं जो परजीवी प्रबंधक खोल सकती है कोई शॉर्टकट नहीं, अधिसूचना अक्षम नहीं कर सकता चैनल अपडेट करें स्थिर बीटा सॉफ़्टवेयर की स्थिरता Xposed API कॉल सुरक्षा Xposed API का उपयोग करने के लिए गतिशील रूप से लोड किए गए मॉड्यूल कोड को ब्लॉक करें, इससे कुछ मॉड्यूल टूट सकते हैं लेकिन सुरक्षा को लाभ होगा रीडमी विज्ञप्ति जानकारी होमपेज सोर्स कोड सहयोगियों संपत्तियां ब्राउज़र में खोलें पुराने संस्करण दिखाएं कोई और रिलीज नहीं मॉड्यूल रेपो लोड करने में विफल: %s पहले अपग्रेड करने योग्य स्थापित %d डाउनलोड %d डाउनलोड सकुरा लाल गुलाबी बैंगनी गहरा बैंगनी नील नीला हल्का नीला रंग सियान टील हरा हल्का हरा नींबू पीला अंबर संतरा गहरा नारंगी भूरा नीला ग्रे
================================================ FILE: app/src/main/res/values-hr/strings.xml ================================================ Pregled Moduli %d modul omogućen %d modula omogućeno %d modula omogućeno Zapisi Postavke Povratna informacija ili prijedlog Informacije Prijavi problem Spremište modula Svi moduli ažurirani Objavljeno u %s Ažurirano u %s %d modul moguće nadograditi %d modula moguće nadograditi %d modula moguće nadograditi Pridružite se našem %2$s kanalu]]> https://github.com/cube2412 Instaliraj Dodirnite za instaliranje LSPosed Nije instalirano LSPosed nije instaliran Aktiviran Djelomično aktiviran SEPolicy nije pravilno učitan Prijavite ovo Magisk programeru.]]> Injektiranje u System Framework nije uspjelo Magisk ili nekim Magisk modulima niske kvalitete.
Pokušajte onemogućiti Magisk module koji nisu Riru i LSPosed ili pošaljite cijeli zapis programerima.]]>
Svojstva sustava nisu ispravna Moduli povremeno mogu biti nedostupni.]]> Treba ažurirati Molimo instalirajte najnoviju verziju LSPosed API verzija Framework verzija Naziv paketa upravitelja System verzija Uređaj System ABI Dex Optimizer Wrapper Omogućeno Nije omogućeno Podržano Nepodržano Verzija Androida nije zadovoljavajuća Srušio se Postavljanje nije uspjelo SELinux je permisivan Pravila SELinuxa je netočna Ažurirajte LSPosed Potvrditi ažuriranje LSPosed? Ovaj će se uređaj ponovno pokrenuti nakon završetka ažuriranja Kopirano u međuspremnik Dobrodošli u LSPosed Koristite parazitski upravitelj, koji može stvoriti prečac ili još uvijek otvoriti iz obavijesti. Koristite parazitski upravitelj koji se može otvoriti iz obavijesti. Napravi prečac Nikad ne pokazuj Parazitski Manager Preporučen LSPosed sada podržava parazitizaciju sustava kako bi se izbjeglo otkrivanje, možete otvoriti parazitski upravitelj iz obavijesti. Preporuča se deinstalirati trenutnu aplikaciju. Sačuvaj Opširni zapisi Zapisi Modula Spremanje dnevnika, pričekajte Zapisi spremljeni Neuspješno spremanje:\n%s Očisti zapis sada Zapis je uspješno izbrisan. Pomaknite se na vrh Učitavanje… Pomaknite se do dna Ponovno učitaj Brisanje zapisa nije uspjelo Prijelom riječi Opširni zapis omogućen Opširni zapis onemogućen (nema opisa) Ovaj modul zahtijeva noviju verziju Xposed (%d) i stoga se ne može aktivirati Ovaj modul je dizajniran za noviju verziju Xposed (%d) i stoga neke funkcije možda neće raditi Ovaj modul ne navodi verziju Xposed koja mu je potrebna. Ovaj modul je stvoren za Xposed verziju %1$d, zbog nekompatibilnih promjena u verziji %2$d, modul je onemogućen Ovaj modul nije moguće učitati jer je instaliran na SD kartici, molimo premjestite ga u internu memoriju Deinstaliraj Postavke modula Pogledaj u Repou Želite li deinstalirati ovaj modul? Deinstalirano %1$s Deinstalacija nije uspjela Dodavanje modula korisniku Dodano %1$s korisniku %2$s Dodavanje modula nije uspjelo Instaliraj na korisnika %s Želite li instalirati %1$s korisniku %2$s? Preporuča se ručna instalacija, prisilna instalacija putem LSPoseda može uzrokovati probleme. proširi sklopi Ponovno optimiziraj Optimizacija… Optimizacija dovršena Pokreni ga Optimizacija nije uspjela: povratna vrijednost je prazna Optimizacija nije uspjela: Naziv aplikacije Naziv paketa Vrijeme instalacije Vrijeme ažuriranja Obrnuto Aplikacije sustava Sortiranje Omogući modul Niste odabrali nijednu aplikaciju. Nastaviti? Igre Moduli Lista zabrane Spremanje popisa opsega primjene nije uspjelo Verzija: %1$s Preporučeno Niste odabrali nijednu aplikaciju. Odaberi preporučene aplikacije? Odaberi preporučene aplikacije? Xposed modul još nije aktiviran Preporučeno Dostupno ažuriranje: %1$s Modul %s je onemogućen jer nije odabrana nijedna aplikacija. System Framework Sigurnosna kopija Sigurnosna kopija Vrati sigurnosnu kopiju Prisilno zaustavi Prisilno zaustaviti? Ako prisilno zaustavite aplikaciju, može doći do nepredvidivog ponašanja. Za primjenu ove promjene potrebno je ponovno pokretanje Ponovno podizanje sustava Sakrij %s je na listi zabrane. Možda neće imati efekta. Na popisu zabrane Pogledaj u drugoj aplikaciji Informacije o aplikaciji ¯\\\\_(ツ)_\/¯\nOvdje nema ničega Framework Onemogući opširne zapise Izvješće o problemima zahtijeva uključivanje opširnih zapisa Crna tamna tema Koristi čistu crnu temu ako je tamna tema omogućena Tema Sigurnosno kopiranje i vraćanje Lista sigurnosnih kopija modula i opširne liste. Lista modula vraćenih iz sigurnosne kopije i opsežne liste. Sigurnosna kopija Sigurnosno kopiranje nije uspjelo:\n%s Molimo omogućite DocumentUI Vrati Nije uspjelo vraćanje:\n%s Mreža DNS preko HTTPS-a Zaobilazno rješenje DNS trovanja u nekim zemljama Boja teme Boja teme sustava Prisilite aplikacije da prikazuju ikone pokretača Nakon Androida 10 aplikacijama nije dopušteno skrivanje ikona pokretača. Isključite prekidač da biste onemogućili ovu značajku sustava. Sustav Jezik Suradnici prijevoda Sudjelujte u prevođenju Pomozite nam prevesti %s na vaš jezik Napravite prečac koji može otvoriti parazitski upravitelj Prečac prikvačen 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 Info Početna stranica Izvorni kod Suradnici Imovina Otvori u pretraživaču Prikaži starije verzije Nema više puštanja Neuspješno učitavanje spremišta modula: %s Prvo nadogradivo instalirano %d preuzimanje %d preuzimanja %d preuzimanja Sakura Crvena Ružičasta Ljubičasta Tamno ljubičasta Indigo Plava Svijetlo plava cijan Teal zelena Svijetlo zelena Vapno Žuta boja jantar naranča Tamno narančasta Smeđa Plavo siva
================================================ FILE: app/src/main/res/values-hu/strings.xml ================================================ Áttekintés Modulok %d modul aktiválva %d modulok aktiválva Napló fájlok Beállítások Visszajelzés vagy javaslat Névjegy Hibabejelentés Modulok Minden modul naprakész Közzétéve: %s Frissítve: %s %d modul frissíthető %d modul frissíthető Iratkozz fel a %2$s csatornánkra]]> عبدو المكحل, Balázs Juhász, Krisztián Molnár Telepítés Koppints az LSPosed telepítéséhez Nincs telepítve Az LSPosed nincs telepítve Aktiválva Részben aktiválva Az SEPolicy nem töltött be megfelelően Kérjük, jelezze ezt a Magisk fejlesztőnek.]]> A System Framework injektálása sikertelen Magisk vagy néhány Magisk modul okozott.
Kérlek próbálj meg deaktiválni néhány Magisk modult a Riru és LSPosed modulokon kívül vagy küldj el egy teljes napló fájlt a fejlesztőknek.]]>
Helytelen rendszertulajdonságok A modulok időnként érvénytelenné válhatnak.]]> Frissítésre van szükség Kérjük, telepítse az LSPosed legújabb verzióját API verzió Keretrendszer verzió Menedzser csomag neve Rendszer verzió Eszköz Rendszer ABI Dex Optimizer Wrapper Engedélyezve Nincs engedélyezve Támogatott Nem támogatott Az Android verzió nem megfelelő Összeomlott A csatolás sikertelen Az SELinux engedélyezett Az SELinux házirend helytelen Az LSPosed frissítése Jóváhagyja az LSPosed frissítését? A készülék a frissítés befejezése után újra fog indulni A vágólapra másolva Üdvözöljük az LSPosed Ön használja a parazita menedzser, amely képes létrehozni parancsikont vagy még mindig nyitva értesítésből. Ön a parazita-kezelőt használja, amely az értesítésből megnyitható. Parancsikon létrehozása Soha ne mutassa Parazita menedzser Ajánlott Az LSPosed mostantól támogatja a rendszerparazitizációt a felismerés elkerülése érdekében, a parazita-kezelőt az értesítésből nyithatja meg. Javasoljuk, hogy távolítsa el az aktuális alkalmazást. Mentés Szöveges naplók Modulok Naplói Napló mentése, kérem várjon Mentett naplók Nem sikerült menteni:\n%s Törölje a naplót most A napló sikeresen törlődött. Görgessen a tetejére Betöltés… Görgessen az aljára Újratöltés Nem sikerült törölni a naplót Word Wrap Szöveges napló engedélyezve Szöveges napló letiltva (nincs leírás megadva) Ez a modul egy újabb Xposed verziót igényel (%d), ezért nem aktiválható Ez a modul egy újabb Xposed verzióhoz készült (%d), ezért előfordulhat, hogy egyes funkciók nem működnek Ez a modul nem határoz meg szükséges Xposed verziót. Ez a modul az Xposed %1$dverziójához készült, de a %2$dverzióban bekövetkezett inkompatibilis változások miatt letiltásra került. Ez a modul nem tölthető be, mert az SD-kártyára van telepítve, kérjük, helyezze át a belső tárhelyre. Eltávolítás Modul beállítások Megtekintés a Repóban Szeretné eltávolítani ezt a modult? %1$s eltávolítva Az eltávolítás sikertelen Modul hozzáadása a felhasználóhoz %1$s hozzáadva a(z) %2$s felhasználóhoz A modul hozzáadása sikertelen Telepítés a felhasználóhoz %s Szeretné telepíteni a %1$s -t a %2$sfelhasználóhoz ? Javasoljuk a manuális telepítést, az LSPosed-en keresztül történő kényszerített telepítés problémákat okozhat. kiterjesztés összecsukás Újraoptimalizálás Optimalizálás… Az optimalizálás befejezve Indítsd el Optimalizálás sikertelen: a visszatérési érték üres Az optimalizálás nem sikerült: Alkalmazás neve Csomag neve Telepítés ideje Frissítés ideje Fordított Rendszeralkalmazások Rendezés Modul engedélyezése Nem választott ki egyetlen alkalmazást sem. Folytatja? Játékok Modulok Tiltólista Nem sikerült elmenteni a hatókör listát Verzió: %1$s Ajánlott Nem választott ki egyetlen alkalmazást sem. Kiválasztja az ajánlott alkalmazásokat? Kiválasztja az ajánlott alkalmazásokat? Az Xposed modul még nincs aktiválva Ajánlott Frissítés elérhető: %1$s A %s modul le lett tiltva, mivel nincs kiválasztott alkalmazás. Rendszer keretrendszer Biztonsági mentés Biztonsági mentés Visszaállítás Erőszakos megállás Erőszakos megállás? Ha egy alkalmazást erőltetett leállítással állít le, az rosszul viselkedhet. A módosítás érvényesítéséhez újraindítás szükséges Újraindítás Rejtsd el %s a denyliston van. Lehet, hogy nem lép hatályba. A denylistán Megtekintés más alkalmazásban Alkalmazás információ ¯\\\\_(ツ)_\/¯\nItt nincs semmi Keretrendszer A szöveges naplózás kikapcsolása Jelentési kérdések kérése a verbózus naplók felvételére Fekete sötét téma Teljesen fekete téma használata, ha a sötét téma engedélyezve van Téma Biztonsági mentés és visszaállítás A modul lista és hatókör listák biztonsági mentése. A modullista és a hatókörlisták visszaállítása. Biztonsági mentés A biztonsági mentés sikertelen:\n%s Kérjük, engedélyezze a DocumentUI-t Visszaállítás Nem sikerült visszaállítani:\n%s Hálózat DNS HTTPS-en keresztül Megoldás DNS-mérgezés esetén egyes országokban Téma színe Rendszertéma színe Az alkalmazások kényszerítése az indító ikonok megjelenítésére Az Android 10 után az alkalmazások nem rejthetik el az indítóikonjaikat. Kapcsolja ki a kapcsolót ennek a rendszerfunkciónak a letiltásához. Rendszer Nyelv A fordításban közreműködők Részvétel a fordításban Segítsen nekünk lefordítani az %s -t az Ön nyelvére Hozzon létre egy parancsikont, amely képes megnyitni a parazita menedzser Parancsikon kitüzve 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 Információ Honlap Forráskód Együttműködők Eszközök Megnyitás a böngészőben Régebbi verziók megjelenítése Nincs több kiadás Nem sikerült betölteni a modul repo-t: %s Frissíthetőek először Telepítve %d letöltés %d letöltések Sakura Piros Rózsaszín Lila Mély lila Indigo Kék Világoskék Cián Zöldeskék Zöld Világoszöld Lime Sárga Borostyán Narancs Mély narancssárga Barna Kékes szürke
================================================ FILE: app/src/main/res/values-in/strings.xml ================================================ Ringkasan Modul %d modul diaktifkan Log Pengaturan Umpan balik atau saran Tentang Laporkan masalah Gudang Semua modul sudah terbaru Diterbitkan pada %s Diperbarui pada %s %d modul dapat ditingkatkan Gabung dengan kami di saluran %2$s]]> pɹɐɥllıʇS Pasang Ketuk untuk memasang LSPosed Tidak terpasang LSPosed tidak terpasang Diaktifkan Diaktifkan sebagian SEPolicy tidak dimuat dengan benar Harap laporkan ini ke pengembang Magisk.]]> Injeksi Kerangka Sistem gagal Magisk atau beberapa modul Magisk berkualitas rendah.
Coba nonaktifkan modul Magisk selain Riru dan LSPosed atau kirimkan log lengkap ke pengembang.]]>
Prop sistem salah Modul terkadang tidak valid.]]> Butuh pembaruan Silakan pasang LSPosed versi terbaru Tips untuk pengembang modul Harap nonaktifkan pengoptimalan penerapan di Android Studio, atau gunakan perintah `gradlew installDebug` untuk menginstal. Jika tidak, apk modul tidak akan diperbarui. Versi API Versi framework Nama paket manajer Versi sistem Perangkat Sistem ABI Pengoptimal Pengemas Dex Diaktifkan Tidak diaktifkan Didukung Tidak didukung Versi Android tidak tersedia Rusak Mount gagal SELinux permisif SELinux policy salah Perbarui LSPosed Konfirmasi untuk pembaruan LSPosed? Perangkat ini akan mulai ulang setelah pembaruan selesai Disalin ke papan klip Selamat datang di LSPosed Anda menggunakan manajer parasit, yang dapat membuat pintasan atau dapat terbuka dari notifikasi. Anda menggunakan manajer parasit, yang dapat dibuka dari notifikasi. Buat pintasan Jangan pernah tampilkan Manajer Parasit Direkomendasikan LSPosed sekarang mendukung parasitisasi sistem untuk menghindari deteksi, Anda dapat membuka manajer parasit dari pemberitahuan. Disarankan untuk menghapus aplikasi saat ini. Simpan Log Verbose Log Modul Menyimpan log, harap tunggu Log disimpan Gagal menyimpan:\n%s Hapus log sekarang Log berhasil dihapus. Gulir ke atas Memuat… Gulir ke bawah Muat ulang Gagal menghapus log Bungkus Kata Log verbose diaktifkan Log verbose dinonaktifkan (tidak ada deskripsi yang diberikan) Modul ini memerlukan versi Xposed yang lebih baru (%d) sehingga tidak dapat diaktifkan Modul ini dirancang untuk versi Xposed yang lebih baru (%d) sehingga beberapa fungsi mungkin tidak berfungsi Modul ini tidak menentukan versi Xposed yang diperlukan. Modul ini dibuat untuk Xposed versi %1$d, tetapi karena perubahan yang tidak kompatibel di versi %2$d, modul ini telah dinonaktifkanModul ini dibuat untuk Xposed versi %1$d, tetapi karena perubahan yang tidak kompatibel di versi %2$d, modul ini telah dinonaktifkan Modul ini tidak dapat dimuat karena terpasang di kartu SD, harap pindahkan ke penyimpanan internal Copot Pengaturan modul Lihat di Repo Apakah Anda ingin mencopot modul ini? Tercopot %1$s Copot pemasangan tidak berhasil Tambahkan modul ke pengguna Ditambahkan %1$s ke pengguna %2$s Gagal menambahkan modul Pasang ke pengguna %s Ingin memasang %1$s ke pengguna %2$s? Disarankan untuk memasang secara manual, memaksa pemasangan melalui LSPosed dapat menyebabkan masalah. perluas ciutkan Mengoptimalkan ulang Mengoptimalkan… Optimalisasi selesai Luncurkan Optimalisasi gagal: nilai yang dihasilkan kosong Optimalisasi gagal: Nama aplikasi Nama paket Waktu pemasangan Waktu pembaruan Terbalik Aplikasi sistem Penyortiran Aktifkan modul Anda tidak memilih aplikasi apa pun. Lanjutkan? Permainan Modul Daftar penolakan Gagal menyimpan ke daftar cakupan Versi: %1$s Direkomendasikan Anda tidak memilih aplikasi apapun. Pilih aplikasi yang disarankan? Pilih aplikasi yang disarankan? Modul Xposed belum diaktifkan Direkomendasikan Pembaruan tersedia: %1$s Modul %s telah dinonaktifkan karena tidak ada aplikasi yang dipilih. Kerangka kerja sistem Cadangan Cadangkan Pulihkan Paksa berhenti Paksa berhenti? Jika Anda menghentikan paksa aplikasi, mungkin dapat bekerja tidak semestinya. Mulai ulang diperlukan agar perubahan ini dapat diterapkan Mulai ulang Sembunyikan %s ada dalam daftar penolakan. Ini mungkin tidak berpengaruh. Berada di daftar tolak Lihat di aplikasi lain Informasi aplikasi ¯\\\\_(ツ)_\/¯\nTidak ada apa-apa di sini 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 Cadangkan dan pulihkan Cadangkan daftar modul dan daftar cakupan. Pulihkan daftar modul dan daftar cakupan. Cadangkan Gagal mencadangkan:\n%s Harap aktifkan DocumentUI Pulihkan Gagal memulihkan:\n%s Jaringan DNS melalui HTTPS Solusi mengatasi masalah DNS di beberapa negara Warna tema Warna tema sistem Paksa aplikasi untuk menampilkan ikon peluncur Setelah Android 10, aplikasi tidak diizinkan menyembunyikan ikon peluncurnya. Matikan untuk menonaktifkan fitur sistem ini. Sistem Bahasa Kontributor terjemahan Berpartisipasi dalam terjemahan Bantu kami menerjemahkan %s ke dalam bahasamu Buat pintasan yang dapat membuka manajer parasit Pintasan disematkan 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 Informasi Beranda Kode sumber Kolaborator Aset Buka di browser Tampilkan versi lama Tidak ada rilis Gagal memuat repo modul: %s Dapat diupgrade terlebih dahulu Terpasang %d unduh %d unduhan Sakura Merah Merah muda Ungu Ungu gelap Biru gelap Biru Biru muda Cyan Hijau toska Hijau Hijau muda Hijau limau Kuning Kuning madu Jingga Jingga gelap Coklat Abu-abu kebiruan
================================================ FILE: app/src/main/res/values-it/strings.xml ================================================ Panoramica Moduli %d modulo abilitato %d moduli abilitati Log Impostazioni Feedback o suggerimenti Informazioni Segnala il problema Repository Tutti i moduli sono aggiornati Pubblicato alle %s Aggiornato alle %s %d modulo aggiornabile %d moduli aggiornabili Unisciti al nostro canale %2$s]]> alex193a, Fs00, xDonatello Installa Tocca per installare LSPosed Non installato LSPosed non è installato Attivo Parzialmente attivo SEPolicy non è caricato correttamente Segnalalo allo sviluppatore di Magisk.]]> Injection del framework di sistema fallita Questo problema si verifica raramente e può essere causato da Magisk o da alcuni moduli Magisk di scarsa qualità.
Prova a disabilitare i moduli Magisk tranne Riru e LSPosed o invia il log completo agli sviluppatori.]]>
Proprietà di sistema errate In alcuni casi i moduli potrebbero non funzionare.]]> Aggiornamento richiesto Installa la versione più recente di LSPosed Suggerimenti per lo sviluppatore del modulo Disattivare le ottimizzazioni di distribuzione su Android Studio, o utilizzare il comando `gradlew installDebug` per eseguire l\'installazione. Altrimenti l\'apk del modulo non verrà aggiornato. Versione API Versione del framework Nome pacchetto del manager Versione del sistema Dispositivo ABI del sistema Dex Optimizer Wrapper Abilitato Non abilitato Supportato Non supportato Versione di Android non soddisfatta Arrestato in modo anomalo Mount fallito SELinux è in modalità permissiva La politica di SELinux non è corretta Aggiorna LSPosed Confermi di voler aggiornare LSPosed? Il dispositivo verrà riavviato dopo il completamento dell\'aggiornamento Copiato negli appunti Benvenuto in LSPosed Stai usando il manager parassitario, che può creare scorciatoie o essere aperto dalla notifica. Stai usando il manager parassitario, che può essere aperto dalla notifica. Crea scorciatoia Non mostrare mai Manager parassitario consigliato LSPosed ora supporta la parassitazione del sistema per evitarne il rilevamento, è possibile aprire il manager parassitario dalla notifica. Si consiglia di disinstallare l\'applicazione attuale. Salva Log verbosi Log dei moduli Salvataggio log, attendere Log salvati Salvataggio non riuscito:\n%s Cancella il log ora Log cancellato con successo. Scorri in alto Caricamento in corso… Scorri in basso Ricarica Impossibile cancellare il log A capo automatico Log verboso abilitato Log verboso disabilitato (nessuna descrizione fornita) Questo modulo richiede una versione più recente di Xposed (%d) e quindi non può essere attivato Questo modulo è progettato per una versione più recente di Xposed (%d) e quindi alcune funzionalità potrebbero non funzionare Questo modulo non specifica la versione Xposed necessaria. Questo modulo è stato creato per la versione %1$d di Xposed ma, a causa di modifiche incompatibili nella versione %2$d, è stato disabilitato Questo modulo non può essere caricato perché è installato sulla scheda SD, spostalo nella memoria interna Disinstalla Impostazioni modulo Visualizza nel repository Vuoi disinstallare questo modulo? %1$s disinstallato Disinstallazione non riuscita Aggiungi modulo all\'utente %1$s aggiunto all\'utente %2$s Aggiunta del modulo fallita Installa per l\'utente %s Vuoi installare %1$s per l\'utente %2$s? Si consiglia di farlo manualmente, forzare l\'installazione da LSPosed potrebbe causare problemi. espandi comprimi Ri-ottimizza Ottimizzazione in corso… Ottimizzazione completata Avvia Ottimizzazione non riuscita: il valore restituito è vuoto Ottimizzazione fallita: Nome dell\'applicazione Nome del pacchetto Data di installazione Data di aggiornamento Inverso Applicazioni di sistema Ordina Abilita modulo Non hai selezionato nessuna app. Continuare? Giochi Moduli Lista di blocco Impossibile salvare l\'elenco delle attivazioni Versione: %1$s Seleziona consigliate Non hai selezionato nessuna app. Selezionare le app consigliate? Selezionare le app consigliate? Il modulo Xposed non è ancora attivo Seleziona consigliate Aggiornamento disponibile: %1$s Il modulo %s è stato disabilitato poiché nessuna app è stata selezionata. Framework di sistema Backup Backup Ripristina Forza l\'arresto Forzare l\'arresto? Se forzi l\'interruzione di un\'app, potrebbe non funzionare correttamente. È necessario riavviare per applicare questa modifica Riavvia Nascondi %s è nella lista di blocco. Potrebbe non avere effetto. Nella lista di blocco Mostra in un\'altra app Informazioni app ¯\\\\_(ツ)_\\/¯\nNon c\'è nulla qui 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 Backup e ripristino Backup dell\'elenco dei moduli e delle attivazioni. Ripristino dell\'elenco dei moduli e delle attivazioni. Backup Salvataggio non riuscito:\n%s Abilitare DocumentUI Ripristina Ripristino fallito:\n%s Rete DNS over HTTPS Aggira l\'avvelenamento della cache DNS in alcune nazioni Colore del tema Colore del tema del sistema Forza le app a mostrare le icone nel launcher A partire da Android 10, le app non possono più nascondere le loro icone nel launcher. Disabilita l\'opzione per disattivare questa funzionalità. Sistema Lingua Contributori alla traduzione Partecipa alla traduzione Aiutaci a tradurre %s nella tua lingua Crea una scorciatoia che può aprire il manager parassitario Scorciatoia fissata 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 Informazioni Pagina web Codice sorgente Collaboratori Risorse Apri nel browser Mostra le versioni precedenti Non ci sono altre versioni Impossibile caricare il repository dei moduli: %s Aggiornabili prima Installato %d download %d downloads Sakura Rosso Rosa Viola Viola scuro Indaco Blu Azzurro Ciano Verde acqua Verde Verde chiaro Lime Giallo Ambra Arancione Arancione scuro Marrone Blu grigio
================================================ FILE: app/src/main/res/values-iw/strings.xml ================================================ סקירה כללית מודולים %d מודול מופעל %d מודולים מופעלים %d מודולים מופעלים %d מודולים מופעלים לוגים הגדרות משוב או הצעה אודות דווח על בעיה מאגר מידע כל המודולים מעודכנים פורסם ב-%s עודכן ב-%s %d מודולים ניתנים לשדרוג %d מודולים ניתנים לשדרוג %d מודולים ניתנים לשדרוג %d מודולים ניתנים לשדרוג הצטרף לערוץ %2$s שלנו]]> ריק התקנה הקש כדי להתקין את LSPosed לא מותקן LSPosed אינו מותקן הופעל הופעל חלקית SEPolicy לא נטען כהלכה נא לדווח על כך למפתחי Magisk .]]> הזרקת תשתית המערכת נכשלה Magisk או מודולי Magisk באיכות נמוכה.
אנא נסה להשבית מודולי Magisk שאינם Riru ו-LSPosed או שלח דיווח לוג מלא למפתחים.]]> מאפיין המערכת שגוי המודולים עשויים להתבטל מדי פעם.]]> צריך לעדכן נא להתקין את הגרסה העדכנית ביותר של LSPosed גרסת API גרסת תשתית שם חבילת מנהל גרסת מערכת מכשיר מערכת ABI עטיפה של Dex Optimizer מופעל לא מופעל נתמך לא נתמך גרסת אנדרואיד לא מספקת התרסק ה-mount נכשל מדיניות SELinux הינה מתירנית מדיניות SELinux הינה שגויה עדכן LSPosed אשר לעדכן את LSPosed? מכשיר זה יאתחל לאחר השלמת העדכון הועתק ללוח ברוכים הבאים ל-LSPosed הנך משתמש בגרסת אפליקציית ניהול בתצורת טפיל, שיכולה ליצור קיצור דרך או עדיין להיפתח מהתראה. הנך משתמש בגרסת אפליקציית ניהול בתצורת טפיל, שיכולה להיפתח מהתראה. צור קיצור דרך לעולם אל תראה מומלצת גרסת אפליקציית ניהול בתצורת טפיל LSPosed תומך כעת בתצורת טפיל כדי למנוע זיהוי, ניתן לפתוח את גרסת אפליקציית הניהול בתצורת טפיל מהתראה. מומלץ להסיר את האפליקציה הנוכחית. שמור לוגים מפורטים לוגים של מודולים שומר יומן, אנא המתן לוגים נשמרו השמירה נכשלה:\n%s נקה לוגים עכשיו לוגים נוקו בהצלחה. גלול למעלה טוען… גלול למטה רענן נכשל בניקוי הלוגים עטיפת מילה לוגים מפורטים מופעלים לוגים מפורטים מושבתים (לא מסופק תיאור) מודול זה דורש גרסה חדשה יותר של LSPosed (%d) ולכן לא יכול להיות מופעל מודול זה מיועד לגרסה חדשה יותר של Xposed (%d) ולכן ייתכן שחלק מהפונקציונליות לא תופעל כהלכה מודול זה לא מציין את גסרת ה- LSPosed שהוא צריך. מודול זה נוצר בשביל LSPosed גרסה %1$d, אך בשל חוסר תאימות לאחור בגרסה %2$d, הוא הופסק מודול זה לא יכול להיטען מכיוון שהוא מותקן על כרטיס ה-SD, אנא העבר אותו לאחסון פנימי הסר התקנה הגדרות מודול הצג ברפוסיטורי האם אתה בטוח שאתה רוצב להסיר את התנקת המודול? %1$s הוסר הסרת ההתקנה הושלמה בהצלחה הוסף מודל למשתמש נוסף %1$s למשתמש %2$s הוספת המודל נכשלה התקן למשתמש %s רוצה להתקין %1$s למשתמש %2$s? מומלץ להתקין באופן ידני, כפיית התקנה באמצעות LSPosed עלולה לגרום לבעיות. להרחיב התמוטטות מטב מחדש ממטב… אופטימיזציה הושלמה. הרץ אופטימיזציה נכשלה או שהערך המוחזר הוא ריק. אופטימיזציה נכשלה: מיין על פי שם האפליקציה מיין על פי שם החבילה מיין על פי זמן ההתקנה מיין על פי זמן העדכון להפוך אפליקציות מערכת ממיין הפעל מודול אתה לא בחרת שום אפליקציה. להמשיך? משחקים מודולים רשימת דחיה נכשל לשמור רשימת תחומים גרסה: %1$s מומלץ אתה לא בחרת שום אפליקציה. לבחור אפליקציות מומלצות? בחר אפליקציות מומלצות? מודול LSPosed עדיין לא הופעל מומלץ עדכון זמין: %1$s מודול %s בוטל מכיוון שלא נבחרה שום אפליקציה. מערכת Framework מגבה גיבוי שחזור אלץ עצירה אצץ עצירה? אם אתה תאלץ עצירה לאפליקציה, היא עלולה להתנהג בצורה לא רצויה. הפעלה מחדש דרושה בכדי שהשינויים יכנסו לתוקף הפעל מחדש הסתר %s נמצא ברשימת הדחיה. ייתכן שהוא לא ייכנס לתוקף. ברשימת הדחיה הצג באפליקציה אחרת מידע על האפליקציה ¯\\\\_(ツ)_\/¯\n אין פה כלום Framework בטל verbose logs בקשת דיווח על בעיות לכלול יומנים מילוליים ערכת נושא שחור כהה השתמש בערכת נושא שחור טהור אם ערכת נושא כהה מופעלת ערכת נושא גיבוי ושחזור גיבוי רשימת מודולים ורשימות היקף. שחזר את רשימת המודולים ורשימות ההיקף. גיבוי גיבוי נכשל:\n%s אנא הפעל את DocumentUI שחזור השחזור נכשל::\n%s רשת DNS על פני HTTPS מעקף הרעלת DNS במדינות מסוימות צבע ערכת נושא צבע נושא המערכת כפה על אפליקציות להציג סמלי מפעיל לאחר Android 10, אפליקציות אינן מורשות להסתיר את סמלי המשגר שלהן. בטל את הלחצן הדו-מצבי כדי להפוך תכונת מערכת זו ללא זמינה. מערכת שפה תורמים לתרגום השתתף בתרגום עזור לנו לתרגם את %s לשפה שלך צור קיצור דרך שיכול לפתוח מנהל טפילי קיצור דרך הוצמד מפעיל ברירת המחדל הנוכחי אינו תומך בקיצורי דרך הודעת סטטוס הצג הודעה שיכולה לפתוח מנהל טפילי אין קיצור דרך, לא ניתן להשבית הודעה ערוץ עדכון יציב בטא בניה לילית הגנה מפני קריאות Xposed API חסום קוד מודול שנטען באופן דינמי לשימוש ב-Xposed API, זה עשוי לשבור מודולים מסוימים אך להועיל לאבטחה קרא אותי גרסאות מידע דף בית קוד מקור מפתחים קבצים פתח בדפדפן הראה גרסאות ישנות אין עוד גרסאות נשכל לטעון מודול: %s ניתן לשדרוג תחילה מוּתקָן %d הורדה %d הורדות %d הורדות %d הורדות סאקורה אדום ורוד סגול סגול עמוק אינדיגו כחול כחול בהיר טורקיז ירוק כחלחל ירוק ירוק בהיר לימון צהוב ענבר כתום כתום עמוק חום כחול אפור
================================================ FILE: app/src/main/res/values-ja/strings.xml ================================================ 概要 モジュール %d 個のモジュールが有効です ログ 設定 フィードバックまたは提案 このアプリについて 問題を報告 リポジトリ 全てのモジュールが最新です 公開日時: %s 更新日時: %s %d 個のモジュールが更新可能です チャンネルに参加するにはこちら: %2$s]]> yoshi818, a1678991, りお(koko0628) インストール タップして LSPosed をインストールします インストールされていません LSPosed はインストールされていません 有効化済み 部分的に有効化済み SEPolicy が正しく読み込まれていません これをMagiskの開発者に報告してください。]]> システムフレームワークへのパッチに失敗しました Magiskまたは低品質のMagiskモジュールが原因である可能性があります。
LSPosed以外のMagiskモジュールを一時的に無効化してみるか、完全なログを開発者に送信してください。]]>
システム設定が正しくありません モジュールが無効化される場合があります。]]> 更新が必要です 最新バージョンの LSPosed をインストールして下さい モジュール開発者向けのヒント Android Studio でデプロイの最適化を無効にするか、「gradlew installDebug」コマンドを使用してインストールしてください。この操作を行わないとモジュールの apk は更新できません。 API バージョン フレームワークバージョン マネージャーパッケージ名 システムバージョン デバイス システム ABI Dex 最適化ラッパー 有効 無効 対応 非対応 Android のバージョンが適合しません クラッシュしました マウントに失敗しました SELinux は Permissive です SELinux のポリシーが正しくありません LSPosed を更新 LSPosed を更新してもよろしいですか?更新の完了後、このデバイスは再起動します クリップボードにコピーされました LSPosed へようこそ パラサイトマネージャーを使用しています。これにより、ショートカットを作成したり、通知から開いたりできます。 通知から開くことができるパラサイトマネージャーを使用しています。 ショートカットを作成 再度表示しない パラサイトマネージャーを使用することを推奨します LSPosed は検出を回避するためのシステムパラサイトに対応し、通知からパラサイトマネージャーを開くことができるようになりました。現在のアプリケーションをアンインストールすることをおすすめします。 保存 詳細ログ モジュールログ ログを保存しています。お待ちください ログを保存しました 保存に失敗しました:\n%s ログを消去 ログの消去に成功しました。 ログの先頭行にスクロール 読み込み中… 一番下までスクロール 再読み込み ログを消去できませんでした 単語を折り返す 詳細ログは有効です 詳細ログは無効です (説明はありません) このモジュールは新しいバージョンの Xposed (%d) が必要なので有効化できません このモジュールは新しい Xposed バージョン (%d) 用に設計されているため、一部の機能が動作しない可能性があります このモジュールは必要な Xposed のバージョンを指定していません。 このモジュールは Xposed バージョン %1$d 用に作成されましたが、バージョン %2$dでの互換性のない変更により無効化されました。 このモジュールは SD カードにインストールされているため読み込むことができません。内部ストレージに移動してください。 アンインストール モジュール設定 リポジトリで表示 このモジュールをアンインストールしますか? 「%1$s」をアンインインストールしました アンインストールに成功 ユーザへモジュールを追加 %1$s をユーザー %2$s へ追加 モジュールの追加に失敗しました ユーザー %s へインストール ユーザー %2$s へ %1$s をインストールしますか?LSPosed 経由での強制インストールは問題が発生する場合があるため、手動でインストールすることをおすすめします。 開く 閉じる 再最適化 最適化中… 最適化が完了しました 起動 最適化に失敗しました: 戻り値が空です 最適化に失敗しました: アプリ名 パッケージ名 インストール日時 更新日時 逆順 システムアプリ 並び順 モジュールを有効化 アプリが選択されていません。よろしいですか? ゲーム モジュール Denylist スコープリストの保存に失敗しました バージョン: %1$s 選択 推奨 アプリが選択されていません。おすすめのアプリを選択しますか? おすすめのアプリを選択しますか? すべて なし 自動的に含む Xposed モジュールが有効化されていません 推奨 アップデートが利用可能です: %1$s アプリが選択されていないため、モジュール「%s」は無効になっています。 システムフレームワーク バックアップ バックアップ 復元 強制停止 強制停止しますか? アプリを強制停止すると、アプリが動作しなくなる可能性があります。 この設定を適用するには再起動が必要です 再起動 非表示 %s が DenyList に登録されています。効果がないかもしれません。 DenyList に登録されています 他のアプリで表示 アプリの情報 ¯\\_(ツ)_\/¯\nリストは空です フレームワーク 詳細ログの無効化 問題を報告する際は、詳細なログを含めるようにしてください ログウォッチドッグを有効化 LSPosed のログウォッチドッグはシステムプロパティを変更し、LSPosed を検出するために悪用される可能性があります 黒のダークテーマ ダークテーマが有効になっている場合は、ピュアブラックテーマを使用します テーマ バックアップと復元 モジュールリストとスコープリストをバックアップします モジュールリストとスコープリストを復元します バックアップ バックアップに失敗しました:\n%s DocumentUI を有効にしてください 復元 復元に失敗しました:\n%s ネットワーク DNS over HTTPS 一部の国向けの DNS ポイズニング回避策 テーマカラー システムテーマカラー ランチャーアイコンを強制的に表示 Android 10 以降、アプリはランチャーアイコンを隠すことができなくなりました。このシステム機能を無効にするには、トグルをオフにしてください。 システム 言語 翻訳貢献者 翻訳に貢献 %s の翻訳にご協力ください パラサイトマネージャーを開くことができるショートカットを作成します ショートカットのピン留め 現在のデフォルトランチャーはショートカットのピン留めをサポートしていません ステータス通知 パラサイトマネージャーを開くことができる通知を表示します ショートカットがないため、通知を無効にできません 更新チャンネル 安定版 ベータ版 ナイトリービルド Xposed API の呼び出し保護 動的にロードされたモジュールコードが Xposed API を使用するのをブロックします。これにより一部のモジュールが動作しなくなる可能性がありますが、セキュリティが向上します。 Readme リリース 情報 ホームページ ソースコード 協力者 アセット ブラウザで表示 以前のバージョンを表示 これ以上のリリースはありません モジュールリポジトリの読み込みに失敗しました: %s 更新があるモジュールを先頭に表示 インストール済み %d 件のダウンロード 桜色 ピンク 深紫色 藍色 水色 シアン 青緑 ライトグリーン ライム 黄色 アンバー オレンジ色 ディープオレンジ 茶色 ブルーグレー ================================================ FILE: app/src/main/res/values-ko/strings.xml ================================================ 개요 모듈 모듈 %d개가 활성화됨 로그 설정 피드백 혹은 제안 정보 문제 보고 저장소 모든 모듈이 최신 버전입니다 %s 에 배포됨 %s에 업데이트됨 %d개의 모듈이 업데이트 가능합니다 %2$s 채널에 가입하세요]]> green1052 설치 LSPosed를 설치하려면 누르세요 설치되지 않음 LSPosed가 설치되지 않았습니다 활성화됨 부분적으로 활성화됨 SEPolicy가 제대로 로드되지 않았습니다 오류를 Magisk 개발자에게 신고해주세요]]> System Framework 삽입 실패 Magisk 또는 일부 Magisk 모듈 때문일 수 있습니다.
Riru 및 LSPosed 이외의 Magisk 모듈을 비활성화 하거나 전체 로그를 개발자에게 제출하세요.]]>
시스템 속성이 잘못됨 모듈은 가끔 무효화될 수 있습니다.]]> 업데이트가 필요합니다 최신 버전으로 LSPosed를 설치해 주세요 모듈 개발자를 위한 팁 Android Studio 에서 배포 최적화를 비활성화 하거나, 설치할때 \'gradlew installDebug\' 명령어를 사용해주세요. 그렇지 않으면 모듈 APK 가 업데이트되지 않을 것입니다. API 버전 Framework 버전 관리자 패키지 이름 System 버전 장치 시스템 ABI Dex 옵티마이저 래퍼 사용 사용 안 함 지원 지원되지 않음 안드로이드 버전이 지원되지 않습니다 충돌 마운트 실패 SELinux가 허용됩니다 SELinux 정책이 잘못되었습니다 LSPosed 업데이트 LSPosed를 업데이트 하시겠습니까? 업데이트 완료 후에 재부팅합니다 클립보드에 복사됨 LSPosed에 오신 것을 환영합니다 바로 가기를 만들거나 알림에서 계속 열 수 있는 기생 관리자를 사용하고 있습니다. 알림에서 열 수 있는 내부 관리자를 사용 중 입니다. 바로 가기 만들기 표시 안 함 기생 매니저 추천 LSPosed는 이제 탐지를 피하기 위해 시스템 기생을 지원하며 알림에서 기생 관리자를 열 수 있습니다. 현재 응용 프로그램을 제거하는 것이 좋습니다. 저장 자세한 로그 모듈 로그 로그를 저장중입니다, 기다려주세요 로그가 저장되었습니다 저장 실패:\n%s 로그 지우기 로그를 성공적으로 지웠습니다. 맨 위로 스크롤 로딩 중… 아래로 스크롤 리로드 로그를 지우지 못했습니다. 줄 바꿈 자세한 로그 사용 자세한 로그 비활성화 (설명 없음) 이 모듈에는 최신 LSPosed (%d) 버전이 필요하므로 활성화할 수 없습니다. 이 모듈은 더 새로운 Xposed 버전 (%d) 에서 만들어졌고 따라서 몇몇 기능들이 작동하지 않을 수도 있습니다 이 모듈에서는 필요한 LSPosed 버전을 지정하지 않습니다. 이 모듈은 LSPosed 버전 %1$d에 대해 생성되었지만 버전 %2$d에서 호환되지 않는 변경으로 인해 비활성화되었습니다. 이 모듈은 SD 카드에 설치되어 있으므로 로드할 수 없습니다. 내부 스토리지로 이동하십시오. 제거 모듈 설정 저장소에서 보기 이 모듈을 제거하시겠습니까? %1$s 제거됨 제거 실패 사용자에게 모듈 추가 %2$s 사용자에 %1$s 추가 모듈 추가 실패 %s 사용자에게 설치 %2$s 사용자에게 %1$s을(를) 설치하시겠습니까? 수동으로 설치하는 것이 좋습니다. LSPosed를 통해 강제로 설치하면 문제가 발생할 수 있습니다. 펼치기 접기 다시 최적화 최적화 중… 최적화 완료 실행 최적화에 실패했거나 반환 값이 비어 있습니다. 최적화 실패: 애플리케이션 이름 패키지 이름 설치 시간 업데이트 시간 역순 시스템 앱 정렬 모듈 활성화 앱을 선택하지 않았습니다. 계속하시겠습니까? 게임 모듈 Denylist 범위 목록 저장에 실패했습니다. 버전: %1$s 권장 앱을 선택하지 않았습니다. 권장 앱을 선택하시겠습니까? 권장 앱을 선택하시겠습니까? LSPosed 모듈이 아직 활성화되지 않았습니다. 권장 업데이트 가능: %1$s 앱을 선택하지 않았기 때문에 %s 모듈이 비활성화되었습니다. 시스템 프레임워크 백업 백업 복원 강제 중지 강제 중지? 앱을 강제로 중지하면 잘못된 동작이 발생할 수 있습니다. 이 변경 내용을 적용하려면 재부팅해야 합니다. 재부팅 숨김 %s 은 거부 목록에 있습니다. 적용되지 않을 수 있습니다. 차단 목록에서 다른 앱으로 보기 앱 정보 ¯\\\\_(ツ)_\/¯\n아무것도 없음 프레임워크 상세한 로그 비활성화 자세한 로그를 포함한 오류 신고 요청 블랙 다크 테마 다크 테마가 활성화된 경우 순수 검은색 테마를 사용합니다. 테마 백업 및 복원 모듈 목록과 스코프 목록을 백업합니다. 모듈 목록과 스코프 목록을 복원합니다. 백업 백업 실패:\n%s DocumentUI를 활성화하십시오 복원 복원 실패:\n%s 네트워크 DNS over HTTPS 일부 국가의 DNS 포이즈닝 문제를 해결합니다 테마 색 System 강조 색 앱에서 시작 프로그램 아이콘 표시 Android 10 이후 앱(특히 Xposed 모듈)은 시작 프로그램 아이콘을 숨길 수 없습니다. 이 기능을 비활성화하려면 토글을 끄십시오. 체계 언어 번역 기여자 번역 참여 %s를 귀하의 언어로 번역하는 데 도움을 주세요. 내부 관리자를 열 수 있는 바로가기 생성 바로 가기 고정 현재 기본 런쳐는 핀 바로가기를 지원하지 않습니다 상태 알림 기생 관리자를 열 수 있는 알림 표시 바로 가기 없음, 알림을 비활성화할 수 없음 업데이트 채널 안정 베타 야간 빌드 Xposed API 호출 보호 Xposed API를 사용하기 위해 로드된 모듈 코드를 동적으로 차단하면 일부 모듈이 손상될 수 있지만 보안에 도움이 됩니다. 읽어보기 릴리스 정보 홈페이지 소스 코드 기여자 자산 브라우저에서 열기 이전 버전 표시 더 이상 출시되지 않음 모듈 저장소를 로드하지 못함: %s 먼저 업그레이드 가능 설치됨 %d 다운로드 벚꽃 빨강 분홍 보라 짙은 보라 군청 파랑 밝은 파랑 청록 암청 초록 연두 라임 노랑 호박 주황색 짙은 주황 갈색 청회색
================================================ FILE: app/src/main/res/values-ku/strings.xml ================================================ کورتەی گشتی مۆدیوولەکان %d مۆدیول چالاک کراوە %d مۆدیول چالاک کراوە لۆگەکان ڕێکخستنەکان فیدباک یان پێشنیار دەربارە پرسی ڕاپۆرت کۆگا Hemî modulên nûjen Di %sde hate weşandin Di %sde hate nûve kirin %d module nûvekirin %d modulên nûvekirî bibînin Tevlî %2$s kanala me bibin]]> null Lêkirin Ji bo sazkirina LSPosed bikirtînin Ne hatiye sazkirin LSPosed nayê Sazkirin Çalak kirin Qismî aktîf kirin SEPolicy bi rêkûpêk nayê barkirin Ji kerema xwe vê yekê ji Magisk pêşdebir re ragihînin.]]> Derzkirina Çarçoveya Pergalê têk çû Magisk an hin modulên Magisk-a kêm-kalîteyê ve çêbibe.
Ji kerema xwe hewl bidin ku modulên Magisk ji bilî Riru û LSPosed neçalak bikin an jî têketinek tevahî ji pêşdebiran re bişînin.]]>
Pêşniyara pergalê xelet e Dibe ku modul carinan betal bibin.]]> Pêdivî ye ku nûve bike Ji kerema xwe guhertoya herî dawî ya LSPosed saz bikin Guhertoya API Guhertoya çarçoveyê Navê pakêta rêveberê Guhertoya pergalê Sazî Pergala ABI Dex Optimizer Wrapper Enabled Ne çalak kirin Piştgirî kirin Piştgirî nekirin Guhertoya Android-ê ne razî ye Qeza kirin Çiya têk çû SELinux destûr e Siyaseta SELinux nerast e LSPosed nûve bikin Piştrast bike ku LSPosed nûve bike? Ev cîhaz dê piştî qedandina nûvekirinê ji nû ve dest pê bike Li clipboardê hate kopî kirin Hûn bi xêr hatin LSPosed Hûn rêveberê parazît bikar tînin, ku dikare kurtebirê biafirîne an hîn jî ji ragihandinê vebe. Hûn rêveberê parazît bikar tînin, ku dikare ji ragihandinê vebe. Kurtenivîsê çêbikin Qet nîşan nedin Rêvebirê Parazît Pêşniyar kirin LSPosed naha parazîtkirina pergalê piştgirî dike da ku ji tespîtê dûr bixe, hûn dikarin rêveberê parazît ji ragihandinê vekin. Tê pêşniyar kirin ku serîlêdana heyî jêbirin. Rizgarkirin Têketinên Verbose Têketinên Modulan پاشەکەوتکردنی لۆگ، تکایە چاوەڕوان بن Têketin xilas kirin Sazkirin bi ser neket:\n%s Naha têketinê paqij bike Têketin bi serkeftî hate paqij kirin. Scroll to top Barkirin… Scroll to bottom Ji nû ve barkirin Paqijkirina têketinê bi ser neket Peyv Wrap Têketinê bi lêker vekir Têketinê bi devkî neçalak bû (bê şirove nehat dayîn) Vê modulê guhertoyek nû ya Xposed (%d) hewce dike û ji ber vê yekê nayê çalak kirin Ev modul ji bo guhertoyek nû ya Xposed (%d) hatî çêkirin û ji ber vê yekê dibe ku hin fonksiyon nexebitin Ev modul guhertoya Xposed ya ku jê re hewce dike diyar nake. Ev modul ji bo guhertoya Xposed %1$dhate afirandin, lê ji ber guheztinên nelihev ên di guhertoya %2$d-ê de, ew hate asteng kirin. Ev modul nikare were barkirin ji ber ku ew li ser qerta SD-ê hatî saz kirin, ji kerema xwe wê biguhezînin hilana hundurîn Rakirin Mîhengên Modulê Di Repo de bibînin Ma hûn dixwazin vê modulê rakin? Rakir %1$s Rakirina neserketî ye Modulê li bikarhênerê zêde bikin %1$s ji bikarhêner %2$sre zêde kirin Zêdekirina modulê têk çû Ji bikarhêner %sre saz bike Dixwazin %1$s ji bikarhêner %2$sre saz bikin? Tête pêşniyar kirin ku bi destan saz bikin, zordariya sazkirinê bi riya LSPosed dibe ku bibe sedema pirsgirêkan. firehkirin jiberhevketin Ji nû ve xweşbîn bikin Optimîzekirin… Optimîzasyon qediya Dest pê bike Optimîzasyon têk çû: nirxa vegerê vala ye Optimîzasyon têk çû: Navê serîlêdanê Navê pakêtê Dema sazkirinê Wextê nûve bike Gara paşî Sepanên pergalê Rêzkirin Modulê çalak bike Te tu sepanê hilnebijart. Berdewamkirin? Games Modules Denylist Hilbijartina navnîşa çarçovê bi ser neket Versiyon: %1$s Pêşniyar kirin Te tu sepanê hilnebijart. Serlêdanên pêşniyarkirî hilbijêrin? Serlêdanên pêşniyarkirî hilbijêrin? Modula Xposed hîn nehatiye çalak kirin Pêşniyar kirin Nûvekirin heye: %1$s Modula %s ji ber ku tu sepan nehat hilbijartî hate neçalak kirin. Çarçoveya Sîstemê Backup Backup Nûvdekirin Bi zorê rawestandin Rawestandina zorê? Ger hûn bi zorê sepanek rawestînin, dibe ku ew xelet tevbigere. Ji bo sepandina vê guherînê ji nû ve destpêkirinê hewce ye Reboot Veşartin %s di lîsteya redkirinê de ye. Dibe ku bandor neke. Li ser redkirina Di sepana din de bibînin Agahdariya Appê ¯\\\\_(ツ)_\/¯\nLi vir tiştek tune Çarçove Têketinên devkî neçalak bike Daxwaza pirsgirêkan rapor bikin ku têketinên devkî tê de bin Mijara reş reş Ger mijara tarî çalak be, mijara reş a paqij bikar bînin Mijad Backup û restore Lîsteya modulê paşvekêşîn û navnîşên çarçovê. Lîsteya modul û navnîşên çarçovê vegerînin. Backup Piştgiriya bi ser neket:\n%s Ji kerema xwe DocumentUI çalak bike Nûvdekirin Vegerandin bi ser neket:\n%s Network DNS li ser HTTPS Li hin welatan jehrîkirina DNS-ê çareser bikin Rengê mijarê Rengê mijara pergalê Bi zorê serlêdanan bikin ku îkonên destpêker nîşan bidin Piştî Android 10, serîlêdan destûr nayê dayîn ku îkonên xwe yên destpêker veşêrin. Ji bo neçalakkirina vê taybetmendiya pergalê, guheztinê vekin. Sîstem Ziman Beşdarên wergerê Beşdarî wergerê bibin Alîkariya me bikin ku %s wergerînin zimanê we Kurtenivîsek çêbikin ku dikare rêveberê parazît veke Kurtenivîs pêçayî 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 Info Homepage Koda çavkaniyê Hevkar Tiştan Di gerokê de vekin Guhertoyên kevntir nîşan bide Bêtir berdan Barkirina depoya modulê têk çû: %s Pêşî nûvekirin Saz kirin %d download %d downloads Sakura sor Pembe Mor binefşî ya kûr Indigo Şîn Şînê vebûyî Cyan Teal Kesk Kesk ronî Lime Zer Aqût porteqalî porteqala kûr qehweyî Şîn gewr
================================================ FILE: app/src/main/res/values-lt/strings.xml ================================================ Apžvalga Moduliai %d modulis įjungtas %d įjungti moduliai %d įjungti moduliai %d įjungti moduliai Žurnalai Nustatymai Atsiliepimai arba pasiūlymas About-face Pranešti apie problemą Saugykla Visi moduliai atnaujinti Paskelbta adresu %s Atnaujinta %s %d modulis atnaujinamas %d atnaujinami moduliai %d atnaujinami moduliai %d atnaujinami moduliai Prisijunkite prie mūsų %2$s kanalo]]> Ace Miller, im sorry Įdiekite Bakstelėkite, jei norite įdiegti LSPosed Neįdiegta \"LSPosed\" nėra įdiegta Aktyvuota Iš dalies aktyvuota \"SEPolicy\" nėra tinkamai įkelta Apie tai praneškite Magisk kūrėjui.]]> Nepavyko įšvirkšti sistemos pagrindo "Magisk" arba kai kurių nekokybiškų "Magisk" modulių.
Pabandykite išjungti kitus "Magisk" modulius, išskyrus "Riru" ir "LSPosed", arba pateikite visą žurnalą kūrėjams.]]>
Neteisingas sistemos rekvizitas Moduliai kartais gali būti pripažinti negaliojančiais.]]> Reikia atnaujinti Įdiekite naujausią \"LSPosed\" versiją API versija Pagrindų versija Valdytojo paketo pavadinimas Sistemos versija Įrenginys Sistemos ABI \"Dex Optimizer Wrapper Įjungta Neįjungta Palaikomas Nepalaikomas \"Android\" versija nepatenkinti Sugedo Montavimas nepavyko \"SELinux\" yra leidžiamoji \"SELinux\" politika yra neteisinga Atnaujinti LSPosed Patvirtinkite, kad atnaujintumėte LSPosed? Baigus atnaujinimą šis prietaisas bus perkrautas Nukopijuota į iškarpinę Sveiki atvykę į LSPosed Jūs naudojate parazitinį tvarkytuvą, kuris gali sukurti nuorodą arba vis dar atidaryti iš pranešimo. Naudojate parazitinį tvarkytuvą, kuris gali būti atidarytas iš pranešimo. Sukurti nuorodą Niekada nerodykite Rekomenduojama parazitinė vadybininkė \"LSPosed\" dabar palaiko sistemos parazitavimą, kad išvengtų aptikimo, galite atidaryti parazitų tvarkyklę iš pranešimo. Rekomenduojama pašalinti dabartinę programą. Išsaugoti Žurnalai su verbaline informacija Modulių žurnalai Įrašomas žurnalas, palaukite Išsaugoti žurnalai Nepavyko išsaugoti:\n%s Išvalyti žurnalą dabar Žurnalas sėkmingai išvalytas. Slinkti į viršų Įkrovimas… Slinkite į apačią Perkrauti Nepavyko išvalyti žurnalo Žodžių apvyniojimas Įjungtas verstinis žurnalas Išjungtas verstinis žurnalas (aprašymas nepateiktas) Šis modulis reikalauja naujesnės \"Xposed\" versijos (%d), todėl negali būti aktyvuotas Šis modulis skirtas naujesnei \"Xposed\" versijai (%d), todėl kai kurios funkcijos gali neveikti. Šis modulis nenurodo jam reikalingos \"Xposed\" versijos. Šis modulis buvo sukurtas \"Xposed\" versijai %1$d, tačiau dėl nesuderinamų pakeitimų versijoje %2$d, jis buvo išjungtas. Šio modulio negalima įkelti, nes jis įdiegtas SD kortelėje, perkelkite jį į vidinę saugyklą Pašalinti Modulio nustatymai Peržiūrėti Repo Ar norite pašalinti šį modulį? Išmontuota %1$s Nesėkmingas pašalinimas Pridėti modulį prie naudotojo Pridėta %1$s naudotojui %2$s Nepavyko pridėti modulio Įdiegti naudotojui %s Norite įdiegti %1$s naudotojui %2$s? Rekomenduojama įdiegti rankiniu būdu, priverstinis diegimas per LSPosed gali sukelti problemų. išplėsti žlugimas Iš naujo optimizuoti Optimizavimas… Optimizavimas baigtas Paleiskite jį Optimizavimas nepavyko: grąžinama vertė yra tuščia Optimizavimas nepavyko: Programos pavadinimas Paketo pavadinimas Diegimo laikas Atnaujinimo laikas Atvirkštinis Sistemos programos Rūšiavimas Įjungti modulį Nepasirinkote jokios programos. Tęsti? Žaidimai Moduliai Denylist Nepavyko išsaugoti srities sąrašo Versija: %1$s Rekomenduojama Nepasirinkote jokios programos. Pasirinkti rekomenduojamas programas? Pasirinkite rekomenduojamas programas? Xposed modulis dar nėra aktyvuotas Rekomenduojama Galimas atnaujinimas: %1$s Modulis %s buvo išjungtas, nes nebuvo pasirinkta jokia programa. Sistemos sistema Atsarginė kopija Atsarginė kopija Atkurti Priverstinis sustabdymas Priverstinis sustojimas? Jei priverstinai sustabdysite programą, ji gali elgtis netinkamai. Kad šis pakeitimas būtų pritaikytas, reikia perkrauti kompiuterį Perkraukite Paslėpti %s yra denylist. Jis gali neįsigalioti. Dėl denylist Peržiūrėti kitoje programoje Programėlės informacija \\\\_(ツ)_\/¯\nČia nieko nėra Sistema Išjungti verbalinius žurnalus Ataskaitos klausimų prašymas įtraukti verbalinius žurnalus Juoda tamsi tema Naudokite grynai juodą temą, jei įjungta tamsi tema Tema Atsarginės kopijos kūrimas ir atkūrimas Atsarginės kopijos modulių ir sričių sąrašai. Atkurti modulių ir sričių sąrašus. Atsarginė kopija Nepavyko sukurti atsarginės kopijos:\n%s Įjunkite DocumentUI Atkurti Nepavyko atkurti:\n%s Tinklas DNS per HTTPS Apėjimas DNS apsinuodijimas kai kuriose šalyse Temos spalva Sistemos temos spalva Priversti programas rodyti paleidiklio piktogramas Po \"Android 10\" programėlėms neleidžiama slėpti paleidimo programos piktogramų. Norėdami išjungti šią sistemos funkciją, išjunkite perjungiklį. Sistema Kalba Vertimo paslaugų teikėjai Dalyvaukite vertimo procese Padėkite mums išversti %s į savo kalbą Sukurti nuorodą, kuri gali atidaryti parazitinį tvarkytuvą Prisegtas trumpinys 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 Informacija Pradžia Šaltinio kodas Bendradarbiai Turtas Atidaryti naršyklėje Rodyti senesnes versijas Jokio išleidimo Nepavyko įkelti modulio atramos: %s Pirmiausia atnaujinamas Įdiegta %d atsisiųsti %d Parsisiųsti %d Parsisiųsti %d Parsisiųsti Sakura Raudona Rožinis Violetinė Tamsiai violetinė Indigo Mėlyna Šviesiai mėlyna Cyan Teal Žalioji Šviesiai žalia Lime Geltona Amber Oranžinė Tamsiai oranžinė Ruda Mėlyna pilka
================================================ FILE: app/src/main/res/values-night/colors.xml ================================================ @color/abc_primary_text_material_light @color/abc_primary_text_material_dark #F06292 #E1F5FE ================================================ FILE: app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: app/src/main/res/values-v29/settings.xml ================================================ true ================================================ FILE: app/src/main/res/values-v30/themes.xml ================================================ ================================================ FILE: app/src/main/res/values-v31/colors.xml ================================================ @android:color/system_accent1_0 @android:color/system_accent1_600 ================================================ FILE: app/src/main/res/values-vi/strings.xml ================================================ Tổng quan Mô-đun %d Mô-đun đã bật Nhật ký Cài đặt Phản hồi hoặc góp ý Giới thiệu Báo cáo sự cố Kho Tất cả các modules đã được cập nhật Xuất bản lúc %s Cập nhật lúc %s %d Modules có bản cập nhật Tham gia kênh %2$s của chúng tôi]]> The Primal Pea, Tuyen Nguyen (tuyennn) Cài đặt Ấn vào đây để cài đặt LSPosed Chưa được cài đặt LSPosed chưa được cài đặt Đã được kích hoạt Một phần đã được kích hoạt Chính sách SELinux không được nạp đúng cách Vui lòng báo cáo đến nhà phát triển.]]> Không thể nhúng vào khung hệ thống Magisk hoặc 1 số mô-đun Magisk kém chất lượng.
Hãy thử vô hiệu hóa những mô-đun Magisk đó, thử chạy riêng Riru và LSPosed mà thôi hoặc thông báo nhật ký tới những nhà phát triển.]]>
Thông tin hệ thống không đúng Đôi khi, các mô-đun có thể sẽ mất hiệu lực.]]> Cần được cập nhật Vui lòng cài đặt phiên bản mới nhất của LSPosed Mẹo cho module developer Vui lòng tắt tối ưu hóa triển khai trên Android Studio hoặc sử dụng lệnh `gradlew installDebug` để cài đặt. Nếu không, apk mô-đun sẽ không được cập nhật. Phiên bản API Phiên bản Framework Tên gói quản lý Phiên bản hệ thống Thiết bị Hệ thống ABI Trình tối ưu Dex Wrapper Đã bật Chưa bật Được hỗ trợ Không được hỗ trợ Phiên bản Android không hỗ trợ Bị lỗi Mount thất bại Cho phép SELinux Chính sách SELinux không chính xác Cập nhật LSPosed Xác nhận cập nhật LSPosed? Thiết bị này sẽ khởi động lại sau khi hoàn tất cập nhật Đã sao chép vào bảng nhớ tạm Chào mừng Đang sử dụng trình quản lý phụ thuộc có thể tạo lối tắt hoặc mở từ thông báo. Đang sử dụng trình quản lý phụ thuộc có thể mở từ thông báo. Tạo lối tắt Không hiện nữa Quản lý phụ thuộc khuyến nghị Ứng dụng hiện hỗ trợ phụ thuộc hệ thống để tránh bị phát hiện có thể mở trình quản lý từ thông báo. Nên gỡ cài đặt ứng dụng hiện tại. Lưu lại Nhật ký Chi tiết Nhật ký các Mô-đun Đang lưu nhật ký, vui lòng đợi Nhật ký đã được lưu Lưu thất bại:\n%s Xóa nhật ký Nhật ký đã được xoá. Cuộn lên trên Đang tải… Cuộn xuống dưới Tải lại Xoá nhật kí thất bại Tự động xuống dòng Nhật ký chi tiết đã được kích hoạt Nhật ký chi tiết đã được vô hiệu hoá (chưa có mô tả) Mô-đun này yêu cầu một phiên bản Xposed mới hơn (%d) và đó là lý do không thể được kích hoạt Tiện ích bổ sung này được làm cho phiên bản ứng dụng mới hơn (%d) nên một số chức năng có thể không hoạt động Mô-đun này không chỉ định phiên bản Xposed cần thiết để khởi chạy. Mô-đun này được tạo bởi phiên bản Xposed %1$d, nhưng vì lý do không tương thích với phiên bản %2$d, nên nó đã bị vô hiệu hóa Mô-đun này không được nạp vì nó được cài đặt trên thẻ nhớ SD, vui lòng chuyển nó vào bộ nhớ trong Gỡ cài đặt Cài đặt Mô-đun Xem ở trên Kho Bạn có muốn gỡ cài đặt mô-đun này? Đã gỡ cài đặt %1$s Gỡ cài đặt không thành công Thêm mô-đun tới người dùng Đã thêm %1$s tới người dùng %2$s Thêm mô-đun thất bại Cài đặt tới người dùng %s Muốn cài %1$s tới người dùng %2$s? Khuyến cáo bạn nên cài đặt thủ công, buộc cài đặt qua LSPosed có thể xảy ra vấn đề không mong muốn. mở đóng Tối ưu lại Đang tối ưu… Tối ưu hoàn tất Khởi chạy Tối ưu thất bại: trả về giá trị trống Tôi ưu thất bại: Tên ứng dụng Tên gói ứng dụng Thời gian cài đặt Thời gian cập nhật Đảo ngược Ứng dụng hệ thống Sắp xếp Kích hoạt mô-đun Bạn đã không lựa chọn bất kỳ ứng dụng nào. Tiếp tục chứ? Trò chơi Mô-đun Danh sách từ chối Lưu danh sách phạm vi thất bại Phiên bản: %1$s Được khuyến cáo Bạn đã không lựa chọn bất kỳ ứng dụng nào. Lựa chọn những ứng dụng được khuyến nghị? Lựa chọn những ứng dụng được khuyến cáo? Mô-đun Xposed chưa được kích hoạt Được khuyến cáo Cập nhật khả dụng: %1$s Mô-đun %s đã bị vô hiệu hoá do không có ứng dụng nào được lựa chọn. Framework Hệ thống Sao lưu Sao lưu Phục hồi Buộc dừng Buộc dừng? Nếu bạn buộc dừng một ứng dụng, nó có thể gặp lỗi. Khởi động lại là bắt buộc cho thay đổi này Khởi động lại Ẩn %s đã trong danh sách từ chối. Nó có thể không có hiệu lực. Trong danh sách từ chối Xem trong ứng dụng khác Thông tin ứng dụng ¯\\\\_(ツ)_\/¯\nKhông có gì ở đây cả 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ủ đề Sao lưu và khôi phục Sao lưu danh sách mô-đun và danh sách phạm vi. Khôi phục danh sách mô-đun và danh sách phạm vi. Sao lưu Sao lưu thất bại:\n%s Vui lòng kích hoạt DocumentUI Phục hồi Phục hồi thất bại:\n%s Mạng DNS qua HTTPS Giải pháp khắc phục tình trạng giả mạo DNS ở một số quốc gia Màu chủ đề Màu chủ đề hệ thống Buộc các ứng dụng hiển thị biểu tượng trên trình khởi chạy Sau Android 10, các ứng dụng không được phép ẩn biểu tượng trên trình khởi chạy. Tắt lựa chọn này để vô hiệu tính năng này của hệ thống. Hệ thống Ngôn ngữ Cộng tác viên phiên dịch Tham gia phiên dịch Giúp chúng tôi dịch %s sang ngôn ngữ của bạn Tạo lối tắt để mở trình quản lý phụ thuộc Đã ghim phím tắt 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 Thông tin Trang chủ Mã nguồn Cộng tác viên Tài sản Mở trong trình duyệt Hiển thị các phiên bản cũ hơn Không có phiên bản nào Tải kho lưu trữ mô-đun thất bại: %s Có thể nâng cấp trước Đã được cài đặt %d lượt tải xuống Hoa anh đào Đỏ Hồng Tím Tím đậm Xanh đậm Xanh Xanh sáng Lục lam Mòng két Xanh lá Xanh lá sáng Xanh chanh Vàng Hổ phách Cam Cam đậm Nâu Xanh xám
================================================ FILE: app/src/main/res/values-zh-rCN/strings.xml ================================================ 概览 模块 已启用 %d 个模块 日志 设置 反馈或建议 关于 反馈问题 仓库 所有模块均已最新 发布于 %s 更新于 %s %d 个模块可更新 加入我们的 %2$s 频道
加入我们的 QQ 频道]]>
LSPosed JingMatrix 安装 点击安装 LSPosed 未安装 LSPosed 未安装 已激活 部分激活 SEPolicy 未被正确加载 请将此问题报告给 Magisk 开发者]]> 系统框架注入失败 Magisk 或低质 Magisk 模块导致。
请尝试禁用除 Riru 和 LSPosed 外的其他 Magisk 模块,或向开发者提供完整日志。]]>
系统属性异常 模块可能会随机失效。]]> 需要更新 请安装新版 LSPosed 给模块开发者的提示 请在 Android Studio 上禁用部署优化,或使用 `gradlew installDebug` 命令进行安装,否则无法更新模块。 API 版本 框架版本 管理器包名 系统版本 设备 系统架构 Dex 优化器包装 已启用 未启用 支持 不支持 系统版本不受支持 崩溃 挂载失败 SELinux 处于宽容模式 SELinux 规则异常 更新 LSPosed 确认更新 LSPosed? 设备将会在完成更新后自动重启。 已复制到剪贴板 欢迎使用 LSPosed 你正在使用寄生管理器,可创建快捷方式或继续从通知中打开。 你正在使用寄生管理器,可以从通知打开它。 创建快捷方式 不再显示 推荐使用寄生管理器 LSPosed 现在支持系统寄生以避免检测,你可以从通知中打开寄生管理器。建议卸载当前应用。 保存 详细日志 模块日志 正在保存日志,请稍后 日志已保存 保存失败:\n%s 立即清空日志 成功清空日志。 滚动到顶部 加载中… 滚动到底部 重新加载 日志清空失败 自动换行 详细日志已启用 详细日志已禁用 (未提供介绍) 此模块需要更新的 Xposed 版本(%d),因此无法激活 此模块是为较新的 Xposed 版本(%d)设计的,因此某些功能可能无法使用 该模块未指定所需的 Xposed 版本 由于该模块开发时所基于 Xposed %1$d 版本不再兼容 %2$d 版本中的变更,该模块现已被停用 此模块因被安装在 SD 卡中而导致无法加载,请将其移动到内部存储 卸载 模块设置 在仓库中查看 确认卸载该模块? 已卸载 %1$s 卸载失败 安装模块到用户 已安装 %1$s 到用户 %2$s 安装失败 安装到用户 %s 确认安装 %1$s 到用户 %2$s?推荐手动用系统自带方法安装,通过 LSPosed 强制安装可能会导致未知异常。 展开 收起 重新优化 优化中… 优化完成 启动 优化失败:返回值为空 优化失败: 应用名 包名 安装时间 更新时间 倒序 系统应用 排序 启用模块 未选择任何应用。继续? 游戏 模块 排除列表 作用域列表保存失败 版本:%1$s 选择 勾选推荐 未选择任何应用。选择推荐的应用? 选择推荐的应用? 全部 自动添加 Xposed 模块尚未激活 推荐应用 可用更新:%1$s 由于未选择任何应用,模块 %s 已被禁用。 系统框架 备份 备份 恢复 强行停止 要强行停止吗? 强行停止某个应用可能会使其异常。 重启以应用此更改 重启系统 隐藏 %s 在排除列表内。模块可能会不生效 在排除列表内 在其它应用中查看 应用信息 ¯\\\\_(ツ)_\/¯\n空空如也 框架 禁用详细日志 报告问题要求包含详细日志 启用日志监控 LSPosed 的日志监视修改了系统属性,可以被利用来检测 LSPosed 纯黑主题 当深色主题启用时使用纯黑主题 主题 备份与恢复 备份模块列表与作用域列表 恢复模块列表与作用域列表 备份 备份失败:\n%s 请启用文档应用 恢复 恢复失败:\n%s 网络 安全 DNS(DoH) 解决某些地区的 DNS 污染问题 主题颜色 系统主题色 强制显示桌面图标 在 Android 10 或更高版本,应用不再允许隐藏桌面图标。关闭该选项以关闭该系统功能。 系统 语言 译者 参与翻译 帮助我们把 %s 翻译到你的语言 创建一个能打开寄生管理器的快捷方式 已创建快捷方式 当前默认桌面不支持固定快捷方式 状态通知 显示一个通知以打开寄生管理器 没有快捷方式,无法禁用通知 模块更新通道 稳定版 测试版 每夜版 Xposed API 调用保护 阻止模块动态加载的代码使用 Xposed API,这会使某些模块失效,但有利于安全性 自述文件 版本 信息 主页 源码 协作者 附件 在浏览器中打开 显示较早版本 无更多版本 模块仓库加载失败:%s 可更新优先 已安装 %d 次下载 樱花 红色 粉色 紫色 深紫 靛青 蓝色 浅蓝 青色 青绿 绿色 浅绿 黄绿 黄色 琥珀 橙色 深橙 棕色 灰蓝
================================================ FILE: app/src/main/res/values-zh-rHK/strings.xml ================================================ 概觀 模組 %d 個模組已啟用 記錄 設定 回饋或建議 關於 回報問題 資料庫 所有模組為最新版本 發佈於 %s 更新於 %s %d 個模組可更新 加入我們的 %2$s 頻道]]> DarKnighT0v0 安裝 點選安裝 LSPosed 未安裝 LSPosed 未安裝 已啟用 部分啟用 SEPolicy 未被正確讀取 請將此回報給 Magisk 開發人員。]]> 系統架構插入失敗 Magisk 或低品質 Magisk 模組導致,
請嘗試停用除 Riru 和 LSPosed 外的 Magisk 模組,或向開發人員提供完整記錄。]]>
系統屬性異常 模組可能會隨機失效。]]> 需要更新 請安裝最新版本的 LSPosed 給模組開發人員的提示 請在 Android Studio 上停用部署最佳化,或使用 `gradlew installDebug` 指令進行安裝。否則模組apk將不會更新。 API版本 架構版本 管理器包名 系統版本 裝置版本 系統架構 Dex 最佳化包裝函式 已啟用 未啟用 支援 不支援 系統版本不受支援 崩潰 加載失敗 SELinux 處於寬容模式 SELinux 原則異常 更新 LSPosed 確認更新LSPosed? 此裝置會於更新完成後重啟 已複製到剪貼簿 歡迎使用 LSPosed 您正在使用寄生管理員,您可以建立捷徑或從通知開啟。 您正在使用寄生管理員,它可以從通知中開啟。 建立捷徑 不再顯示 建議使用寄生管理員 LSPosed 現在支援系統寄生以避免偵測,您可以從通知開啟寄生管理員。建議解除安裝目前應用程式。 儲存 詳細記錄 模組記錄 正在保存日誌,請稍候 記錄已儲存 儲存失敗:\n%s 立即清理記錄 記錄清理成功 移至頂端 正在載入… 移至底端 重新載入 記錄清理失敗 自動換行 詳細記錄已啟用 詳細記錄已停用 (未提供描述) 此模組需要更新版本的 Xposed 版本 (%d),因此無法被啟用 此模組專為較新的 Xposed 版本 (%d) 而設計,因此某些功能可能無法運作 該模組未指定需要的 Xposed 版本 此模組適用於 %1$d 版本的 Xposed ,由於版本 %2$d 的變更不相容,因此已經停用此模組 由於此模組被安裝在SD卡中而無法載入,請將其移動到內部儲存空間 解除安裝 模組設定 在存放庫中檢視 您確定要移除此模組嗎? 已移除 %1$s 移除失敗 為用戶安裝模組 已為用戶 %2$s 安裝模組 %1$s 模組安裝失敗 為用戶 %s 安裝 確定要為用戶 %2$s 安裝 %1$s 嗎?建議手動安裝或多開,透過 LSPosed 強制安裝可能會出現問題。 展開 收起 重新最佳化 正在最佳化… 最佳化完成 執行 最佳化失敗或返回值為空 最佳化失敗: 應用程式名稱 套件名稱 安裝時間 更新時間 遞減 系統應用程式 排序 啟用模組 未選擇任何應用程式,是否繼續? 遊戲 模組 Magisk 排除清單 作用範圍清單儲存失敗 版本:%1$s 選擇 推薦應用程式 未選擇任何應用程式,選擇推薦的應用程式? 選擇推薦的應用程式? 全部 自動添加 Xposed 模組尚未啟用 推薦應用程式 可用更新:%1$s 由於未選擇任何應用程式,模組 %s 已被停用。 系統架構 備份 備份 還原 強制停止 確定要強制停止? 如果您強制停止應用程式,可能會導致行為異常。 重啟以套用變更 重啟系統 隱藏 %s 在排除清單中,模組可能無法生效。 在排除清單內 在其他應用程式中檢視 應用程式資訊 ¯\\\\_(ツ)_\/¯\n這裡甚麼都沒有 框架 禁用詳細紀錄檔 回報問題要求包含詳細記錄檔 啟用日誌監控 LSPosed 的日誌監控會修改系統屬性,可能被利用來偵測 LSPosed 使用純黑深色主題 使用純黑色背景當深色模式已啟用 主題 備份與還原 備份模組清單和作用範圍清單 還原模組清單和作用範圍清單 備份 備份失敗:\n%s 請啟用 DocumentUI 還原 還原失敗:\n%s 網絡 安全 DNS(DoH) 解決部分地區的 DNS 中毒問題 主題色彩 系統主題色彩 強制應用程式在啟動器中顯示圖示 Android 10之後不允許隱藏桌面圖示。關閉開關以禁用此功能 系統 語言 譯者 參與翻譯 幫助我們翻譯 %s 到您的語言 建立可以開啟寄生管理員的捷徑 捷徑已釘選 目前的預設啟動器不支援釘選捷徑 狀態通知 顯示通知以便開啟寄生管理員 沒有捷徑,無法停用通知 更新頻道 穩定版 測試版 每夜構建 Xposed API 呼叫保護 封鎖動態載入的模組代碼以使用 Xposed API,這可能會損毀某些模組但有利於安全性 自述文件 版本 資訊 首頁 原始程式碼 合作者 附件 在瀏覽器中打開 顯示較舊版本 沒有更舊版本 模組存放庫載入失敗:%s 可更新優先 已安裝 %d 次下載 櫻花 紅色 粉色 紫色 深紫 靛青 藍色 淺藍 青色 青綠 綠色 淺綠 萊姆綠 黃色 琥珀 橙色 深橙 棕色 藍灰
================================================ FILE: app/src/main/res/values-zh-rTW/strings.xml ================================================ 概觀 模組 %d 個模組已啟用 日誌 設定 回饋或建議 關於 回報問題 倉庫 所有模組為最新版本 發佈於 %s 更新於 %s %d 個模組可更新 加入我們的 %2$s 頻道]]> 孟武. 尼德霍格. 龍、david082321、beigua87 安裝 點選安裝 LSPosed 未安裝 LSPosed 未安裝 已啟用 部分啟用 SEPolicy 未被正確讀取 請將此回報給 Magisk 開發人員。]]> 系統框架注入失敗 Magisk 或低品質 Magisk 模組導致,
請嘗試停用除 Riru 和 LSPosed 外的 Magisk 模組,或向開發者提供完整日誌。]]>
系統屬性異常 模組可能會隨機失效。]]> 需要更新 請安裝最新版本的 LSPosed 給模組開發人員的提示 請在 Android Studio 上停用部署最佳化,或使用 `gradlew installDebug` 指令進行安裝。否則模組apk將不會更新。 API 版本 框架版本 管理器包名 系統版本 裝置版本 系統架構 Dex 優化器包裝器 已啟用 未啟用 支援 不支援 系統版本不受支援 當機 掛載失敗 SELinux 處於寬容模式 SELinux 規則異常 更新 LSPosed 確定要更新LSPosed嗎?更新完成後將會重啟裝置 已複製到剪貼簿 歡迎使用 LSPosed 您正在使用寄生管理員,您可以建立捷徑或從通知開啟。 您正在使用寄生管理員,它可以從通知中開啟。 建立捷徑 不再顯示 建議使用寄生管理員 LSPosed 現在支援系統寄生以避免偵測,您可以從通知開啟寄生管理員。建議解除安裝目前應用程式。 儲存 詳細日誌 模組日誌 正在保存日誌,請稍候 日誌已儲存 儲存失敗:\n%s 立即清理日誌 日誌清理成功 移至頂端 正在載入…… 移至底端 重新載入 日誌清理失敗 自動換行 詳細日誌已啟用 詳細日誌已禁用 (未提供介紹) 該模組需要更新版本的 Xposed(%d),因此無法被啟用 此模組專為較新的 Xposed 版本 (%d) 而設計,因此某些功能可能無法運作 該模組未指定需要的 Xposed 版本 此模組適用於 %1$d 版本的 Xposed ,由於版本 %2$d 的變更不相容,因此已經停用此模組 由於此模組被安裝在SD卡中而無法載入,請將其移動到內部儲存空間 解除安裝 模組設定 在倉庫中檢視 您確定要移除此模組嗎? 已移除 %1$s 移除失敗 為使用者安裝模組 已為使用者 %2$s 安裝模組 %1$s 模組安裝失敗 為使用者 %s 安裝 確定要為使用者 %2$s 安裝 %1$s 嗎?建議手動安裝或多開,透過 LSPosed 強制安裝可能會出現問題。 展開 收起 重新最佳化 正在最佳化…… 最佳化完成 執行 最佳化失敗或返回值為空 最佳化失敗: 程式名稱 套件名稱 安裝時間 更新時間 遞減 系統程式 排序 啟用模組 未選擇任何程式,是否繼續? 遊戲 模組 Magisk 排除列表 作用域列表儲存失敗 版本:%1$s 推薦程式 未選擇任何程式,選擇推薦的程式? 選擇推薦的程式? Xposed 模組尚未啟用 推薦啟用 可用更新:%1$s 由於未選擇任何程式,模組 %s 已被停用。 系統框架 備份 備份 還原 強制停止 確定要強制停止? 如果您強制停止應用程式,可能導致行為異常。 需要重新啟動以套用此變更 重啟系統 隱藏 %s 在排除列表中,模組可能無法生效。 在排除列表內 在其他應用程式中檢視 程式資訊 ¯\_(ツ)_/¯\n空空如也 框架 停用詳細日誌 回報問題要求包含詳細日誌 啟用日誌看門狗 LSPosed 的日誌看門狗會修改系統屬性,可被利用來偵測 LSPosed 黑色主題 當深色主題啟用時使用純黑色主題 主題 備份與還原 備份模組列表和作用域清單 還原模組列表和作用域清單 備份 備份失敗:\n%s 請啟用 DocumentUI 還原 還原失敗:\n%s 網路 安全 DNS(DoH) 解決部分地區的 DNS 中毒問題 主題強調色 系統主題顏色 強制應用程式在啟動器中顯示圖示 在 Android 10 之後,應用(特別是 Xposed 模組)不被允許隱藏啟動器圖示。關閉本選項以停用此功能。 系統 語言 譯者 參與翻譯 幫助我們翻譯 %s 到您的語言 建立可以開啟寄生管理員的捷徑 捷徑已釘選 目前的預設啟動器不支援釘選捷徑 狀態通知 顯示通知以便開啟寄生管理員 沒有捷徑,無法停用通知 更新通道 穩定版 測試版 每夜構建 Xposed API 呼叫保護 封鎖動態載入的模組代碼以使用 Xposed API,這可能會損毀某些模組但有益於安全性 自述檔案 版本 資訊 首頁 原始碼 合作者 附件 在瀏覽器中開啟 顯示較舊的版本 沒有更舊的版本 模組倉庫載入失敗:%s 可更新的優先 已安裝 %d 次下載 櫻花 紅色 粉色 紫色 深紫 靛青 藍色 淺藍 青色 青綠 綠色 淺綠 萊姆綠 黃色 琥珀 橙色 深橙 棕色 藍灰
================================================ FILE: app/src/main/res/xml/prefs.xml ================================================ ================================================ FILE: app/src/main/res/xml/shortcuts.xml ================================================ ================================================ FILE: build.gradle.kts ================================================ import com.android.build.api.dsl.ApplicationDefaultConfig import com.android.build.api.dsl.CommonExtension import com.android.build.gradle.api.AndroidBasePlugin import com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask import java.io.ByteArrayOutputStream import javax.inject.Inject import org.gradle.api.provider.ValueSource import org.gradle.api.provider.ValueSourceParameters import org.gradle.process.ExecOperations plugins { alias(libs.plugins.agp.lib) apply false alias(libs.plugins.agp.app) apply false alias(libs.plugins.kotlin) apply false alias(libs.plugins.ktfmt) } /** A ValueSource that executes 'git rev-list --count' to get the total commit count. */ abstract class GitCommitCountValueSource : ValueSource { @get:Inject abstract val execOperations: ExecOperations override fun obtain(): String { val output = ByteArrayOutputStream() val result = execOperations.exec { commandLine("git", "rev-list", "--count", "refs/remotes/origin/master") standardOutput = output isIgnoreExitValue = true } // Return the count if successful, otherwise a default of "1". return if (result.exitValue == 0 && output.toString().isNotBlank()) { output.toString().trim() } else { "1" } } } /** A ValueSource that executes 'git tag' to get the latest version tag. */ abstract class GitLatestTagValueSource : ValueSource { @get:Inject abstract val execOperations: ExecOperations override fun obtain(): String { val output = ByteArrayOutputStream() val result = execOperations.exec { commandLine("git", "tag", "--list", "--sort=-v:refname") standardOutput = output isIgnoreExitValue = true } // If successful, parse the first line. Provide a default if no tags are found. return if (result.exitValue == 0 && output.toString().isNotBlank()) { output.toString().lineSequence().first().removePrefix("v") } else { "1.0" } } } // This defers the execution of the git commands and allows Gradle to cache the results. val versionCodeProvider by extra(providers.of(GitCommitCountValueSource::class.java) {}) val versionNameProvider by extra(providers.of(GitLatestTagValueSource::class.java) {}) val injectedPackageName by extra("com.android.shell") val injectedPackageUid by extra(2000) val defaultManagerPackageName by extra("org.lsposed.manager") val androidTargetSdkVersion by extra(36) val androidMinSdkVersion by extra(27) val androidBuildToolsVersion by extra("36.0.0") val androidCompileSdkVersion by extra(36) val androidCompileNdkVersion by extra("29.0.13113456") val androidSourceCompatibility by extra(JavaVersion.VERSION_21) val androidTargetCompatibility by extra(JavaVersion.VERSION_21) subprojects { plugins.withType(AndroidBasePlugin::class.java) { extensions.configure(CommonExtension::class.java) { compileSdk = androidCompileSdkVersion ndkVersion = androidCompileNdkVersion buildToolsVersion = androidBuildToolsVersion buildFeatures { buildConfig = true } externalNativeBuild { cmake { version = "3.29.8+" buildStagingDirectory = layout.buildDirectory.get().asFile } } defaultConfig { minSdk = androidMinSdkVersion ndk { abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")) } if (this is ApplicationDefaultConfig) { targetSdk = androidTargetSdkVersion versionCode = versionCodeProvider.get().toInt() versionName = versionNameProvider.get() } val flags = listOf( "-DVERSION_CODE=${versionCodeProvider.get()}", "-DVERSION_NAME='\"${versionNameProvider.get()}\"'", ) val args = listOf( "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", "-DVECTOR_ROOT=${rootDir.absolutePath}", ) externalNativeBuild { cmake { cFlags.addAll(flags) cppFlags.addAll(flags) arguments.addAll(args) } } } buildTypes { getByName("release") { externalNativeBuild { cmake { arguments.add( "-DDEBUG_SYMBOLS_PATH=${ layout.buildDirectory.dir("symbols").get().asFile.absolutePath }" ) } } } } lint { abortOnError = true checkReleaseBuilds = false } compileOptions { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility } } } plugins.withType(JavaPlugin::class.java) { extensions.configure(JavaPluginExtension::class.java) { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility } } } tasks.register("format") { source = project.fileTree(rootDir) include( "*.gradle.kts", "*/build.gradle.kts", "hiddenapi/*/build.gradle.kts", "services/*-service/build.gradle.kts", ) dependsOn(":xposed:ktfmtFormat") dependsOn(":zygisk:ktfmtFormat") } ktfmt { kotlinLangStyle() } ================================================ FILE: core/.gitignore ================================================ /build /.cxx /src/main/jni/src/config.cpp ================================================ FILE: core/build.gradle.kts ================================================ val versionCodeProvider: Provider by rootProject.extra val versionNameProvider: Provider by rootProject.extra plugins { alias(libs.plugins.agp.lib) } android { namespace = "org.lsposed.lspd.core" androidResources { enable = false } defaultConfig { consumerProguardFiles("proguard-rules.pro") buildConfigField("String", "FRAMEWORK_NAME", """"${rootProject.name}"""") buildConfigField("String", "VERSION_NAME", """"${versionCodeProvider.get()}"""") buildConfigField("long", "VERSION_CODE", versionCodeProvider.get()) } buildTypes { release { isMinifyEnabled = true proguardFiles("proguard-rules.pro") } } } dependencies { api(projects.xposed) implementation(projects.external.apache) implementation(projects.external.axml) implementation(projects.hiddenapi.bridge) implementation(projects.services.daemonService) implementation(projects.services.managerService) compileOnly(libs.androidx.annotation) compileOnly(projects.hiddenapi.stubs) } ================================================ FILE: core/proguard-rules.pro ================================================ -keep class android.** { *; } -keep class de.robv.android.xposed.** {*;} -keep class io.github.libxposed.** {*;} -keep class org.lsposed.lspd.core.* {*;} -keep class org.lsposed.lspd.hooker.HandleSystemServerProcessHooker {*;} -keep class org.lsposed.lspd.hooker.HandleSystemServerProcessHooker$Callback {*;} -keep class org.lsposed.lspd.impl.LSPosedBridge$NativeHooker {*;} -keep class org.lsposed.lspd.impl.LSPosedBridge$HookerCallback {*;} -keep class org.lsposed.lspd.util.Hookers {*;} -keepnames class org.lsposed.lspd.impl.LSPosedHelper { public ; } -keepattributes RuntimeVisibleAnnotations -keepclasseswithmembers,includedescriptorclasses class * { native ; } -keepclassmembers class org.lsposed.lspd.impl.LSPosedContext { public ; } -keepclassmembers class org.lsposed.lspd.impl.LSPosedHookCallback { public ; } -keepclassmembers,allowoptimization class ** implements io.github.libxposed.api.XposedInterface$Hooker { public static *** before(); public static *** before(io.github.libxposed.api.XposedInterface$BeforeHookCallback); public static void after(); public static void after(io.github.libxposed.api.XposedInterface$AfterHookCallback); public static void after(io.github.libxposed.api.XposedInterface$AfterHookCallback, ***); } -assumenosideeffects class android.util.Log { public static *** v(...); public static *** d(...); } -repackageclasses -allowaccessmodification ================================================ FILE: core/src/main/java/android/app/AndroidAppHelper.java ================================================ /* * 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 */ package android.app; import static de.robv.android.xposed.XposedHelpers.findClass; import static de.robv.android.xposed.XposedHelpers.findFieldIfExists; import static de.robv.android.xposed.XposedHelpers.findMethodExactIfExists; import static de.robv.android.xposed.XposedHelpers.getObjectField; import static de.robv.android.xposed.XposedHelpers.newInstance; import static de.robv.android.xposed.XposedHelpers.setFloatField; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.view.Display; import java.lang.ref.WeakReference; import java.util.Map; import de.robv.android.xposed.XSharedPreferences; import de.robv.android.xposed.XposedBridge; /** * Contains various methods for information about the current app. * *

For historical reasons, this class is in the {@code android.app} package. It can't be moved * without breaking compatibility with existing modules. */ public final class AndroidAppHelper { private AndroidAppHelper() {} private static final Class CLASS_RESOURCES_KEY; private static final boolean HAS_IS_THEMEABLE; private static final boolean HAS_THEME_CONFIG_PARAMETER; static { CLASS_RESOURCES_KEY = findClass("android.content.res.ResourcesKey", null); HAS_IS_THEMEABLE = findFieldIfExists(CLASS_RESOURCES_KEY, "mIsThemeable") != null; HAS_THEME_CONFIG_PARAMETER = HAS_IS_THEMEABLE && findMethodExactIfExists("android.app.ResourcesManager", null, "getThemeConfig") != null; } @SuppressWarnings({ "unchecked", "rawtypes" }) private static Map getResourcesMap(ActivityThread activityThread) { Object resourcesManager = getObjectField(activityThread, "mResourcesManager"); return (Map) getObjectField(resourcesManager, "mResourceImpls"); } /* For SDK 24+ */ private static Object createResourcesKey(String resDir, String[] splitResDirs, String[] overlayDirs, String[] libDirs, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo) { try { return newInstance(CLASS_RESOURCES_KEY, resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfiguration, compatInfo); } catch (Throwable t) { XposedBridge.log(t); return null; } } /** @hide */ public static void addActiveResource(String resDir, float scale, boolean isThemeable, Resources resources) { addActiveResource(resDir, resources); } /** @hide */ public static void addActiveResource(String resDir, Resources resources) { ActivityThread thread = ActivityThread.currentActivityThread(); if (thread == null) { return; } Object resourcesKey; CompatibilityInfo compatInfo = (CompatibilityInfo) newInstance(CompatibilityInfo.class); setFloatField(compatInfo, "applicationScale", resources.hashCode()); resourcesKey = createResourcesKey(resDir, null, null, null, Display.DEFAULT_DISPLAY, null, compatInfo); if (resourcesKey != null) { Object resImpl = getObjectField(resources, "mResourcesImpl"); getResourcesMap(thread).put(resourcesKey, new WeakReference<>(resImpl)); } } /** * Returns the name of the current process. It's usually the same as the main package name. */ public static String currentProcessName() { String processName = ActivityThread.currentPackageName(); if (processName == null) return "android"; return processName; } /** * Returns information about the main application in the current process. * *

In a few cases, multiple apps might run in the same process, e.g. the SystemUI and the * Keyguard which both have {@code android:process="com.android.systemui"} set in their * manifest. In those cases, the first application that was initialized will be returned. */ public static ApplicationInfo currentApplicationInfo() { ActivityThread am = ActivityThread.currentActivityThread(); if (am == null) return null; Object boundApplication = getObjectField(am, "mBoundApplication"); if (boundApplication == null) return null; return (ApplicationInfo) getObjectField(boundApplication, "appInfo"); } /** * Returns the Android package name of the main application in the current process. * *

In a few cases, multiple apps might run in the same process, e.g. the SystemUI and the * Keyguard which both have {@code android:process="com.android.systemui"} set in their * manifest. In those cases, the first application that was initialized will be returned. */ public static String currentPackageName() { ApplicationInfo ai = currentApplicationInfo(); return (ai != null) ? ai.packageName : "android"; } /** * Returns the main {@link android.app.Application} object in the current process. * *

In a few cases, multiple apps might run in the same process, e.g. the SystemUI and the * Keyguard which both have {@code android:process="com.android.systemui"} set in their * manifest. In those cases, the first application that was initialized will be returned. */ public static Application currentApplication() { return ActivityThread.currentApplication(); } /** @deprecated Use {@link XSharedPreferences} instead. */ @SuppressWarnings("UnusedParameters") @Deprecated public static SharedPreferences getSharedPreferencesForPackage(String packageName, String prefFileName, int mode) { return new XSharedPreferences(packageName, prefFileName); } /** @deprecated Use {@link XSharedPreferences} instead. */ @Deprecated public static SharedPreferences getDefaultSharedPreferencesForPackage(String packageName) { return new XSharedPreferences(packageName); } /** @deprecated Use {@link XSharedPreferences#reload} instead. */ @Deprecated public static void reloadSharedPreferencesIfNeeded(SharedPreferences pref) { if (pref instanceof XSharedPreferences) { ((XSharedPreferences) pref).reload(); } } } ================================================ FILE: core/src/main/java/android/content/res/XModuleResources.java ================================================ /* * 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 */ package android.content.res; import android.app.AndroidAppHelper; import android.util.DisplayMetrics; import de.robv.android.xposed.IXposedHookInitPackageResources; import de.robv.android.xposed.IXposedHookZygoteInit; import de.robv.android.xposed.IXposedHookZygoteInit.StartupParam; import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResourcesParam; import hidden.HiddenApiBridge; /** * Provides access to resources from a certain path (usually the module's own path). */ public class XModuleResources extends Resources { private XModuleResources(AssetManager assets, DisplayMetrics metrics, Configuration config) { super(assets, metrics, config); } /** * Creates a new instance. * *

This is usually called with {@link StartupParam#modulePath} from * {@link IXposedHookZygoteInit#initZygote} and {@link InitPackageResourcesParam#res} from * {@link IXposedHookInitPackageResources#handleInitPackageResources} (or {@code null} for * system-wide replacements). * * @param path The path to the APK from which the resources should be loaded. * @param origRes The resources object from which settings like the display metrics and the * configuration should be copied. May be {@code null}. */ public static XModuleResources createInstance(String path, XResources origRes) { if (path == null) throw new IllegalArgumentException("path must not be null"); AssetManager assets = new AssetManager(); HiddenApiBridge.AssetManager_addAssetPath(assets, path); XModuleResources res; if (origRes != null) res = new XModuleResources(assets, origRes.getDisplayMetrics(), origRes.getConfiguration()); else res = new XModuleResources(assets, null, null); AndroidAppHelper.addActiveResource(path, res); return res; } /** * Creates an {@link XResForwarder} instance that forwards requests to {@code id} in this resource. */ public XResForwarder fwd(int id) { return new XResForwarder(this, id); } } ================================================ FILE: core/src/main/java/android/content/res/XResForwarder.java ================================================ /* * 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 */ package android.content.res; /** * Instances of this class can be used for {@link XResources#setReplacement(String, String, String, Object)} * and its variants. They forward the resource request to a different {@link android.content.res.Resources} * instance with a possibly different ID. * *

Usually, instances aren't created directly but via {@link XModuleResources#fwd}. */ public class XResForwarder { private final Resources res; private final int id; /** * Creates a new instance. * * @param res The target {@link android.content.res.Resources} instance to forward requests to. * @param id The target resource ID. */ public XResForwarder(Resources res, int id) { this.res = res; this.id = id; } /** Returns the target {@link android.content.res.Resources} instance. */ public Resources getResources() { return res; } /** Returns the target resource ID. */ public int getId() { return id; } } ================================================ FILE: core/src/main/java/android/content/res/XResources.java ================================================ /* * 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 */ package android.content.res; import static org.matrix.vector.nativebridge.ResourcesHook.rewriteXmlReferencesNative; import static de.robv.android.xposed.XposedHelpers.decrementMethodDepth; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; import static de.robv.android.xposed.XposedHelpers.getBooleanField; import static de.robv.android.xposed.XposedHelpers.getLongField; import static de.robv.android.xposed.XposedHelpers.getObjectField; import static de.robv.android.xposed.XposedHelpers.incrementMethodDepth; import android.content.Context; import android.content.pm.PackageParser; import android.content.pm.PackageParser.PackageParserException; import android.graphics.Color; import android.graphics.Movie; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.StrictMode; import android.text.Html; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.SparseArray; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import org.xmlpull.v1.XmlPullParser; import java.io.File; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.WeakHashMap; import de.robv.android.xposed.IXposedHookZygoteInit; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XC_MethodHook.MethodHookParam; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedBridge.CopyOnWriteSortedSet; import de.robv.android.xposed.XposedInit; import de.robv.android.xposed.callbacks.XC_LayoutInflated; import de.robv.android.xposed.callbacks.XC_LayoutInflated.LayoutInflatedParam; import de.robv.android.xposed.callbacks.XCallback; import xposed.dummy.XResourcesSuperClass; import xposed.dummy.XTypedArraySuperClass; /** * {@link android.content.res.Resources} subclass that allows replacing individual resources. * *

Xposed replaces the standard resources with this class, which overrides the methods used for * retrieving individual resources and adds possibilities to replace them. These replacements can * be set using the methods made available via the API methods in this class. */ @SuppressWarnings("JniMissingFunction") public class XResources extends XResourcesSuperClass { private static final SparseArray> sReplacements = new SparseArray<>(); private static final SparseArray> sResourceNames = new SparseArray<>(); // A resource ID is a 32 bit number of the form: PPTTNNNN. PP is the package the resource is for; // TT is the type of the resource; // NNNN is the name of the resource in that type. // For applications resources, PP is always 0x7f. private static final byte[] sSystemReplacementsCache = new byte[256]; // bitmask: 0x000700ff => 2048 bit => 256 bytes private byte[] mReplacementsCache; // bitmask: 0x0007007f => 1024 bit => 128 bytes private static final HashMap sReplacementsCacheMap = new HashMap<>(); private static final SparseArray sColorStateListCache = new SparseArray<>(0); private static final SparseArray>> sLayoutCallbacks = new SparseArray<>(); private static final WeakHashMap sXmlInstanceDetails = new WeakHashMap<>(); private static final String EXTRA_XML_INSTANCE_DETAILS = "xmlInstanceDetails"; private static final ThreadLocal> sIncludedLayouts = ThreadLocal.withInitial(() -> new LinkedList<>()); private static final HashMap sResDirLastModified = new HashMap<>(); private static final HashMap sResDirPackageNames = new HashMap<>(); private static ThreadLocal sLatestResKey = null; private String mResDir; private String mPackageName; public XResources(ClassLoader classLoader, String resDir) { super(classLoader); this.mResDir = resDir; this.mPackageName = getPackageName(resDir); if (resDir != null) { synchronized (sReplacementsCacheMap) { mReplacementsCache = sReplacementsCacheMap.computeIfAbsent(resDir, k -> new byte[128]); } } } /** Dummy, will never be called (objects are transferred to this class only). */ // private XResources() { // throw new UnsupportedOperationException(); // } /** @hide */ public boolean isFirstLoad() { synchronized (sReplacements) { if (mResDir == null) return false; final StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads(); Long lastModification = new File(mResDir).lastModified(); Long oldModified = sResDirLastModified.get(mResDir); StrictMode.setThreadPolicy(policy); if (lastModification.equals(oldModified)) return false; sResDirLastModified.put(mResDir, lastModification); if (oldModified == null) return true; // file was changed meanwhile => remove old replacements for (int i = 0; i < sReplacements.size(); i++) { sReplacements.valueAt(i).remove(mResDir); } Arrays.fill(mReplacementsCache, (byte) 0); return true; } } /** @hide */ public static void setPackageNameForResDir(String packageName, String resDir) { synchronized (sResDirPackageNames) { sResDirPackageNames.put(resDir, packageName); } } /** * Returns the name of the package that these resources belong to, or "android" for system resources. */ @NonNull public String getPackageName() { return mPackageName; } private static String getPackageName(String resDir) { if (resDir == null) return "android"; String packageName; synchronized (sResDirPackageNames) { packageName = sResDirPackageNames.get(resDir); } if (packageName != null) return packageName; PackageParser.PackageLite pkgInfo; try { pkgInfo = PackageParser.parsePackageLite(new File(resDir), 0); } catch (PackageParserException e) { throw new IllegalStateException("Could not determine package name for " + resDir, e); } if (pkgInfo != null && pkgInfo.packageName != null) { // Log.w(XposedBridge.TAG, "Package name for " + resDir + " had to be retrieved via parser"); packageName = pkgInfo.packageName; setPackageNameForResDir(packageName, resDir); return packageName; } throw new IllegalStateException("Could not determine package name for " + resDir); } /** * Special case of {@link #getPackageName} during object creation. * *

For a short moment during/after the creation of a new {@link android.content.res Resources} * object, it isn't an instance of {@link XResources} yet. For any hooks that need information * about the just created object during this particular stage, this method will return the * package name. * *

If you call this method outside of {@code getTopLevelResources()}, it * throws an {@code IllegalStateException}. */ public static String getPackageNameDuringConstruction() { Object key; if (sLatestResKey == null || (key = sLatestResKey.get()) == null) throw new IllegalStateException("This method can only be called during getTopLevelResources()"); String resDir = (String) getObjectField(key, "mResDir"); return getPackageName(resDir); } /** @hide */ public static void init(ThreadLocal latestResKey) throws Exception { sLatestResKey = latestResKey; findAndHookMethod(LayoutInflater.class, "inflate", XmlPullParser.class, ViewGroup.class, boolean.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { if (param.hasThrowable()) return; XMLInstanceDetails details; synchronized (sXmlInstanceDetails) { details = sXmlInstanceDetails.get(param.args[0]); } if (details != null) { LayoutInflatedParam liparam = new LayoutInflatedParam(details.callbacks); liparam.view = (View) param.getResult(); liparam.resNames = details.resNames; liparam.variant = details.variant; liparam.res = details.res; XCallback.callAll(liparam); } } }); final XC_MethodHook parseIncludeHook = new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { sIncludedLayouts.get().push(param); } @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { sIncludedLayouts.get().pop(); if (param.hasThrowable()) return; // filled in by our implementation of getLayout() XMLInstanceDetails details = (XMLInstanceDetails) param.getObjectExtra(EXTRA_XML_INSTANCE_DETAILS); if (details != null) { LayoutInflatedParam liparam = new LayoutInflatedParam(details.callbacks); ViewGroup group = (ViewGroup) param.args[2]; liparam.view = group.getChildAt(group.getChildCount() - 1); liparam.resNames = details.resNames; liparam.variant = details.variant; liparam.res = details.res; XCallback.callAll(liparam); } } }; findAndHookMethod(LayoutInflater.class, "parseInclude", XmlPullParser.class, Context.class, View.class, AttributeSet.class, parseIncludeHook); } /** * Wrapper for information about an indiviual resource. */ public static class ResourceNames { /** The resource ID. */ public final int id; /** The resource package name as returned by {@link #getResourcePackageName}. */ public final String pkg; /** The resource entry name as returned by {@link #getResourceEntryName}. */ public final String name; /** The resource type name as returned by {@link #getResourceTypeName}. */ public final String type; /** The full resource nameas returned by {@link #getResourceName}. */ public final String fullName; private ResourceNames(int id, String pkg, String name, String type) { this.id = id; this.pkg = pkg; this.name = name; this.type = type; this.fullName = pkg + ":" + type + "/" + name; } /** * Returns whether all non-null parameters match the values of this object. */ public boolean equals(String pkg, String name, String type, int id) { return (pkg == null || pkg.equals(this.pkg)) && (name == null || name.equals(this.name)) && (type == null || type.equals(this.type)) && (id == 0 || id == this.id); } } private ResourceNames getResourceNames(int id) { return new ResourceNames( id, getResourcePackageName(id), getResourceTypeName(id), getResourceEntryName(id)); } private static ResourceNames getSystemResourceNames(int id) { Resources sysRes = getSystem(); return new ResourceNames( id, sysRes.getResourcePackageName(id), sysRes.getResourceTypeName(id), sysRes.getResourceEntryName(id)); } private static void putResourceNames(String resDir, ResourceNames resNames) { int id = resNames.id; synchronized (sResourceNames) { HashMap inner = sResourceNames.get(id); if (inner == null) { inner = new HashMap<>(); sResourceNames.put(id, inner); } synchronized (inner) { inner.put(resDir, resNames); } } } // ======================================================= // DEFINING REPLACEMENTS // ======================================================= /** * Sets a replacement for an individual resource. See {@link #setReplacement(String, String, String, Object)}. * * @param id The ID of the resource which should be replaced. * @param replacement The replacement, see above. */ public void setReplacement(int id, Object replacement) { setReplacement(id, replacement, this); } /** * Sets a replacement for an individual resource. See {@link #setReplacement(String, String, String, Object)}. * * @deprecated Use {@link #setReplacement(String, String, String, Object)} instead. * * @param fullName The full resource name, e.g. {@code com.example.myapplication:string/app_name}. * See {@link #getResourceName}. * @param replacement The replacement. */ @Deprecated public void setReplacement(String fullName, Object replacement) { int id = getIdentifier(fullName, null, null); if (id == 0) throw new NotFoundException(fullName); setReplacement(id, replacement, this); } /** * Sets a replacement for an individual resource. If called more than once for the same ID, the * replacement from the last call is used. Setting the replacement to {@code null} removes it. * *

The allowed replacements depend on the type of the source. All types accept an * {@link XResForwarder} object, which is usually created with {@link XModuleResources#fwd}. * The resource request will then be forwarded to another {@link android.content.res.Resources} * object. In addition to that, the following replacement types are accepted: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Resource type Additional allowed replacement types (*) Returned from (**)
Animation none{@link #getAnimation}
Bool{@link Boolean}{@link #getBoolean}
Color{@link Integer} (you might want to use {@link Color#parseColor}){@link #getColor}
* {@link #getDrawable} (creates a {@link ColorDrawable})
* {@link #getColorStateList} (calls {@link android.content.res.ColorStateList#valueOf}) *
Color State List{@link android.content.res.ColorStateList}
* {@link Integer} (calls {@link android.content.res.ColorStateList#valueOf}) *
{@link #getColorStateList}
Dimension{@link DimensionReplacement} (since v50){@link #getDimension}
* {@link #getDimensionPixelOffset}
* {@link #getDimensionPixelSize} *
Drawable * (including mipmap){@link DrawableLoader}
* {@link Integer} (creates a {@link ColorDrawable}) *
{@link #getDrawable}
* {@link #getDrawableForDensity} *
Fraction none{@link #getFraction}
Integer{@link Integer}{@link #getInteger}
Integer Array{@code int[]}{@link #getIntArray}
Layout none, but see {@link #hookLayout}{@link #getLayout}
Movie none{@link #getMovie}
Quantity Strings (Plurals) none{@link #getQuantityString}
* {@link #getQuantityText} *
String{@link String}
* {@link CharSequence} (for styled texts, see also {@link Html#fromHtml}) *
{@link #getString}
* {@link #getText} *
String Array{@code String[]}
* {@code CharSequence[]} (for styled texts, see also {@link Html#fromHtml}) *
{@link #getStringArray}
* {@link #getTextArray} *
XML none{@link #getXml}
* {@link #getQuantityText} *
* *

Other resource types, such as * styles/themes, * {@linkplain #openRawResource raw resources} and * typed arrays * can't be replaced. * *

* * Auto-boxing allows you to use literals like {@code 123} where an {@link Integer} is * accepted, so you don't neeed to call methods like {@link Integer#valueOf(int)} manually.
* ** Some of these methods have multiple variants, only one of them is mentioned here. *
* * @param pkg The package name, e.g. {@code com.example.myapplication}. * See {@link #getResourcePackageName}. * @param type The type name, e.g. {@code string}. * See {@link #getResourceTypeName}. * @param name The entry name, e.g. {@code app_name}. * See {@link #getResourceEntryName}. * @param replacement The replacement. */ public void setReplacement(String pkg, String type, String name, Object replacement) { int id = getIdentifier(name, type, pkg); if (id == 0) throw new NotFoundException(pkg + ":" + type + "/" + name); setReplacement(id, replacement, this); } /** * Sets a replacement for an individual Android framework resource (in the {@code android} package). * See {@link #setSystemWideReplacement(String, String, String, Object)}. * * @param id The ID of the resource which should be replaced. * @param replacement The replacement. */ public static void setSystemWideReplacement(int id, Object replacement) { setReplacement(id, replacement, null); } /** * Sets a replacement for an individual Android framework resource (in the {@code android} package). * See {@link #setSystemWideReplacement(String, String, String, Object)}. * * @deprecated Use {@link #setSystemWideReplacement(String, String, String, Object)} instead. * * @param fullName The full resource name, e.g. {@code android:string/yes}. * See {@link #getResourceName}. * @param replacement The replacement. */ @Deprecated public static void setSystemWideReplacement(String fullName, Object replacement) { int id = getSystem().getIdentifier(fullName, null, null); if (id == 0) throw new NotFoundException(fullName); setReplacement(id, replacement, null); } /** * Sets a replacement for an individual Android framework resource (in the {@code android} package). * *

Some resources are part of the Android framework and can be used in any app. They're * accessible via {@link android.R android.R} and are not bound to a specific * {@link android.content.res.Resources} instance. Such resources can be replaced in * {@link IXposedHookZygoteInit#initZygote initZygote()} for all apps. As there is no * {@link XResources} object easily available in that scope, this static method can be used * to set resource replacements. All other details (e.g. how certain types can be replaced) are * mentioned in {@link #setReplacement(String, String, String, Object)}. * * @param pkg The package name, should always be {@code android} here. * See {@link #getResourcePackageName}. * @param type The type name, e.g. {@code string}. * See {@link #getResourceTypeName}. * @param name The entry name, e.g. {@code yes}. * See {@link #getResourceEntryName}. * @param replacement The replacement. */ public static void setSystemWideReplacement(String pkg, String type, String name, Object replacement) { int id = getSystem().getIdentifier(name, type, pkg); if (id == 0) throw new NotFoundException(pkg + ":" + type + "/" + name); setReplacement(id, replacement, null); } private static void setReplacement(int id, Object replacement, XResources res) { String resDir = (res != null) ? res.mResDir : null; if (res == null) { try { XposedInit.hookResources(); } catch (Throwable throwable) { throw new IllegalStateException("Failed to initialize resources hook"); } } if (id == 0) throw new IllegalArgumentException("id 0 is not an allowed resource identifier"); else if (resDir == null && id >= 0x7f000000) throw new IllegalArgumentException("ids >= 0x7f000000 are app specific and cannot be set for the framework"); if (replacement instanceof Drawable) throw new IllegalArgumentException("Drawable replacements are deprecated since Xposed 2.1. Use DrawableLoader instead."); // Cache that we have a replacement for this ID, false positives are accepted to save memory. if (id < 0x7f000000) { int cacheKey = (id & 0x00070000) >> 11 | (id & 0xf8) >> 3; synchronized (sSystemReplacementsCache) { sSystemReplacementsCache[cacheKey] |= 1 << (id & 7); } } else { int cacheKey = (id & 0x00070000) >> 12 | (id & 0x78) >> 3; synchronized (res.mReplacementsCache) { res.mReplacementsCache[cacheKey] |= 1 << (id & 7); } } synchronized (sReplacements) { HashMap inner = sReplacements.get(id); if (inner == null) { inner = new HashMap<>(); sReplacements.put(id, inner); } inner.put(resDir, replacement); } } // ======================================================= // RETURNING REPLACEMENTS // ======================================================= private Object getReplacement(int id) { if (id <= 0) return null; // Check the cache whether it's worth looking for replacements if (id < 0x7f000000) { int cacheKey = (id & 0x00070000) >> 11 | (id & 0xf8) >> 3; if ((sSystemReplacementsCache[cacheKey] & (1 << (id & 7))) == 0) return null; } else if (mResDir != null) { int cacheKey = (id & 0x00070000) >> 12 | (id & 0x78) >> 3; if ((mReplacementsCache[cacheKey] & (1 << (id & 7))) == 0) return null; } HashMap inner; synchronized (sReplacements) { inner = sReplacements.get(id); } if (inner == null) return null; synchronized (inner) { Object result = inner.get(mResDir); if (result != null || mResDir == null) return result; return inner.get(null); } } /** @hide */ @Override public XmlResourceParser getAnimation(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); boolean loadedFromCache = isXmlCached(repRes, repId); XmlResourceParser result = repRes.getAnimation(repId); if (!loadedFromCache) { long parseState = getLongField(result, "mParseState"); rewriteXmlReferencesNative(parseState, this, repRes); } return result; } return super.getAnimation(id); } /** @hide */ @Override public boolean getBoolean(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof Boolean) { return (Boolean) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getBoolean(repId); } return super.getBoolean(id); } /** @hide */ @Override public int getColor(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getColor(repId); } return super.getColor(id); } /** @hide */ @Override public ColorStateList getColorStateList(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof ColorStateList) { return (ColorStateList) replacement; } else if (replacement instanceof Integer) { int color = (Integer) replacement; synchronized (sColorStateListCache) { ColorStateList result = sColorStateListCache.get(color); if (result == null) { result = ColorStateList.valueOf(color); sColorStateListCache.put(color, result); } return result; } } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getColorStateList(repId); } return super.getColorStateList(id); } /** @hide */ @Override public float getDimension(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof DimensionReplacement) { return ((DimensionReplacement) replacement).getDimension(getDisplayMetrics()); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimension(repId); } return super.getDimension(id); } /** @hide */ @Override public int getDimensionPixelOffset(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof DimensionReplacement) { return ((DimensionReplacement) replacement).getDimensionPixelOffset(getDisplayMetrics()); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelOffset(repId); } return super.getDimensionPixelOffset(id); } /** @hide */ @Override public int getDimensionPixelSize(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof DimensionReplacement) { return ((DimensionReplacement) replacement).getDimensionPixelSize(getDisplayMetrics()); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelSize(repId); } return super.getDimensionPixelSize(id); } /** @hide */ @Override public Drawable getDrawable(int id) throws NotFoundException { try { if (incrementMethodDepth("getDrawable") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawable(this, id); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawable(repId); } } return super.getDrawable(id); } finally { decrementMethodDepth("getDrawable"); } } /** @hide */ @Override public Drawable getDrawable(int id, Theme theme) throws NotFoundException { try { if (incrementMethodDepth("getDrawable") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawable(this, id); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawable(repId); } } return super.getDrawable(id, theme); } finally { decrementMethodDepth("getDrawable"); } } /** @hide */ @Override public Drawable getDrawableForDensity(int id, int density) throws NotFoundException { try { if (incrementMethodDepth("getDrawableForDensity") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawableForDensity(this, id, density); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawableForDensity(repId, density); } } return super.getDrawableForDensity(id, density); } finally { decrementMethodDepth("getDrawableForDensity"); } } /** @hide */ @Override public Drawable getDrawableForDensity(int id, int density, Theme theme) throws NotFoundException { try { if (incrementMethodDepth("getDrawableForDensity") == 1) { Object replacement = getReplacement(id); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawableForDensity(this, id, density); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawableForDensity(repId, density); } } return super.getDrawableForDensity(id, density, theme); } finally { decrementMethodDepth("getDrawableForDensity"); } } /** @hide */ @RequiresApi(Build.VERSION_CODES.Q) @Override public float getFloat(int id) { Object replacement = getReplacement(id); if (replacement instanceof Float) { return (Float) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getFloat(repId); } return super.getFloat(id); } /** @hide */ @Override public Typeface getFont(int id) { Object replacement = getReplacement(id); if (replacement instanceof Typeface) { return (Typeface) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getFont(repId); } return super.getFont(id); } /** @hide */ @Override public float getFraction(int id, int base, int pbase) { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getFraction(repId, base, pbase); } return super.getFraction(id, base, pbase); } /** @hide */ @Override public int getInteger(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getInteger(repId); } return super.getInteger(id); } /** @hide */ @Override public int[] getIntArray(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof int[]) { return (int[]) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getIntArray(repId); } return super.getIntArray(id); } /** @hide */ @Override public XmlResourceParser getLayout(int id) throws NotFoundException { XmlResourceParser result; Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); boolean loadedFromCache = isXmlCached(repRes, repId); result = repRes.getLayout(repId); if (!loadedFromCache) { long parseState = getLongField(result, "mParseState"); rewriteXmlReferencesNative(parseState, this, repRes); } } else { result = super.getLayout(id); } // Check whether this layout is hooked HashMap> inner; synchronized (sLayoutCallbacks) { inner = sLayoutCallbacks.get(id); } if (inner != null) { CopyOnWriteSortedSet callbacks; synchronized (inner) { callbacks = inner.get(mResDir); if (callbacks == null && mResDir != null) callbacks = inner.get(null); } if (callbacks != null) { String variant = "layout"; TypedValue value = (TypedValue) getObjectField(this, "mTmpValue"); getValue(id, value, true); if (value.type == TypedValue.TYPE_STRING) { String[] components = value.string.toString().split("/", 3); if (components.length == 3) variant = components[1]; else XposedBridge.log("Unexpected resource path \"" + value.string.toString() + "\" for resource id 0x" + Integer.toHexString(id)); } else { XposedBridge.log(new NotFoundException("Could not find file name for resource id 0x") + Integer.toHexString(id)); } synchronized (sXmlInstanceDetails) { synchronized (sResourceNames) { HashMap resNamesInner = sResourceNames.get(id); if (resNamesInner != null) { synchronized (resNamesInner) { XMLInstanceDetails details = new XMLInstanceDetails(resNamesInner.get(mResDir), variant, callbacks); sXmlInstanceDetails.put(result, details); // if we were called inside LayoutInflater.parseInclude, store the details for it MethodHookParam top = sIncludedLayouts.get().peek(); if (top != null) top.setObjectExtra(EXTRA_XML_INSTANCE_DETAILS, details); } } } } } } return result; } /** @hide */ @Override public Movie getMovie(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getMovie(repId); } return super.getMovie(id); } /** @hide */ @Override public CharSequence getQuantityText(int id, int quantity) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getQuantityText(repId, quantity); } return super.getQuantityText(id, quantity); } // these are handled by getQuantityText: // public String getQuantityString(int id, int quantity); // public String getQuantityString(int id, int quantity, Object... formatArgs); /** @hide */ @Override public String[] getStringArray(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof String[]) { return (String[]) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getStringArray(repId); } return super.getStringArray(id); } /** @hide */ @Override public CharSequence getText(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof CharSequence) { return (CharSequence) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getText(repId); } return super.getText(id); } // these are handled by getText: // public String getString(int id); // public String getString(int id, Object... formatArgs); /** @hide */ @Override public CharSequence getText(int id, CharSequence def) { Object replacement = getReplacement(id); if (replacement instanceof CharSequence) { return (CharSequence) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getText(repId, def); } return super.getText(id, def); } /** @hide */ @Override public CharSequence[] getTextArray(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof CharSequence[]) { return (CharSequence[]) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getTextArray(repId); } return super.getTextArray(id); } /** @hide */ @Override public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); repRes.getValue(repId, outValue, resolveRefs); } else { if (replacement != null) { XposedBridge.log("Replacement of resource ID #0x" + Integer.toHexString(id) + " escaped because of deprecated replacement. Please use XResForwarder instead."); } super.getValue(id, outValue, resolveRefs); } } /** @hide */ @Override public void getValueForDensity(int id, int density, TypedValue outValue, boolean resolveRefs) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); repRes.getValueForDensity(repId, density, outValue, resolveRefs); } else { if (replacement != null) { XposedBridge.log("Replacement of resource ID #0x" + Integer.toHexString(id) + " escaped because of deprecated replacement. Please use XResForwarder instead."); } super.getValueForDensity(id, density, outValue, resolveRefs); } } /** @hide */ @Override public XmlResourceParser getXml(int id) throws NotFoundException { Object replacement = getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); boolean loadedFromCache = isXmlCached(repRes, repId); XmlResourceParser result = repRes.getXml(repId); if (!loadedFromCache) { long parseState = getLongField(result, "mParseState"); rewriteXmlReferencesNative(parseState, this, repRes); } return result; } return super.getXml(id); } private static boolean isXmlCached(Resources res, int id) { int[] mCachedXmlBlockIds = (int[]) getObjectField(getObjectField(res, "mResourcesImpl"), "mCachedXmlBlockCookies"); synchronized (mCachedXmlBlockIds) { for (int cachedId : mCachedXmlBlockIds) { if (cachedId == id) return true; } } return false; } /** * Used to replace reference IDs in XMLs. * * When resource requests are forwarded to modules, the may include references to resources with the same * name as in the original resources, but the IDs generated by aapt will be different. rewriteXmlReferencesNative * walks through all references and calls this function to find out the original ID, which it then writes to * the compiled XML file in the memory. */ private static int translateResId(int id, XResources origRes, Resources repRes) { try { String entryName = repRes.getResourceEntryName(id); String entryType = repRes.getResourceTypeName(id); String origPackage = origRes.mPackageName; int origResId = 0; try { // look for a resource with the same name and type in the original package origResId = origRes.getIdentifier(entryName, entryType, origPackage); } catch (NotFoundException ignored) {} boolean repResDefined = false; try { final TypedValue tmpValue = new TypedValue(); repRes.getValue(id, tmpValue, false); // if a resource has not been defined (i.e. only a resource ID has been created), it will equal "false" // this means a boolean "false" value is not detected of it is directly referenced in an XML file repResDefined = !(tmpValue.type == TypedValue.TYPE_INT_BOOLEAN && tmpValue.data == 0); } catch (NotFoundException ignored) {} if (!repResDefined && origResId == 0 && !entryType.equals("id")) { XposedBridge.log(entryType + "/" + entryName + " is neither defined in module nor in original resources"); return 0; } // exists only in module, so create a fake resource id if (origResId == 0) origResId = getFakeResId(repRes, id); // IDs will never be loaded, no need to set a replacement if (repResDefined && !entryType.equals("id")) origRes.setReplacement(origResId, new XResForwarder(repRes, id)); return origResId; } catch (Exception e) { XposedBridge.log(e); return id; } } /** * Generates a fake resource ID. * *

The parameter is just hashed, it doesn't have a deeper meaning. However, it's recommended * to use values with a low risk for conflicts, such as a full resource name. Calling this * method multiple times will return the same ID. * * @param resName A used for hashing, see above. * @return The fake resource ID. */ public static int getFakeResId(String resName) { return 0x7e000000 | (resName.hashCode() & 0x00ffffff); } /** * Generates a fake resource ID. * *

This variant uses the result of {@link #getResourceName} to create the hash that the ID is * based on. The given resource doesn't need to match the {@link XResources} instance for which * the fake resource ID is going to be used. * * @param res The {@link android.content.res.Resources} object to be used for hashing. * @param id The resource ID to be used for hashing. * @return The fake resource ID. */ public static int getFakeResId(Resources res, int id) { return getFakeResId(res.getResourceName(id)); } /** * Makes any individual resource available from another {@link android.content.res.Resources} * instance available in this {@link XResources} instance. * *

This method combines calls to {@link #getFakeResId(Resources, int)} and * {@link #setReplacement(int, Object)} to generate a fake resource ID and set up a replacement * for it which forwards to the given resource. * *

The returned ID can only be used to retrieve the resource, it won't work for methods like * {@link #getResourceName} etc. * * @param res The target {@link android.content.res.Resources} instance. * @param id The target resource ID. * @return The fake resource ID (see above). */ public int addResource(Resources res, int id) { int fakeId = getFakeResId(res, id); synchronized (sReplacements) { if (sReplacements.indexOfKey(fakeId) < 0) setReplacement(fakeId, new XResForwarder(res, id)); } return fakeId; } /** * Similar to {@link #translateResId}, but used to determine the original ID of attribute names. */ private static int translateAttrId(String attrName, XResources origRes) { String origPackage = origRes.mPackageName; int origAttrId = 0; try { origAttrId = origRes.getIdentifier(attrName, "attr", origPackage); } catch (NotFoundException e) { XposedBridge.log("Attribute " + attrName + " not found in original resources"); } return origAttrId; } // ======================================================= // XTypedArray class // ======================================================= /** * {@link android.content.res.TypedArray} replacement that replaces values on-the-fly. * Mainly used when inflating layouts. * @hide */ public static class XTypedArray extends XTypedArraySuperClass { public XTypedArray(Resources resources) { super(resources); } /** Dummy, will never be called (objects are transferred to this class only). */ // private XTypedArray() { // super(null, null, null, 0); // throw new UnsupportedOperationException(); // } @Override public boolean getBoolean(int index, boolean defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof Boolean) { return (Boolean) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getBoolean(repId); } return super.getBoolean(index, defValue); } @Override public int getColor(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getColor(repId); } return super.getColor(index, defValue); } @Override public ColorStateList getColorStateList(int index) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof ColorStateList) { return (ColorStateList) replacement; } else if (replacement instanceof Integer) { int color = (Integer) replacement; synchronized (sColorStateListCache) { ColorStateList result = sColorStateListCache.get(color); if (result == null) { result = ColorStateList.valueOf(color); sColorStateListCache.put(color, result); } return result; } } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getColorStateList(repId); } return super.getColorStateList(index); } @Override public float getDimension(int index, float defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimension(repId); } return super.getDimension(index, defValue); } @Override public int getDimensionPixelOffset(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelOffset(repId); } return super.getDimensionPixelOffset(index, defValue); } @Override public int getDimensionPixelSize(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelSize(repId); } return super.getDimensionPixelSize(index, defValue); } @Override public Drawable getDrawable(int index) { final int resId = getResourceId(index, 0); XResources xres = (XResources) getResources(); Object replacement = xres.getReplacement(resId); if (replacement instanceof DrawableLoader) { try { Drawable result = ((DrawableLoader) replacement).newDrawable(xres, resId); if (result != null) return result; } catch (Throwable t) { XposedBridge.log(t); } } else if (replacement instanceof Integer) { return new ColorDrawable((Integer) replacement); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDrawable(repId); } return super.getDrawable(index); } @Override public float getFloat(int index, float defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); // dimensions seem to be the only way to define floats by references return repRes.getDimension(repId); } return super.getFloat(index, defValue); } @Override public Typeface getFont(int index) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof Typeface) { return (Typeface) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getFont(repId); } return super.getFont(index); } @Override public float getFraction(int index, int base, int pbase, float defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); // dimensions seem to be the only way to define floats by references return repRes.getFraction(repId, base, pbase); } return super.getFraction(index, base, pbase, defValue); } @Override public int getInt(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getInteger(repId); } return super.getInt(index, defValue); } @Override public int getInteger(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof Integer) { return (Integer) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getInteger(repId); } return super.getInteger(index, defValue); } @Override public int getLayoutDimension(int index, int defValue) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelSize(repId); } return super.getLayoutDimension(index, defValue); } @Override public int getLayoutDimension(int index, String name) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getDimensionPixelSize(repId); } return super.getLayoutDimension(index, name); } @Override public String getString(int index) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof CharSequence) { return replacement.toString(); } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getString(repId); } return super.getString(index); } @Override public CharSequence getText(int index) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof CharSequence) { return (CharSequence) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getText(repId); } return super.getText(index); } @Override public CharSequence[] getTextArray(int index) { Object replacement = ((XResources) getResources()).getReplacement(getResourceId(index, 0)); if (replacement instanceof CharSequence[]) { return (CharSequence[]) replacement; } else if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); return repRes.getTextArray(repId); } return super.getTextArray(index); } @Override public boolean getValue(int index, TypedValue outValue) { var id = getResourceId(index, 0); Object replacement = ((XResources) getResources()).getReplacement(id); if (replacement instanceof XResForwarder) { Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); repRes.getValue(repId, outValue, true); return outValue.type != TypedValue.TYPE_NULL; } else { if (replacement != null) { XposedBridge.log("Replacement of resource ID #0x" + Integer.toHexString(id) + " escaped because of deprecated replacement. Please use XResForwarder instead."); } return super.getValue(index, outValue); } } @Override public TypedValue peekValue(int index) { var id = getResourceId(index, 0); Object replacement = ((XResources) getResources()).getReplacement(id); if (replacement instanceof XResForwarder) { if (getBooleanField(this, "mRecycled")) { throw new RuntimeException("Cannot make calls to a recycled instance!"); } final TypedValue value = (TypedValue) getObjectField(this, "mValue"); Resources repRes = ((XResForwarder) replacement).getResources(); int repId = ((XResForwarder) replacement).getId(); repRes.getValue(repId, value, true); return value; } else { if (replacement != null) { XposedBridge.log("Replacement of resource ID #0x" + Integer.toHexString(id) + " escaped because of deprecated replacement. Please use XResForwarder instead."); } return super.peekValue(index); } } } // ======================================================= // DrawableLoader class // ======================================================= /** * Callback for drawable replacements. Instances of this class can passed to * {@link #setReplacement(String, String, String, Object)} and its variants. * *

Make sure to always return new {@link Drawable} instances, as drawables * usually can't be reused. */ @SuppressWarnings("UnusedParameters") public static abstract class DrawableLoader { /** * Constructor. */ public DrawableLoader() {} /** * Called when the hooked drawable resource has been requested. * * @param res The {@link XResources} object in which the hooked drawable resides. * @param id The resource ID which has been requested. * @return The {@link Drawable} which should be used as replacement. {@code null} is ignored. * @throws Throwable Everything the callback throws is caught and logged. */ public abstract Drawable newDrawable(XResources res, int id) throws Throwable; /** * Like {@link #newDrawable}, but called for {@link #getDrawableForDensity}. The default * implementation is to use the result of {@link #newDrawable}. * * @param res The {@link XResources} object in which the hooked drawable resides. * @param id The resource ID which has been requested. * @param density The desired screen density indicated by the resource as found in * {@link DisplayMetrics}. * @return The {@link Drawable} which should be used as replacement. {@code null} is ignored. * @throws Throwable Everything the callback throws is caught and logged. */ public Drawable newDrawableForDensity(XResources res, int id, int density) throws Throwable { return newDrawable(res, id); } } // ======================================================= // DimensionReplacement class // ======================================================= /** * Callback for dimension replacements. Instances of this class can passed to * {@link #setReplacement(String, String, String, Object)} and its variants. */ public static class DimensionReplacement { private final float mValue; private final int mUnit; /** * Creates an instance that can be used for {@link #setReplacement(String, String, String, Object)} * to replace a dimension resource. * * @param value The value of the replacement, in the unit specified with the next parameter. * @param unit One of the {@code COMPLEX_UNIT_*} constants in {@link TypedValue}. */ public DimensionReplacement(float value, int unit) { mValue = value; mUnit = unit; } /** Called by {@link android.content.res.Resources#getDimension}. */ public float getDimension(DisplayMetrics metrics) { return TypedValue.applyDimension(mUnit, mValue, metrics); } /** Called by {@link android.content.res.Resources#getDimensionPixelOffset}. */ public int getDimensionPixelOffset(DisplayMetrics metrics) { return (int) TypedValue.applyDimension(mUnit, mValue, metrics); } /** Called by {@link android.content.res.Resources#getDimensionPixelSize}. */ public int getDimensionPixelSize(DisplayMetrics metrics) { final float f = TypedValue.applyDimension(mUnit, mValue, metrics); final int res = (int)(f+0.5f); if (res != 0) return res; if (mValue == 0) return 0; if (mValue > 0) return 1; return -1; } } // ======================================================= // INFLATING LAYOUTS // ======================================================= private class XMLInstanceDetails { public final ResourceNames resNames; public final String variant; public final CopyOnWriteSortedSet callbacks; public final XResources res = XResources.this; private XMLInstanceDetails(ResourceNames resNames, String variant, CopyOnWriteSortedSet callbacks) { this.resNames = resNames; this.variant = variant; this.callbacks = callbacks; } } /** * Hook the inflation of a layout. * * @param id The ID of the resource which should be replaced. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ public XC_LayoutInflated.Unhook hookLayout(int id, XC_LayoutInflated callback) { return hookLayoutInternal(mResDir, id, getResourceNames(id), callback); } /** * Hook the inflation of a layout. * * @deprecated Use {@link #hookLayout(String, String, String, XC_LayoutInflated)} instead. * * @param fullName The full resource name, e.g. {@code com.android.systemui:layout/statusbar}. * See {@link #getResourceName}. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ @Deprecated public XC_LayoutInflated.Unhook hookLayout(String fullName, XC_LayoutInflated callback) { int id = getIdentifier(fullName, null, null); if (id == 0) throw new NotFoundException(fullName); return hookLayout(id, callback); } /** * Hook the inflation of a layout. * * @param pkg The package name, e.g. {@code com.android.systemui}. * See {@link #getResourcePackageName}. * @param type The type name, e.g. {@code layout}. * See {@link #getResourceTypeName}. * @param name The entry name, e.g. {@code statusbar}. * See {@link #getResourceEntryName}. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ public XC_LayoutInflated.Unhook hookLayout(String pkg, String type, String name, XC_LayoutInflated callback) { int id = getIdentifier(name, type, pkg); if (id == 0) throw new NotFoundException(pkg + ":" + type + "/" + name); return hookLayout(id, callback); } /** * Hook the inflation of an Android framework layout (in the {@code android} package). * See {@link #hookSystemWideLayout(String, String, String, XC_LayoutInflated)}. * * @param id The ID of the resource which should be replaced. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ public static XC_LayoutInflated.Unhook hookSystemWideLayout(int id, XC_LayoutInflated callback) { if (id >= 0x7f000000) throw new IllegalArgumentException("ids >= 0x7f000000 are app specific and cannot be set for the framework"); return hookLayoutInternal(null, id, getSystemResourceNames(id), callback); } /** * Hook the inflation of an Android framework layout (in the {@code android} package). * See {@link #hookSystemWideLayout(String, String, String, XC_LayoutInflated)}. * * @deprecated Use {@link #hookSystemWideLayout(String, String, String, XC_LayoutInflated)} instead. * * @param fullName The full resource name, e.g. {@code android:layout/simple_list_item_1}. * See {@link #getResourceName}. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ @Deprecated public static XC_LayoutInflated.Unhook hookSystemWideLayout(String fullName, XC_LayoutInflated callback) { int id = getSystem().getIdentifier(fullName, null, null); if (id == 0) throw new NotFoundException(fullName); return hookSystemWideLayout(id, callback); } /** * Hook the inflation of an Android framework layout (in the {@code android} package). * *

Some layouts are part of the Android framework and can be used in any app. They're * accessible via {@link android.R.layout android.R.layout} and are not bound to a specific * {@link android.content.res.Resources} instance. Such resources can be replaced in * {@link IXposedHookZygoteInit#initZygote initZygote()} for all apps. As there is no * {@link XResources} object easily available in that scope, this static method can be used * to hook layouts. * * @param pkg The package name, e.g. {@code android}. * See {@link #getResourcePackageName}. * @param type The type name, e.g. {@code layout}. * See {@link #getResourceTypeName}. * @param name The entry name, e.g. {@code simple_list_item_1}. * See {@link #getResourceEntryName}. * @param callback The callback to be executed when the layout has been inflated. * @return An object which can be used to remove the callback again. */ public static XC_LayoutInflated.Unhook hookSystemWideLayout(String pkg, String type, String name, XC_LayoutInflated callback) { int id = getSystem().getIdentifier(name, type, pkg); if (id == 0) throw new NotFoundException(pkg + ":" + type + "/" + name); return hookSystemWideLayout(id, callback); } private static XC_LayoutInflated.Unhook hookLayoutInternal(String resDir, int id, ResourceNames resNames, XC_LayoutInflated callback) { if (id == 0) throw new IllegalArgumentException("id 0 is not an allowed resource identifier"); if (resDir == null) { try { XposedInit.hookResources(); } catch (Throwable throwable) { throw new IllegalStateException("Failed to initialize resources hook", throwable); } } HashMap> inner; synchronized (sLayoutCallbacks) { inner = sLayoutCallbacks.get(id); if (inner == null) { inner = new HashMap<>(); sLayoutCallbacks.put(id, inner); } } CopyOnWriteSortedSet callbacks; synchronized (inner) { callbacks = inner.get(resDir); if (callbacks == null) { callbacks = new CopyOnWriteSortedSet<>(); inner.put(resDir, callbacks); } } callbacks.add(callback); putResourceNames(resDir, resNames); return callback.new Unhook(resDir, id); } /** @hide */ public static void unhookLayout(String resDir, int id, XC_LayoutInflated callback) { HashMap> inner; synchronized (sLayoutCallbacks) { inner = sLayoutCallbacks.get(id); if (inner == null) return; } CopyOnWriteSortedSet callbacks; synchronized (inner) { callbacks = inner.get(resDir); if (callbacks == null) return; } callbacks.remove(callback); } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/IXposedHookCmdInit.java ================================================ /* * 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 */ package de.robv.android.xposed; /** * Hook the initialization of Java-based command-line tools (like pm). * * @hide Xposed no longer hooks command-line tools, therefore this interface shouldn't be * implemented anymore. */ public interface IXposedHookCmdInit extends IXposedMod { /** * Called very early during startup of a command-line tool. * * @param startupParam Details about the module itself and the started process. * @throws Throwable Everything is caught, but it will prevent further initialization of the module. */ void initCmdApp(StartupParam startupParam) throws Throwable; /** * Data holder for {@link #initCmdApp}. */ final class StartupParam { /*package*/ StartupParam() { } /** * The path to the module's APK. */ public String modulePath; /** * The class name of the tools that the hook was invoked for. */ public String startClassName; } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/IXposedHookInitPackageResources.java ================================================ package de.robv.android.xposed; import android.content.res.XResources; import de.robv.android.xposed.callbacks.XC_InitPackageResources; import de.robv.android.xposed.callbacks.XC_InitPackageResources.InitPackageResourcesParam; /** * Get notified when the resources for an app are initialized. * In {@link #handleInitPackageResources}, resource replacements can be created. * *

This interface should be implemented by the module's main class. Xposed will take care of * registering it as a callback automatically. */ public interface IXposedHookInitPackageResources extends IXposedMod { /** * This method is called when resources for an app are being initialized. * Modules can call special methods of the {@link XResources} class in order to replace resources. * * @param resparam Information about the resources. * @throws Throwable Everything the callback throws is caught and logged. */ void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable; /** @hide */ final class Wrapper extends XC_InitPackageResources { private final IXposedHookInitPackageResources instance; public Wrapper(IXposedHookInitPackageResources instance) { this.instance = instance; } @Override public void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable { instance.handleInitPackageResources(resparam); } } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/IXposedHookLoadPackage.java ================================================ package de.robv.android.xposed; import android.app.Application; import de.robv.android.xposed.callbacks.XC_LoadPackage; import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam; /** * Get notified when an app ("Android package") is loaded. * This is especially useful to hook some app-specific methods. * *

This interface should be implemented by the module's main class. Xposed will take care of * registering it as a callback automatically. */ public interface IXposedHookLoadPackage extends IXposedMod { /** * This method is called when an app is loaded. It's called very early, even before * {@link Application#onCreate} is called. * Modules can set up their app-specific hooks here. * * @param lpparam Information about the app. * @throws Throwable Everything the callback throws is caught and logged. */ void handleLoadPackage(LoadPackageParam lpparam) throws Throwable; /** @hide */ final class Wrapper extends XC_LoadPackage { private final IXposedHookLoadPackage instance; public Wrapper(IXposedHookLoadPackage instance) { this.instance = instance; } @Override public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable { instance.handleLoadPackage(lpparam); } } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/IXposedHookZygoteInit.java ================================================ package de.robv.android.xposed; /** * Hook the initialization of Zygote process(es), from which all the apps are forked. * *

Implement this interface in your module's main class in order to be notified when Android is * starting up. In {@link IXposedHookZygoteInit}, you can modify objects and place hooks that should * be applied for every app. Only the Android framework/system classes are available at that point * in time. Use {@code null} as class loader for {@link XposedHelpers#findAndHookMethod(String, ClassLoader, String, Object...)} * and its variants. * *

If you want to hook one/multiple specific apps, use {@link IXposedHookLoadPackage} instead. */ public interface IXposedHookZygoteInit extends IXposedMod { /** * Called very early during startup of Zygote. * @param startupParam Details about the module itself and the started process. * @throws Throwable everything is caught, but will prevent further initialization of the module. */ void initZygote(StartupParam startupParam) throws Throwable; /** Data holder for {@link #initZygote}. */ final class StartupParam { /*package*/ StartupParam() {} /** The path to the module's APK. */ public String modulePath; /** * Always {@code true} on 32-bit ROMs. On 64-bit, it's only {@code true} for the primary * process that starts the system_server. */ public boolean startsSystemServer; } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/IXposedMod.java ================================================ /* * 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 */ package de.robv.android.xposed; /** * Marker interface for Xposed modules. Cannot be implemented directly. */ /* package */ interface IXposedMod { } ================================================ FILE: core/src/main/java/de/robv/android/xposed/SELinuxHelper.java ================================================ /* * 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 */ package de.robv.android.xposed; import de.robv.android.xposed.services.BaseService; import de.robv.android.xposed.services.DirectAccessService; /** * A helper to work with (or without) SELinux, abstracting much of its big complexity. */ public final class SELinuxHelper { private SELinuxHelper() { } /** * Determines whether SELinux is disabled or enabled. * * @return A boolean indicating whether SELinux is enabled. */ public static boolean isSELinuxEnabled() { // lsp: always enabled return true; } /** * Determines whether SELinux is permissive or enforcing. * * @return A boolean indicating whether SELinux is enforcing. */ public static boolean isSELinuxEnforced() { // lsp: always enforcing return true; } /** * Gets the security context of the current process. * * @return A String representing the security context of the current process. */ public static String getContext() { return null; } /** * Retrieve the service to be used when accessing files in {@code /data/data/*}. * *

IMPORTANT: If you call this from the Zygote process, * don't re-use the result in different process! * * @return An instance of the service. */ public static BaseService getAppDataFileService() { return sServiceAppDataFile; } private static final BaseService sServiceAppDataFile = new DirectAccessService(); // ed: initialized directly } ================================================ FILE: core/src/main/java/de/robv/android/xposed/XC_MethodHook.java ================================================ /* * 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 */ package de.robv.android.xposed; import java.lang.reflect.Executable; import java.lang.reflect.Member; import java.util.HashMap; import de.robv.android.xposed.callbacks.IXUnhook; import de.robv.android.xposed.callbacks.XCallback; /** * Callback class for method hooks. * *

Usually, anonymous subclasses of this class are created which override * {@link #beforeHookedMethod} and/or {@link #afterHookedMethod}. */ public abstract class XC_MethodHook extends XCallback { /** * Creates a new callback with default priority. */ @SuppressWarnings("deprecation") public XC_MethodHook() { super(); } /** * Creates a new callback with a specific priority. * *

Note that {@link #afterHookedMethod} will be called in reversed order, i.e. * the callback with the highest priority will be called last. This way, the callback has the * final control over the return value. {@link #beforeHookedMethod} is called as usual, i.e. * highest priority first. * * @param priority See {@link XCallback#priority}. */ public XC_MethodHook(int priority) { super(priority); } /** * Called before the invocation of the method. * *

You can use {@link MethodHookParam#setResult} and {@link MethodHookParam#setThrowable} * to prevent the original method from being called. * *

Note that implementations shouldn't call {@code super(param)}, it's not necessary. * * @param param Information about the method call. * @throws Throwable Everything the callback throws is caught and logged. */ protected void beforeHookedMethod(MethodHookParam param) throws Throwable { } public void callBeforeHookedMethod(MethodHookParam param) throws Throwable { beforeHookedMethod(param); } /** * Called after the invocation of the method. * *

You can use {@link MethodHookParam#setResult} and {@link MethodHookParam#setThrowable} * to modify the return value of the original method. * *

Note that implementations shouldn't call {@code super(param)}, it's not necessary. * * @param param Information about the method call. * @throws Throwable Everything the callback throws is caught and logged. */ protected void afterHookedMethod(MethodHookParam param) throws Throwable { } public void callAfterHookedMethod(MethodHookParam param) throws Throwable { afterHookedMethod(param); } /** * Wraps information about the method call and allows to influence it. */ public static final class MethodHookParam extends XCallback.Param { /** * @hide */ @SuppressWarnings("deprecation") public MethodHookParam() { super(); } /** * The hooked method/constructor. */ public Member method; /** * The {@code this} reference for an instance method, or {@code null} for static methods. */ public Object thisObject; /** * Arguments to the method call. */ public Object[] args; public Object result = null; public Throwable throwable = null; public boolean returnEarly = false; private final HashMap extras = new HashMap<>(); /** * Returns the result of the method call. */ public Object getResult() { return result; } /** * Modify the result of the method call. * *

If called from {@link #beforeHookedMethod}, it prevents the call to the original method. */ public void setResult(Object result) { this.result = result; this.throwable = null; this.returnEarly = true; } /** * Returns the {@link Throwable} thrown by the method, or {@code null}. */ public Throwable getThrowable() { return throwable; } /** * Returns true if an exception was thrown by the method. */ public boolean hasThrowable() { return throwable != null; } /** * Modify the exception thrown of the method call. * *

If called from {@link #beforeHookedMethod}, it prevents the call to the original method. */ public void setThrowable(Throwable throwable) { this.throwable = throwable; this.result = null; this.returnEarly = true; } /** * Returns the result of the method call, or throws the Throwable caused by it. */ public Object getResultOrThrowable() throws Throwable { if (throwable != null) throw throwable; return result; } } /** * An object with which the method/constructor can be unhooked. */ public class Unhook implements IXUnhook { private final Member hookMethod; /*package*/ Unhook(Member hookMethod) { this.hookMethod = hookMethod; } /** * Returns the method/constructor that has been hooked. */ public Member getHookedMethod() { return hookMethod; } @Override public XC_MethodHook getCallback() { return XC_MethodHook.this; } @SuppressWarnings("deprecation") @Override public void unhook() { XposedBridge.unhookMethod(hookMethod, XC_MethodHook.this); } } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/XC_MethodReplacement.java ================================================ /* * 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 */ package de.robv.android.xposed; import de.robv.android.xposed.callbacks.XCallback; /** * A special case of {@link XC_MethodHook} which completely replaces the original method. */ public abstract class XC_MethodReplacement extends XC_MethodHook { /** * Creates a new callback with default priority. */ public XC_MethodReplacement() { super(); } /** * Creates a new callback with a specific priority. * * @param priority See {@link XCallback#priority}. */ public XC_MethodReplacement(int priority) { super(priority); } /** * @hide */ @Override protected final void beforeHookedMethod(MethodHookParam param) throws Throwable { try { Object result = replaceHookedMethod(param); param.setResult(result); } catch (Throwable t) { param.setThrowable(t); } } /** * @hide */ @Override @SuppressWarnings("EmptyMethod") protected final void afterHookedMethod(MethodHookParam param) throws Throwable { } /** * Shortcut for replacing a method completely. Whatever is returned/thrown here is taken * instead of the result of the original method (which will not be called). * *

Note that implementations shouldn't call {@code super(param)}, it's not necessary. * * @param param Information about the method call. * @throws Throwable Anything that is thrown by the callback will be passed on to the original caller. */ @SuppressWarnings("UnusedParameters") protected abstract Object replaceHookedMethod(MethodHookParam param) throws Throwable; /** * Predefined callback that skips the method without replacements. */ public static final XC_MethodReplacement DO_NOTHING = new XC_MethodReplacement(PRIORITY_HIGHEST * 2) { @Override protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { return null; } }; /** * Creates a callback which always returns a specific value. * * @param result The value that should be returned to callers of the hooked method. */ public static XC_MethodReplacement returnConstant(final Object result) { return returnConstant(PRIORITY_DEFAULT, result); } /** * Like {@link #returnConstant(Object)}, but allows to specify a priority for the callback. * * @param priority See {@link XCallback#priority}. * @param result The value that should be returned to callers of the hooked method. */ public static XC_MethodReplacement returnConstant(int priority, final Object result) { return new XC_MethodReplacement(priority) { @Override protected Object replaceHookedMethod(MethodHookParam param) throws Throwable { return result; } }; } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/XSharedPreferences.java ================================================ /* * 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 - 2022 LSPosed Contributors */ package de.robv.android.xposed; import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.os.Environment; import android.preference.PreferenceManager; import com.android.internal.util.XmlUtils; import org.lsposed.lspd.core.BuildConfig; import org.lsposed.lspd.util.MetaDataReader; import org.lsposed.lspd.util.Utils.Log; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.file.AccessDeniedException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.security.MessageDigest; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import de.robv.android.xposed.services.FileResult; /** * This class is basically the same as SharedPreferencesImpl from AOSP, but * read-only and without listeners support. Instead, it is made to be * compatible with all ROMs. */ public final class XSharedPreferences implements SharedPreferences { private static final String TAG = "XSharedPreferences"; private static final HashMap sWatcherKeyInstances = new HashMap<>(); private static final Object sContent = new Object(); private static Thread sWatcherDaemon = null; private static WatchService sWatcher; private final HashMap mListeners = new HashMap<>(); private final File mFile; private final String mFilename; private Map mMap; private boolean mLoaded = false; private long mLastModified; private long mFileSize; private WatchKey mWatchKey; private static void initWatcherDaemon() { sWatcherDaemon = new Thread() { @Override public void run() { if (BuildConfig.DEBUG) Log.d(TAG, "Watcher daemon thread started"); while (true) { WatchKey key; try { key = sWatcher.take(); } catch (ClosedWatchServiceException ignored) { if (BuildConfig.DEBUG) Log.d(TAG, "Watcher daemon thread finished"); sWatcher = null; return; } catch (InterruptedException ignored) { return; } for (WatchEvent event : key.pollEvents()) { WatchEvent.Kind kind = event.kind(); if (kind == StandardWatchEventKinds.OVERFLOW) { continue; } Path dir = (Path) key.watchable(); Path path = dir.resolve((Path) event.context()); String pathStr = path.toString(); if (BuildConfig.DEBUG) Log.v(TAG, "File " + path.toString() + " event: " + kind.name()); // We react to both real and backup files due to rare race conditions if (pathStr.endsWith(".bak")) { if (kind != StandardWatchEventKinds.ENTRY_DELETE) { continue; } } else if (SELinuxHelper.getAppDataFileService().checkFileExists(pathStr + ".bak")) { continue; } PrefsData data = sWatcherKeyInstances.get(key); if (data != null && data.hasChanged()) { for (OnSharedPreferenceChangeListener l : data.mPrefs.mListeners.keySet()) { try { l.onSharedPreferenceChanged(data.mPrefs, null); } catch (Throwable t) { if (BuildConfig.DEBUG) Log.e(TAG, "Fail in preference change listener", t); } } } } key.reset(); } } }; sWatcherDaemon.setName(TAG + "-Daemon"); sWatcherDaemon.setDaemon(true); sWatcherDaemon.start(); } /** * Read settings from the specified file. * * @param prefFile The file to read the preferences from. */ public XSharedPreferences(File prefFile) { mFile = prefFile; mFilename = prefFile.getAbsolutePath(); init(); } /** * Read settings from the default preferences for a package. * These preferences are returned by {@link PreferenceManager#getDefaultSharedPreferences}. * * @param packageName The package name. */ public XSharedPreferences(String packageName) { this(packageName, packageName + "_preferences"); } /** * Read settings from a custom preferences file for a package. * These preferences are returned by {@link Context#getSharedPreferences(String, int)}. * * @param packageName The package name. * @param prefFileName The file name without ".xml". */ public XSharedPreferences(String packageName, String prefFileName) { boolean newModule = false; var m = XposedInit.getLoadedModules().getOrDefault(packageName, Optional.empty()); if (m.isPresent()) { boolean isModule = false; int xposedminversion = -1; boolean xposedsharedprefs = false; try { Map metaData = MetaDataReader.getMetaData(new File(m.get())); isModule = metaData.containsKey("xposedminversion"); if (isModule) { Object minVersionRaw = metaData.get("xposedminversion"); if (minVersionRaw instanceof Integer) { xposedminversion = (Integer) minVersionRaw; } else if (minVersionRaw instanceof String) { xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw); } xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); } } catch (NumberFormatException | IOException e) { Log.w(TAG, "Apk parser fails: " + e); } newModule = isModule && (xposedminversion > 92 || xposedsharedprefs); } if (newModule) { mFile = new File(serviceClient.getPrefsPath(packageName), prefFileName + ".xml"); } else { mFile = new File(Environment.getDataDirectory(), "data/" + packageName + "/shared_prefs/" + prefFileName + ".xml"); } mFilename = mFile.getAbsolutePath(); init(); } private void tryRegisterWatcher() { if (mWatchKey != null && mWatchKey.isValid()) { return; } synchronized (sWatcherKeyInstances) { Path path = mFile.toPath(); try { if (sWatcher == null) { sWatcher = new File(serviceClient.getPrefsPath("")).toPath().getFileSystem().newWatchService(); if (BuildConfig.DEBUG) Log.d(TAG, "Created WatchService instance"); } mWatchKey = path.getParent().register(sWatcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); sWatcherKeyInstances.put(mWatchKey, new PrefsData(this)); if (sWatcherDaemon == null || !sWatcherDaemon.isAlive()) { initWatcherDaemon(); } if (BuildConfig.DEBUG) Log.d(TAG, "tryRegisterWatcher: registered file watcher for " + path); } catch (AccessDeniedException accDeniedEx) { if (BuildConfig.DEBUG) Log.e(TAG, "tryRegisterWatcher: access denied to " + path); } catch (Exception e) { Log.e(TAG, "tryRegisterWatcher: failed to register file watcher", e); } } } private void tryUnregisterWatcher() { synchronized (sWatcherKeyInstances) { if (mWatchKey != null) { sWatcherKeyInstances.remove(mWatchKey); mWatchKey.cancel(); mWatchKey = null; } boolean atLeastOneValid = false; for (WatchKey key : sWatcherKeyInstances.keySet()) { atLeastOneValid |= key.isValid(); } if (!atLeastOneValid) { try { sWatcher.close(); } catch (Exception ignore) { } } } } private void init() { startLoadFromDisk(); } private static long tryGetFileSize(String filename) { try { return SELinuxHelper.getAppDataFileService().getFileSize(filename); } catch (IOException ignored) { return 0; } } private static byte[] tryGetFileHash(String filename) { try { MessageDigest md = MessageDigest.getInstance("MD5"); try (InputStream is = SELinuxHelper.getAppDataFileService().getFileInputStream(filename)) { byte[] buf = new byte[4096]; int read; while ((read = is.read(buf)) != -1) { md.update(buf, 0, read); } } return md.digest(); } catch (Exception ignored) { return new byte[0]; } } /** * Tries to make the preferences file world-readable. * *

Warning: This is only meant to work around permission "fix" functions that are part * of some recoveries. It doesn't replace the need to open preferences with {@code MODE_WORLD_READABLE} * in the module's UI code. Otherwise, Android will set stricter permissions again during the next save. * *

This will only work if executed as root (e.g. {@code initZygote()}) and only if SELinux is disabled. * * @return {@code true} in case the file could be made world-readable. */ @SuppressLint("SetWorldReadable") public boolean makeWorldReadable() { if (!SELinuxHelper.getAppDataFileService().hasDirectFileAccess()) return false; // It doesn't make much sense to make the file readable if we wouldn't be able to access it anyway. if (!mFile.exists()) // Just in case - the file should never be created if it doesn't exist. return false; if (!mFile.setReadable(true, false)) return false; // Watcher service needs read access to parent directory (looks like execute is not enough) if (mFile.getParentFile() != null) { mFile.getParentFile().setReadable(true, false); } if (!mListeners.isEmpty()) { tryRegisterWatcher(); } return true; } /** * Returns the file that is backing these preferences. * *

Warning: The file might not be accessible directly. */ public File getFile() { return mFile; } private void startLoadFromDisk() { synchronized (this) { mLoaded = false; } new Thread("XSharedPreferences-load") { @Override public void run() { synchronized (XSharedPreferences.this) { loadFromDiskLocked(); } } }.start(); } @SuppressWarnings({"rawtypes", "unchecked"}) private void loadFromDiskLocked() { if (mLoaded) { return; } Map map = null; FileResult result = null; try { result = SELinuxHelper.getAppDataFileService().getFileInputStream(mFilename, mFileSize, mLastModified); if (result.stream != null) { map = XmlUtils.readMapXml(result.stream); result.stream.close(); } else { // The file is unchanged, keep the current values map = mMap; } } catch (XmlPullParserException e) { Log.w(TAG, "getSharedPreferences failed for: " + mFilename, e); } catch (FileNotFoundException ignored) { // SharedPreferencesImpl has a canRead() check, so it doesn't log anything in case the file doesn't exist } catch (IOException e) { Log.w(TAG, "getSharedPreferences failed for: " + mFilename, e); } finally { if (result != null && result.stream != null) { try { result.stream.close(); } catch (RuntimeException rethrown) { throw rethrown; } catch (Exception ignored) { } } } mLoaded = true; if (map != null) { mMap = map; mLastModified = result.mtime; mFileSize = result.size; } else { mMap = new HashMap<>(); } notifyAll(); } /** * Reload the settings from file if they have changed. * *

Warning: With enforcing SELinux, this call might be quite expensive. */ public synchronized void reload() { if (hasFileChanged()) { init(); } } /** * Check whether the file has changed since the last time it has been loaded. * *

Warning: With enforcing SELinux, this call might be quite expensive. */ public synchronized boolean hasFileChanged() { try { FileResult result = SELinuxHelper.getAppDataFileService().statFile(mFilename); return mLastModified != result.mtime || mFileSize != result.size; } catch (FileNotFoundException ignored) { // SharedPreferencesImpl doesn't log anything in case the file doesn't exist return true; } catch (IOException e) { Log.w(TAG, "hasFileChanged", e); return true; } } private void awaitLoadedLocked() { while (!mLoaded) { try { wait(); } catch (InterruptedException unused) { } } } /** * @hide */ @Override public Map getAll() { synchronized (this) { awaitLoadedLocked(); return new HashMap<>(mMap); } } /** * @hide */ @Override public String getString(String key, String defValue) { synchronized (this) { awaitLoadedLocked(); String v = (String) mMap.get(key); return v != null ? v : defValue; } } /** * @hide */ @Override @SuppressWarnings("unchecked") public Set getStringSet(String key, Set defValues) { synchronized (this) { awaitLoadedLocked(); Set v = (Set) mMap.get(key); return v != null ? v : defValues; } } /** * @hide */ @Override public int getInt(String key, int defValue) { synchronized (this) { awaitLoadedLocked(); Integer v = (Integer) mMap.get(key); return v != null ? v : defValue; } } /** * @hide */ @Override public long getLong(String key, long defValue) { synchronized (this) { awaitLoadedLocked(); Long v = (Long) mMap.get(key); return v != null ? v : defValue; } } /** * @hide */ @Override public float getFloat(String key, float defValue) { synchronized (this) { awaitLoadedLocked(); Float v = (Float) mMap.get(key); return v != null ? v : defValue; } } /** * @hide */ @Override public boolean getBoolean(String key, boolean defValue) { synchronized (this) { awaitLoadedLocked(); Boolean v = (Boolean) mMap.get(key); return v != null ? v : defValue; } } /** * @hide */ @Override public boolean contains(String key) { synchronized (this) { awaitLoadedLocked(); return mMap.containsKey(key); } } /** * @deprecated Not supported by this implementation. */ @Deprecated @Override public Editor edit() { throw new UnsupportedOperationException("read-only implementation"); } /** * Registers a callback to be invoked when a change happens to a preference file.
* Note that it is not possible to determine which preference changed exactly and thus * preference key in callback invocation will always be null. * * @param listener The callback that will run. * @see #unregisterOnSharedPreferenceChangeListener */ @Override public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { if (listener == null) throw new IllegalArgumentException("listener cannot be null"); synchronized (this) { if (mListeners.put(listener, sContent) == null) { tryRegisterWatcher(); } } } /** * Unregisters a previous callback. * * @param listener The callback that should be unregistered. * @see #registerOnSharedPreferenceChangeListener */ @Override public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized (this) { if (mListeners.remove(listener) != null && mListeners.isEmpty()) { tryUnregisterWatcher(); } } } private static class PrefsData { public final XSharedPreferences mPrefs; private long mSize; private byte[] mHash; public PrefsData(XSharedPreferences prefs) { mPrefs = prefs; mSize = tryGetFileSize(prefs.mFilename); mHash = tryGetFileHash(prefs.mFilename); } public boolean hasChanged() { long size = tryGetFileSize(mPrefs.mFilename); if (size < 1) { if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring empty prefs file"); return false; } if (size != mSize) { mSize = size; mHash = tryGetFileHash(mPrefs.mFilename); if (BuildConfig.DEBUG) Log.d(TAG, "Prefs file size changed"); return true; } byte[] hash = tryGetFileHash(mPrefs.mFilename); if (!Arrays.equals(hash, mHash)) { mHash = hash; if (BuildConfig.DEBUG) Log.d(TAG, "Prefs file hash changed"); return true; } if (BuildConfig.DEBUG) Log.d(TAG, "Prefs file not changed"); return false; } } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/XposedBridge.java ================================================ /* * 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 - 2022 LSPosed Contributors */ package de.robv.android.xposed; import android.app.ActivityThread; import android.content.res.Resources; import android.content.res.TypedArray; import android.util.Log; import org.lsposed.lspd.impl.LSPosedBridge; import org.lsposed.lspd.impl.LSPosedHookCallback; import org.matrix.vector.nativebridge.HookBridge; import org.matrix.vector.nativebridge.ResourcesHook; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import de.robv.android.xposed.callbacks.XC_InitPackageResources; import de.robv.android.xposed.callbacks.XC_LoadPackage; import io.github.libxposed.api.XposedInterface; /** * This class contains most of Xposed's central logic, such as initialization and callbacks used by * the native side. It also includes methods to add new hooks. */ public final class XposedBridge { /** * The system class loader which can be used to locate Android framework classes. * Application classes cannot be retrieved from it. * * @see ClassLoader#getSystemClassLoader */ public static final ClassLoader BOOTCLASSLOADER = XposedBridge.class.getClassLoader(); /** * @hide */ public static final String TAG = "LSPosed-Bridge"; /** * @deprecated Use {@link #getXposedVersion()} instead. */ @Deprecated public static int XPOSED_BRIDGE_VERSION; private static final Object[] EMPTY_ARRAY = new Object[0]; // built-in handlers public static final CopyOnWriteArraySet sLoadedPackageCallbacks = new CopyOnWriteArraySet<>(); /*package*/ static final CopyOnWriteArraySet sInitPackageResourcesCallbacks = new CopyOnWriteArraySet<>(); private XposedBridge() { } public static volatile ClassLoader dummyClassLoader = null; public static void initXResources() { if (dummyClassLoader != null) { return; } try { Resources res = Resources.getSystem(); Class resClass = res.getClass(); Class taClass = TypedArray.class; try { try { TypedArray ta = res.obtainTypedArray(res.getIdentifier( "preloaded_drawables", "array", "android")); taClass = ta.getClass(); ta.recycle(); } catch (NullPointerException npe) { // For ZUI devices, the creation of TypedArray needs to check the configuration // from ActivityThread.currentActivityThread. However, we do not have a valid // ActivityThread for now and the call will throw an NPE. Luckily they check the // nullability of the result configuration. So we hereby set a dummy // ActivityThread to bypass such a situation. var fake = XposedHelpers.newInstance(ActivityThread.class); XposedHelpers.setStaticObjectField(ActivityThread.class, "sCurrentActivityThread", fake); try { TypedArray ta = res.obtainTypedArray(res.getIdentifier( "preloaded_drawables", "array", "android")); taClass = ta.getClass(); ta.recycle(); } finally { XposedHelpers.setStaticObjectField(ActivityThread.class, "sCurrentActivityThread", null); } } } catch (Resources.NotFoundException nfe) { XposedBridge.log(nfe); } ResourcesHook.makeInheritable(resClass); ResourcesHook.makeInheritable(taClass); ClassLoader myCL = XposedBridge.class.getClassLoader(); assert myCL != null; dummyClassLoader = ResourcesHook.buildDummyClassLoader(myCL.getParent(), resClass.getName(), taClass.getName()); dummyClassLoader.loadClass("xposed.dummy.XResourcesSuperClass"); dummyClassLoader.loadClass("xposed.dummy.XTypedArraySuperClass"); XposedHelpers.setObjectField(myCL, "parent", dummyClassLoader); } catch (Throwable throwable) { XposedBridge.log(throwable); XposedInit.disableResources = true; } } /** * Returns the currently installed version of the Xposed framework. */ public static int getXposedVersion() { return XposedInterface.API; } /** * Writes a message to the Xposed modules log. * *

DON'T FLOOD THE LOG!!! This is only meant for error logging. * If you want to write information/debug messages, use logcat. * * @param text The log message. */ public synchronized static void log(String text) { Log.i(TAG, text); } /** * Logs a stack trace to the Xposed modules log. * *

DON'T FLOOD THE LOG!!! This is only meant for error logging. * If you want to write information/debug messages, use logcat. * * @param t The Throwable object for the stack trace. */ public synchronized static void log(Throwable t) { String logStr = Log.getStackTraceString(t); Log.e(TAG, logStr); } /** * Deoptimize a method to avoid callee being inlined. * * @param deoptimizedMethod The method to deoptmize. Generally it should be a caller of a method * that is inlined. */ public static void deoptimizeMethod(Member deoptimizedMethod) { if (!(deoptimizedMethod instanceof Executable)) { throw new IllegalArgumentException("Only methods and constructors can be deoptimized: " + deoptimizedMethod); } else if (Modifier.isAbstract(deoptimizedMethod.getModifiers())) { throw new IllegalArgumentException("Cannot deoptimize abstract methods: " + deoptimizedMethod); } else if (Proxy.isProxyClass(deoptimizedMethod.getDeclaringClass())) { throw new IllegalArgumentException("Cannot deoptimize methods from proxy class: " + deoptimizedMethod); } HookBridge.deoptimizeMethod((Executable) deoptimizedMethod); } /** * Hook any method (or constructor) with the specified callback. See below for some wrappers * that make it easier to find a method/constructor in one step. * * @param hookMethod The method to be hooked. * @param callback The callback to be executed when the hooked method is called. * @return An object that can be used to remove the hook. * @see XposedHelpers#findAndHookMethod(String, ClassLoader, String, Object...) * @see XposedHelpers#findAndHookMethod(Class, String, Object...) * @see #hookAllMethods * @see XposedHelpers#findAndHookConstructor(String, ClassLoader, Object...) * @see XposedHelpers#findAndHookConstructor(Class, Object...) * @see #hookAllConstructors */ public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) { if (!(hookMethod instanceof Executable)) { throw new IllegalArgumentException("Only methods and constructors can be hooked: " + hookMethod); } else if (Modifier.isAbstract(hookMethod.getModifiers())) { throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod); } else if (hookMethod.getDeclaringClass().getClassLoader() == XposedBridge.class.getClassLoader()) { throw new IllegalArgumentException("Do not allow hooking inner methods"); } else if (hookMethod.getDeclaringClass() == Method.class && hookMethod.getName().equals("invoke")) { throw new IllegalArgumentException("Cannot hook Method.invoke"); } if (callback == null) { throw new IllegalArgumentException("callback should not be null!"); } if (!HookBridge.hookMethod(false, (Executable) hookMethod, LSPosedBridge.NativeHooker.class, callback.priority, callback)) { log("Failed to hook " + hookMethod); return null; } return callback.new Unhook(hookMethod); } /** * Removes the callback for a hooked method/constructor. * * @param hookMethod The method for which the callback should be removed. * @param callback The reference to the callback as specified in {@link #hookMethod}. * @deprecated Use {@link XC_MethodHook.Unhook#unhook} instead. An instance of the {@code Unhook} * class is returned when you hook the method. */ @Deprecated public static void unhookMethod(Member hookMethod, XC_MethodHook callback) { if (hookMethod instanceof Executable) { HookBridge.unhookMethod(false, (Executable) hookMethod, callback); } } /** * Hooks all methods with a certain name that were declared in the specified class. Inherited * methods and constructors are not considered. For constructors, use * {@link #hookAllConstructors} instead. * * @param hookClass The class to check for declared methods. * @param methodName The name of the method(s) to hook. * @param callback The callback to be executed when the hooked methods are called. * @return A set containing one object for each found method which can be used to unhook it. */ @SuppressWarnings("UnusedReturnValue") public static Set hookAllMethods(Class hookClass, String methodName, XC_MethodHook callback) { Set unhooks = new HashSet<>(); for (Member method : hookClass.getDeclaredMethods()) if (method.getName().equals(methodName)) unhooks.add(hookMethod(method, callback)); return unhooks; } /** * Hook all constructors of the specified class. * * @param hookClass The class to check for constructors. * @param callback The callback to be executed when the hooked constructors are called. * @return A set containing one object for each found constructor which can be used to unhook it. */ @SuppressWarnings("UnusedReturnValue") public static Set hookAllConstructors(Class hookClass, XC_MethodHook callback) { Set unhooks = new HashSet<>(); for (Member constructor : hookClass.getDeclaredConstructors()) unhooks.add(hookMethod(constructor, callback)); return unhooks; } /** * Adds a callback to be executed when an app ("Android package") is loaded. * *

You probably don't need to call this. Simply implement {@link IXposedHookLoadPackage} * in your module class and Xposed will take care of registering it as a callback. * * @param callback The callback to be executed. * @hide */ public static void hookLoadPackage(XC_LoadPackage callback) { synchronized (sLoadedPackageCallbacks) { sLoadedPackageCallbacks.add(callback); } } /** * Adds a callback to be executed when the resources for an app are initialized. * *

You probably don't need to call this. Simply implement {@link IXposedHookInitPackageResources} * in your module class and Xposed will take care of registering it as a callback. * * @param callback The callback to be executed. * @hide */ public static void hookInitPackageResources(XC_InitPackageResources callback) { synchronized (sInitPackageResourcesCallbacks) { sInitPackageResourcesCallbacks.add(callback); } } /** * Basically the same as {@link Method#invoke}, but calls the original method * as it was before the interception by Xposed. Also, access permissions are not checked. * *

There are very few cases where this method is needed. A common mistake is * to replace a method and then invoke the original one based on dynamic conditions. This * creates overhead and skips further hooks by other modules. Instead, just hook (don't replace) * the method and call {@code param.setResult(null)} in {@link XC_MethodHook#beforeHookedMethod} * if the original method should be skipped. * * @param method The method to be called. * @param thisObject For non-static calls, the "this" pointer, otherwise {@code null}. * @param args Arguments for the method call as Object[] array. * @return The result returned from the invoked method. * @throws NullPointerException if {@code receiver == null} for a non-static method * @throws IllegalAccessException if this method is not accessible (see {@link AccessibleObject}) * @throws IllegalArgumentException if the number of arguments doesn't match the number of parameters, the receiver * is incompatible with the declaring class, or an argument could not be unboxed * or converted by a widening conversion to the corresponding parameter type * @throws InvocationTargetException if an exception was thrown by the invoked method */ public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args) throws Throwable { if (args == null) { args = EMPTY_ARRAY; } if (!(method instanceof Executable)) { throw new IllegalArgumentException("method must be of type Method or Constructor"); } return HookBridge.invokeOriginalMethod((Executable) method, thisObject, args); } /** * @hide */ public static final class CopyOnWriteSortedSet { private transient volatile Object[] elements = EMPTY_ARRAY; @SuppressWarnings("UnusedReturnValue") public synchronized boolean add(E e) { int index = indexOf(e); if (index >= 0) return false; Object[] newElements = new Object[elements.length + 1]; System.arraycopy(elements, 0, newElements, 0, elements.length); newElements[elements.length] = e; Arrays.sort(newElements); elements = newElements; return true; } @SuppressWarnings("UnusedReturnValue") public synchronized boolean remove(E e) { int index = indexOf(e); if (index == -1) return false; Object[] newElements = new Object[elements.length - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, elements.length - index - 1); elements = newElements; return true; } private int indexOf(Object o) { for (int i = 0; i < elements.length; i++) { if (o.equals(elements[i])) return i; } return -1; } public Object[] getSnapshot() { return elements; } public T[] getSnapshot(T[] a) { var snapshot = getSnapshot(); return (T[]) Arrays.copyOf(snapshot, snapshot.length, a.getClass()); } public synchronized void clear() { elements = EMPTY_ARRAY; } } public static class LegacyApiSupport { private final XC_MethodHook.MethodHookParam param; private final LSPosedHookCallback callback; private final Object[] snapshot; private int beforeIdx; public LegacyApiSupport(LSPosedHookCallback callback, Object[] legacySnapshot) { this.param = new XC_MethodHook.MethodHookParam<>(); this.callback = callback; this.snapshot = legacySnapshot; } public void handleBefore() { syncronizeApi(param, callback, true); for (beforeIdx = 0; beforeIdx < snapshot.length; beforeIdx++) { try { var cb = (XC_MethodHook) snapshot[beforeIdx]; cb.beforeHookedMethod(param); } catch (Throwable t) { XposedBridge.log(t); // reset result (ignoring what the unexpectedly exiting callback did) param.setResult(null); param.returnEarly = false; continue; } if (param.returnEarly) { // skip remaining "before" callbacks and corresponding "after" callbacks beforeIdx++; break; } } syncronizeApi(param, callback, false); } public void handleAfter() { syncronizeApi(param, callback, true); for (int afterIdx = beforeIdx - 1; afterIdx >= 0; afterIdx--) { Object lastResult = param.getResult(); Throwable lastThrowable = param.getThrowable(); try { var cb = (XC_MethodHook) snapshot[afterIdx]; cb.afterHookedMethod(param); } catch (Throwable t) { XposedBridge.log(t); // reset to last result (ignoring what the unexpectedly exiting callback did) if (lastThrowable == null) { param.setResult(lastResult); } else { param.setThrowable(lastThrowable); } } } syncronizeApi(param, callback, false); } private void syncronizeApi(XC_MethodHook.MethodHookParam param, LSPosedHookCallback callback, boolean forward) { if (forward) { param.method = callback.method; param.thisObject = callback.thisObject; param.args = callback.args; param.result = callback.result; param.throwable = callback.throwable; param.returnEarly = callback.isSkipped; } else { callback.method = param.method; callback.thisObject = param.thisObject; callback.args = param.args; callback.result = param.result; callback.throwable = param.throwable; callback.isSkipped = param.returnEarly; } } } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/XposedHelpers.java ================================================ /* * 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 */ package de.robv.android.xposed; import android.content.res.AssetManager; import android.content.res.Resources; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.apache.commons.lang3.ClassUtilsX; import org.apache.commons.lang3.reflect.MemberUtilsX; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * Helpers that simplify hooking and calling methods/constructors, getting and settings fields, ... */ public final class XposedHelpers { private XposedHelpers() { } private static final ConcurrentHashMap> fieldCache = new ConcurrentHashMap<>(); private static final ConcurrentHashMap> methodCache = new ConcurrentHashMap<>(); private static final ConcurrentHashMap>> constructorCache = new ConcurrentHashMap<>(); private static final WeakHashMap> additionalFields = new WeakHashMap<>(); private static final HashMap> sMethodDepth = new HashMap<>(); /** * Note that we use object key instead of string here, because string calculation will lose all * the benefits of 'HashMap', this is basically the solution of performance traps. *

* So in fact we only need to use the structural comparison results of the reflection object. * * @see benchmarks for ART * @see benchmarks for JVM */ private abstract static class MemberCacheKey { private final int hash; protected MemberCacheKey(int hash) { this.hash = hash; } @Override public abstract boolean equals(@Nullable Object obj); @Override public final int hashCode() { return hash; } static final class Constructor extends MemberCacheKey { private final Class clazz; private final Class[] parameters; private final boolean isExact; public Constructor(Class clazz, Class[] parameters, boolean isExact) { super(31 * Objects.hash(clazz, isExact) + Arrays.hashCode(parameters)); this.clazz = clazz; this.parameters = parameters; this.isExact = isExact; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Constructor)) return false; Constructor that = (Constructor) o; return isExact == that.isExact && Objects.equals(clazz, that.clazz) && Arrays.equals(parameters, that.parameters); } @NonNull @Override public String toString() { var str = clazz.getName() + getParametersString(parameters); if (isExact) { return str + "#exact"; } else { return str; } } } static final class Field extends MemberCacheKey { private final Class clazz; private final String name; public Field(Class clazz, String name) { super(Objects.hash(clazz, name)); this.clazz = clazz; this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Field)) return false; Field field = (Field) o; return Objects.equals(clazz, field.clazz) && Objects.equals(name, field.name); } @NonNull @Override public String toString() { return clazz.getName() + "#" + name; } } static final class Method extends MemberCacheKey { private final Class clazz; private final String name; private final Class[] parameters; private final boolean isExact; public Method(Class clazz, String name, Class[] parameters, boolean isExact) { super(31 * Objects.hash(clazz, name, isExact) + Arrays.hashCode(parameters)); this.clazz = clazz; this.name = name; this.parameters = parameters; this.isExact = isExact; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Method)) return false; Method method = (Method) o; return isExact == method.isExact && Objects.equals(clazz, method.clazz) && Objects.equals(name, method.name) && Arrays.equals(parameters, method.parameters); } @NonNull @Override public String toString() { var str = clazz.getName() + '#' + name + getParametersString(parameters); if (isExact) { return str + "#exact"; } else { return str; } } } } /** * Look up a class with the specified class loader. * *

There are various allowed syntaxes for the class name, but it's recommended to use one of * these: *

    *
  • {@code java.lang.String} *
  • {@code java.lang.String[]} (array) *
  • {@code android.app.ActivityThread.ResourcesKey} *
  • {@code android.app.ActivityThread$ResourcesKey} *
* * @param className The class name in one of the formats mentioned above. * @param classLoader The class loader, or {@code null} for the boot class loader. * @return A reference to the class. * @throws ClassNotFoundError In case the class was not found. */ public static Class findClass(String className, ClassLoader classLoader) { if (classLoader == null) classLoader = XposedBridge.BOOTCLASSLOADER; try { return ClassUtilsX.getClass(classLoader, className, false); } catch (ClassNotFoundException e) { throw new ClassNotFoundError(e); } } /** * Look up and return a class if it exists. * Like {@link #findClass}, but doesn't throw an exception if the class doesn't exist. * * @param className The class name. * @param classLoader The class loader, or {@code null} for the boot class loader. * @return A reference to the class, or {@code null} if it doesn't exist. */ public static Class findClassIfExists(String className, ClassLoader classLoader) { try { return findClass(className, classLoader); } catch (ClassNotFoundError e) { return null; } } /** * Look up a field in a class and set it to accessible. * * @param clazz The class which either declares or inherits the field. * @param fieldName The field name. * @return A reference to the field. * @throws NoSuchFieldError In case the field was not found. */ public static Field findField(Class clazz, String fieldName) { var key = new MemberCacheKey.Field(clazz, fieldName); return fieldCache.computeIfAbsent(key, k -> { try { Field newField = findFieldRecursiveImpl(k.clazz, k.name); newField.setAccessible(true); return Optional.of(newField); } catch (NoSuchFieldException e) { return Optional.empty(); } }).orElseThrow(() -> new NoSuchFieldError(key.toString())); } /** * Look up and return a field if it exists. * Like {@link #findField}, but doesn't throw an exception if the field doesn't exist. * * @param clazz The class which either declares or inherits the field. * @param fieldName The field name. * @return A reference to the field, or {@code null} if it doesn't exist. */ public static Field findFieldIfExists(Class clazz, String fieldName) { try { return findField(clazz, fieldName); } catch (NoSuchFieldError e) { return null; } } private static Field findFieldRecursiveImpl(Class clazz, String fieldName) throws NoSuchFieldException { try { return clazz.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { while (true) { clazz = clazz.getSuperclass(); if (clazz == null || clazz.equals(Object.class)) break; try { return clazz.getDeclaredField(fieldName); } catch (NoSuchFieldException ignored) { } } throw e; } } /** * Returns the first field of the given type in a class. * Might be useful for Proguard'ed classes to identify fields with unique types. * * @param clazz The class which either declares or inherits the field. * @param type The type of the field. * @return A reference to the first field of the given type. * @throws NoSuchFieldError In case no matching field was not found. */ public static Field findFirstFieldByExactType(Class clazz, Class type) { Class clz = clazz; do { for (Field field : clz.getDeclaredFields()) { if (field.getType() == type) { field.setAccessible(true); return field; } } } while ((clz = clz.getSuperclass()) != null); throw new NoSuchFieldError("Field of type " + type.getName() + " in class " + clazz.getName()); } /** * Look up a method and hook it. See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} * for details. */ public static XC_MethodHook.Unhook findAndHookMethod(Class clazz, String methodName, Object... parameterTypesAndCallback) { if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length - 1] instanceof XC_MethodHook)) throw new IllegalArgumentException("no callback defined"); XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length - 1]; Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback)); return XposedBridge.hookMethod(m, callback); } /** * Look up a method and hook it. The last argument must be the callback for the hook. * *

This combines calls to {@link #findMethodExact(Class, String, Object...)} and * {@link XposedBridge#hookMethod}. * *

The method must be declared or overridden in the given class, inherited * methods are not considered! That's because each method implementation exists only once in * the memory, and when classes inherit it, they just get another reference to the implementation. * Hooking a method therefore applies to all classes inheriting the same implementation. You * have to expect that the hook applies to subclasses (unless they override the method), but you * shouldn't have to worry about hooks applying to superclasses, hence this "limitation". * There could be undesired or even dangerous hooks otherwise, e.g. if you hook * {@code SomeClass.equals()} and that class doesn't override the {@code equals()} on some ROMs, * making you hook {@code Object.equals()} instead. * *

There are two ways to specify the parameter types. If you already have a reference to the * {@link Class}, use that. For Android framework classes, you can often use something like * {@code String.class}. If you don't have the class reference, you can simply use the * full class name as a string, e.g. {@code java.lang.String} or {@code com.example.MyClass}. * It will be passed to {@link #findClass} with the same class loader that is used for the target * method, see its documentation for the allowed notations. * *

Primitive types, such as {@code int}, can be specified using {@code int.class} (recommended) * or {@code Integer.TYPE}. Note that {@code Integer.class} doesn't refer to {@code int} but to * {@code Integer}, which is a normal class (boxed primitive). Therefore it must not be used when * the method expects an {@code int} parameter - it has to be used for {@code Integer} parameters * though, so check the method signature in detail. * *

As last argument to this method (after the list of target method parameters), you need * to specify the callback that should be executed when the method is invoked. It's usually * an anonymous subclass of {@link XC_MethodHook} or {@link XC_MethodReplacement}. * *

Example *

     * // In order to hook this method ...
     * package com.example;
     * public class SomeClass {
     *   public int doSomething(String s, int i, MyClass m) {
     *     ...
     *   }
     * }
     *
     * // ... you can use this call:
     * findAndHookMethod("com.example.SomeClass", lpparam.classLoader, String.class, int.class, "com.example.MyClass", new XC_MethodHook() {
     *   @Override
     *   protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
     *     String oldText = (String) param.args[0];
     *     Log.d("MyModule", oldText);
     *
     *     param.args[0] = "test";
     *     param.args[1] = 42; // auto-boxing is working here
     *     setBooleanField(param.args[2], "great", true);
     *
     *     // This would not work (as MyClass can't be resolved at compile time):
     *     //   MyClass myClass = (MyClass) param.args[2];
     *     //   myClass.great = true;
     *   }
     * });
     * 
* * @param className The name of the class which implements the method. * @param classLoader The class loader for resolving the target and parameter classes. * @param methodName The target method name. * @param parameterTypesAndCallback The parameter types of the target method, plus the callback. * @return An object which can be used to remove the callback again. * @throws NoSuchMethodError In case the method was not found. * @throws ClassNotFoundError In case the target class or one of the parameter types couldn't be resolved. */ public static XC_MethodHook.Unhook findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback) { return findAndHookMethod(findClass(className, classLoader), methodName, parameterTypesAndCallback); } /** * Look up a method in a class and set it to accessible. * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. */ public static Method findMethodExact(Class clazz, String methodName, Object... parameterTypes) { return findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypes)); } /** * Look up and return a method if it exists. * See {@link #findMethodExactIfExists(String, ClassLoader, String, Object...)} for details. */ public static Method findMethodExactIfExists(Class clazz, String methodName, Object... parameterTypes) { try { return findMethodExact(clazz, methodName, parameterTypes); } catch (ClassNotFoundError | NoSuchMethodError e) { return null; } } /** * Look up a method in a class and set it to accessible. * The method must be declared or overridden in the given class. * *

See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} for details about * the method and parameter type resolution. * * @param className The name of the class which implements the method. * @param classLoader The class loader for resolving the target and parameter classes. * @param methodName The target method name. * @param parameterTypes The parameter types of the target method. * @return A reference to the method. * @throws NoSuchMethodError In case the method was not found. * @throws ClassNotFoundError In case the target class or one of the parameter types couldn't be resolved. */ public static Method findMethodExact(String className, ClassLoader classLoader, String methodName, Object... parameterTypes) { return findMethodExact(findClass(className, classLoader), methodName, getParameterClasses(classLoader, parameterTypes)); } /** * Look up and return a method if it exists. * Like {@link #findMethodExact(String, ClassLoader, String, Object...)}, but doesn't throw an * exception if the method doesn't exist. * * @param className The name of the class which implements the method. * @param classLoader The class loader for resolving the target and parameter classes. * @param methodName The target method name. * @param parameterTypes The parameter types of the target method. * @return A reference to the method, or {@code null} if it doesn't exist. */ public static Method findMethodExactIfExists(String className, ClassLoader classLoader, String methodName, Object... parameterTypes) { try { return findMethodExact(className, classLoader, methodName, parameterTypes); } catch (ClassNotFoundError | NoSuchMethodError e) { return null; } } /** * Look up a method in a class and set it to accessible. * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. * *

This variant requires that you already have reference to all the parameter types. */ public static Method findMethodExact(Class clazz, String methodName, Class... parameterTypes) { var key = new MemberCacheKey.Method(clazz, methodName, parameterTypes, true); return methodCache.computeIfAbsent(key, k -> { try { Method method = k.clazz.getDeclaredMethod(k.name, k.parameters); method.setAccessible(true); return Optional.of(method); } catch (NoSuchMethodException e) { return Optional.empty(); } }).orElseThrow(() -> new NoSuchMethodError(key.toString())); } /** * Returns an array of all methods declared/overridden in a class with the specified parameter types. * *

The return type is optional, it will not be compared if it is {@code null}. * Use {@code void.class} if you want to search for methods returning nothing. * * @param clazz The class to look in. * @param returnType The return type, or {@code null} (see above). * @param parameterTypes The parameter types. * @return An array with matching methods, all set to accessible already. */ public static Method[] findMethodsByExactParameters(Class clazz, Class returnType, Class... parameterTypes) { List result = new LinkedList<>(); for (Method method : clazz.getDeclaredMethods()) { if (returnType != null && returnType != method.getReturnType()) continue; Class[] methodParameterTypes = method.getParameterTypes(); if (parameterTypes.length != methodParameterTypes.length) continue; boolean match = true; for (int i = 0; i < parameterTypes.length; i++) { if (parameterTypes[i] != methodParameterTypes[i]) { match = false; break; } } if (!match) continue; method.setAccessible(true); result.add(method); } return result.toArray(new Method[result.size()]); } /** * Look up a method in a class and set it to accessible. * *

This does'nt only look for exact matches, but for the best match. All considered candidates * must be compatible with the given parameter types, i.e. the parameters must be assignable * to the method's formal parameters. Inherited methods are considered here. * * @param clazz The class which declares, inherits or overrides the method. * @param methodName The method name. * @param parameterTypes The types of the method's parameters. * @return A reference to the best-matching method. * @throws NoSuchMethodError In case no suitable method was found. */ public static Method findMethodBestMatch(Class clazz, String methodName, Class... parameterTypes) { // find the exact matching method first try { return findMethodExact(clazz, methodName, parameterTypes); } catch (NoSuchMethodError ignored) { } // then find the best match var key = new MemberCacheKey.Method(clazz, methodName, parameterTypes, false); return methodCache.computeIfAbsent(key, k -> { Method bestMatch = null; Class clz = k.clazz; boolean considerPrivateMethods = true; do { for (Method method : clz.getDeclaredMethods()) { // don't consider private methods of superclasses if (!considerPrivateMethods && Modifier.isPrivate(method.getModifiers())) continue; // compare name and parameters if (method.getName().equals(k.name) && ClassUtilsX.isAssignable( k.parameters, method.getParameterTypes(), true)) { // get accessible version of method if (bestMatch == null || MemberUtilsX.compareMethodFit( method, bestMatch, k.parameters) < 0) { bestMatch = method; } } } considerPrivateMethods = false; } while ((clz = clz.getSuperclass()) != null); if (bestMatch != null) { bestMatch.setAccessible(true); return Optional.of(bestMatch); } else { return Optional.empty(); } }).orElseThrow(() -> new NoSuchMethodError(key.toString())); } /** * Look up a method in a class and set it to accessible. * *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant * determines the parameter types from the classes of the given objects. */ public static Method findMethodBestMatch(Class clazz, String methodName, Object... args) { return findMethodBestMatch(clazz, methodName, getParameterTypes(args)); } /** * Look up a method in a class and set it to accessible. * *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant * determines the parameter types from the classes of the given objects. For any item that is * {@code null}, the type is taken from {@code parameterTypes} instead. */ public static Method findMethodBestMatch(Class clazz, String methodName, Class[] parameterTypes, Object[] args) { Class[] argsClasses = null; for (int i = 0; i < parameterTypes.length; i++) { if (parameterTypes[i] != null) continue; if (argsClasses == null) argsClasses = getParameterTypes(args); parameterTypes[i] = argsClasses[i]; } return findMethodBestMatch(clazz, methodName, parameterTypes); } /** * Returns an array with the classes of the given objects. */ public static Class[] getParameterTypes(Object... args) { Class[] clazzes = new Class[args.length]; for (int i = 0; i < args.length; i++) { clazzes[i] = (args[i] != null) ? args[i].getClass() : null; } return clazzes; } /** * Retrieve classes from an array, where each element might either be a Class * already, or a String with the full class name. */ private static Class[] getParameterClasses(ClassLoader classLoader, Object[] parameterTypesAndCallback) { Class[] parameterClasses = null; for (int i = parameterTypesAndCallback.length - 1; i >= 0; i--) { Object type = parameterTypesAndCallback[i]; if (type == null) throw new ClassNotFoundError("parameter type must not be null", null); // ignore trailing callback if (type instanceof XC_MethodHook) continue; if (parameterClasses == null) parameterClasses = new Class[i + 1]; if (type instanceof Class) parameterClasses[i] = (Class) type; else if (type instanceof String) parameterClasses[i] = findClass((String) type, classLoader); else throw new ClassNotFoundError("parameter type must either be specified as Class or String", null); } // if there are no arguments for the method if (parameterClasses == null) parameterClasses = new Class[0]; return parameterClasses; } /** * Returns an array of the given classes. */ public static Class[] getClassesAsArray(Class... clazzes) { return clazzes; } private static String getParametersString(Class... clazzes) { StringBuilder sb = new StringBuilder("("); boolean first = true; for (Class clazz : clazzes) { if (first) first = false; else sb.append(","); if (clazz != null) sb.append(clazz.getCanonicalName()); else sb.append("null"); } sb.append(")"); return sb.toString(); } /** * Look up a constructor of a class and set it to accessible. * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. */ public static Constructor findConstructorExact(Class clazz, Object... parameterTypes) { return findConstructorExact(clazz, getParameterClasses(clazz.getClassLoader(), parameterTypes)); } /** * Look up and return a constructor if it exists. * See {@link #findMethodExactIfExists(String, ClassLoader, String, Object...)} for details. */ public static Constructor findConstructorExactIfExists(Class clazz, Object... parameterTypes) { try { return findConstructorExact(clazz, parameterTypes); } catch (ClassNotFoundError | NoSuchMethodError e) { return null; } } /** * Look up a constructor of a class and set it to accessible. * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. */ public static Constructor findConstructorExact(String className, ClassLoader classLoader, Object... parameterTypes) { return findConstructorExact(findClass(className, classLoader), getParameterClasses(classLoader, parameterTypes)); } /** * Look up and return a constructor if it exists. * See {@link #findMethodExactIfExists(String, ClassLoader, String, Object...)} for details. */ public static Constructor findConstructorExactIfExists(String className, ClassLoader classLoader, Object... parameterTypes) { try { return findConstructorExact(className, classLoader, parameterTypes); } catch (ClassNotFoundError | NoSuchMethodError e) { return null; } } /** * Look up a constructor of a class and set it to accessible. * See {@link #findMethodExact(String, ClassLoader, String, Object...)} for details. */ public static Constructor findConstructorExact(Class clazz, Class... parameterTypes) { var key = new MemberCacheKey.Constructor(clazz, parameterTypes, true); return constructorCache.computeIfAbsent(key, k -> { try { Constructor constructor = k.clazz.getDeclaredConstructor(k.parameters); constructor.setAccessible(true); return Optional.of(constructor); } catch (NoSuchMethodException e) { return Optional.empty(); } }).orElseThrow(() -> new NoSuchMethodError(key.toString())); } /** * Look up a constructor and hook it. See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} * for details. */ public static XC_MethodHook.Unhook findAndHookConstructor(Class clazz, Object... parameterTypesAndCallback) { if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length - 1] instanceof XC_MethodHook)) throw new IllegalArgumentException("no callback defined"); XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length - 1]; Constructor m = findConstructorExact(clazz, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback)); return XposedBridge.hookMethod(m, callback); } /** * Look up a constructor and hook it. See {@link #findAndHookMethod(String, ClassLoader, String, Object...)} * for details. */ public static XC_MethodHook.Unhook findAndHookConstructor(String className, ClassLoader classLoader, Object... parameterTypesAndCallback) { return findAndHookConstructor(findClass(className, classLoader), parameterTypesAndCallback); } /** * Look up a constructor in a class and set it to accessible. * *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. */ public static Constructor findConstructorBestMatch(Class clazz, Class... parameterTypes) { // find the exact matching constructor first try { return findConstructorExact(clazz, parameterTypes); } catch (NoSuchMethodError ignored) { } // then find the best match var key = new MemberCacheKey.Constructor(clazz, parameterTypes, false); return constructorCache.computeIfAbsent(key, k -> { Constructor bestMatch = null; Constructor[] constructors = k.clazz.getDeclaredConstructors(); for (Constructor constructor : constructors) { // compare name and parameters if (ClassUtilsX.isAssignable( k.parameters, constructor.getParameterTypes(), true)) { // get accessible version of method if (bestMatch == null || MemberUtilsX.compareConstructorFit( constructor, bestMatch, k.parameters) < 0) { bestMatch = constructor; } } } if (bestMatch != null) { bestMatch.setAccessible(true); return Optional.of(bestMatch); } else { return Optional.empty(); } }).orElseThrow(() -> new NoSuchMethodError(key.toString())); } /** * Look up a constructor in a class and set it to accessible. * *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant * determines the parameter types from the classes of the given objects. */ public static Constructor findConstructorBestMatch(Class clazz, Object... args) { return findConstructorBestMatch(clazz, getParameterTypes(args)); } /** * Look up a constructor in a class and set it to accessible. * *

See {@link #findMethodBestMatch(Class, String, Class...)} for details. This variant * determines the parameter types from the classes of the given objects. For any item that is * {@code null}, the type is taken from {@code parameterTypes} instead. */ public static Constructor findConstructorBestMatch(Class clazz, Class[] parameterTypes, Object[] args) { Class[] argsClasses = null; for (int i = 0; i < parameterTypes.length; i++) { if (parameterTypes[i] != null) continue; if (argsClasses == null) argsClasses = getParameterTypes(args); parameterTypes[i] = argsClasses[i]; } return findConstructorBestMatch(clazz, parameterTypes); } /** * Thrown when a class loader is unable to find a class. Unlike {@link ClassNotFoundException}, * callers are not forced to explicitly catch this. If uncaught, the error will be passed to the * next caller in the stack. */ public static final class ClassNotFoundError extends Error { private static final long serialVersionUID = -1070936889459514628L; /** * @hide */ public ClassNotFoundError(Throwable cause) { super(cause); } /** * @hide */ public ClassNotFoundError(String detailMessage, Throwable cause) { super(detailMessage, cause); } } /** * Returns the index of the first parameter declared with the given type. * * @throws NoSuchFieldError if there is no parameter with that type. * @hide */ public static int getFirstParameterIndexByType(Member method, Class type) { Class[] classes = (method instanceof Method) ? ((Method) method).getParameterTypes() : ((Constructor) method).getParameterTypes(); for (int i = 0; i < classes.length; i++) { if (classes[i] == type) { return i; } } throw new NoSuchFieldError("No parameter of type " + type + " found in " + method); } /** * Returns the index of the parameter declared with the given type, ensuring that there is exactly one such parameter. * * @throws NoSuchFieldError if there is no or more than one parameter with that type. * @hide */ public static int getParameterIndexByType(Member method, Class type) { Class[] classes = (method instanceof Method) ? ((Method) method).getParameterTypes() : ((Constructor) method).getParameterTypes(); int idx = -1; for (int i = 0; i < classes.length; i++) { if (classes[i] == type) { if (idx == -1) { idx = i; } else { throw new NoSuchFieldError("More than one parameter of type " + type + " found in " + method); } } } if (idx != -1) { return idx; } else { throw new NoSuchFieldError("No parameter of type " + type + " found in " + method); } } //################################################################################################# /** * Sets the value of an object field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static void setObjectField(Object obj, String fieldName, Object value) { try { findField(obj.getClass(), fieldName).set(obj, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a {@code boolean} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static void setBooleanField(Object obj, String fieldName, boolean value) { try { findField(obj.getClass(), fieldName).setBoolean(obj, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a {@code byte} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static void setByteField(Object obj, String fieldName, byte value) { try { findField(obj.getClass(), fieldName).setByte(obj, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a {@code char} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static void setCharField(Object obj, String fieldName, char value) { try { findField(obj.getClass(), fieldName).setChar(obj, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a {@code double} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static void setDoubleField(Object obj, String fieldName, double value) { try { findField(obj.getClass(), fieldName).setDouble(obj, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a {@code float} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static void setFloatField(Object obj, String fieldName, float value) { try { findField(obj.getClass(), fieldName).setFloat(obj, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of an {@code int} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static void setIntField(Object obj, String fieldName, int value) { try { findField(obj.getClass(), fieldName).setInt(obj, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a {@code long} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static void setLongField(Object obj, String fieldName, long value) { try { findField(obj.getClass(), fieldName).setLong(obj, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a {@code short} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static void setShortField(Object obj, String fieldName, short value) { try { findField(obj.getClass(), fieldName).setShort(obj, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } //################################################################################################# /** * Returns the value of an object field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static Object getObjectField(Object obj, String fieldName) { try { return findField(obj.getClass(), fieldName).get(obj); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * For inner classes, returns the surrounding instance, i.e. the {@code this} reference of the surrounding class. */ public static Object getSurroundingThis(Object obj) { return getObjectField(obj, "this$0"); } /** * Returns the value of a {@code boolean} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean getBooleanField(Object obj, String fieldName) { try { return findField(obj.getClass(), fieldName).getBoolean(obj); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Returns the value of a {@code byte} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static byte getByteField(Object obj, String fieldName) { try { return findField(obj.getClass(), fieldName).getByte(obj); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Returns the value of a {@code char} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static char getCharField(Object obj, String fieldName) { try { return findField(obj.getClass(), fieldName).getChar(obj); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Returns the value of a {@code double} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static double getDoubleField(Object obj, String fieldName) { try { return findField(obj.getClass(), fieldName).getDouble(obj); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Returns the value of a {@code float} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static float getFloatField(Object obj, String fieldName) { try { return findField(obj.getClass(), fieldName).getFloat(obj); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Returns the value of an {@code int} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static int getIntField(Object obj, String fieldName) { try { return findField(obj.getClass(), fieldName).getInt(obj); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Returns the value of a {@code long} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static long getLongField(Object obj, String fieldName) { try { return findField(obj.getClass(), fieldName).getLong(obj); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Returns the value of a {@code short} field in the given object instance. A class reference is not sufficient! See also {@link #findField}. */ public static short getShortField(Object obj, String fieldName) { try { return findField(obj.getClass(), fieldName).getShort(obj); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } //################################################################################################# /** * Sets the value of a static object field in the given class. See also {@link #findField}. */ public static void setStaticObjectField(Class clazz, String fieldName, Object value) { try { findField(clazz, fieldName).set(null, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code boolean} field in the given class. See also {@link #findField}. */ public static void setStaticBooleanField(Class clazz, String fieldName, boolean value) { try { findField(clazz, fieldName).setBoolean(null, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code byte} field in the given class. See also {@link #findField}. */ public static void setStaticByteField(Class clazz, String fieldName, byte value) { try { findField(clazz, fieldName).setByte(null, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code char} field in the given class. See also {@link #findField}. */ public static void setStaticCharField(Class clazz, String fieldName, char value) { try { findField(clazz, fieldName).setChar(null, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code double} field in the given class. See also {@link #findField}. */ public static void setStaticDoubleField(Class clazz, String fieldName, double value) { try { findField(clazz, fieldName).setDouble(null, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code float} field in the given class. See also {@link #findField}. */ public static void setStaticFloatField(Class clazz, String fieldName, float value) { try { findField(clazz, fieldName).setFloat(null, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code int} field in the given class. See also {@link #findField}. */ public static void setStaticIntField(Class clazz, String fieldName, int value) { try { findField(clazz, fieldName).setInt(null, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code long} field in the given class. See also {@link #findField}. */ public static void setStaticLongField(Class clazz, String fieldName, long value) { try { findField(clazz, fieldName).setLong(null, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code short} field in the given class. See also {@link #findField}. */ public static void setStaticShortField(Class clazz, String fieldName, short value) { try { findField(clazz, fieldName).setShort(null, value); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } //################################################################################################# /** * Returns the value of a static object field in the given class. See also {@link #findField}. */ public static Object getStaticObjectField(Class clazz, String fieldName) { try { return findField(clazz, fieldName).get(null); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Returns the value of a static {@code boolean} field in the given class. See also {@link #findField}. */ public static boolean getStaticBooleanField(Class clazz, String fieldName) { try { return findField(clazz, fieldName).getBoolean(null); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code byte} field in the given class. See also {@link #findField}. */ public static byte getStaticByteField(Class clazz, String fieldName) { try { return findField(clazz, fieldName).getByte(null); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code char} field in the given class. See also {@link #findField}. */ public static char getStaticCharField(Class clazz, String fieldName) { try { return findField(clazz, fieldName).getChar(null); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code double} field in the given class. See also {@link #findField}. */ public static double getStaticDoubleField(Class clazz, String fieldName) { try { return findField(clazz, fieldName).getDouble(null); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code float} field in the given class. See also {@link #findField}. */ public static float getStaticFloatField(Class clazz, String fieldName) { try { return findField(clazz, fieldName).getFloat(null); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code int} field in the given class. See also {@link #findField}. */ public static int getStaticIntField(Class clazz, String fieldName) { try { return findField(clazz, fieldName).getInt(null); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code long} field in the given class. See also {@link #findField}. */ public static long getStaticLongField(Class clazz, String fieldName) { try { return findField(clazz, fieldName).getLong(null); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } /** * Sets the value of a static {@code short} field in the given class. See also {@link #findField}. */ public static short getStaticShortField(Class clazz, String fieldName) { try { return findField(clazz, fieldName).getShort(null); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } } //################################################################################################# /** * Calls an instance or static method of the given object. * The method is resolved using {@link #findMethodBestMatch(Class, String, Object...)}. * * @param obj The object instance. A class reference is not sufficient! * @param methodName The method name. * @param args The arguments for the method call. * @throws NoSuchMethodError In case no suitable method was found. * @throws InvocationTargetError In case an exception was thrown by the invoked method. */ public static Object callMethod(Object obj, String methodName, Object... args) { try { return findMethodBestMatch(obj.getClass(), methodName, args).invoke(obj, args); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } catch (InvocationTargetException e) { throw new InvocationTargetError(e.getCause()); } } /** * Calls an instance or static method of the given object. * See {@link #callMethod(Object, String, Object...)}. * *

This variant allows you to specify parameter types, which can help in case there are multiple * methods with the same name, especially if you call it with {@code null} parameters. */ public static Object callMethod(Object obj, String methodName, Class[] parameterTypes, Object... args) { try { return findMethodBestMatch(obj.getClass(), methodName, parameterTypes, args).invoke(obj, args); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } catch (InvocationTargetException e) { throw new InvocationTargetError(e.getCause()); } } /** * Calls a static method of the given class. * The method is resolved using {@link #findMethodBestMatch(Class, String, Object...)}. * * @param clazz The class reference. * @param methodName The method name. * @param args The arguments for the method call. * @throws NoSuchMethodError In case no suitable method was found. * @throws InvocationTargetError In case an exception was thrown by the invoked method. */ public static Object callStaticMethod(Class clazz, String methodName, Object... args) { try { return findMethodBestMatch(clazz, methodName, args).invoke(null, args); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } catch (InvocationTargetException e) { throw new InvocationTargetError(e.getCause()); } } /** * Calls a static method of the given class. * See {@link #callStaticMethod(Class, String, Object...)}. * *

This variant allows you to specify parameter types, which can help in case there are multiple * methods with the same name, especially if you call it with {@code null} parameters. */ public static Object callStaticMethod(Class clazz, String methodName, Class[] parameterTypes, Object... args) { try { return findMethodBestMatch(clazz, methodName, parameterTypes, args).invoke(null, args); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } catch (InvocationTargetException e) { throw new InvocationTargetError(e.getCause()); } } /** * This class provides a wrapper for an exception thrown by a method invocation. * * @see #callMethod(Object, String, Object...) * @see #callStaticMethod(Class, String, Object...) * @see #newInstance(Class, Object...) */ public static final class InvocationTargetError extends Error { private static final long serialVersionUID = -1070936889459514628L; /** * @hide */ public InvocationTargetError(Throwable cause) { super(cause); } } //################################################################################################# /** * Creates a new instance of the given class. * The constructor is resolved using {@link #findConstructorBestMatch(Class, Object...)}. * * @param clazz The class reference. * @param args The arguments for the constructor call. * @throws NoSuchMethodError In case no suitable constructor was found. * @throws InvocationTargetError In case an exception was thrown by the invoked method. * @throws InstantiationError In case the class cannot be instantiated. */ public static Object newInstance(Class clazz, Object... args) { try { return findConstructorBestMatch(clazz, args).newInstance(args); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } catch (InvocationTargetException e) { throw new InvocationTargetError(e.getCause()); } catch (InstantiationException e) { throw new InstantiationError(e.getMessage()); } } /** * Creates a new instance of the given class. * See {@link #newInstance(Class, Object...)}. * *

This variant allows you to specify parameter types, which can help in case there are multiple * constructors with the same name, especially if you call it with {@code null} parameters. */ public static Object newInstance(Class clazz, Class[] parameterTypes, Object... args) { try { return findConstructorBestMatch(clazz, parameterTypes, args).newInstance(args); } catch (IllegalAccessException e) { // should not happen XposedBridge.log(e); throw new IllegalAccessError(e.getMessage()); } catch (IllegalArgumentException e) { throw e; } catch (InvocationTargetException e) { throw new InvocationTargetError(e.getCause()); } catch (InstantiationException e) { throw new InstantiationError(e.getMessage()); } } //################################################################################################# /** * Attaches any value to an object instance. This simulates adding an instance field. * The value can be retrieved again with {@link #getAdditionalInstanceField}. * * @param obj The object instance for which the value should be stored. * @param key The key in the value map for this object instance. * @param value The value to store. * @return The previously stored value for this instance/key combination, or {@code null} if there was none. */ public static Object setAdditionalInstanceField(Object obj, String key, Object value) { if (obj == null) throw new NullPointerException("object must not be null"); if (key == null) throw new NullPointerException("key must not be null"); HashMap objectFields; synchronized (additionalFields) { objectFields = additionalFields.get(obj); if (objectFields == null) { objectFields = new HashMap<>(); additionalFields.put(obj, objectFields); } } synchronized (objectFields) { return objectFields.put(key, value); } } /** * Returns a value which was stored with {@link #setAdditionalInstanceField}. * * @param obj The object instance for which the value has been stored. * @param key The key in the value map for this object instance. * @return The stored value for this instance/key combination, or {@code null} if there is none. */ public static Object getAdditionalInstanceField(Object obj, String key) { if (obj == null) throw new NullPointerException("object must not be null"); if (key == null) throw new NullPointerException("key must not be null"); HashMap objectFields; synchronized (additionalFields) { objectFields = additionalFields.get(obj); if (objectFields == null) return null; } synchronized (objectFields) { return objectFields.get(key); } } /** * Removes and returns a value which was stored with {@link #setAdditionalInstanceField}. * * @param obj The object instance for which the value has been stored. * @param key The key in the value map for this object instance. * @return The previously stored value for this instance/key combination, or {@code null} if there was none. */ public static Object removeAdditionalInstanceField(Object obj, String key) { if (obj == null) throw new NullPointerException("object must not be null"); if (key == null) throw new NullPointerException("key must not be null"); HashMap objectFields; synchronized (additionalFields) { objectFields = additionalFields.get(obj); if (objectFields == null) return null; } synchronized (objectFields) { return objectFields.remove(key); } } /** * Like {@link #setAdditionalInstanceField}, but the value is stored for the class of {@code obj}. */ public static Object setAdditionalStaticField(Object obj, String key, Object value) { return setAdditionalInstanceField(obj.getClass(), key, value); } /** * Like {@link #getAdditionalInstanceField}, but the value is returned for the class of {@code obj}. */ public static Object getAdditionalStaticField(Object obj, String key) { return getAdditionalInstanceField(obj.getClass(), key); } /** * Like {@link #removeAdditionalInstanceField}, but the value is removed and returned for the class of {@code obj}. */ public static Object removeAdditionalStaticField(Object obj, String key) { return removeAdditionalInstanceField(obj.getClass(), key); } /** * Like {@link #setAdditionalInstanceField}, but the value is stored for {@code clazz}. */ public static Object setAdditionalStaticField(Class clazz, String key, Object value) { return setAdditionalInstanceField(clazz, key, value); } /** * Like {@link #setAdditionalInstanceField}, but the value is returned for {@code clazz}. */ public static Object getAdditionalStaticField(Class clazz, String key) { return getAdditionalInstanceField(clazz, key); } /** * Like {@link #setAdditionalInstanceField}, but the value is removed and returned for {@code clazz}. */ public static Object removeAdditionalStaticField(Class clazz, String key) { return removeAdditionalInstanceField(clazz, key); } //################################################################################################# /** * Loads an asset from a resource object and returns the content as {@code byte} array. * * @param res The resources from which the asset should be loaded. * @param path The path to the asset, as in {@link AssetManager#open}. * @return The content of the asset. */ public static byte[] assetAsByteArray(Resources res, String path) throws IOException { return inputStreamToByteArray(res.getAssets().open(path)); } /*package*/ static byte[] inputStreamToByteArray(InputStream is) throws IOException { ByteArrayOutputStream buf = new ByteArrayOutputStream(); byte[] temp = new byte[1024]; int read; while ((read = is.read(temp)) > 0) { buf.write(temp, 0, read); } is.close(); return buf.toByteArray(); } /** * Returns the lowercase hex string representation of a file's MD5 hash sum. */ public static String getMD5Sum(String file) throws IOException { try { MessageDigest digest = MessageDigest.getInstance("MD5"); InputStream is = new FileInputStream(file); byte[] buffer = new byte[8192]; int read; while ((read = is.read(buffer)) > 0) { digest.update(buffer, 0, read); } is.close(); byte[] md5sum = digest.digest(); BigInteger bigInt = new BigInteger(1, md5sum); return bigInt.toString(16); } catch (NoSuchAlgorithmException e) { return ""; } } //################################################################################################# /** * Increments the depth counter for the given method. * *

The intention of the method depth counter is to keep track of the call depth for recursive * methods, e.g. to override parameters only for the outer call. The Xposed framework uses this * to load drawable replacements only once per call, even when multiple * {@link Resources#getDrawable} variants call each other. * * @param method The method name. Should be prefixed with a unique, module-specific string. * @return The updated depth. */ public static int incrementMethodDepth(String method) { return getMethodDepthCounter(method).get().incrementAndGet(); } /** * Decrements the depth counter for the given method. * See {@link #incrementMethodDepth} for details. * * @param method The method name. Should be prefixed with a unique, module-specific string. * @return The updated depth. */ public static int decrementMethodDepth(String method) { return getMethodDepthCounter(method).get().decrementAndGet(); } /** * Returns the current depth counter for the given method. * See {@link #incrementMethodDepth} for details. * * @param method The method name. Should be prefixed with a unique, module-specific string. * @return The updated depth. */ public static int getMethodDepth(String method) { return getMethodDepthCounter(method).get().get(); } private static ThreadLocal getMethodDepthCounter(String method) { synchronized (sMethodDepth) { ThreadLocal counter = sMethodDepth.get(method); if (counter == null) { counter = new ThreadLocal() { @Override protected AtomicInteger initialValue() { return new AtomicInteger(); } }; sMethodDepth.put(method, counter); } return counter; } } //################################################################################################# // TODO helpers for view traversing /*To make it easier, I will try and implement some more helpers: - add view before/after existing view (I already mentioned that I think) - get index of view in its parent - get next/previous sibling (maybe with an optional argument "type", that might be ImageView.class and gives you the next sibling that is an ImageView)? - get next/previous element (similar to the above, but would also work if the next element has a different parent, it would just go up the hierarchy and then down again until it finds a matching element) - find the first child that is an instance of a specified class - find all (direct or indirect) children of a specified class */ } ================================================ FILE: core/src/main/java/de/robv/android/xposed/XposedInit.java ================================================ /* * 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 */ package de.robv.android.xposed; import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; import static org.lsposed.lspd.deopt.PrebuiltMethodsDeopter.deoptResourceMethods; import static de.robv.android.xposed.XposedBridge.hookAllMethods; import static de.robv.android.xposed.XposedHelpers.callMethod; import static de.robv.android.xposed.XposedHelpers.findAndHookMethod; import static de.robv.android.xposed.XposedHelpers.getObjectField; import static de.robv.android.xposed.XposedHelpers.getParameterIndexByType; import static de.robv.android.xposed.XposedHelpers.setStaticObjectField; import android.app.ActivityThread; import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.content.res.ResourcesImpl; import android.content.res.TypedArray; import android.content.res.XResources; import android.os.Build; import android.os.IBinder; import android.os.Process; import android.util.ArrayMap; import org.lsposed.lspd.impl.LSPosedContext; import org.lsposed.lspd.models.PreLoadedApk; import org.matrix.vector.nativebridge.NativeAPI; import org.matrix.vector.nativebridge.ResourcesHook; import org.lsposed.lspd.util.LspModuleClassLoader; import org.lsposed.lspd.util.Utils.Log; import java.io.File; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import de.robv.android.xposed.callbacks.XC_InitPackageResources; import de.robv.android.xposed.callbacks.XCallback; import hidden.HiddenApiBridge; public final class XposedInit { private static final String TAG = XposedBridge.TAG; public static boolean startsSystemServer = false; public static volatile boolean disableResources = false; public static AtomicBoolean resourceInit = new AtomicBoolean(false); public static void hookResources() throws Throwable { if (disableResources || !resourceInit.compareAndSet(false, true)) { return; } deoptResourceMethods(); if (!ResourcesHook.initXResourcesNative()) { Log.e(TAG, "Cannot hook resources"); disableResources = true; return; } findAndHookMethod("android.app.ApplicationPackageManager", null, "getResourcesForApplication", ApplicationInfo.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) { ApplicationInfo app = (ApplicationInfo) param.args[0]; XResources.setPackageNameForResDir(app.packageName, app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir); } }); /* * getTopLevelResources(a) * -> getTopLevelResources(b) * -> key = new ResourcesKey() * -> r = new Resources() * -> mActiveResources.put(key, r) * -> return r */ final Class classGTLR; final Class classResKey; final ThreadLocal latestResKey = new ThreadLocal<>(); final ArrayList createResourceMethods = new ArrayList<>(); classGTLR = android.app.ResourcesManager.class; classResKey = android.content.res.ResourcesKey.class; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { createResourceMethods.add("createResources"); createResourceMethods.add("createResourcesForActivity"); } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { createResourceMethods.add("createResources"); } else { createResourceMethods.add("getOrCreateResources"); } final Class classActivityRes = XposedHelpers.findClassIfExists("android.app.ResourcesManager$ActivityResource", classGTLR.getClassLoader()); var hooker = new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) { // At least on OnePlus 5, the method has an additional parameter compared to AOSP. Object activityToken = null; try { final int activityTokenIdx = getParameterIndexByType(param.method, IBinder.class); activityToken = param.args[activityTokenIdx]; } catch (NoSuchFieldError ignored) { } final int resKeyIdx = getParameterIndexByType(param.method, classResKey); String resDir = (String) getObjectField(param.args[resKeyIdx], "mResDir"); XResources newRes = cloneToXResources(param, resDir); if (newRes == null) { return; } //noinspection SynchronizeOnNonFinalField synchronized (param.thisObject) { ArrayList resourceReferences; if (activityToken != null) { Object activityResources = callMethod(param.thisObject, "getOrCreateActivityResourcesStructLocked", activityToken); //noinspection unchecked resourceReferences = (ArrayList) getObjectField(activityResources, "activityResources"); } else { //noinspection unchecked resourceReferences = (ArrayList) getObjectField(param.thisObject, "mResourceReferences"); } if (activityToken == null || classActivityRes == null) { resourceReferences.add(new WeakReference<>(newRes)); } else { // Android S createResourcesForActivity() var activityRes = XposedHelpers.newInstance(classActivityRes); XposedHelpers.setObjectField(activityRes, "resources", new WeakReference<>(newRes)); resourceReferences.add(activityRes); } } } }; for (var createResourceMethod : createResourceMethods) { hookAllMethods(classGTLR, createResourceMethod, hooker); } findAndHookMethod(TypedArray.class, "obtain", Resources.class, int.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { if (param.getResult() instanceof XResources.XTypedArray) { return; } if (!(param.args[0] instanceof XResources)) { return; } XResources.XTypedArray newResult = new XResources.XTypedArray((Resources) param.args[0]); int len = (int) param.args[1]; Method resizeMethod = XposedHelpers.findMethodBestMatch( TypedArray.class, "resize", int.class); resizeMethod.setAccessible(true); resizeMethod.invoke(newResult, len); param.setResult(newResult); } }); // Replace system resources XResources systemRes = new XResources( (ClassLoader) XposedHelpers.getObjectField(Resources.getSystem(), "mClassLoader"), null); HiddenApiBridge.Resources_setImpl(systemRes, (ResourcesImpl) XposedHelpers.getObjectField(Resources.getSystem(), "mResourcesImpl")); setStaticObjectField(Resources.class, "mSystem", systemRes); XResources.init(latestResKey); } private static XResources cloneToXResources(XC_MethodHook.MethodHookParam param, String resDir) { Object result = param.getResult(); if (result == null || result instanceof XResources) { return null; } // Replace the returned resources with our subclass. var newRes = new XResources( (ClassLoader) XposedHelpers.getObjectField(param.getResult(), "mClassLoader"), resDir); HiddenApiBridge.Resources_setImpl(newRes, (ResourcesImpl) XposedHelpers.getObjectField(param.getResult(), "mResourcesImpl")); // Invoke handleInitPackageResources(). if (newRes.isFirstLoad()) { String packageName = newRes.getPackageName(); XC_InitPackageResources.InitPackageResourcesParam resparam = new XC_InitPackageResources.InitPackageResourcesParam(XposedBridge.sInitPackageResourcesCallbacks); resparam.packageName = packageName; resparam.res = newRes; XCallback.callAll(resparam); } param.setResult(newRes); return newRes; } // only legacy modules have non-empty value private static final Map> loadedModules = new ConcurrentHashMap<>(); public static Map> getLoadedModules() { return loadedModules; } public static void loadLegacyModules() { var moduleList = serviceClient.getLegacyModulesList(); moduleList.forEach(module -> { var apk = module.apkPath; var name = module.packageName; var file = module.file; loadedModules.put(name, Optional.of(apk)); // temporarily add it for XSharedPreference if (!loadModule(name, apk, file)) { loadedModules.remove(name); } }); } public static void loadModules(ActivityThread at) { var packages = (ArrayMap) XposedHelpers.getObjectField(at, "mPackages"); serviceClient.getModulesList().forEach(module -> { loadedModules.put(module.packageName, Optional.empty()); if (!LSPosedContext.loadModule(at, module)) { loadedModules.remove(module.packageName); } else { packages.remove(module.packageName); } }); } /** * Load all so from an APK by reading assets/native_init. * It will only store the so names but not doing anything. */ private static void initNativeModule(List moduleLibraryNames) { moduleLibraryNames.forEach(NativeAPI::recordNativeEntrypoint); } private static boolean initModule(ClassLoader mcl, String apk, List moduleClassNames) { var count = 0; for (var moduleClassName : moduleClassNames) { try { Log.i(TAG, " Loading class " + moduleClassName); Class moduleClass = mcl.loadClass(moduleClassName); if (!IXposedMod.class.isAssignableFrom(moduleClass)) { Log.e(TAG, " This class doesn't implement any sub-interface of IXposedMod, skipping it"); continue; } final Object moduleInstance = moduleClass.newInstance(); if (moduleInstance instanceof IXposedHookZygoteInit) { IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam(); param.modulePath = apk; param.startsSystemServer = startsSystemServer; ((IXposedHookZygoteInit) moduleInstance).initZygote(param); count++; } if (moduleInstance instanceof IXposedHookLoadPackage) { XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance)); count++; } if (moduleInstance instanceof IXposedHookInitPackageResources) { hookResources(); XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance)); count++; } } catch (Throwable t) { Log.e(TAG, " Failed to load class " + moduleClassName, t); } } return count > 0; } /** * Load a module from an APK by calling the init(String) method for all classes defined * in assets/xposed_init. */ private static boolean loadModule(String name, String apk, PreLoadedApk file) { Log.i(TAG, "Loading legacy module " + name + " from " + apk); var sb = new StringBuilder(); var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS; for (String abi : abis) { sb.append(apk).append("!/lib/").append(abi).append(File.pathSeparator); } var librarySearchPath = sb.toString(); var initLoader = XposedInit.class.getClassLoader(); var mcl = LspModuleClassLoader.loadApk(apk, file.preLoadedDexes, librarySearchPath, initLoader); try { if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) { Log.e(TAG, " Cannot load module: " + name); Log.e(TAG, " The Xposed API classes are compiled into the module's APK."); Log.e(TAG, " This may cause strange issues and must be fixed by the module developer."); Log.e(TAG, " For details, see: https://api.xposed.info/using.html"); return false; } } catch (ClassNotFoundException ignored) { return false; } initNativeModule(file.moduleLibraryNames); return initModule(mcl, apk, file.moduleClassNames); } public final static Set loadedPackagesInProcess = ConcurrentHashMap.newKeySet(1); } ================================================ FILE: core/src/main/java/de/robv/android/xposed/callbacks/IXUnhook.java ================================================ /* * 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 */ package de.robv.android.xposed.callbacks; import de.robv.android.xposed.IXposedHookZygoteInit; /** * Interface for objects that can be used to remove callbacks. * *

Just like hooking methods etc., unhooking applies only to the current process. * In other process (or when the app is removed from memory and then restarted), the hook will still * be active. The Zygote process (see {@link IXposedHookZygoteInit}) is an exception, the hook won't * be inherited by any future processes forked from it in the future. * * @param The class of the callback. */ public interface IXUnhook { /** * Returns the callback that has been registered. */ T getCallback(); /** * Removes the callback. */ void unhook(); } ================================================ FILE: core/src/main/java/de/robv/android/xposed/callbacks/XC_InitPackageResources.java ================================================ /* * 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 */ package de.robv.android.xposed.callbacks; import android.content.res.XResources; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.concurrent.CopyOnWriteArraySet; import de.robv.android.xposed.IXposedHookInitPackageResources; import io.github.libxposed.api.XposedModuleInterface; /** * This class is only used for internal purposes, except for the {@link InitPackageResourcesParam} * subclass. */ public abstract class XC_InitPackageResources extends XCallback implements IXposedHookInitPackageResources { /** * Creates a new callback with default priority. * * @hide */ @SuppressWarnings("deprecation") public XC_InitPackageResources() { super(); } /** * Creates a new callback with a specific priority. * * @param priority See {@link XCallback#priority}. * @hide */ public XC_InitPackageResources(int priority) { super(priority); } /** * Wraps information about the resources being initialized. */ public static final class InitPackageResourcesParam extends XCallback.Param { /** * @hide */ public InitPackageResourcesParam(CopyOnWriteArraySet callbacks) { super(callbacks.toArray(new XCallback[0])); } /** * The name of the package for which resources are being loaded. */ public String packageName; /** * Reference to the resources that can be used for calls to * {@link XResources#setReplacement(String, String, String, Object)}. */ public XResources res; } /** * @hide */ @Override protected void call(Param param) throws Throwable { if (param instanceof InitPackageResourcesParam) handleInitPackageResources((InitPackageResourcesParam) param); } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/callbacks/XC_LayoutInflated.java ================================================ /* * 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 */ package de.robv.android.xposed.callbacks; import android.content.res.XResources; import android.content.res.XResources.ResourceNames; import android.view.View; import de.robv.android.xposed.XposedBridge.CopyOnWriteSortedSet; /** * Callback for hooking layouts. Such callbacks can be passed to {@link XResources#hookLayout} * and its variants. */ public abstract class XC_LayoutInflated extends XCallback implements Comparable { /** * Creates a new callback with default priority. */ @SuppressWarnings("deprecation") public XC_LayoutInflated() { super(); } /** * Creates a new callback with a specific priority. * * @param priority See {@link XCallback#priority}. */ public XC_LayoutInflated(int priority) { super(priority); } /** * Wraps information about the inflated layout. */ public static final class LayoutInflatedParam extends XCallback.Param { /** * @hide */ public LayoutInflatedParam(CopyOnWriteSortedSet callbacks) { super(callbacks.getSnapshot(new XCallback[0])); } /** * The view that has been created from the layout. */ public View view; /** * Container with the ID and name of the underlying resource. */ public ResourceNames resNames; /** * Directory from which the layout was actually loaded (e.g. "layout-sw600dp"). */ public String variant; /** * Resources containing the layout. */ public XResources res; } /** @hide */ @Override public int compareTo(XC_LayoutInflated other) { if (this == other) return 0; // order descending by priority if (other.priority != this.priority) return other.priority - this.priority; // then randomly else if (System.identityHashCode(this) < System.identityHashCode(other)) return -1; else return 1; } /** * @hide */ @Override protected void call(Param param) throws Throwable { if (param instanceof LayoutInflatedParam) handleLayoutInflated((LayoutInflatedParam) param); } /** * This method is called when the hooked layout has been inflated. * * @param liparam Information about the layout and the inflated view. * @throws Throwable Everything the callback throws is caught and logged. */ public abstract void handleLayoutInflated(LayoutInflatedParam liparam) throws Throwable; /** * An object with which the callback can be removed. */ public class Unhook implements IXUnhook { private final String resDir; private final int id; /** * @hide */ public Unhook(String resDir, int id) { this.resDir = resDir; this.id = id; } /** * Returns the resource ID of the hooked layout. */ public int getId() { return id; } @Override public XC_LayoutInflated getCallback() { return XC_LayoutInflated.this; } @Override public void unhook() { XResources.unhookLayout(resDir, id, XC_LayoutInflated.this); } } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/callbacks/XC_LoadPackage.java ================================================ /* * 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 */ package de.robv.android.xposed.callbacks; import android.content.pm.ApplicationInfo; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.concurrent.CopyOnWriteArraySet; import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.XposedBridge.CopyOnWriteSortedSet; import io.github.libxposed.api.XposedModuleInterface; /** * This class is only used for internal purposes, except for the {@link LoadPackageParam} * subclass. */ public abstract class XC_LoadPackage extends XCallback implements IXposedHookLoadPackage { /** * Creates a new callback with default priority. * * @hide */ @SuppressWarnings("deprecation") public XC_LoadPackage() { super(); } /** * Creates a new callback with a specific priority. * * @param priority See {@link XCallback#priority}. * @hide */ public XC_LoadPackage(int priority) { super(priority); } /** * Wraps information about the app being loaded. */ public static final class LoadPackageParam extends XCallback.Param { /** * @hide */ public LoadPackageParam(CopyOnWriteArraySet callbacks) { super(callbacks.toArray(new XCallback[0])); } /** * The name of the package being loaded. */ public String packageName; /** * The process in which the package is executed. */ public String processName; /** * The ClassLoader used for this package. */ public ClassLoader classLoader; /** * More information about the application being loaded. */ public ApplicationInfo appInfo; /** * Set to {@code true} if this is the first (and main) application for this process. */ public boolean isFirstApplication; } /** * @hide */ @Override protected void call(Param param) throws Throwable { if (param instanceof LoadPackageParam) handleLoadPackage((LoadPackageParam) param); } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/callbacks/XCallback.java ================================================ /* * 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 */ package de.robv.android.xposed.callbacks; import android.os.Bundle; import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter; import java.io.Serializable; import de.robv.android.xposed.XposedBridge; /** * Base class for Xposed callbacks. *

* This class only keeps a priority for ordering multiple callbacks. * The actual (abstract) callback methods are added by subclasses. */ abstract public class XCallback { /** * Callback priority, higher number means earlier execution. * *

This is usually set to {@link #PRIORITY_DEFAULT}. However, in case a certain callback should * be executed earlier or later a value between {@link #PRIORITY_HIGHEST} and {@link #PRIORITY_LOWEST} * can be set instead. The values are just for orientation though, Xposed doesn't enforce any * boundaries on the priority values. */ public final int priority; /** * @deprecated This constructor can't be hidden for technical reasons. Nevertheless, don't use it! */ @Deprecated public XCallback() { this.priority = PRIORITY_DEFAULT; } /** * @hide */ public XCallback(int priority) { this.priority = priority; } /** * Base class for Xposed callback parameters. */ public static abstract class Param { /** * @hide */ public final XCallback[] callbacks; private Bundle extra; /** * @deprecated This constructor can't be hidden for technical reasons. Nevertheless, don't use it! */ @Deprecated protected Param() { callbacks = null; } /** * @hide */ protected Param(XCallback[] callbacks) { this.callbacks = callbacks; } /** * This can be used to store any data for the scope of the callback. * *

Use this instead of instance variables, as it has a clear reference to e.g. each * separate call to a method, even when the same method is called recursively. * * @see #setObjectExtra * @see #getObjectExtra */ public synchronized Bundle getExtra() { if (extra == null) extra = new Bundle(); return extra; } /** * Returns an object stored with {@link #setObjectExtra}. */ public Object getObjectExtra(String key) { Serializable o = getExtra().getSerializable(key); if (o instanceof SerializeWrapper) return ((SerializeWrapper) o).object; return null; } /** * Stores any object for the scope of the callback. For data types that support it, use * the {@link Bundle} returned by {@link #getExtra} instead. */ public void setObjectExtra(String key, Object o) { getExtra().putSerializable(key, new SerializeWrapper(o)); } private static class SerializeWrapper implements Serializable { private static final long serialVersionUID = 1L; private final Object object; public SerializeWrapper(Object o) { object = o; } } } /** * @hide */ public static void callAll(Param param) { if (param instanceof XC_LoadPackage.LoadPackageParam) { // deopt methods in system apps or priv-apps, this would be not necessary // only if we found out how to recompile their apks XC_LoadPackage.LoadPackageParam lpp = (XC_LoadPackage.LoadPackageParam) param; PrebuiltMethodsDeopter.deoptMethods(lpp.packageName, lpp.classLoader); } if (param.callbacks == null) throw new IllegalStateException("This object was not created for use with callAll"); for (int i = 0; i < param.callbacks.length; i++) { try { param.callbacks[i].call(param); } catch (Throwable t) { XposedBridge.log(t); } } } /** * @hide */ protected void call(Param param) throws Throwable { } /** * The default priority, see {@link #priority}. */ public static final int PRIORITY_DEFAULT = 50; /** * Execute this callback late, see {@link #priority}. */ public static final int PRIORITY_LOWEST = -10000; /** * Execute this callback early, see {@link #priority}. */ public static final int PRIORITY_HIGHEST = 10000; } ================================================ FILE: core/src/main/java/de/robv/android/xposed/services/BaseService.java ================================================ /* * 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 */ package de.robv.android.xposed.services; import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import de.robv.android.xposed.SELinuxHelper; /** * General definition of a file access service provided by the Xposed framework. * *

References to a concrete subclass should generally be retrieved from {@link SELinuxHelper}. */ public abstract class BaseService { /** * Flag for {@link #checkFileAccess}: Read access. */ public static final int R_OK = 4; /** * Flag for {@link #checkFileAccess}: Write access. */ public static final int W_OK = 2; /** * Flag for {@link #checkFileAccess}: Executable access. */ public static final int X_OK = 1; /** * Flag for {@link #checkFileAccess}: File/directory exists. */ public static final int F_OK = 0; /** * Checks whether the services accesses files directly (instead of using IPC). * * @return {@code true} in case direct access is possible. */ public boolean hasDirectFileAccess() { return false; } /** * Check whether a file is accessible. SELinux might enforce stricter checks. * * @param filename The absolute path of the file to check. * @param mode The mode for POSIX's {@code access()} function. * @return The result of the {@code access()} function. */ public abstract boolean checkFileAccess(String filename, int mode); /** * Check whether a file exists. * * @param filename The absolute path of the file to check. * @return The result of the {@code access()} function. */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean checkFileExists(String filename) { return checkFileAccess(filename, F_OK); } /** * Determine the size and modification time of a file. * * @param filename The absolute path of the file to check. * @return A {@link FileResult} object holding the result. * @throws IOException In case an error occurred while retrieving the information. */ public abstract FileResult statFile(String filename) throws IOException; /** * Determine the size time of a file. * * @param filename The absolute path of the file to check. * @return The file size. * @throws IOException In case an error occurred while retrieving the information. */ public long getFileSize(String filename) throws IOException { return statFile(filename).size; } /** * Determine the size time of a file. * * @param filename The absolute path of the file to check. * @return The file modification time. * @throws IOException In case an error occurred while retrieving the information. */ public long getFileModificationTime(String filename) throws IOException { return statFile(filename).mtime; } /** * Read a file into memory. * * @param filename The absolute path of the file to read. * @return A {@code byte} array with the file content. * @throws IOException In case an error occurred while reading the file. */ public abstract byte[] readFile(String filename) throws IOException; /** * Read a file into memory, but only if it has changed since the last time. * * @param filename The absolute path of the file to read. * @param previousSize File size of last read. * @param previousTime File modification time of last read. * @return A {@link FileResult} object holding the result. *

The {@link FileResult#content} field might be {@code null} if the file * is unmodified ({@code previousSize} and {@code previousTime} are still valid). * @throws IOException In case an error occurred while reading the file. */ public abstract FileResult readFile(String filename, long previousSize, long previousTime) throws IOException; /** * Read a file into memory, optionally only if it has changed since the last time. * * @param filename The absolute path of the file to read. * @param offset Number of bytes to skip at the beginning of the file. * @param length Number of bytes to read (0 means read to end of file). * @param previousSize Optional: File size of last read. * @param previousTime Optional: File modification time of last read. * @return A {@link FileResult} object holding the result. *

The {@link FileResult#content} field might be {@code null} if the file * is unmodified ({@code previousSize} and {@code previousTime} are still valid). * @throws IOException In case an error occurred while reading the file. */ public abstract FileResult readFile(String filename, int offset, int length, long previousSize, long previousTime) throws IOException; /** * Get a stream to the file content. * Depending on the service, it may or may not be read completely into memory. * * @param filename The absolute path of the file to read. * @return An {@link InputStream} to the file content. * @throws IOException In case an error occurred while reading the file. */ public InputStream getFileInputStream(String filename) throws IOException { return new ByteArrayInputStream(readFile(filename)); } /** * Get a stream to the file content, but only if it has changed since the last time. * Depending on the service, it may or may not be read completely into memory. * * @param filename The absolute path of the file to read. * @param previousSize Optional: File size of last read. * @param previousTime Optional: File modification time of last read. * @return A {@link FileResult} object holding the result. *

The {@link FileResult#stream} field might be {@code null} if the file * is unmodified ({@code previousSize} and {@code previousTime} are still valid). * @throws IOException In case an error occurred while reading the file. */ public FileResult getFileInputStream(String filename, long previousSize, long previousTime) throws IOException { FileResult result = readFile(filename, previousSize, previousTime); if (result.content == null) return result; return new FileResult(new ByteArrayInputStream(result.content), result.size, result.mtime); } // ---------------------------------------------------------------------------- /*package*/ BaseService() { } /*package*/ static void ensureAbsolutePath(String filename) { if (!filename.startsWith("/")) { throw new IllegalArgumentException("Only absolute filenames are allowed: " + filename); } } /*package*/ static void throwCommonIOException(int errno, String errorMsg, String filename, String defaultText) throws IOException { switch (errno) { case 1: // EPERM case 13: // EACCES throw new FileNotFoundException(errorMsg != null ? errorMsg : "Permission denied: " + filename); case 2: // ENOENT throw new FileNotFoundException(errorMsg != null ? errorMsg : "No such file or directory: " + filename); case 12: // ENOMEM throw new OutOfMemoryError(errorMsg); case 21: // EISDIR throw new FileNotFoundException(errorMsg != null ? errorMsg : "Is a directory: " + filename); default: throw new IOException(errorMsg != null ? errorMsg : "Error " + errno + defaultText + filename); } } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/services/DirectAccessService.java ================================================ /* * 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 */ package de.robv.android.xposed.services; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /** * @hide */ public final class DirectAccessService extends BaseService { @Override public boolean hasDirectFileAccess() { return true; } @SuppressWarnings("RedundantIfStatement") @Override public boolean checkFileAccess(String filename, int mode) { File file = new File(filename); if (mode == F_OK && !file.exists()) return false; if ((mode & R_OK) != 0 && !file.canRead()) return false; if ((mode & W_OK) != 0 && !file.canWrite()) return false; if ((mode & X_OK) != 0 && !file.canExecute()) return false; return true; } @Override public boolean checkFileExists(String filename) { return new File(filename).exists(); } @Override public FileResult statFile(String filename) throws IOException { File file = new File(filename); return new FileResult(file.length(), file.lastModified()); } @Override public byte[] readFile(String filename) throws IOException { File file = new File(filename); byte content[] = new byte[(int) file.length()]; FileInputStream fis = new FileInputStream(file); fis.read(content); fis.close(); return content; } @Override public FileResult readFile(String filename, long previousSize, long previousTime) throws IOException { File file = new File(filename); long size = file.length(); long time = file.lastModified(); if (previousSize == size && previousTime == time) return new FileResult(size, time); return new FileResult(readFile(filename), size, time); } @Override public FileResult readFile(String filename, int offset, int length, long previousSize, long previousTime) throws IOException { File file = new File(filename); long size = file.length(); long time = file.lastModified(); if (previousSize == size && previousTime == time) return new FileResult(size, time); // Shortcut for the simple case if (offset <= 0 && length <= 0) return new FileResult(readFile(filename), size, time); // Check range if (offset > 0 && offset >= size) { throw new IllegalArgumentException("Offset " + offset + " is out of range for " + filename); } else if (offset < 0) { offset = 0; } if (length > 0 && (offset + length) > size) { throw new IllegalArgumentException("Length " + length + " is out of range for " + filename); } else if (length <= 0) { length = (int) (size - offset); } byte content[] = new byte[length]; FileInputStream fis = new FileInputStream(file); fis.skip(offset); fis.read(content); fis.close(); return new FileResult(content, size, time); } /** * {@inheritDoc} *

This implementation returns a BufferedInputStream instead of loading the file into memory. */ @Override public InputStream getFileInputStream(String filename) throws IOException { return new BufferedInputStream(new FileInputStream(filename), 16 * 1024); } /** * {@inheritDoc} *

This implementation returns a BufferedInputStream instead of loading the file into memory. */ @Override public FileResult getFileInputStream(String filename, long previousSize, long previousTime) throws IOException { File file = new File(filename); long size = file.length(); long time = file.lastModified(); if (previousSize == size && previousTime == time) return new FileResult(size, time); return new FileResult(new BufferedInputStream(new FileInputStream(filename), 16 * 1024), size, time); } } ================================================ FILE: core/src/main/java/de/robv/android/xposed/services/FileResult.java ================================================ /* * 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 */ package de.robv.android.xposed.services; import java.io.InputStream; /** * Holder for the result of a {@link BaseService#readFile} or {@link BaseService#statFile} call. */ public final class FileResult { /** * File content, might be {@code null} if the file wasn't read. */ public final byte[] content; /** * File input stream, might be {@code null} if the file wasn't read. */ public final InputStream stream; /** * File size. */ public final long size; /** * File last modification time. */ public final long mtime; /*package*/ FileResult(long size, long mtime) { this.content = null; this.stream = null; this.size = size; this.mtime = mtime; } /*package*/ FileResult(byte[] content, long size, long mtime) { this.content = content; this.stream = null; this.size = size; this.mtime = mtime; } /*package*/ FileResult(InputStream stream, long size, long mtime) { this.content = null; this.stream = stream; this.size = size; this.mtime = mtime; } /** * @hide */ @Override public String toString() { StringBuilder sb = new StringBuilder("{"); if (content != null) { sb.append("content.length: "); sb.append(content.length); sb.append(", "); } if (stream != null) { sb.append("stream: "); sb.append(stream.toString()); sb.append(", "); } sb.append("size: "); sb.append(size); sb.append(", mtime: "); sb.append(mtime); sb.append("}"); return sb.toString(); } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/core/ApplicationServiceClient.java ================================================ /* * 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.core; import android.os.Bundle; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import androidx.annotation.NonNull; import org.lsposed.lspd.models.Module; import org.lsposed.lspd.service.ILSPApplicationService; import org.lsposed.lspd.util.Utils; import java.util.Collections; import java.util.List; public class ApplicationServiceClient implements ILSPApplicationService, IBinder.DeathRecipient { public static ApplicationServiceClient serviceClient = null; final ILSPApplicationService service; final String processName; private ApplicationServiceClient(@NonNull ILSPApplicationService service, @NonNull String processName) throws RemoteException { this.service = service; this.processName = processName; this.service.asBinder().linkToDeath(this, 0); } synchronized static void Init(ILSPApplicationService service, String niceName) { var binder = service.asBinder(); if (serviceClient == null && binder != null) { try { serviceClient = new ApplicationServiceClient(service, niceName); } catch (RemoteException e) { Utils.logE("link to death error: ", e); } } } @Override public boolean isLogMuted() { try { return service.isLogMuted(); } catch (RemoteException | NullPointerException ignored) { } return false; } @Override public List getLegacyModulesList() { try { return service.getLegacyModulesList(); } catch (RemoteException | NullPointerException ignored) { } return Collections.emptyList(); } @Override public List getModulesList() { try { return service.getModulesList(); } catch (RemoteException | NullPointerException ignored) { } return Collections.emptyList(); } @Override public String getPrefsPath(String packageName) { try { return service.getPrefsPath(packageName); } catch (RemoteException | NullPointerException ignored) { } return null; } @Override public ParcelFileDescriptor requestInjectedManagerBinder(List binder) { try { return service.requestInjectedManagerBinder(binder); } catch (RemoteException | NullPointerException ignored) { } return null; } @Override public IBinder asBinder() { return service.asBinder(); } @Override public void binderDied() { service.asBinder().unlinkToDeath(this, 0); serviceClient = null; } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/core/Startup.java ================================================ /* * 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 - 2022 LSPosed Contributors */ package org.lsposed.lspd.core; import android.app.ActivityThread; import android.app.LoadedApk; import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.os.IBinder; import com.android.internal.os.ZygoteInit; import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter; import org.lsposed.lspd.hooker.AttachHooker; import org.lsposed.lspd.hooker.CrashDumpHooker; import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker; import org.lsposed.lspd.hooker.LoadedApkCtorHooker; import org.lsposed.lspd.hooker.LoadedApkCreateCLHooker; import org.lsposed.lspd.hooker.OpenDexFileHooker; import org.lsposed.lspd.hooker.StartBootstrapServicesHooker; import org.lsposed.lspd.impl.LSPosedContext; import org.lsposed.lspd.impl.LSPosedHelper; import org.lsposed.lspd.service.ILSPApplicationService; import org.lsposed.lspd.util.Utils; import java.util.List; import dalvik.system.DexFile; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedInit; public class Startup { private static void startBootstrapHook(boolean isSystem) { Utils.logD("startBootstrapHook starts: isSystem = " + isSystem); LSPosedHelper.hookMethod(CrashDumpHooker.class, Thread.class, "dispatchUncaughtException", Throwable.class); if (isSystem) { LSPosedHelper.hookAllMethods(HandleSystemServerProcessHooker.class, ZygoteInit.class, "handleSystemServerProcess"); } else { LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openDexFile"); LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFile"); LSPosedHelper.hookAllMethods(OpenDexFileHooker.class, DexFile.class, "openInMemoryDexFiles"); } LSPosedHelper.hookConstructor(LoadedApkCtorHooker.class, LoadedApk.class, ActivityThread.class, ApplicationInfo.class, CompatibilityInfo.class, ClassLoader.class, boolean.class, boolean.class, boolean.class); LSPosedHelper.hookMethod(LoadedApkCreateCLHooker.class, LoadedApk.class, "createOrUpdateClassLoaderLocked", List.class); LSPosedHelper.hookAllMethods(AttachHooker.class, ActivityThread.class, "attach"); } public static void bootstrapXposed(boolean systemServerStarted) { // Initialize the Xposed framework try { startBootstrapHook(XposedInit.startsSystemServer); XposedInit.loadLegacyModules(); } catch (Throwable t) { Utils.logE("error during Xposed initialization", t); } if (systemServerStarted) { Utils.logD("Manually triggering system_server module load for late injection"); IBinder activityService = android.os.ServiceManager.getService("activity"); if (activityService == null) { Utils.logE("Activity service not found! Cannot get SystemServer ClassLoader."); return; } // Maintain state consistency for the rest of the Vector framework HandleSystemServerProcessHooker.systemServerCL = activityService.getClass().getClassLoader(); HandleSystemServerProcessHooker.after(); StartBootstrapServicesHooker.before(); Utils.logI("Late system_server injection successfully completed."); } } public static void initXposed(boolean isSystem, String processName, String appDir, ILSPApplicationService service) { // init logger ApplicationServiceClient.Init(service, processName); XposedBridge.initXResources(); XposedInit.startsSystemServer = isSystem; LSPosedContext.isSystemServer = isSystem; LSPosedContext.appDir = appDir; LSPosedContext.processName = processName; PrebuiltMethodsDeopter.deoptBootMethods(); // do it once for secondary zygote } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/deopt/InlinedMethodCallers.java ================================================ /* * 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 */ package org.lsposed.lspd.deopt; import android.app.Instrumentation; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; import android.util.DisplayMetrics; import android.util.TypedValue; import java.util.HashMap; /** * Providing a whitelist of methods which are the callers of the target methods we want to hook. * Because the target methods are inlined into the callers, we deoptimize the callers to * run in intercept mode to make target methods hookable. *

* Only for methods which are included in pre-compiled framework codes. * TODO recompile system apps and priv-apps since their original dex files are available */ public class InlinedMethodCallers { public static final String KEY_BOOT_IMAGE = "boot_image"; public static final String KEY_BOOT_IMAGE_MIUI_RES = "boot_image_miui_res"; public static final String KEY_SYSTEM_SERVER = "system_server"; /** * Key should be {@link #KEY_BOOT_IMAGE}, {@link #KEY_SYSTEM_SERVER}, or a package name * of system apps or priv-apps i.e. com.android.systemui */ private static final HashMap CALLERS = new HashMap<>(); /** * format for each row: {className, methodName, methodSig} */ private static final Object[][] BOOT_IMAGE = { // callers of Application#attach(Context) {"android.app.Instrumentation", "newApplication", ClassLoader.class, String.class, Context.class}, {"android.app.Instrumentation", "newApplication", ClassLoader.class, Context.class}, // callers of Instrumentation#newApplication(ClassLoader, String, Context) {"android.app.LoadedApk", "makeApplicationInner", Boolean.TYPE, Instrumentation.class, Boolean.TYPE}, {"android.app.LoadedApk", "makeApplicationInner", Boolean.TYPE, Instrumentation.class}, {"android.app.LoadedApk", "makeApplication", Boolean.TYPE, Instrumentation.class}, {"android.app.ContextImpl", "getSharedPreferencesPath", String.class} }; // TODO deprecate this private static final Object[][] BOOT_IMAGE_FOR_MIUI_RES = { // for MIUI resources hooking {"android.content.res.MiuiResources", "init", String.class}, {"android.content.res.MiuiResources", "updateMiuiImpl"}, {"android.content.res.MiuiResources", "setImpl", "android.content.res.ResourcesImpl"}, {"android.content.res.MiuiResources", "loadOverlayValue", TypedValue.class, int.class}, {"android.content.res.MiuiResources", "getThemeString", CharSequence.class}, {"android.content.res.MiuiResources", "", ClassLoader.class}, {"android.content.res.MiuiResources", ""}, {"android.content.res.MiuiResources", "", AssetManager.class, DisplayMetrics.class, Configuration.class}, {"android.miui.ResourcesManager", "initMiuiResource", Resources.class, String.class}, {"android.app.LoadedApk", "getResources", Resources.class}, {"android.content.res.Resources", "getSystem", Resources.class}, {"android.app.ApplicationPackageManager", "getResourcesForApplication", ApplicationInfo.class}, {"android.app.ContextImpl", "setResources", Resources.class}, }; private static final Object[][] SYSTEM_SERVER = {}; private static final Object[][] SYSTEM_UI = {}; static { CALLERS.put(KEY_BOOT_IMAGE, BOOT_IMAGE); CALLERS.put(KEY_BOOT_IMAGE_MIUI_RES, BOOT_IMAGE_FOR_MIUI_RES); CALLERS.put(KEY_SYSTEM_SERVER, SYSTEM_SERVER); CALLERS.put("com.android.systemui", SYSTEM_UI); } public static HashMap getAll() { return CALLERS; } public static Object[][] get(String where) { return CALLERS.get(where); } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/deopt/PrebuiltMethodsDeopter.java ================================================ /* * 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 */ package org.lsposed.lspd.deopt; import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_BOOT_IMAGE; import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_BOOT_IMAGE_MIUI_RES; import static org.lsposed.lspd.deopt.InlinedMethodCallers.KEY_SYSTEM_SERVER; import org.matrix.vector.nativebridge.HookBridge; import org.lsposed.lspd.util.Hookers; import org.lsposed.lspd.util.Utils; import java.lang.reflect.Executable; import java.util.Arrays; import de.robv.android.xposed.XposedHelpers; public class PrebuiltMethodsDeopter { public static void deoptMethods(String where, ClassLoader cl) { Object[][] callers = InlinedMethodCallers.get(where); if (callers == null) { return; } for (Object[] caller : callers) { try { if (caller.length < 2) continue; if (!(caller[0] instanceof String)) continue; if (!(caller[1] instanceof String)) continue; Executable method; Object[] params = new Object[caller.length - 2]; System.arraycopy(caller, 2, params, 0, params.length); if ("".equals(caller[1])) { method = XposedHelpers.findConstructorExactIfExists((String) caller[0], cl, params); } else { method = XposedHelpers.findMethodExactIfExists((String) caller[0], cl, (String) caller[1], params); } if (method != null) { Hookers.logD("deoptimizing " + method); HookBridge.deoptimizeMethod(method); } } catch (Throwable throwable) { Utils.logE("error when deopting method: " + Arrays.toString(caller), throwable); } } } public static void deoptBootMethods() { // todo check if has been done before deoptMethods(KEY_BOOT_IMAGE, null); } public static void deoptResourceMethods() { if (Utils.isMIUI) { //deopt these only for MIUI deoptMethods(KEY_BOOT_IMAGE_MIUI_RES, null); } } public static void deoptSystemServerMethods(ClassLoader sysCL) { deoptMethods(KEY_SYSTEM_SERVER, sysCL); } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/hooker/AttachHooker.java ================================================ package org.lsposed.lspd.hooker; import android.app.ActivityThread; import de.robv.android.xposed.XposedInit; import io.github.libxposed.api.XposedInterface; public class AttachHooker implements XposedInterface.Hooker { public static void after(XposedInterface.AfterHookCallback callback) { XposedInit.loadModules((ActivityThread) callback.getThisObject()); } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/hooker/CrashDumpHooker.java ================================================ package org.lsposed.lspd.hooker; import org.lsposed.lspd.impl.LSPosedBridge; import org.lsposed.lspd.util.Utils.Log; import io.github.libxposed.api.XposedInterface; public class CrashDumpHooker implements XposedInterface.Hooker { public static void before(XposedInterface.BeforeHookCallback callback) { try { var e = (Throwable) callback.getArgs()[0]; LSPosedBridge.log("Crash unexpectedly: " + Log.getStackTraceString(e)); } catch (Throwable ignored) { } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/hooker/HandleSystemServerProcessHooker.java ================================================ /* * 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 */ package org.lsposed.lspd.hooker; import android.annotation.SuppressLint; import org.lsposed.lspd.deopt.PrebuiltMethodsDeopter; import org.lsposed.lspd.impl.LSPosedHelper; import org.lsposed.lspd.util.Hookers; import io.github.libxposed.api.XposedInterface; // system_server initialization public class HandleSystemServerProcessHooker implements XposedInterface.Hooker { public interface Callback { void onSystemServerLoaded(ClassLoader classLoader); } public static volatile ClassLoader systemServerCL = null; public static volatile Callback callback = null; @SuppressLint("PrivateApi") public static void after() { Hookers.logD("ZygoteInit#handleSystemServerProcess() starts"); try { if (systemServerCL == null) { // get system_server classLoader systemServerCL = Thread.currentThread().getContextClassLoader(); } // deopt methods in SYSTEMSERVERCLASSPATH PrebuiltMethodsDeopter.deoptSystemServerMethods(systemServerCL); var clazz = Class.forName("com.android.server.SystemServer", false, systemServerCL); LSPosedHelper.hookAllMethods(StartBootstrapServicesHooker.class, clazz, "startBootstrapServices"); if (callback != null) callback.onSystemServerLoaded(systemServerCL); } catch (Throwable t) { Hookers.logE("error when hooking systemMain", t); } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCreateCLHooker.java ================================================ /* * 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 */ package org.lsposed.lspd.hooker; import static org.lsposed.lspd.core.ApplicationServiceClient.serviceClient; import android.annotation.SuppressLint; import android.app.ActivityThread; import android.app.LoadedApk; import android.content.pm.ApplicationInfo; import android.os.Build; import androidx.annotation.NonNull; import org.lsposed.lspd.impl.LSPosedContext; import org.lsposed.lspd.util.Hookers; import org.lsposed.lspd.util.MetaDataReader; import org.lsposed.lspd.util.Utils; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XC_MethodReplacement; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.XposedInit; import de.robv.android.xposed.callbacks.XC_LoadPackage; import io.github.libxposed.api.XposedInterface; import io.github.libxposed.api.XposedModuleInterface; @SuppressLint("BlockedPrivateApi") public class LoadedApkCreateCLHooker implements XposedInterface.Hooker { private final static Field defaultClassLoaderField; private final static Set loadedApks = ConcurrentHashMap.newKeySet(); static { Field field = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { try { field = LoadedApk.class.getDeclaredField("mDefaultClassLoader"); field.setAccessible(true); } catch (Throwable ignored) { } } defaultClassLoaderField = field; } static void addLoadedApk(LoadedApk loadedApk) { loadedApks.add(loadedApk); } public static void after(XposedInterface.AfterHookCallback callback) { LoadedApk loadedApk = (LoadedApk) callback.getThisObject(); if (callback.getArgs()[0] != null || !loadedApks.contains(loadedApk)) { return; } try { Hookers.logD("LoadedApk#createClassLoader starts"); String packageName = ActivityThread.currentPackageName(); String processName = ActivityThread.currentProcessName(); boolean isFirstPackage = packageName != null && processName != null && packageName.equals(loadedApk.getPackageName()); if (!isFirstPackage) { packageName = loadedApk.getPackageName(); processName = ActivityThread.currentPackageName(); } else if (packageName.equals("android")) { packageName = "system"; } Object mAppDir = XposedHelpers.getObjectField(loadedApk, "mAppDir"); ClassLoader classLoader = (ClassLoader) XposedHelpers.getObjectField(loadedApk, "mClassLoader"); Hookers.logD("LoadedApk#createClassLoader ends: " + mAppDir + " -> " + classLoader); if (classLoader == null) { return; } if (!isFirstPackage && !XposedHelpers.getBooleanField(loadedApk, "mIncludeCode")) { Hookers.logD("LoadedApk# mIncludeCode == false: " + mAppDir); return; } if (!isFirstPackage && !XposedInit.getLoadedModules().getOrDefault(packageName, Optional.of("")).isPresent()) { return; } XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam( XposedBridge.sLoadedPackageCallbacks); lpparam.packageName = packageName; lpparam.processName = processName; lpparam.classLoader = classLoader; lpparam.appInfo = loadedApk.getApplicationInfo(); lpparam.isFirstApplication = isFirstPackage; if (isFirstPackage && XposedInit.getLoadedModules().getOrDefault(packageName, Optional.empty()).isPresent()) { hookNewXSP(lpparam); } Hookers.logD("Call handleLoadedPackage: packageName=" + lpparam.packageName + " processName=" + lpparam.processName + " isFirstPackage=" + isFirstPackage + " classLoader=" + lpparam.classLoader + " appInfo=" + lpparam.appInfo); XC_LoadPackage.callAll(lpparam); LSPosedContext.callOnPackageLoaded(new XposedModuleInterface.PackageLoadedParam() { @NonNull @Override public String getPackageName() { return loadedApk.getPackageName(); } @NonNull @Override public ApplicationInfo getApplicationInfo() { return loadedApk.getApplicationInfo(); } @NonNull @Override public ClassLoader getDefaultClassLoader() { try { return (ClassLoader) defaultClassLoaderField.get(loadedApk); } catch (Throwable t) { throw new IllegalStateException(t); } } @NonNull @Override public ClassLoader getClassLoader() { return classLoader; } @Override public boolean isFirstPackage() { return isFirstPackage; } }); } catch (Throwable t) { Hookers.logE("error when hooking LoadedApk#createClassLoader", t); } finally { loadedApks.remove(loadedApk); } } private static void hookNewXSP(XC_LoadPackage.LoadPackageParam lpparam) { int xposedminversion = -1; boolean xposedsharedprefs = false; try { Map metaData = MetaDataReader.getMetaData(new File(lpparam.appInfo.sourceDir)); Object minVersionRaw = metaData.get("xposedminversion"); if (minVersionRaw instanceof Integer) { xposedminversion = (Integer) minVersionRaw; } else if (minVersionRaw instanceof String) { xposedminversion = MetaDataReader.extractIntPart((String) minVersionRaw); } xposedsharedprefs = metaData.containsKey("xposedsharedprefs"); } catch (NumberFormatException | IOException e) { Hookers.logE("ApkParser fails", e); } if (xposedminversion > 92 || xposedsharedprefs) { Utils.logI("New modules detected, hook preferences"); XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "checkMode", int.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) { if (((int) param.args[0] & 1/*Context.MODE_WORLD_READABLE*/) != 0) { param.setThrowable(null); } } }); XposedHelpers.findAndHookMethod("android.app.ContextImpl", lpparam.classLoader, "getPreferencesDir", new XC_MethodReplacement() { @Override protected Object replaceHookedMethod(MethodHookParam param) { return new File(serviceClient.getPrefsPath(lpparam.packageName)); } }); } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/hooker/LoadedApkCtorHooker.java ================================================ /* * 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 */ package org.lsposed.lspd.hooker; import android.app.LoadedApk; import android.content.res.XResources; import org.lsposed.lspd.util.Hookers; import org.lsposed.lspd.util.Utils.Log; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.XposedInit; import io.github.libxposed.api.XposedInterface; // when a package is loaded for an existing process, trigger the callbacks as well public class LoadedApkCtorHooker implements XposedInterface.Hooker { public static void after(XposedInterface.AfterHookCallback callback) { Hookers.logD("LoadedApk# starts"); try { LoadedApk loadedApk = (LoadedApk) callback.getThisObject(); assert loadedApk != null; String packageName = loadedApk.getPackageName(); Object mAppDir = XposedHelpers.getObjectField(loadedApk, "mAppDir"); Hookers.logD("LoadedApk# ends: " + mAppDir); if (!XposedInit.disableResources) { XResources.setPackageNameForResDir(packageName, loadedApk.getResDir()); } if (packageName.equals("android")) { if (XposedInit.startsSystemServer) { Hookers.logD("LoadedApk# is android, skip: " + mAppDir); return; } else { packageName = "system"; } } if (!XposedInit.loadedPackagesInProcess.add(packageName)) { Hookers.logD("LoadedApk# has been loaded before, skip: " + mAppDir); return; } // OnePlus magic... if (Log.getStackTraceString(new Throwable()). contains("android.app.ActivityThread$ApplicationThread.schedulePreload")) { Hookers.logD("LoadedApk# maybe oneplus's custom opt, skip"); return; } LoadedApkCreateCLHooker.addLoadedApk(loadedApk); } catch (Throwable t) { Hookers.logE("error when hooking LoadedApk.", t); } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/hooker/OpenDexFileHooker.java ================================================ package org.lsposed.lspd.hooker; import android.os.Build; import org.lsposed.lspd.impl.LSPosedBridge; import org.matrix.vector.nativebridge.HookBridge; import io.github.libxposed.api.XposedInterface; public class OpenDexFileHooker implements XposedInterface.Hooker { public static void after(XposedInterface.AfterHookCallback callback) { ClassLoader classLoader = null; for (var arg : callback.getArgs()) { if (arg instanceof ClassLoader) { classLoader = (ClassLoader) arg; } } if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && classLoader == null) { classLoader = LSPosedBridge.class.getClassLoader(); } while (classLoader != null) { if (classLoader == LSPosedBridge.class.getClassLoader()) { HookBridge.setTrusted(callback.getResult()); return; } else { classLoader = classLoader.getParent(); } } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/hooker/StartBootstrapServicesHooker.java ================================================ /* * 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 */ package org.lsposed.lspd.hooker; import static org.lsposed.lspd.util.Utils.logD; import androidx.annotation.NonNull; import org.lsposed.lspd.impl.LSPosedContext; import org.lsposed.lspd.util.Hookers; import de.robv.android.xposed.XposedBridge; import de.robv.android.xposed.XposedInit; import de.robv.android.xposed.callbacks.XC_LoadPackage; import io.github.libxposed.api.XposedInterface; import io.github.libxposed.api.XposedModuleInterface; public class StartBootstrapServicesHooker implements XposedInterface.Hooker { public static void before() { logD("SystemServer#startBootstrapServices() starts"); try { XposedInit.loadedPackagesInProcess.add("android"); XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks); lpparam.packageName = "android"; lpparam.processName = "android"; // it's actually system_server, but other functions return this as well lpparam.classLoader = HandleSystemServerProcessHooker.systemServerCL; lpparam.appInfo = null; lpparam.isFirstApplication = true; XC_LoadPackage.callAll(lpparam); LSPosedContext.callOnSystemServerLoaded(new XposedModuleInterface.SystemServerLoadedParam() { @Override @NonNull public ClassLoader getClassLoader() { return HandleSystemServerProcessHooker.systemServerCL; } }); } catch (Throwable t) { Hookers.logE("error when hooking startBootstrapServices", t); } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/impl/LSPosedBridge.java ================================================ package org.lsposed.lspd.impl; import androidx.annotation.NonNull; import org.matrix.vector.nativebridge.HookBridge; import org.lsposed.lspd.util.Utils.Log; import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import de.robv.android.xposed.XposedBridge; import io.github.libxposed.api.XposedInterface; import io.github.libxposed.api.errors.HookFailedError; public class LSPosedBridge { private static final String TAG = "LSPosed-Bridge"; private static final String castException = "Return value's type from hook callback does not match the hooked method"; private static final Method getCause; static { Method tmp; try { tmp = InvocationTargetException.class.getMethod("getCause"); } catch (Throwable e) { tmp = null; } getCause = tmp; } public static class HookerCallback { @NonNull final Method beforeInvocation; @NonNull final Method afterInvocation; final int beforeParams; final int afterParams; public HookerCallback(@NonNull Method beforeInvocation, @NonNull Method afterInvocation) { this.beforeInvocation = beforeInvocation; this.afterInvocation = afterInvocation; this.beforeParams = beforeInvocation.getParameterCount(); this.afterParams = afterInvocation.getParameterCount(); } } public static void log(String text) { Log.i(TAG, text); } public static void log(Throwable t) { String logStr = Log.getStackTraceString(t); Log.e(TAG, logStr); } public static class NativeHooker { private final Object params; private NativeHooker(Executable method) { var isStatic = Modifier.isStatic(method.getModifiers()); Object returnType; if (method instanceof Method) { returnType = ((Method) method).getReturnType(); } else { returnType = null; } params = new Object[]{ method, returnType, isStatic, }; } // This method is quite critical. We should try not to use system methods to avoid // endless recursive public Object callback(Object[] args) throws Throwable { LSPosedHookCallback callback = new LSPosedHookCallback<>(); var array = ((Object[]) params); var method = (T) array[0]; var returnType = (Class) array[1]; var isStatic = (Boolean) array[2]; callback.method = method; if (isStatic) { callback.thisObject = null; callback.args = args; } else { callback.thisObject = args[0]; callback.args = new Object[args.length - 1]; //noinspection ManualArrayCopy for (int i = 0; i < args.length - 1; ++i) { callback.args[i] = args[i + 1]; } } Object[][] callbacksSnapshot = HookBridge.callbackSnapshot(HookerCallback.class, method); Object[] modernSnapshot = callbacksSnapshot[0]; Object[] legacySnapshot = callbacksSnapshot[1]; if (modernSnapshot.length == 0 && legacySnapshot.length == 0) { try { return HookBridge.invokeOriginalMethod(method, callback.thisObject, callback.args); } catch (InvocationTargetException ite) { throw (Throwable) HookBridge.invokeOriginalMethod(getCause, ite); } } Object[] ctxArray = new Object[modernSnapshot.length]; XposedBridge.LegacyApiSupport legacy = null; // call "before method" callbacks int beforeIdx; for (beforeIdx = 0; beforeIdx < modernSnapshot.length; beforeIdx++) { try { var hooker = (HookerCallback) modernSnapshot[beforeIdx]; if (hooker.beforeParams == 0) { ctxArray[beforeIdx] = hooker.beforeInvocation.invoke(null); } else { ctxArray[beforeIdx] = hooker.beforeInvocation.invoke(null, callback); } } catch (Throwable t) { LSPosedBridge.log(t); // reset result (ignoring what the unexpectedly exiting callback did) callback.setResult(null); callback.isSkipped = false; continue; } if (callback.isSkipped) { // skip remaining "before" callbacks and corresponding "after" callbacks beforeIdx++; break; } } if (!callback.isSkipped && legacySnapshot.length != 0) { // TODO: Separate classloader legacy = new XposedBridge.LegacyApiSupport<>(callback, legacySnapshot); legacy.handleBefore(); } // call original method if not requested otherwise if (!callback.isSkipped) { try { var result = HookBridge.invokeOriginalMethod(method, callback.thisObject, callback.args); callback.setResult(result); } catch (InvocationTargetException e) { var throwable = (Throwable) HookBridge.invokeOriginalMethod(getCause, e); callback.setThrowable(throwable); } } // call "after method" callbacks for (int afterIdx = beforeIdx - 1; afterIdx >= 0; afterIdx--) { Object lastResult = callback.getResult(); Throwable lastThrowable = callback.getThrowable(); var hooker = (HookerCallback) modernSnapshot[afterIdx]; try { if (hooker.afterParams == 0) { hooker.afterInvocation.invoke(null); } else if (hooker.afterParams == 1) { hooker.afterInvocation.invoke(null, callback); } else { hooker.afterInvocation.invoke(null, callback, ctxArray[afterIdx]); } } catch (Throwable t) { LSPosedBridge.log(t); // reset to last result (ignoring what the unexpectedly exiting callback did) if (lastThrowable == null) { callback.setResult(lastResult); } else { callback.setThrowable(lastThrowable); } } } if (legacy != null) { legacy.handleAfter(); } // return var t = callback.getThrowable(); if (t != null) { throw t; } else { var result = callback.getResult(); if (returnType != null && !returnType.isPrimitive() && !HookBridge.instanceOf(result, returnType)) { throw new ClassCastException(castException); } return result; } } } public static void dummyCallback() { } public static XposedInterface.MethodUnhooker doHook(T hookMethod, int priority, Class hooker) { if (Modifier.isAbstract(hookMethod.getModifiers())) { throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod); } else if (hookMethod.getDeclaringClass().getClassLoader() == LSPosedContext.class.getClassLoader()) { throw new IllegalArgumentException("Do not allow hooking inner methods"); } else if (hookMethod.getDeclaringClass() == Method.class && hookMethod.getName().equals("invoke")) { throw new IllegalArgumentException("Cannot hook Method.invoke"); } else if (hooker == null) { throw new IllegalArgumentException("hooker should not be null!"); } Method beforeInvocation = null, afterInvocation = null; var modifiers = Modifier.PUBLIC | Modifier.STATIC; for (var method : hooker.getDeclaredMethods()) { if (method.getName().equals("before")) { if (beforeInvocation != null) { throw new IllegalArgumentException("More than one method named before"); } boolean valid = (method.getModifiers() & modifiers) == modifiers; var params = method.getParameterTypes(); if (params.length == 1) { valid &= params[0].equals(XposedInterface.BeforeHookCallback.class); } else if (params.length != 0) { valid = false; } if (!valid) { throw new IllegalArgumentException("before method format is invalid"); } beforeInvocation = method; } else if (method.getName().equals("after")) { if (afterInvocation != null) { throw new IllegalArgumentException("More than one method named after"); } boolean valid = (method.getModifiers() & modifiers) == modifiers; valid &= method.getReturnType().equals(void.class); var params = method.getParameterTypes(); if (params.length == 1 || params.length == 2) { valid &= params[0].equals(XposedInterface.AfterHookCallback.class); } else if (params.length != 0) { valid = false; } if (!valid) { throw new IllegalArgumentException("after method format is invalid"); } afterInvocation = method; } } if (beforeInvocation == null && afterInvocation == null) { throw new IllegalArgumentException("No method named before or after found in " + hooker.getName()); } try { if (beforeInvocation == null) { beforeInvocation = LSPosedBridge.class.getMethod("dummyCallback"); } else if (afterInvocation == null) { afterInvocation = LSPosedBridge.class.getMethod("dummyCallback"); } else { var ret = beforeInvocation.getReturnType(); var params = afterInvocation.getParameterTypes(); if (ret != void.class && params.length == 2 && !ret.equals(params[1])) { throw new IllegalArgumentException("before and after method format is invalid"); } } } catch (NoSuchMethodException e) { throw new HookFailedError(e); } var callback = new LSPosedBridge.HookerCallback(beforeInvocation, afterInvocation); if (HookBridge.hookMethod(true, hookMethod, LSPosedBridge.NativeHooker.class, priority, callback)) { return new XposedInterface.MethodUnhooker<>() { @NonNull @Override public T getOrigin() { return hookMethod; } @Override public void unhook() { HookBridge.unhookMethod(true, hookMethod, callback); } }; } throw new HookFailedError("Cannot hook " + hookMethod); } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/impl/LSPosedContext.java ================================================ package org.lsposed.lspd.impl; import android.annotation.SuppressLint; import android.app.ActivityThread; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.os.Build; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.lsposed.lspd.core.BuildConfig; import org.lsposed.lspd.models.Module; import org.matrix.vector.nativebridge.HookBridge; import org.matrix.vector.nativebridge.NativeAPI; import org.lsposed.lspd.service.ILSPInjectedModuleService; import org.lsposed.lspd.util.LspModuleClassLoader; import org.lsposed.lspd.util.Utils.Log; import org.matrix.vector.impl.utils.VectorDexParser; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.nio.ByteBuffer; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import io.github.libxposed.api.XposedInterface; import io.github.libxposed.api.XposedModule; import io.github.libxposed.api.XposedModuleInterface; import io.github.libxposed.api.errors.XposedFrameworkError; import io.github.libxposed.api.utils.DexParser; @SuppressLint("NewApi") public class LSPosedContext implements XposedInterface { private static final String TAG = "LSPosedContext"; public static boolean isSystemServer; public static String appDir; public static String processName; static final Set modules = ConcurrentHashMap.newKeySet(); private final String mPackageName; private final ApplicationInfo mApplicationInfo; private final ILSPInjectedModuleService service; private final Map mRemotePrefs = new ConcurrentHashMap<>(); LSPosedContext(String packageName, ApplicationInfo applicationInfo, ILSPInjectedModuleService service) { this.mPackageName = packageName; this.mApplicationInfo = applicationInfo; this.service = service; } public static void callOnPackageLoaded(XposedModuleInterface.PackageLoadedParam param) { for (XposedModule module : modules) { try { module.onPackageLoaded(param); } catch (Throwable t) { Log.e(TAG, "Error when calling onPackageLoaded of " + module.getApplicationInfo().packageName, t); } } } public static void callOnSystemServerLoaded(XposedModuleInterface.SystemServerLoadedParam param) { for (XposedModule module : modules) { try { module.onSystemServerLoaded(param); } catch (Throwable t) { Log.e(TAG, "Error when calling onSystemServerLoaded of " + module.getApplicationInfo().packageName, t); } } } @SuppressLint("DiscouragedPrivateApi") public static boolean loadModule(ActivityThread at, Module module) { try { Log.d(TAG, "Loading module " + module.packageName); var sb = new StringBuilder(); var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS; for (String abi : abis) { sb.append(module.apkPath).append("!/lib/").append(abi).append(File.pathSeparator); } var librarySearchPath = sb.toString(); var initLoader = XposedModule.class.getClassLoader(); var mcl = LspModuleClassLoader.loadApk(module.apkPath, module.file.preLoadedDexes, librarySearchPath, initLoader); if (mcl.loadClass(XposedModule.class.getName()).getClassLoader() != initLoader) { Log.e(TAG, " Cannot load module: " + module.packageName); Log.e(TAG, " The Xposed API classes are compiled into the module's APK."); Log.e(TAG, " This may cause strange issues and must be fixed by the module developer."); return false; } var ctx = new LSPosedContext(module.packageName, module.applicationInfo, module.service); for (var entry : module.file.moduleClassNames) { var moduleClass = mcl.loadClass(entry); Log.d(TAG, " Loading class " + moduleClass); if (!XposedModule.class.isAssignableFrom(moduleClass)) { Log.e(TAG, " This class doesn't implement any sub-interface of XposedModule, skipping it"); continue; } try { var moduleEntry = moduleClass.getConstructor(XposedInterface.class, XposedModuleInterface.ModuleLoadedParam.class); var moduleContext = (XposedModule) moduleEntry.newInstance(ctx, new XposedModuleInterface.ModuleLoadedParam() { @Override public boolean isSystemServer() { return isSystemServer; } @NonNull @Override public String getProcessName() { return processName; } }); modules.add(moduleContext); } catch (Throwable e) { Log.e(TAG, " Failed to load class " + moduleClass, e); } } module.file.moduleLibraryNames.forEach(NativeAPI::recordNativeEntrypoint); Log.d(TAG, "Loaded module " + module.packageName + ": " + ctx); } catch (Throwable e) { Log.d(TAG, "Loading module " + module.packageName, e); return false; } return true; } @NonNull @Override public String getFrameworkName() { return BuildConfig.FRAMEWORK_NAME; } @NonNull @Override public String getFrameworkVersion() { return BuildConfig.VERSION_NAME; } @Override public long getFrameworkVersionCode() { return BuildConfig.VERSION_CODE; } @Override public int getFrameworkPrivilege() { try { return service.getFrameworkPrivilege(); } catch (RemoteException ignored) { return -1; } } @Override @NonNull public MethodUnhooker hook(@NonNull Method origin, @NonNull Class hooker) { return LSPosedBridge.doHook(origin, PRIORITY_DEFAULT, hooker); } @Override @NonNull public MethodUnhooker hook(@NonNull Method origin, int priority, @NonNull Class hooker) { return LSPosedBridge.doHook(origin, priority, hooker); } @Override @NonNull public MethodUnhooker> hook(@NonNull Constructor origin, @NonNull Class hooker) { return LSPosedBridge.doHook(origin, PRIORITY_DEFAULT, hooker); } @Override @NonNull public MethodUnhooker> hook(@NonNull Constructor origin, int priority, @NonNull Class hooker) { return LSPosedBridge.doHook(origin, priority, hooker); } @Override @NonNull public MethodUnhooker> hookClassInitializer(@NonNull Class origin, @NonNull Class hooker) { return hookClassInitializer(origin, PRIORITY_DEFAULT, hooker); } @Override @NonNull @SuppressWarnings({"unchecked", "rawtypes"}) public MethodUnhooker> hookClassInitializer(@NonNull Class origin, int priority, @NonNull Class hooker) { Method staticInitializer = HookBridge.getStaticInitializer(origin); // The class might not have a static initializer block if (staticInitializer == null) { throw new IllegalArgumentException("Class " + origin.getName() + " has no static initializer"); } // Use the existing doHook logic. It will return a MethodUnhooker. return (MethodUnhooker) LSPosedBridge.doHook(staticInitializer, priority, hooker); } private static boolean doDeoptimize(@NonNull Executable method) { if (Modifier.isAbstract(method.getModifiers())) { throw new IllegalArgumentException("Cannot deoptimize abstract methods: " + method); } else if (Proxy.isProxyClass(method.getDeclaringClass())) { throw new IllegalArgumentException("Cannot deoptimize methods from proxy class: " + method); } return HookBridge.deoptimizeMethod(method); } @Override public boolean deoptimize(@NonNull Method method) { return doDeoptimize(method); } @Override public boolean deoptimize(@NonNull Constructor constructor) { return doDeoptimize(constructor); } @Nullable @Override public Object invokeOrigin(@NonNull Method method, @Nullable Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { return HookBridge.invokeOriginalMethod(method, thisObject, args); } @Override public void invokeOrigin(@NonNull Constructor constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { // The bridge returns an Object (null for void/constructors), which we discard. HookBridge.invokeOriginalMethod(constructor, thisObject, args); } private static char getTypeShorty(Class type) { if (type == int.class) { return 'I'; } else if (type == long.class) { return 'J'; } else if (type == float.class) { return 'F'; } else if (type == double.class) { return 'D'; } else if (type == boolean.class) { return 'Z'; } else if (type == byte.class) { return 'B'; } else if (type == char.class) { return 'C'; } else if (type == short.class) { return 'S'; } else if (type == void.class) { return 'V'; } else { return 'L'; } } private static char[] getExecutableShorty(Executable executable) { var parameterTypes = executable.getParameterTypes(); var shorty = new char[parameterTypes.length + 1]; shorty[0] = getTypeShorty(executable instanceof Method ? ((Method) executable).getReturnType() : void.class); for (int i = 1; i < shorty.length; i++) { shorty[i] = getTypeShorty(parameterTypes[i - 1]); } return shorty; } @Nullable @Override public Object invokeSpecial(@NonNull Method method, @NonNull Object thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { if (Modifier.isStatic(method.getModifiers())) { throw new IllegalArgumentException("Cannot invoke special on static method: " + method); } return HookBridge.invokeSpecialMethod(method, getExecutableShorty(method), method.getDeclaringClass(), thisObject, args); } @Override public void invokeSpecial(@NonNull Constructor constructor, @NonNull T thisObject, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException { HookBridge.invokeSpecialMethod(constructor, getExecutableShorty(constructor), constructor.getDeclaringClass(), thisObject, args); } @NonNull @Override public T newInstanceOrigin(@NonNull Constructor constructor, Object... args) throws InvocationTargetException, IllegalAccessException, InstantiationException { var obj = HookBridge.allocateObject(constructor.getDeclaringClass()); HookBridge.invokeOriginalMethod(constructor, obj, args); return obj; } @NonNull @Override public U newInstanceSpecial(@NonNull Constructor constructor, @NonNull Class subClass, Object... args) throws InvocationTargetException, IllegalArgumentException, IllegalAccessException, InstantiationException { var superClass = constructor.getDeclaringClass(); if (!superClass.isAssignableFrom(subClass)) { throw new IllegalArgumentException(subClass + " is not inherited from " + superClass); } var obj = HookBridge.allocateObject(subClass); HookBridge.invokeSpecialMethod(constructor, getExecutableShorty(constructor), superClass, obj, args); return obj; } @Override public void log(@NonNull String message) { Log.i(TAG, mPackageName + ": " + message); } @Override public void log(@NonNull String message, @NonNull Throwable throwable) { Log.e(TAG, mPackageName + ": " + message, throwable); } @Override public DexParser parseDex(@NonNull ByteBuffer dexData, boolean includeAnnotations) throws IOException { return new VectorDexParser(dexData, includeAnnotations); } @NonNull @Override public ApplicationInfo getApplicationInfo() { return mApplicationInfo; } @NonNull @Override public SharedPreferences getRemotePreferences(String name) { if (name == null) throw new IllegalArgumentException("name must not be null"); return mRemotePrefs.computeIfAbsent(name, n -> { try { return new LSPosedRemotePreferences(service, n); } catch (RemoteException e) { log("Failed to get remote preferences", e); throw new XposedFrameworkError(e); } }); } @NonNull @Override public String[] listRemoteFiles() { try { return service.getRemoteFileList(); } catch (RemoteException e) { log("Failed to list remote files", e); throw new XposedFrameworkError(e); } } @NonNull @Override public ParcelFileDescriptor openRemoteFile(String name) throws FileNotFoundException { if (name == null) throw new IllegalArgumentException("name must not be null"); try { return service.openRemoteFile(name); } catch (RemoteException e) { throw new FileNotFoundException(e.getMessage()); } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/impl/LSPosedHelper.java ================================================ package org.lsposed.lspd.impl; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Set; import io.github.libxposed.api.XposedInterface; import io.github.libxposed.api.errors.HookFailedError; public class LSPosedHelper { @SuppressWarnings("UnusedReturnValue") public static XposedInterface.MethodUnhooker hookMethod(Class hooker, Class clazz, String methodName, Class... parameterTypes) { try { var method = clazz.getDeclaredMethod(methodName, parameterTypes); return LSPosedBridge.doHook(method, XposedInterface.PRIORITY_DEFAULT, hooker); } catch (NoSuchMethodException e) { throw new HookFailedError(e); } } @SuppressWarnings("UnusedReturnValue") public static Set> hookAllMethods(Class hooker, Class clazz, String methodName) { var unhooks = new HashSet>(); for (var method : clazz.getDeclaredMethods()) { if (method.getName().equals(methodName)) { unhooks.add(LSPosedBridge.doHook(method, XposedInterface.PRIORITY_DEFAULT, hooker)); } } return unhooks; } @SuppressWarnings("UnusedReturnValue") public static XposedInterface.MethodUnhooker> hookConstructor(Class hooker, Class clazz, Class... parameterTypes) { try { var constructor = clazz.getDeclaredConstructor(parameterTypes); return LSPosedBridge.doHook(constructor, XposedInterface.PRIORITY_DEFAULT, hooker); } catch (NoSuchMethodException e) { throw new HookFailedError(e); } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/impl/LSPosedHookCallback.java ================================================ package org.lsposed.lspd.impl; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.reflect.Executable; import java.lang.reflect.Member; import io.github.libxposed.api.XposedInterface; public class LSPosedHookCallback implements XposedInterface.BeforeHookCallback, XposedInterface.AfterHookCallback { public Member method; public Object thisObject; public Object[] args; public Object result; public Throwable throwable; public boolean isSkipped; public LSPosedHookCallback() { } // Both before and after @NonNull @Override public Member getMember() { return this.method; } @Nullable @Override public Object getThisObject() { return this.thisObject; } @NonNull @Override public Object[] getArgs() { return this.args; } // Before @Override public void returnAndSkip(@Nullable Object result) { this.result = result; this.throwable = null; this.isSkipped = true; } @Override public void throwAndSkip(@Nullable Throwable throwable) { this.result = null; this.throwable = throwable; this.isSkipped = true; } // After @Nullable @Override public Object getResult() { return this.result; } @Nullable @Override public Throwable getThrowable() { return this.throwable; } @Override public boolean isSkipped() { return this.isSkipped; } @Override public void setResult(@Nullable Object result) { this.result = result; this.throwable = null; } @Override public void setThrowable(@Nullable Throwable throwable) { this.result = null; this.throwable = throwable; } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/impl/LSPosedRemotePreferences.java ================================================ package org.lsposed.lspd.impl; import android.content.SharedPreferences; import android.os.Bundle; import android.os.RemoteException; import android.util.ArraySet; import androidx.annotation.Nullable; import org.lsposed.lspd.service.ILSPInjectedModuleService; import org.lsposed.lspd.service.IRemotePreferenceCallback; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; @SuppressWarnings("unchecked") public class LSPosedRemotePreferences implements SharedPreferences { private final Map mMap = new ConcurrentHashMap<>(); final HashSet mListeners = new HashSet<>(); IRemotePreferenceCallback callback = new IRemotePreferenceCallback.Stub() { @Override synchronized public void onUpdate(Bundle bundle) { Set changes = new ArraySet<>(); if (bundle.containsKey("delete")) { var deletes = (Set) bundle.getSerializable("delete"); changes.addAll(deletes); for (var key : deletes) { mMap.remove(key); } } if (bundle.containsKey("put")) { var puts = (Map) bundle.getSerializable("put"); mMap.putAll(puts); changes.addAll(puts.keySet()); } synchronized (mListeners) { for (var key : changes) { mListeners.forEach(listener -> listener.onSharedPreferenceChanged(LSPosedRemotePreferences.this, key)); } } } }; public LSPosedRemotePreferences(ILSPInjectedModuleService service, String group) throws RemoteException { Bundle output = service.requestRemotePreferences(group, callback); if (output.containsKey("map")) { mMap.putAll((Map) output.getSerializable("map")); } } @Override public Map getAll() { return new TreeMap<>(mMap); } @Nullable @Override public String getString(String key, @Nullable String defValue) { var v = (String) mMap.getOrDefault(key, defValue); if (v != null) return v; return defValue; } @Nullable @Override public Set getStringSet(String key, @Nullable Set defValues) { var v = (Set) mMap.getOrDefault(key, defValues); if (v != null) return v; return defValues; } @Override public int getInt(String key, int defValue) { var v = (Integer) mMap.getOrDefault(key, defValue); if (v != null) return v; return defValue; } @Override public long getLong(String key, long defValue) { var v = (Long) mMap.getOrDefault(key, defValue); if (v != null) return v; return defValue; } @Override public float getFloat(String key, float defValue) { var v = (Float) mMap.getOrDefault(key, defValue); if (v != null) return v; return defValue; } @Override public boolean getBoolean(String key, boolean defValue) { var v = (Boolean) mMap.getOrDefault(key, defValue); if (v != null) return v; return defValue; } @Override public boolean contains(String key) { return mMap.containsKey(key); } @Override public Editor edit() { throw new UnsupportedOperationException("Read only implementation"); } @Override public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized (mListeners) { mListeners.add(listener); } } @Override public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { synchronized (mListeners) { mListeners.remove(listener); } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/util/ClassPathURLStreamHandler.java ================================================ package org.lsposed.lspd.util; import java.io.File; import java.io.FileNotFoundException; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import sun.net.www.ParseUtil; import sun.net.www.protocol.jar.Handler; final class ClassPathURLStreamHandler extends Handler { private final String fileUri; private final JarFile jarFile; ClassPathURLStreamHandler(String jarFileName) throws IOException { jarFile = new JarFile(jarFileName); fileUri = new File(jarFileName).toURI().toString(); } URL getEntryUrlOrNull(String entryName) { if (jarFile.getEntry(entryName) != null) { try { String encodedName = ParseUtil.encodePath(entryName, false); return new URL("jar", null, -1, fileUri + "!/" + encodedName, this); } catch (MalformedURLException e) { throw new RuntimeException("Invalid entry name", e); } } return null; } @Override protected URLConnection openConnection(URL url) throws IOException { return new ClassPathURLConnection(url); } @Override protected void finalize() throws IOException { jarFile.close(); } private final class ClassPathURLConnection extends JarURLConnection { private JarFile connectionJarFile = null; private ZipEntry jarEntry = null; private InputStream jarInput = null; private boolean closed = false; private ClassPathURLConnection(URL url) throws MalformedURLException { super(url); setUseCaches(false); } @Override public void setUseCaches(boolean usecaches) { super.setUseCaches(false); } @Override public void connect() throws IOException { if (closed) { throw new IllegalStateException("JarURLConnection has been closed"); } if (!connected) { jarEntry = jarFile.getEntry(getEntryName()); if (jarEntry == null) { throw new FileNotFoundException("URL=" + url + ", zipfile=" + jarFile.getName()); } connected = true; } } @Override public JarFile getJarFile() throws IOException { connect(); if (connectionJarFile != null) return connectionJarFile; return connectionJarFile = new JarFile(jarFile.getName()); } @Override public InputStream getInputStream() throws IOException { connect(); if (jarInput != null) return jarInput; return jarInput = new FilterInputStream(jarFile.getInputStream(jarEntry)) { @Override public void close() throws IOException { super.close(); closed = true; jarFile.close(); if (connectionJarFile != null) connectionJarFile.close(); } }; } @Override public String getContentType() { String cType = guessContentTypeFromName(getEntryName()); if (cType == null) { cType = "content/unknown"; } return cType; } @Override public int getContentLength() { try { connect(); return (int) getJarEntry().getSize(); } catch (IOException ignored) { } return -1; } } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/util/Hookers.java ================================================ /* * 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 */ package org.lsposed.lspd.util; import android.app.ActivityThread; public class Hookers { public static void logD(String prefix) { Utils.logD(String.format("%s: pkg=%s, prc=%s", prefix, ActivityThread.currentPackageName(), ActivityThread.currentProcessName())); } public static void logE(String prefix, Throwable throwable) { Utils.logE(String.format("%s: pkg=%s, prc=%s", prefix, ActivityThread.currentPackageName(), ActivityThread.currentProcessName()), throwable); } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java ================================================ package org.lsposed.lspd.util; import static de.robv.android.xposed.XposedBridge.TAG; import android.os.Build; import android.os.SharedMemory; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Objects; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import org.lsposed.lspd.util.Utils.Log; import hidden.ByteBufferDexClassLoader; import sun.misc.CompoundEnumeration; @SuppressWarnings("ConstantConditions") public final class LspModuleClassLoader extends ByteBufferDexClassLoader { private static final String zipSeparator = "!/"; private static final List systemNativeLibraryDirs = splitPaths(System.getProperty("java.library.path")); private final String apk; private final List nativeLibraryDirs = new ArrayList<>(); private static List splitPaths(String searchPath) { var result = new ArrayList(); if (searchPath == null) return result; for (var path : searchPath.split(File.pathSeparator)) { result.add(new File(path)); } return result; } private LspModuleClassLoader(ByteBuffer[] dexBuffers, ClassLoader parent, String apk) { super(dexBuffers, parent); this.apk = apk; } @RequiresApi(Build.VERSION_CODES.Q) private LspModuleClassLoader(ByteBuffer[] dexBuffers, String librarySearchPath, ClassLoader parent, String apk) { super(dexBuffers, librarySearchPath, parent); initNativeLibraryDirs(librarySearchPath); this.apk = apk; } private void initNativeLibraryDirs(String librarySearchPath) { nativeLibraryDirs.addAll(splitPaths(librarySearchPath)); nativeLibraryDirs.addAll(systemNativeLibraryDirs); } @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { var cl = findLoadedClass(name); if (cl != null) { return cl; } try { return Object.class.getClassLoader().loadClass(name); } catch (ClassNotFoundException ignored) { } ClassNotFoundException fromSuper; try { return findClass(name); } catch (ClassNotFoundException ex) { fromSuper = ex; } try { return getParent().loadClass(name); } catch (ClassNotFoundException cnfe) { throw fromSuper; } } @Override public String findLibrary(String libraryName) { var fileName = System.mapLibraryName(libraryName); for (var file : nativeLibraryDirs) { var path = file.getPath(); if (path.contains(zipSeparator)) { var split = path.split(zipSeparator, 2); try (var jarFile = new JarFile(split[0])) { var entryName = split[1] + '/' + fileName; var entry = jarFile.getEntry(entryName); if (entry != null && entry.getMethod() == ZipEntry.STORED) { return split[0] + zipSeparator + entryName; } } catch (IOException e) { Log.e(TAG, "Can not open " + split[0], e); } } else if (file.isDirectory()) { var entryPath = new File(file, fileName).getPath(); try { var fd = Os.open(entryPath, OsConstants.O_RDONLY, 0); Os.close(fd); return entryPath; } catch (ErrnoException ignored) { } } } return null; } @Override public String getLdLibraryPath() { var result = new StringBuilder(); for (var directory : nativeLibraryDirs) { if (result.length() > 0) { result.append(':'); } result.append(directory); } return result.toString(); } @Override protected URL findResource(String name) { try { var urlHandler = new ClassPathURLStreamHandler(apk); var url = urlHandler.getEntryUrlOrNull(name); if (url == null) { // noinspection FinalizeCalledExplicitly urlHandler.finalize(); } return url; } catch (IOException e) { return null; } } @Override protected Enumeration findResources(String name) { var result = new ArrayList(); var url = findResource(name); if (url != null) result.add(url); return Collections.enumeration(result); } @Override public URL getResource(String name) { var resource = Object.class.getClassLoader().getResource(name); if (resource != null) return resource; resource = findResource(name); if (resource != null) return resource; final var cl = getParent(); return (cl == null) ? null : cl.getResource(name); } @Override public Enumeration getResources(String name) throws IOException { @SuppressWarnings("unchecked") final var resources = (Enumeration[]) new Enumeration[]{ Object.class.getClassLoader().getResources(name), findResources(name), getParent() == null ? null : getParent().getResources(name)}; return new CompoundEnumeration<>(resources); } @NonNull @Override public String toString() { if (apk == null) return "LspModuleClassLoader[instantiating]"; return "LspModuleClassLoader[module=" + apk + ", " + super.toString() + "]"; } public static ClassLoader loadApk(String apk, List dexes, String librarySearchPath, ClassLoader parent) { var dexBuffers = dexes.stream().parallel().map(dex -> { try { return dex.mapReadOnly(); } catch (ErrnoException e) { Log.w(TAG, "Can not map " + dex, e); return null; } }).filter(Objects::nonNull).toArray(ByteBuffer[]::new); LspModuleClassLoader cl; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { cl = new LspModuleClassLoader(dexBuffers, librarySearchPath, parent, apk); } else { cl = new LspModuleClassLoader(dexBuffers, parent, apk); cl.initNativeLibraryDirs(librarySearchPath); } Arrays.stream(dexBuffers).parallel().forEach(SharedMemory::unmap); dexes.stream().parallel().forEach(SharedMemory::close); return cl; } } ================================================ FILE: core/src/main/java/org/lsposed/lspd/util/MetaDataReader.java ================================================ /* * 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 */ package org.lsposed.lspd.util; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.jar.JarFile; import pxb.android.axml.AxmlReader; import pxb.android.axml.AxmlVisitor; import pxb.android.axml.NodeVisitor; public class MetaDataReader { private final HashMap metaData = new HashMap<>(); public static Map getMetaData(File apk) throws IOException { return new MetaDataReader(apk).metaData; } private MetaDataReader(File apk) throws IOException { try (JarFile zip = new JarFile(apk); var is = zip.getInputStream(zip.getEntry("AndroidManifest.xml"))) { var reader = new AxmlReader(getBytesFromInputStream(is)); reader.accept(new AxmlVisitor() { @Override public NodeVisitor child(String ns, String name) { NodeVisitor child = super.child(ns, name); return new ManifestTagVisitor(child); } }); } } public static byte[] getBytesFromInputStream(InputStream inputStream) throws IOException { try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { byte[] b = new byte[1024]; int n; while ((n = inputStream.read(b)) != -1) { bos.write(b, 0, n); } return bos.toByteArray(); } } private class ManifestTagVisitor extends NodeVisitor { public ManifestTagVisitor(NodeVisitor child) { super(child); } @Override public NodeVisitor child(String ns, String name) { NodeVisitor child = super.child(ns, name); if ("application".equals(name)) { return new ApplicationTagVisitor(child); } return child; } private class ApplicationTagVisitor extends NodeVisitor { public ApplicationTagVisitor(NodeVisitor child) { super(child); } @Override public NodeVisitor child(String ns, String name) { NodeVisitor child = super.child(ns, name); if ("meta-data".equals(name)) { return new MetaDataVisitor(child); } return child; } } } private class MetaDataVisitor extends NodeVisitor { public String name = null; public Object value = null; public MetaDataVisitor(NodeVisitor child) { super(child); } @Override public void attr(String ns, String name, int resourceId, int type, Object obj) { if (type == 3 && "name".equals(name)) { this.name = (String) obj; } if ("value".equals(name)) { value = obj; } super.attr(ns, name, resourceId, type, obj); } @Override public void end() { if (name != null && value != null) { metaData.put(name, value); } super.end(); } } public static int extractIntPart(String str) { int result = 0, length = str.length(); for (int offset = 0; offset < length; offset++) { char c = str.charAt(offset); if ('0' <= c && c <= '9') result = result * 10 + (c - '0'); else break; } return result; } } ================================================ FILE: crowdin.yml ================================================ project_id_env: CROWDIN_PROJECT_ID api_token_env: CROWDIN_API_TOKEN base_path: . base_url: 'https://api.crowdin.com' pull_request_title: '[translation] Update translation from Crowdin' preserve_hierarchy: 1 files: - source: /app/src/main/res/values/strings.xml translation: /app/src/main/res/values-%two_letters_code%/%original_file_name% type: android dest: /app/strings.xml - source: /daemon/src/main/res/values/strings.xml translation: /daemon/src/main/res/values-%two_letters_code%/%original_file_name% type: android dest: /daemon/strings.xml ================================================ FILE: daemon/.gitignore ================================================ /build /.cxx ================================================ FILE: daemon/build.gradle.kts ================================================ /* * 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 */ import com.android.build.api.dsl.ApplicationExtension import com.android.ide.common.signing.KeystoreHelper import java.io.PrintStream plugins { alias(libs.plugins.agp.app) alias(libs.plugins.lsplugin.resopt) } val daemonName = "LSPosed" val injectedPackageName: String by rootProject.extra val injectedPackageUid: Int by rootProject.extra val agpVersion: String by project val defaultManagerPackageName: String by rootProject.extra android { buildFeatures { prefab = true buildConfig = true } defaultConfig { applicationId = "org.lsposed.daemon" buildConfigField( "String", "DEFAULT_MANAGER_PACKAGE_NAME", """"$defaultManagerPackageName"""", ) buildConfigField("String", "MANAGER_INJECTED_PKG_NAME", """"$injectedPackageName"""") buildConfigField("int", "MANAGER_INJECTED_UID", """$injectedPackageUid""") } 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") } } namespace = "org.lsposed.daemon" } android.applicationVariants.all { 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/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 = {${ certificateInfo.certificate.encoded.joinToString(",") }}; |}""" .trimMargin() ) } } registerJavaGeneratingTask(signInfoTask, outSrcDir.asFile) } 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) } ================================================ FILE: daemon/proguard-rules.pro ================================================ -keepclasseswithmembers,includedescriptorclasses class * { native ; } -keepclasseswithmembers class org.lsposed.lspd.Main { 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.lsposed.lspd.service.LogcatService { private int refreshFd(boolean); } -keepclassmembers class ** implements android.content.ContextWrapper { public int getUserId(); public android.os.UserHandle getUser(); } -assumenosideeffects class android.util.Log { public static *** v(...); public static *** d(...); } -repackageclasses -allowaccessmodification ================================================ FILE: daemon/src/main/AndroidManifest.xml ================================================ ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/Main.java ================================================ package org.lsposed.lspd; import org.lsposed.lspd.service.ServiceManager; public class Main { public static void main(String[] args) { ServiceManager.start(args); } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/ActivityManagerService.java ================================================ /* * 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); } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/BridgeService.java ================================================ 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); } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/ConfigFileManager.java ================================================ /* * 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(); } } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/ConfigManager.java ================================================ /* * 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]); } } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/Dex2OatService.java ================================================ /* * 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(); } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/LSPApplicationService.java ================================================ /* * 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; } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/LSPInjectedModuleService.java ================================================ 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 int getFrameworkPrivilege() { return IXposedService.FRAMEWORK_PRIVILEGE_ROOT; } @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); } } } } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/LSPManagerService.java ================================================ /* * 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.API; } @Override public int 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); } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/LSPModuleService.java ================================================ /* * 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 API; } @Override public String getFrameworkName() throws RemoteException { ensureModule(); return "LSPosed"; } @Override public String getFrameworkVersion() throws RemoteException { ensureModule(); return BuildConfig.VERSION_NAME; } @Override public long getFrameworkVersionCode() throws RemoteException { ensureModule(); return BuildConfig.VERSION_CODE; } @Override public int getFrameworkPrivilege() throws RemoteException { ensureModule(); return IXposedService.FRAMEWORK_PRIVILEGE_ROOT; } @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(String packageName, IXposedScopeCallback callback) throws RemoteException { var userId = ensureModule(); if (ConfigManager.getInstance().scopeRequestBlocked(loadedModule.packageName)) { callback.onScopeRequestDenied(packageName); } else { LSPNotificationManager.requestModuleScope(loadedModule.packageName, userId, packageName, callback); callback.onScopeRequestPrompted(packageName); } } @Override public String removeScope(String packageName) throws RemoteException { var userId = ensureModule(); try { if (!ConfigManager.getInstance().removeModuleScope(loadedModule.packageName, packageName, userId)) { return "Invalid request"; } return null; } catch (Throwable e) { return e.getMessage(); } } @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()); } } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/LSPNotificationManager.java ================================================ 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.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", "LSPosed"); 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", "LSPosed"); 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", "LSPosed"); try { var nm = getNotificationManager(); nm.enqueueNotificationWithTag("android", opPkg, modulePackageName, pushAndGetNotificationId(SCOPE_CHANNEL_ID, modulePackageName, moduleUserId), notification, 0); } catch (RemoteException e) { try { callback.onScopeRequestFailed(scopePackageName, 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); } } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/LSPSystemServerService.java ================================================ /* * 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"); } } } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/LSPosedService.java ================================================ /* * 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.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(scopePackageName, "Package not found"); return; } switch (action) { case "approve" -> { ConfigManager.getInstance().setModuleScope(packageName, scopePackageName, userId); iCallback.onScopeRequestApproved(scopePackageName); } case "deny" -> iCallback.onScopeRequestDenied(scopePackageName); case "delete" -> iCallback.onScopeRequestTimeout(scopePackageName); case "block" -> { ConfigManager.getInstance().blockScopeRequest(packageName); iCallback.onScopeRequestDenied(scopePackageName); } } Log.i(TAG, action + " scope " + scopePackageName + " for " + packageName + " in user " + userId); } catch (RemoteException e) { try { iCallback.onScopeRequestFailed(scopePackageName, 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); } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/LogcatService.java ================================================ 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); } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/ObfuscationManager.java ================================================ 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(); } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/PackageService.java ================================================ /* * 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.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; 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(); for (var user : UserService.getUsers()) { // in case pkginfo of other users in primary user ParceledListSlice infos; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { infos = pm.getInstalledPackages((long) flags, user.id); } else { infos = pm.getInstalledPackages(flags, user.id); } res.addAll(infos .getList().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); } } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/PowerService.java ================================================ /* * */ 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); } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/ServiceManager.java ================================================ /* * 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; } } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/service/UserService.java ================================================ /* * 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); } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/util/FakeContext.java ================================================ 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); } } ================================================ FILE: daemon/src/main/java/org/lsposed/lspd/util/InstallerVerifier.java ================================================ 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); } } } ================================================ FILE: daemon/src/main/jni/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) project(daemon) set(CMAKE_CXX_STANDARD 23) add_subdirectory(${VECTOR_ROOT}/external external) set(SOURCES dex2oat.cpp logcat.cpp obfuscation.cpp ) add_library(${PROJECT_NAME} SHARED ${SOURCES}) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(${PROJECT_NAME} PRIVATE lsplant_static dex_builder_static android log) if (DEFINED DEBUG_SYMBOLS_PATH) message(STATUS "Debug symbols will be placed at ${DEBUG_SYMBOLS_PATH}") add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug COMMAND ${CMAKE_STRIP} --strip-all $ COMMAND ${CMAKE_OBJCOPY} --add-gnu-debuglink ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug $) endif() ================================================ FILE: daemon/src/main/jni/dex2oat.cpp ================================================ /* * 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 #include #include #include #include #include #include "logging.h" extern "C" JNIEXPORT void JNICALL Java_org_lsposed_lspd_service_Dex2OatService_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 (pid_t pid = fork(); pid > 0) { // parent 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); if (enabled) { LOGI("Enable dex2oat wrapper"); if (r32) { mount(dex2oat32, r32p, nullptr, MS_BIND, nullptr); mount(nullptr, r32p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } if (d32) { mount(dex2oat32, d32p, nullptr, MS_BIND, nullptr); mount(nullptr, d32p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } if (r64) { mount(dex2oat64, r64p, nullptr, MS_BIND, nullptr); mount(nullptr, r64p, nullptr, MS_BIND | MS_REMOUNT | MS_RDONLY, nullptr); } if (d64) { 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); execlp("resetprop", "resetprop", "dalvik.vm.dex2oat-flags", "--inline-max-code-units=0", nullptr); } PLOGE("Failed to resetprop"); exit(1); } } 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); if (fd < 0) return -1; int ret; if (context) { do { ret = write(fd, context, strlen(context) + 1); } while (ret < 0 && errno == EINTR); } else { do { 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); int ret = setsockcreatecon_raw(context); env->ReleaseStringUTFChars(contextStr, context); return ret == 0; } extern "C" JNIEXPORT jstring JNICALL Java_org_lsposed_lspd_service_Dex2OatService_getSockPath(JNIEnv *env, jobject) { return env->NewStringUTF("5291374ceda0aef7c5d86cd2a4f6a3ac\0"); } ================================================ FILE: daemon/src/main/jni/logcat.cpp ================================================ #include "logcat.h" #include #include #include #include #include #include #include #include #include using namespace std::string_view_literals; using namespace std::chrono_literals; constexpr size_t kMaxLogSize = 4 * 1024 * 1024; constexpr long kLogBufferSize = 128 * 1024; namespace { constexpr std::array kLogChar = { /*ANDROID_LOG_UNKNOWN*/ '?', /*ANDROID_LOG_DEFAULT*/ '?', /*ANDROID_LOG_VERBOSE*/ 'V', /*ANDROID_LOG_DEBUG*/ 'D', /*ANDROID_LOG_INFO*/ 'I', /*ANDROID_LOG_WARN*/ 'W', /*ANDROID_LOG_ERROR*/ 'E', /*ANDROID_LOG_FATAL*/ 'F', /*ANDROID_LOG_SILENT*/ 'S', }; long ParseUint(const char *s) { if (s[0] == '\0') return -1; while (isspace(*s)) { s++; } if (s[0] == '-') { return -1; } int base = (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) ? 16 : 10; char *end; auto result = strtoull(s, &end, base); if (end == s) { return -1; } if (*end != '\0') { const char *suffixes = "bkmgtpe"; const char *suffix; if ((suffix = strchr(suffixes, tolower(*end))) == nullptr || __builtin_mul_overflow(result, 1ULL << (10 * (suffix - suffixes)), &result)) { return -1; } } if (std::numeric_limits::max() < result) { return -1; } return static_cast(result); } inline long GetByteProp(std::string_view prop, long def = -1) { std::array buf{}; if (__system_property_get(prop.data(), buf.data()) < 0) return def; return ParseUint(buf.data()); } inline std::string GetStrProp(std::string_view prop, std::string def = {}) { std::array buf{}; if (__system_property_get(prop.data(), buf.data()) < 0) return def; return {buf.data()}; } inline bool SetIntProp(std::string_view prop, int val) { auto buf = std::to_string(val); return __system_property_set(prop.data(), buf.data()) >= 0; } inline bool SetStrProp(std::string_view prop, std::string_view val) { return __system_property_set(prop.data(), val.data()) >= 0; } } // namespace class UniqueFile : public std::unique_ptr> { inline static deleter_type deleter = [](auto f) { f &&f != stdout &&fclose(f); }; public: explicit UniqueFile(FILE *f) : std::unique_ptr>(f, deleter) {} UniqueFile(int fd, const char *mode) : UniqueFile(fd > 0 ? fdopen(fd, mode) : stdout) {}; UniqueFile() : UniqueFile(stdout) {}; }; class Logcat { public: explicit Logcat(JNIEnv *env, jobject thiz, jmethodID method) : env_(env), thiz_(thiz), refresh_fd_method_(method) {} [[noreturn]] void Run(); private: inline void RefreshFd(bool is_verbose); inline void Log(std::string_view str); void OnCrash(int err); void ProcessBuffer(struct log_msg *buf); static size_t PrintLogLine(const AndroidLogEntry &entry, FILE *out); void StartLogWatchDog(); JNIEnv *env_; jobject thiz_; jmethodID refresh_fd_method_; UniqueFile modules_file_{}; size_t modules_file_part_ = 0; size_t modules_print_count_ = 0; UniqueFile verbose_file_{}; size_t verbose_file_part_ = 0; size_t verbose_print_count_ = 0; pid_t my_pid_ = getpid(); bool verbose_ = true; std::atomic enable_watchdog = std::atomic(false); }; size_t Logcat::PrintLogLine(const AndroidLogEntry &entry, FILE *out) { if (!out) return 0; constexpr static size_t kMaxTimeBuff = 64; struct tm tm{}; std::array time_buff{}; auto now = entry.tv_sec; auto nsec = entry.tv_nsec; auto message_len = entry.messageLen; const auto *message = entry.message; if (now < 0) { nsec = NS_PER_SEC - nsec; } if (message_len >= 1 && message[message_len - 1] == '\n') { --message_len; } localtime_r(&now, &tm); strftime(time_buff.data(), time_buff.size(), "%Y-%m-%dT%H:%M:%S", &tm); int len = fprintf(out, "[ %s.%03ld %8d:%6d:%6d %c/%-15.*s ] %.*s\n", time_buff.data(), nsec / MS_PER_NSEC, entry.uid, entry.pid, entry.tid, kLogChar[entry.priority], static_cast(entry.tagLen), entry.tag, static_cast(message_len), message); fflush(out); // trigger overflow when failed to generate a new fd if (len <= 0) len = kMaxLogSize; return static_cast(len); } void Logcat::RefreshFd(bool is_verbose) { constexpr auto start = "----part %zu start----\n"; constexpr auto end = "-----part %zu end----\n"; if (is_verbose) { verbose_print_count_ = 0; fprintf(verbose_file_.get(), end, verbose_file_part_); fflush(verbose_file_.get()); verbose_file_ = UniqueFile(env_->CallIntMethod(thiz_, refresh_fd_method_, JNI_TRUE), "a"); verbose_file_part_++; fprintf(verbose_file_.get(), start, verbose_file_part_); fflush(verbose_file_.get()); } else { modules_print_count_ = 0; fprintf(modules_file_.get(), end, modules_file_part_); fflush(modules_file_.get()); modules_file_ = UniqueFile(env_->CallIntMethod(thiz_, refresh_fd_method_, JNI_FALSE), "a"); modules_file_part_++; fprintf(modules_file_.get(), start, modules_file_part_); fflush(modules_file_.get()); } } inline void Logcat::Log(std::string_view str) { if (verbose_) { fprintf(verbose_file_.get(), "%.*s", static_cast(str.size()), str.data()); fflush(verbose_file_.get()); } fprintf(modules_file_.get(), "%.*s", static_cast(str.size()), str.data()); fflush(modules_file_.get()); } void Logcat::OnCrash(int err) { using namespace std::string_literals; constexpr size_t max_restart_logd_wait = 1U << 10; static size_t kLogdCrashCount = 0; static size_t kLogdRestartWait = 1 << 3; if (++kLogdCrashCount >= kLogdRestartWait) { Log("\nLogd crashed too many times, trying manually start...\n"); __system_property_set("ctl.restart", "logd"); if (kLogdRestartWait < max_restart_logd_wait) { kLogdRestartWait <<= 1; } else { kLogdCrashCount = 0; } } else { Log("\nLogd maybe crashed (err="s + strerror(err) + "), retrying in 1s...\n"); } std::this_thread::sleep_for(1s); } void Logcat::ProcessBuffer(struct log_msg *buf) { AndroidLogEntry entry; 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") [[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]] { verbose_print_count_ += PrintLogLine(entry, verbose_file_.get()); } if (entry.pid == my_pid_ && tag == "LSPosedLogcat"sv) [[unlikely]] { std::string_view msg(entry.message, entry.messageLen); if (msg == "!!start_verbose!!"sv) { verbose_ = true; verbose_print_count_ += PrintLogLine(entry, verbose_file_.get()); } else if (msg == "!!stop_verbose!!"sv) { verbose_ = false; } else if (msg == "!!refresh_modules!!"sv) { 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; RefreshFd(true); RefreshFd(false); while (true) { std::unique_ptr logger_list{ android_logger_list_alloc(0, tail, 0), &android_logger_list_free}; tail = tail_after_crash; for (log_id id : {LOG_ID_MAIN, LOG_ID_CRASH}) { auto *logger = android_logger_open(logger_list.get(), id); if (logger == nullptr) continue; if (auto size = android_logger_get_log_size(logger); size >= 0 && static_cast(size) < kLogBufferSize) { android_logger_set_log_size(logger, kLogBufferSize); } } struct log_msg msg{}; while (true) { if (android_logger_list_read(logger_list.get(), &msg) <= 0) [[unlikely]] break; ProcessBuffer(&msg); if (verbose_print_count_ >= kMaxLogSize) [[unlikely]] RefreshFd(true); 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) { jclass clazz = env->GetObjectClass(thiz); jmethodID method = env->GetMethodID(clazz, "refreshFd", "(Z)I"); Logcat logcat(env, thiz, method); logcat.Run(); } ================================================ FILE: daemon/src/main/jni/logcat.h ================================================ #pragma once #include #include #include #define NS_PER_SEC 1000000000L #define MS_PER_NSEC 1000000 #define LOGGER_ENTRY_MAX_LEN (5 * 1024) #ifdef __cplusplus extern "C" { #endif typedef struct AndroidLogEntry_t { time_t tv_sec; long tv_nsec; android_LogPriority priority; int32_t uid; int32_t pid; int32_t tid; const char *tag; size_t tagLen; size_t messageLen; const char *message; } AndroidLogEntry; struct logger_entry { uint16_t len; /* length of the payload */ uint16_t hdr_size; /* sizeof(struct logger_entry) */ int32_t pid; /* generating process's pid */ uint32_t tid; /* generating process's tid */ uint32_t sec; /* seconds since Epoch */ uint32_t nsec; /* nanoseconds */ uint32_t lid; /* log id of the payload, bottom 4 bits currently */ uint32_t uid; /* generating process's uid */ }; struct log_msg { union alignas(4) { unsigned char buf[LOGGER_ENTRY_MAX_LEN + 1]; struct logger_entry entry; }; #ifdef __cplusplus log_id_t id() { return static_cast(entry.lid); } #endif }; struct logger; struct logger_list; 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); int android_logger_list_read(struct logger_list *logger_list, struct log_msg *log_msg); struct logger *android_logger_open(struct logger_list *logger_list, log_id_t id); int android_log_processLogBuffer(struct logger_entry *buf, AndroidLogEntry *entry); #ifdef __cplusplus } #endif ================================================ FILE: daemon/src/main/jni/logging.h ================================================ /* * 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 #include #include #ifndef LOG_TAG #define LOG_TAG "LSPosed" #endif #ifdef LOG_DISABLED #define LOGD(...) 0 #define LOGV(...) 0 #define LOGI(...) 0 #define LOGW(...) 0 #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__) #else #define LOGD(...) 0 #define LOGV(...) 0 #endif #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__) #define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) #endif #endif // _LOGGING_H ================================================ FILE: daemon/src/main/jni/obfuscation.cpp ================================================ #include "obfuscation.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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/", ""}, }; jclass class_file_descriptor = nullptr; jmethodID method_file_descriptor_ctor = nullptr; jclass class_shared_memory = nullptr; jmethodID method_shared_memory_ctor = nullptr; } // anonymous namespace // Converts Dex signatures to Java format. // Trailing slashes are translated to dots, which correctly aligns with // Java's string matching expectations for package prefixes. static std::string to_java(const std::string &signature) { std::string java(signature, 1); std::replace(java.begin(), java.end(), '/', '.'); return java; } static void ensureInitialized(JNIEnv *env) { // Thread-safe one-time initialization std::call_once(init_flag, [&]() { LOGD("ObfuscationManager.init"); if (auto file_descriptor = lsplant::JNI_FindClass(env, "java/io/FileDescriptor")) { class_file_descriptor = static_cast(lsplant::JNI_NewGlobalRef(env, file_descriptor)); } else return; method_file_descriptor_ctor = lsplant::JNI_GetMethodID(env, class_file_descriptor, "", "(I)V"); if (auto shared_memory = lsplant::JNI_FindClass(env, "android/os/SharedMemory")) { class_shared_memory = static_cast(lsplant::JNI_NewGlobalRef(env, shared_memory)); } else return; method_shared_memory_ctor = lsplant::JNI_GetMethodID(env, class_shared_memory, "", "(Ljava/io/FileDescriptor;)V"); auto regen = [](std::string_view original_signature) { static constexpr auto chrs = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; thread_local static std::mt19937 rg{std::random_device{}()}; thread_local static std::uniform_int_distribution pick( 0, strlen(chrs) - 1); thread_local static std::uniform_int_distribution choose_slash( 0, 10); std::string out; size_t length = original_signature.size(); out.reserve(length); out += "L"; for (size_t i = 1; i < length - 1; i++) { if (choose_slash(rg) > 8 && // 20% chance for a slash out.back() != '/' && // Avoid consecutive slashes i != 1 && // No slash immediately after 'L' i != length - 2) { // No slash right before the end out += "/"; } else { out += chrs[pick(rg)]; } } // Respect the original termination character type to prevent if (original_signature.back() == '/') { out += "/"; } else { out += chrs[pick(rg)]; } if (out.length() != original_signature.length()) { LOGE("Length mismatch! Org: %zu vs New: %zu. '%s' -> '%s'", original_signature.length(), out.length(), std::string(original_signature).c_str(), out.c_str()); } return out; }; for (auto &i : signatures) { i.second = regen(i.first); LOGD("%s => %s", i.first.c_str(), i.second.c_str()); } LOGD("ObfuscationManager init successfully"); }); } static jobject stringMapToJavaHashMap(JNIEnv *env, const std::map &map) { jclass mapClass = env->FindClass("java/util/HashMap"); if (mapClass == nullptr) return nullptr; jmethodID init = env->GetMethodID(mapClass, "", "()V"); jobject hashMap = env->NewObject(mapClass, init); jmethodID put = env->GetMethodID(mapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); for (const auto &[key, value] : map) { jstring keyJava = env->NewStringUTF(key.c_str()); jstring valueJava = env->NewStringUTF(value.c_str()); env->CallObjectMethod(hashMap, put, keyJava, valueJava); env->DeleteLocalRef(keyJava); env->DeleteLocalRef(valueJava); } jobject hashMapGlobal = env->NewGlobalRef(hashMap); env->DeleteLocalRef(hashMap); env->DeleteLocalRef(mapClass); return hashMapGlobal; } extern "C" JNIEXPORT jobject JNICALL Java_org_lsposed_lspd_service_ObfuscationManager_getSignatures( JNIEnv *env, [[maybe_unused]] jclass clazz) { ensureInitialized(env); static jobject signatures_jni = nullptr; static std::once_flag jni_map_flag; // Thread-safe, one-time JNI HashMap translation std::call_once(jni_map_flag, [&]() { std::map signatures_java; for (const auto &i : signatures) { signatures_java[to_java(i.first)] = to_java(i.second); } signatures_jni = stringMapToJavaHashMap(env, signatures_java); }); return signatures_jni; } static int obfuscateDexBuffer(const void *dex_data, size_t size) { // LOGD("obfuscateDexBuffer: dex_data=%p, size=%zu", dex_data, size); dex::Reader reader{reinterpret_cast(dex_data), size}; reader.CreateFullIr(); auto ir = reader.GetIr(); LOGD("Mutating strings in-place"); // Mutate strings in-place. for (auto &i : ir->strings) { const char *s = i->c_str(); for (const auto &signature : signatures) { char *p = const_cast(strstr(s, signature.first.c_str())); if (p) memcpy(p, signature.second.c_str(), signature.first.length()); } } dex::Writer writer(ir); size_t new_size; DexAllocator allocator; // CreateImage calls allocator.Allocate() auto *image = writer.CreateImage(&allocator, &new_size); LOGD("writer.CreateImage returned: %p", image); return allocator.GetFd(); } extern "C" JNIEXPORT jobject JNICALL Java_org_lsposed_lspd_service_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); // 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. void *mem = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mem == MAP_FAILED) { LOGE("Failed to map input dex"); close(fd); return nullptr; } // Process the DEX and obtain a new file descriptor for the output int new_fd = obfuscateDexBuffer(mem, size); // Safely unmap and close the input buffer mapping munmap(mem, size); close(fd); if (new_fd < 0) { LOGE("Obfuscation failed to create new dex buffer"); return nullptr; } // Construct new SharedMemory object around the new_fd auto java_fd = lsplant::JNI_NewObject(env, class_file_descriptor, method_file_descriptor_ctor, new_fd); auto java_sm = lsplant::JNI_NewObject(env, class_shared_memory, method_shared_memory_ctor, java_fd); return java_sm.release(); } ================================================ FILE: daemon/src/main/jni/obfuscation.h ================================================ #pragma once #include #include #include #include #include "logging.h" // Custom allocator for dex::Writer that creates an ashmem region. // Manages the virtual memory mapping lifecycle to prevent memory leaks. class DexAllocator : public dex::Writer::Allocator { void* mapped_mem_ = nullptr; size_t mapped_size_ = 0; int fd_ = -1; public: inline void* Allocate(size_t size) override { LOGD("DexAllocator: attempting to allocate %zu bytes", size); fd_ = ASharedMemory_create("obfuscated_dex", size); if (fd_ < 0) { // Log the specific error PLOGE("DexAllocator: ASharedMemory_create"); return nullptr; } mapped_size_ = size; // MAP_SHARED is required for the output buffer so that Slicer's writes // are immediately reflected in the underlying file descriptor. mapped_mem_ = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0); if (mapped_mem_ == MAP_FAILED) { PLOGE("DexAllocator: mmap"); close(fd_); fd_ = -1; mapped_mem_ = nullptr; } LOGD("DexAllocator: success, mapped at %p, fd=%d", mapped_mem_, fd_); return mapped_mem_; } inline void Free(void* ptr) override { if (ptr == mapped_mem_ && mapped_mem_ != nullptr) { munmap(mapped_mem_, mapped_size_); close(fd_); mapped_mem_ = nullptr; fd_ = -1; mapped_size_ = 0; } } inline int GetFd() const { return fd_; } inline ~DexAllocator() { // Unmap the virtual memory upon destruction to prevent memory leaks. if (mapped_mem_ != nullptr && mapped_mem_ != MAP_FAILED) { munmap(mapped_mem_, mapped_size_); } // Notice: We do NOT close(fd_) here! // The file descriptor is extracted via GetFd() and handed over to Java's SharedMemory, // which assumes lifecycle ownership of it. } }; ================================================ FILE: daemon/src/main/res/drawable/ic_baseline_block_24.xml ================================================ ================================================ FILE: daemon/src/main/res/drawable/ic_baseline_check_24.xml ================================================ ================================================ FILE: daemon/src/main/res/drawable/ic_baseline_close_24.xml ================================================ ================================================ FILE: daemon/src/main/res/drawable/ic_notification.xml ================================================ ================================================ FILE: daemon/src/main/res/values/strings.xml ================================================ Xposed 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 %s has been updated, please force stop and restart apps in its scope Xposed 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 Scope Request %1$s on user %2$s requests to add %3$s to its scope. Scope request Approve Deny Never Ask ================================================ FILE: daemon/src/main/res/values-af/strings.xml ================================================ Xposed module is not activated yet %1$s is geïnstalleer, maar is nog nie geaktiveer nie %1$s 已為用戶 %2$s 安裝,但尚未激活 %d module enabled %s is opgedateer, forseer asseblief stop en herbegin programme binne die omvang daarvan 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 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 ================================================ FILE: daemon/src/main/res/values-ar/strings.xml ================================================ وحدة Xposed لم يتم تفعيلها بعد %1$s تم التثبيت ولكنه لم يفعل بعد %1$s تم تثبيته للمستخدم %2$s ولكن لم يتم تفعيله بعد وحدة Xposed تم تحديثها %s تم تحديثه، يرجى فرض إيقاف وإعادة تشغيل التطبيقات في نطاقه تم تحديث وحدة Xposed، مطلوب إعادة تشغيل النظام %s تم تحديثه، نظراً لأن النطاق يحتوي على إطار النظام، يتطلب إعادة التشغيل لتطبيق التغييرات اكتمل تحديث الوحدة حالة LSPosed تم تحميل LSPosed اضغط على الإشعار لفتح المدير Scope Request %1$s على %2$s طلبات المستخدم لإضافة %3$s إلى نطاقه. طلب النطاق اوافق رفض لا تسأل أبدا ================================================ FILE: daemon/src/main/res/values-bg/strings.xml ================================================ Модулът Xposed все още не е активиран %1$s е инсталиран, но все още не е активиран %1$s е инсталиран на потребител %2$s, но все още не е активиран Актуализиран модул Xposed %s е актуализиран, моля, спрете принудително и рестартирайте приложенията в неговия обхват Актуализиран модул Xposed, изисква се рестартиране на системата %s е актуализиран, тъй като обхватът съдържа System Framework, необходимо е рестартиране, за да се приложат промените Актуализация на модула е завършена Предложен статус на LSP LSPosed заредени Докоснете известието, за да отворите мениджъра Заявка за обхват %1$s при заявки от страна на потребителя %2$s за добавяне на %3$s към неговия обхват. Искане за обхват Одобряване на Отказ Никога не питайте ================================================ FILE: daemon/src/main/res/values-bn/strings.xml ================================================ Xposed মডিউল এখনও সক্রিয় করা হয় নি %1$s ইনস্টল করা হয়েছে, কিন্তু এখনও সক্রিয় করা হয়নি %1$s ব্যবহারকারী %2$sএ ইনস্টল করা হয়েছে, কিন্তু এখনও সক্রিয় করা হয়নি এক্সপোজড মডিউল আপডেট করা হয়েছে %s আপডেট করা হয়েছে, অনুগ্রহ করে এর সুযোগে অ্যাপগুলিকে জোর করে থামান এবং পুনরায় চালু করুন Xposed মডিউল আপডেট করা হয়েছে, সিস্টেম রিবুট প্রয়োজন %s আপডেট করা হয়েছে, যেহেতু সুযোগে সিস্টেম ফ্রেমওয়ার্ক রয়েছে, পরিবর্তনগুলি প্রয়োগ করার জন্য রিবুট প্রয়োজন মডিউল আপডেট সম্পূর্ণ LSPপোজড স্ট্যাটাস LSPosed লোড ম্যানেজার খুলতে বিজ্ঞপ্তিতে ট্যাপ করুন সুযোগ অনুরোধ ব্যবহারকারী %2$s এর সুযোগে %3$s যোগ করার অনুরোধে %1$s। সুযোগ অনুরোধ অনুমোদন করুন অস্বীকার করুন কখনো জিজ্ঞাসা করবেন না ================================================ FILE: daemon/src/main/res/values-ca/strings.xml ================================================ El mòdul Xposed encara no està activat %1$s s\'ha instal·lat, però encara no està activat %1$s s\'ha instal·lat a l\'usuari %2$s, però encara no està activat Mòdul Xposed actualitzat %s s\'ha actualitzat, si us plau, força l\'aturada i reinici de les aplicacions del seu abast 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 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 ================================================ FILE: daemon/src/main/res/values-cs/strings.xml ================================================ Xposed modul ještě není aktivován %1$s byl nainstalován, ale ještě není aktivován %1$s byl nainstalován uživateli %2$s, ale ještě není aktivován Xposed modul byl aktualizován %s byl aktualizován, prosím, násilně zastavte aplikace a restartujte je 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 Žá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 ================================================ FILE: daemon/src/main/res/values-da/strings.xml ================================================ Xposed modul er endnu ikke aktiveret %1$s er blevet installeret, men er ikke aktiveret endnu %1$s er blevet installeret på brugeren %2$s, men er endnu ikke aktiveret Xposed modul opdateret %s er blevet opdateret, gennemtving stop og genstart apps i dets anvendelsesområde 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 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 ================================================ FILE: daemon/src/main/res/values-de/strings.xml ================================================ Das Xposed-Modul ist noch nicht aktiviert %1$s wurde installiert, ist aber noch nicht aktiviert %1$s wurde unter dem Benutzer %2$s installiert, ist aber noch nicht aktiviert Xposed-Modul aktualisiert %s wurde aktualisiert, bitte Stopp erzwingen und die Apps in deren Scope neu starten 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 Scope-Anfrage %1$s von Benutzer %2$s fordert an, %3$s zu seinem Scope-Bereich hinzuzufügen. Scope-Anfrage Genehmigen Verweigern Niemals fragen ================================================ FILE: daemon/src/main/res/values-el/strings.xml ================================================ Το Xposed πρόσθετο δεν έχει ενεργοποιηθεί ακόμα %1$s έχει εγκατασταθεί, αλλά δεν έχει ενεργοποιηθεί ακόμα %1$s έχει εγκατασταθεί στον χρήστη %2$s, αλλά δεν έχει ενεργοποιηθεί ακόμη Το πρόσθετο Xposed ενημερώθηκε %s ενημερώθηκε, παρακαλώ κλείστε εξαναγκαστικά και επανεκκινήστε τις εφαρμογές στο πεδίο εφαρμογής της Το πρόσθετο Xposed ενημερώθηκε, απαιτείται επανεκκίνηση συστήματος %s έχει ενημερωθεί, δεδομένου ότι το πεδίο εφαρμογής περιέχει Πλαίσιο Συστήματος, απαιτείται επανεκκίνηση για να εφαρμοστούν οι αλλαγές Η ενημέρωση πρόσθετου ολοκληρώθηκε Κατάσταση LSPosed LSPosed φορτώθηκε Πατήστε την ειδοποίηση για άνοιγμα διαχειριστή Αίτηση για το πεδίο εφαρμογής %1$s στις αιτήσεις του χρήστη %2$s για την προσθήκη του %3$s στο πεδίο εφαρμογής του. Αίτημα πεδίου εφαρμογής Έγκριση Άρνηση Ποτέ μην ρωτάς ================================================ FILE: daemon/src/main/res/values-es/strings.xml ================================================ El módulo Xposed aún no está activado %1$s ha sido instalado, pero aún no está activado %1$s ha sido instalado al usuario %2$s, pero no está activado todavía Módulo Xpose actualizado %s ha sido actualizado, fuerce la detención y el reinicio de las aplicaciones en su alcance Módulo Xposed actualizado, es necesario reiniciar el sistema %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 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 ================================================ FILE: daemon/src/main/res/values-et/strings.xml ================================================ Xposed moodul ei ole aktiveeritud %1$s on paigaldatud, kuid ei ole aktiveeritud %1$s on paigaldatud kasutajale %2$s, kuid ei ole aktiveeritud Xposed moodul uuendatud %s on uuendatud, siis peatage ja taaskäivitage rakendused, mis kuuluvad selle kohaldamisalasse. 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 Ulatuse Taotlus %1$s kasutajal %2$s taotleb %3$s lisamist oma ulatusse. Ulatuse taotlus Kinnita Keela Ära Enam Küsi ================================================ FILE: daemon/src/main/res/values-fa/strings.xml ================================================ ماژول Xposed هنوز فعال نشده است %1$s نصب شده اما هنوز فعال نشده است %1$s برای کاربر %2$s نصب شده اما هنوز فعال نشده است ماژول Xposed به‌روزرسانی شد %s به‌روزرسانی شده است، لطفاً برنامه‌های مربوطه را به‌زور متوقف و مجدداً راه‌اندازی کنید ماژول Xposed به‌روزرسانی شد، نیاز به راه‌اندازی مجدد سیستم %s به‌روزرسانی شده است؛ از آنجا که محدوده شامل چارچوب سیستم است، برای اعمال تغییرات نیاز به راه‌اندازی مجدد سیستم است به‌روزرسانی ماژول کامل شد وضعیت LSPosed LSPosed بارگذاری شد برای باز کردن مدیر روی اعلان ضربه بزنید درخواست محدوده %1$s روی کاربر %2$s درخواست افزودن %3$s به محدوده خود را دارد. درخواست محدوده تأیید رد هرگز نپرس ================================================ FILE: daemon/src/main/res/values-fi/strings.xml ================================================ Xposed moduuli ei ole vielä aktivoitu %1$s on asennettu, mutta sitä ei ole vielä aktivoitu. %1$s on asennettu käyttäjälle %2$s, mutta sitä ei ole vielä aktivoitu. Xposed moduuli päivitetty %s on päivitetty, paina pysäytä ja käynnistä sovellukset uudelleen sen laajuudessa 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 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 ================================================ FILE: daemon/src/main/res/values-fr/strings.xml ================================================ Le module LSPosed 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 %s a été mis à jour, merci de forcer l\’arrêt ou de redémarrer les applis dans leurs champs d\’application 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 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 ================================================ FILE: daemon/src/main/res/values-hi/strings.xml ================================================ एक्सपोज़ड मॉड्यूल अभी तक सक्रिय नहीं है %1$s इंस्टॉल कर दिया गया है, लेकिन अब तक एक्टिवेट नही किया गया %1$s को %2$s यूजर में इंस्टॉल कर दिया गया है, लेकिन अब तक एक्टिवेट नही किया गया है एक्सपोज़ड मॉड्यूल अपडेट किया गया %s अपडेट कर दिया गया है, कृपया बलपूर्वक रोकें और इसके दायरे में ऐप्स को पुनरारंभ करें एक्सपोज़ड मॉड्यूल अपडेट किया गया, सिस्टम रीबूट की आवश्यकता है %s को अपडेट कर दिया गया है, क्योंकि स्कोप में सिस्टम फ्रेमवर्क है, परिवर्तनों को लागू करने के लिए रीबूट की आवश्यकता है मॉड्यूल अद्यतन पूर्ण एलएसपोस्ड स्थिति LSPosed लोड किया गया मैनेजर खोलने के लिए नोटिफिकेशन पर टैप करें गुंजाइश अनुरोध उपयोगकर्ता %2$s पर %1$s इसके दायरे में %3$s जोड़ने का अनुरोध करता है। दायरा अनुरोध मंज़ूरी देना अस्वीकार करना कभी मत पूछो ================================================ FILE: daemon/src/main/res/values-hr/strings.xml ================================================ Xposed modul još nije aktiviran %1$s je instaliran, ali još nije aktiviran %1$s je instaliran korisniku %2$s, ali još nije aktiviran Modul Xposed ažuriran %s je ažuriran, prisilno zaustavite i ponovno pokrenite aplikacije u njegovom opsegu 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 Zahtjev za opseg %1$s na korisniku %2$s zahtijeva dodavanje %3$s svom opsegu. Zahtjev za opseg Odobriti Poreći Nikad ne pitaj ================================================ FILE: daemon/src/main/res/values-hu/strings.xml ================================================ Az Xposed modul még nincs aktiválva %1$s telepítve lett, de még nincs aktiválva. %1$s telepítve lett a %2$sfelhasználóhoz, de még nincs aktiválva. Xposed modul frissítve %s frissítésre került, kérjük, kényszerítse az alkalmazások leállítását és újraindítását a hatókörében. 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 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á ================================================ FILE: daemon/src/main/res/values-in/strings.xml ================================================ Modul Xposed belum diaktifkan %1$s sudah diinstal, tetapi belum diaktifkan %1$s telah diinstal ke pengguna %2$s, tetapi belum diaktifkan Modul xposed diperbarui %s telah diperbarui, harap paksa berhenti dan mulai ulang aplikasi dalam cakupannya 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 Permintaan Cakupan %1$s pada pengguna %2$s meminta untuk menambahkan %3$s ke dalam cakupannya. Permintaan cakupan Menyetujui Menolak Jangan Pernah Bertanya ================================================ FILE: daemon/src/main/res/values-it/strings.xml ================================================ Il modulo Xposed non è ancora attivo %1$s è stato installato, ma non è ancora attivo %1$s è stato installato sull\'utente %2$s, ma non è ancora attivo Modulo Xposed aggiornato %s è stato aggiornato, arresta e riavvia le applicazioni per le quali è abilitato 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 Richiesta attivazione %1$s sull\'utente %2$s richiede di aggiungere %3$s alle sue attivazioni. Richiesta attivazione Approva Nega Non chiedere mai ================================================ FILE: daemon/src/main/res/values-iw/strings.xml ================================================ מודול LSPosed עדיין לא הופעל %1$s הותקן, אך אינו מופעל עדיין %1$s הותקן למשתמש %2$s, אך אינו מופעל עדיין מודול LSPosed עודכן %s עודכן מודול Xposed עודכן, נדרש אתחול המערכת %s עודכן, מכיוון שההיקף מכיל System Framework, נדרש אתחול כדי להחיל שינויים עדכון המודול הושלם סטטוס LSPost LSPosed נטען הקש על ההודעה כדי לפתוח את המנהל Xposed_מודול_מבקש_כותרת_תחום %1$s על משתמש %2$s מבקש להוסיף %3$s להיקף שלו. תחום_שם_ערוץ אישור_תחום לְהַכּחִישׁ לעולם אל תשאל ================================================ FILE: daemon/src/main/res/values-ja/strings.xml ================================================ Xposed モジュールが有効化されていません %1$s はインストールされましたが、 有効化されていません %1$s はユーザー %2$s にインストールされましたが、 有効化されていません Xposed モジュールが更新されました %s が更新されました。スコープ内のアプリを強制停止してから再起動してください Xposed モジュールが更新されました。システムの再起動が必要です %s が更新されました。スコープにシステムフレームワークが含まれているため、変更を適用するには再起動が必要です モジュールの更新完了通知 LSPosed のステータス通知 LSPosed の読み込み完了通知 通知をタップしてマネージャーを開きます スコープのリクエスト ユーザー %2$s の %1$s が %3$s をそのスコープに追加するようリクエストしています。 スコープのリクエスト 許可 拒否 再度表示しない ================================================ FILE: daemon/src/main/res/values-ko/strings.xml ================================================ Xposed 모듈이 아직 활성화되지 않았습니다. %1$s이(가) 설치되었지만 아직 활성화되지 않았습니다. 사용자 %2$s님에게 %1$s 이(가) 설치되었지만 아직 활성화되지 않았습니다. Xposed 모듈 업데이트 %s이(가) 업데이트되었습니다. Xposed 모듈이 업데이트되었습니다, 재부팅이 필요합니다. 범위에 시스템 프레임워크가 포함되어 있으므로 %s 이 업데이트되었습니다. 변경 사항을 적용하려면 재부팅해야 합니다. 모듈 업데이트 완료 LS포즈 상태 LSPosed 로드됨 알림을 탭하여 관리자 열기 범위 요청 사용자 %2$s 의 %1$s 은 해당 범위에 %3$s 을 추가하도록 요청합니다. 범위 요청 승인 거부 다시 묻지 않음 ================================================ FILE: daemon/src/main/res/values-ku/strings.xml ================================================ Modula Xposed hîn nehatiye çalak kirin %1$s hatiye saz kirin, lê hîn nehatiye aktîfkirin %1$s ji bikarhêner %2$sre hate saz kirin, lê hêj nehatiye çalak kirin Modula Xposed hate nûve kirin %s hate nûve kirin, ji kerema xwe bi zorê sepanan rawestînin û di çarçoveya wê de ji nû ve bidin destpêkirin Modula Xposed hate nûve kirin, pêdivî ye ku pergalê ji nû ve dest pê bike %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 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 ================================================ FILE: daemon/src/main/res/values-lt/strings.xml ================================================ Xposed modulis dar nėra aktyvuotas \"%1$s\" buvo įdiegta, tačiau liko dar nesuaktyvuota \"%1$s\" buvo įdiegta vartotojui \"%2$s\", tačiau liko dar nesuaktyvuota Atnaujintas Xposed modulis %s buvo atnaujintas, priverstinai sustabdykite ir iš naujo paleiskite jo taikymo srityje esančias programas Atnaujintas \"Xposed\" modulis, reikalingas sistemos perkrovimas %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ę Apimties prašymas %1$s pagal naudotojo %2$s užklausas įtraukti %3$s į jo taikymo sritį. Apimties prašymas Patvirtinti Atsisakyti Niekada neklauskite ================================================ FILE: daemon/src/main/res/values-nl/strings.xml ================================================ LSPosed 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 %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 Scope verzoek %1$s op gebruiker %2$s verzoeken om %3$s toe te voegen aan het toepassingsgebied. Scope verzoek Goedkeuren Weiger Nooit vragen ================================================ FILE: daemon/src/main/res/values-no/strings.xml ================================================ Xposed modul er ikke aktivert enda %1$s er installert, men er ikke aktivert ennå %1$s har blitt installert til bruker %2$s, men er ikke aktivert ennå Xposed modul er oppdatert %s har blitt oppdatert, vennligst tvang-stopp og start apper på nytt i virkeområdet Xposed modul oppdatert, system-omstart kreves %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 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 ================================================ FILE: daemon/src/main/res/values-pl/strings.xml ================================================ Moduł Xposed nie jest jeszcze aktywowany %1$s został zainstalowany, ale nie jest jeszcze aktywny %1$s został zainstalowany na użytkowniku %2$s, ale nie jest jeszcze aktywowany Moduł Xposed zaktualizowany %s został zaktualizowany, wymuś zatrzymanie i ponownie uruchom aplikacje w jego zakresie 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 Żądanie Zakresu %1$s w użytkowniku %2$s żąda dodania %3$s do jego zakresu. Żądanie zakresu Zatwierdź Odrzuć Nigdy nie pytaj ================================================ FILE: daemon/src/main/res/values-pt/strings.xml ================================================ O módulo ainda não está ativo %1$s foi instalado, mas ainda não está ativo %1$s foi instalado no usuário %2$s, mas ainda não está ativo Módulo Atualizado %s foi atualizado. Force a parada do módulo e reinicie os apps que estão em seu escopo 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 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 ================================================ FILE: daemon/src/main/res/values-pt-rBR/strings.xml ================================================ O módulo ainda não está ativo %1$s foi instalado, mas ainda não está ativo %1$s foi instalado no usuário %2$s, mas ainda não está ativo Módulo atualizado %s foi atualizado. Force a parada e reinicie os apps que estão em seu escopo. 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 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 ================================================ FILE: daemon/src/main/res/values-ro/strings.xml ================================================ Modulul Xposed nu este încă activat Modulul %1$s este instalat, dar nu este încă activat Modulul %1$s a fost instalat pentru utilizatorul %2$s, dar nu este încă activat Modulul Xposed a fost actualizat Modulul %s a fost actualizat, vă rugăm să reporniți aplicațiile din cadrul configurației sale 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 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 ================================================ FILE: daemon/src/main/res/values-ru/strings.xml ================================================ Модуль Xposed пока не активирован %1$s установлен, но пока не активирован %1$s установлен (пользователь %2$s), но пока не активирован Модуль Xposed обновлён %s обновлён, выполните остановку приложений в его «охвате» и перезапустите их Модуль Xposed обновлён, требуется перезагрузка устройства %s обновлён; ввиду того, что системный фреймворк (System Framework) в его «охвате», требуется перезагрузка для применения изменений Обновление модуля завершено Статус LSPosed LSPosed загружен Нажмите уведомление, чтобы открыть LSPosed Manager Запрос «охвата» %1$s (пользователь %2$s): запрашивается добавление %3$s в «охват». Запрос «охвата» Принять Отклонить Больше не спрашивать ================================================ FILE: daemon/src/main/res/values-si/strings.xml ================================================ Xposed මොඩියුලය තවම සක්‍රිය කර නැත %1$s ස්ථාපනය කර ඇත, නමුත් තවමත් සක්රිය කර නැත පරිශීලක %2$sවෙත %1$s ස්ථාපනය කර ඇත, නමුත් තවමත් සක්‍රිය කර නොමැත Xposed මොඩියුලය යාවත්කාලීන කරන ලදී %s යාවත්කාලීන කර ඇත, කරුණාකර එහි විෂය පථය තුළ යෙදුම් බලහත්කාරයෙන් නතර කර නැවත ආරම්භ කරන්න Xposed මොඩියුලය යාවත්කාලීන කරන ලදි, පද්ධතිය නැවත ආරම්භ කිරීම අවශ්‍යයි %s යාවත්කාලීන කර ඇත, විෂය පථයේ පද්ධති රාමුව අඩංගු බැවින්, වෙනස්කම් යෙදීමට නැවත පණගැන්වීම අවශ්‍ය වේ මොඩියුල යාවත්කාලීන කිරීම සම්පූර්ණයි එල්එස්පී තත්ත්වය LSPposed පටවා ඇත කළමනාකරු විවෘත කිරීමට දැනුම්දීම තට්ටු කරන්න විෂය පථය ඉල්ලීම පරිශීලක %2$s හි %1$s එහි විෂය පථයට %3$s එකතු කරන ලෙස ඉල්ලා සිටී. විෂය පථය ඉල්ලීම අනුමත කරන්න ප්රතික්ෂේප කරන්න කවදාවත් අහන්න එපා ================================================ FILE: daemon/src/main/res/values-sk/strings.xml ================================================ Modul Xposed ešte nie je aktivovaný %1$s bol nainštalovaný, ale ešte nie je aktivovaný %1$s bol nainštalovaný na stránke používateľa %2$s, ale ešte nie je aktivovaný. Aktualizovaný modul Xposed %s bola aktualizovaná, vynúťte si zastavenie a reštartovanie aplikácií v jej rozsahu Aktualizácia modulu Xposed, potrebný reštart systému %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 Ž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 ================================================ FILE: daemon/src/main/res/values-sv/strings.xml ================================================ Xposed modul är inte aktiverad än %1$s har installerats, men är ännu inte aktiverad. %1$s har installerats för användaren %2$s, men är ännu inte aktiverad. Xposed modul uppdaterad %s har uppdaterats. Tvinga stopp och starta om appar i dess omfattning 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 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 ================================================ FILE: daemon/src/main/res/values-th/strings.xml ================================================ โมดูล Xposed ยังไม่ได้เปิดใช้งาน %1$s ถูกติดตั้งแล้ว แต่ยังไม่ได้เปิดใช้งาน ติดตั้ง %1$s ให้กับผู้ใช้แล้ว %2$s แต่ยังไม่ได้เปิดใช้งาน โมดูล Xposed อัปเดตแล้ว %s ได้รับการอัปเดตแล้ว โปรดบังคับหยุดและรีสตาร์ทแอปที่อยู่ใน Scope. อัปเดตโมดูล Xposed จำเป็นต้องรีสตาร์ทเครื่อง %s ได้รับการอัปเดตแล้ว เนื่องจาก Scope มี System Framework จึงจำเป็นต้องรีสตาร์ทเครื่องเพื่อใช้การเปลี่ยนแปลง การอัปเดตโมดูลเสร็จสมบูรณ์ สถานะ LSPosed LSPosed โหลดแล้ว แตะการแจ้งเตือนเพื่อเปิดตัวจัดการ คำขอ Scope %1$s ผูัใช้นี้ %2$s ขอให้เพิ่ม %3$s ใน List ของ scope. คำขอ Scope. อนุมัติ ปฎิเสธ ไม่เคยถาม ================================================ FILE: daemon/src/main/res/values-tr/strings.xml ================================================ Xposed modülü henüz aktif değil! %1$s kuruldu, ancak henüz etkinleştirilmedi %1$s, kullanıcıya %2$syüklendi, ancak henüz etkinleştirilmedi Xposed modülü güncellendi %s güncellendi, lütfen kapsamındaki uygulamaları durdurmaya ve yeniden başlatmaya zorlayın 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 Kapsam Talebi Kullanıcı %2$s %1$s kapsamına %3$s eklemek ister. Kapsam talebi Onayla Reddet Asla Sorma ================================================ FILE: daemon/src/main/res/values-uk/strings.xml ================================================ Модуль Xposed ще не активований %1$s було встановлено, але ще не активовано %1$s було встановлено до користувача %2$s, але ще не активовано Модуль Xposed оновлено %s було оновлено, будь ласка, примусово перезапустіть програми з області модуля Модуль Xposed оновлено, потрібно перезавантаження системи %s було оновлено, оскільки область містить System Framework, необхідне перезавантаження для застосування змін Оновлення модуля завершено Статус LSPosed LSPosed завантажено Натисніть на повідомлення, щоб відкрити менеджер Запит на визначення обсягу робіт %1$s на запит користувача %2$s з проханням додати %3$s до своєї області видимості. Запит обсягу робіт Затвердити Відхилити Ніколи не питай ================================================ FILE: daemon/src/main/res/values-ur/strings.xml ================================================ Xposed ماڈیول ابھی تک چالو نہیں ہوا ہے۔ %1$s انسٹال ہو چکا ہے، لیکن ابھی تک چالو نہیں ہوا ہے۔ %1$s کو صارف %2$sپر انسٹال کر دیا گیا ہے، لیکن ابھی تک فعال نہیں ہوا ہے۔ Xposed ماڈیول کو اپ ڈیٹ کر دیا گیا۔ %s کو اپ ڈیٹ کر دیا گیا ہے، براہ کرم اس کے دائرہ کار میں ایپس کو زبردستی روکنے اور دوبارہ شروع کریں۔ Xposed ماڈیول کو اپ ڈیٹ کر دیا گیا، سسٹم ریبوٹ درکار ہے۔ %s کو اپ ڈیٹ کر دیا گیا ہے، چونکہ دائرہ کار میں سسٹم فریم ورک ہے، تبدیلیاں لاگو کرنے کے لیے ریبوٹ کی ضرورت ہے۔ ماڈیول اپ ڈیٹ مکمل ہو گیا۔ ایل ایس پیز کی حیثیت ایل ایس پیز لوڈ شدہ مینیجر کو کھولنے کے لیے نوٹیفکیشن کو تھپتھپائیں۔ دائرہ کار کی درخواست صارف %2$s پر %1$s اپنے دائرہ کار میں %3$s شامل کرنے کی درخواست کرتا ہے۔ دائرہ کار کی درخواست منظور کرو انکار کرنا کبھی نہ پوچھیں۔ ================================================ FILE: daemon/src/main/res/values-vi/strings.xml ================================================ Mô-đun Xposed chưa được kích hoạt %1$s đã được cài đặt, nhưng chưa được kích hoạt %1$s vừa được cài đặt cho người dùng %2$s, nhưng chưa được kích hoạt Mô-đun Xposed đã được cập nhật %s đã được cập nhật, xin hãy buộc dừng và khởi động lại ứng dụng liên quan Mô-đun Xposed đã được cập nhật, yêu cầu khởi động lại hệ thống %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ý 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 ================================================ FILE: daemon/src/main/res/values-zh-rCN/strings.xml ================================================ Xposed 模块尚未激活 %1$s 已安装,但尚未激活 %1$s 已安装到用户 %2$s,但尚未激活 Xposed 模块已更新 %s 已更新,请强行停止并重新打开其作用域内的应用 Xposed 模块已更新,需要重新启动 %s 已更新,由于作用域包含系统框架,需重启以应用更改 模块更新完成 LSPosed 状态 LSPosed 已加载 点按通知以打开管理器 作用域请求 用户 %2$s 上的 %1$s 请求将 %3$s 添加至其作用域。 作用域请求 允许 拒绝 不再询问 ================================================ FILE: daemon/src/main/res/values-zh-rHK/strings.xml ================================================ Xposed 模組尚未啟用 %1$s 已安裝,但尚未啟用 %1$s 已安裝到用戶 %2$s,但尚未啟用 Xposed 模組已更新 %s 已更新,請強制停止並重新開啟其作用範圍內的應用程式 Xposed 模組已更新,需要重新啟動。 %s 已更新,由於作用範圍包含系統架構,需要重新啟動以套用修改。 模块更新完成 LSPosed 狀態 LSPosed 已載入 輕觸通知以開啟管理員 作用範圍要求 用戶 %2$s 上的 %1$s 要求將 %3$s 新增至其作用範圍。 作用範圍要求 核准 拒絕 永不詢問 ================================================ FILE: daemon/src/main/res/values-zh-rTW/strings.xml ================================================ Xposed 模組尚未啟用 %1$s 已安裝,但尚未啟用 %1$s 已安裝到使用者 %2$s,但尚未啟用 Xposed 模組已更新 %s 已更新,請強制停止並重新打開其作用域內的程式 Xposed 模組已更新,需要重新啟動。 %s 已更新,由於作用域包含系統框架,需要重新啟動以套用修改。 模組更新完成 LSPosed 狀態 LSPosed 已載入 輕觸通知以開啟管理員 作用範圍要求 使用者 %2$s 上的 %1$s 要求將 %3$s 新增至其作用範圍。 作用範圍要求 核准 拒絕 永不詢問 ================================================ FILE: dex2oat/.gitignore ================================================ /build /.cxx ================================================ FILE: dex2oat/README.md ================================================ # VectorDex2Oat VectorDex2Oat is a specialized wrapper and instrumentation suite for the Android `dex2oat` (Ahead-of-Time compiler) binary. It is designed to intercept the compilation process, force specific compiler behaviors (specifically disabling method inlining), and transparently spoof the resulting OAT metadata to hide the presence of the wrapper. ## Overview In the Android Runtime (ART), `dex2oat` compiles DEX files into OAT files. Modern ART optimizations often inline methods, making it difficult for instrumentation tools to hook specific function calls. This project consists of two primary components: 1. **dex2oat (Wrapper):** A replacement binary that intercepts the execution, communicates via Unix Domain Sockets to obtain the original compiler binary, and executes it with forced flags. 2. **liboat_hook.so (Hooker):** A shared library injected into the `dex2oat` process via `LD_PRELOAD` that utilizes PLT hooking to sanitize the OAT header's command-line metadata. ## Key Features * **Inlining Suppression:** Appends `--inline-max-code-units=0` to the compiler arguments, ensuring all methods remain discrete and hookable. * **FD-Based Execution:** Executes the original `dex2oat` via the system linker using `/proc/self/fd/` paths, avoiding direct execution of files on the disk. * **Metadata Spoofing:** Intercepts `art::OatHeader::ComputeChecksum` or `art::OatHeader::GetKeyValueStore` to remove traces of the wrapper and its injected flags from the final `.oat` file. * **Abstract Socket Communication:** Uses the Linux Abstract Namespace for Unix sockets to coordinate file descriptor passing between the controller and the wrapper. ## Architecture ### The Wrapper [dex2oat.cpp](src/main/cpp/dex2oat.cpp) The wrapper acts as a "man-in-the-middle" for the compiler. When called by the system, it 1. connects to a predefined Unix socket (the stub name `5291374ceda0...` will be replaced during installation of `Vector`); 2. identifies the target architecture (32-bit vs 64-bit) and debug status; 3. receives File Descriptors (FDs) for both the original `dex2oat` binary and the `oat_hook` library; 4. reconstructs the command line, replacing the wrapper path with the original binary path and appending the "no-inline" flags; 5. clears `LD_LIBRARY_PATH` and sets `LD_PRELOAD` to the hooker library's FD; 6. invokes the dynamic linker (`linker64`) to execute the compiler. ### The Hooker [oat_hook.cpp](src/main/cpp/oat_hook.cpp) The hooker library is preloaded into the compiler's address space. It uses the [LSPlt](https://github.com/JingMatrix/LSPlt) library to: 1. Scan the memory map to find the `dex2oat` binary. 2. Locate and hook internal ART functions: * [art::OatHeader::GetKeyValueStore](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366) * [art::OatHeader::ComputeChecksum](https://cs.android.com/android/platform/superproject/+/android-latest-release:art/runtime/oat/oat.cc;l=366) 3. When the compiler attempts to write the "dex2oat-cmdline" key into the OAT header, the hooker intercepts the call, parses the key-value store, and removes the wrapper-specific flags and paths. ================================================ FILE: dex2oat/build.gradle.kts ================================================ plugins { alias(libs.plugins.agp.lib) } android { namespace = "org.matrix.vector.dex2oat" androidResources { enable = false } externalNativeBuild { cmake { path("src/main/cpp/CMakeLists.txt") } } } ================================================ FILE: dex2oat/src/main/cpp/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) project(dex2oat) add_executable(dex2oat dex2oat.cpp) add_library(oat_hook SHARED oat_hook.cpp) OPTION(LSPLT_BUILD_SHARED OFF) add_subdirectory(${VECTOR_ROOT}/external/lsplt/lsplt/src/main/jni external) target_include_directories(oat_hook PUBLIC include) target_include_directories(dex2oat PUBLIC include) target_link_libraries(dex2oat log) target_link_libraries(oat_hook log lsplt_static) if (DEFINED DEBUG_SYMBOLS_PATH) message(STATUS "Debug symbols will be placed at ${DEBUG_SYMBOLS_PATH}") add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug COMMAND ${CMAKE_STRIP} --strip-all $ COMMAND ${CMAKE_OBJCOPY} --add-gnu-debuglink ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug $) endif() ================================================ FILE: dex2oat/src/main/cpp/dex2oat.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include "logging.h" // Access to the process environment variables extern "C" char **environ; #if defined(__LP64__) #define LP_SELECT(lp32, lp64) lp64 #else #define LP_SELECT(lp32, lp64) lp32 #endif namespace { constexpr char kSockName[] = "5291374ceda0aef7c5d86cd2a4f6a3ac"; /** * Calculates a vector ID based on architecture and debug status. */ inline int get_id_vec(bool is64, bool is_debug) { return (static_cast(is64) << 1) | static_cast(is_debug); } /** * Wraps recvmsg with error logging. */ ssize_t xrecvmsg(int sockfd, struct msghdr *msg, int flags) { ssize_t rec = recvmsg(sockfd, msg, flags); if (rec < 0) { PLOGE("recvmsg"); } return rec; } /** * Receives file descriptors passed over a Unix domain socket using SCM_RIGHTS. * * @return Pointer to the FD data on success, nullptr on failure. */ void *recv_fds(int sockfd, char *cmsgbuf, size_t bufsz, int cnt) { struct iovec iov = { .iov_base = &cnt, .iov_len = sizeof(cnt), }; struct msghdr msg = {.msg_name = nullptr, .msg_namelen = 0, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = cmsgbuf, .msg_controllen = bufsz, .msg_flags = 0}; if (xrecvmsg(sockfd, &msg, MSG_WAITALL) < 0) return nullptr; struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); if (msg.msg_controllen != bufsz || cmsg == nullptr || cmsg->cmsg_len != CMSG_LEN(sizeof(int) * cnt) || cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) { return nullptr; } return CMSG_DATA(cmsg); } /** * Helper to receive a single FD from the socket. */ int recv_fd(int sockfd) { char cmsgbuf[CMSG_SPACE(sizeof(int))]; void *data = recv_fds(sockfd, cmsgbuf, sizeof(cmsgbuf), 1); if (data == nullptr) return -1; int result; std::memcpy(&result, data, sizeof(int)); return result; } /** * Reads an integer acknowledgment from the socket. */ int read_int(int fd) { int val; if (read(fd, &val, sizeof(val)) != sizeof(val)) return -1; return val; } /** * Writes an integer command/ID to the socket. */ void write_int(int fd, int val) { if (fd < 0) return; (void)write(fd, &val, sizeof(val)); } } // namespace int main(int argc, char **argv) { LOGD("dex2oat wrapper ppid=%d", getppid()); // Prepare Unix domain socket address (Abstract Namespace) struct sockaddr_un sock = {}; sock.sun_family = AF_UNIX; // sock.sun_path[0] is already \0, so we copy name into sun_path + 1 std::strncpy(sock.sun_path + 1, kSockName, sizeof(sock.sun_path) - 2); // Abstract socket length: family + leading \0 + string length socklen_t len = sizeof(sock.sun_family) + strlen(kSockName) + 1; // 1. Get original dex2oat binary FD int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (connect(sock_fd, reinterpret_cast(&sock), len)) { PLOGE("failed to connect to %s", sock.sun_path + 1); return 1; } bool is_debug = (argv[0] != nullptr && std::strstr(argv[0], "dex2oatd") != nullptr); write_int(sock_fd, get_id_vec(LP_SELECT(false, true), is_debug)); int stock_fd = recv_fd(sock_fd); read_int(sock_fd); // Sync close(sock_fd); // 2. Get liboat_hook.so FD sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (connect(sock_fd, reinterpret_cast(&sock), len)) { PLOGE("failed to connect to %s", sock.sun_path + 1); return 1; } write_int(sock_fd, LP_SELECT(4, 5)); int hooker_fd = recv_fd(sock_fd); read_int(sock_fd); // Sync close(sock_fd); if (hooker_fd == -1) { LOGE("failed to read liboat_hook.so"); } else { int mem_fd = syscall(__NR_memfd_create, "liboat_hook_memfd", 0); if (mem_fd >= 0) { // Get the exact size of the original library LOGD("Copying %d as mem_fd %d", hooker_fd, mem_fd); struct stat st; if (fstat(hooker_fd, &st) == 0) { // Tell the kernel to copy the entire file directly to the memfd off_t offset = 0; sendfile(mem_fd, hooker_fd, &offset, st.st_size); // Swap the old FD with the new memfd close(hooker_fd); hooker_fd = mem_fd; } else { PLOGE("fstat failed"); close(mem_fd); } } else { PLOGE("memfd_create failed, falling back to original fd"); } } LOGD("sock: %s stock_fd: %d", sock.sun_path + 1, stock_fd); // Prepare arguments for execve // Logic: [linker] [/proc/self/fd/stock_fd] [original_args...] [--inline-max-code-units=0] std::vector exec_argv; const char *linker_path = LP_SELECT("/apex/com.android.runtime/bin/linker", "/apex/com.android.runtime/bin/linker64"); char stock_fd_path[64]; std::snprintf(stock_fd_path, sizeof(stock_fd_path), "/proc/self/fd/%d", stock_fd); exec_argv.push_back(linker_path); exec_argv.push_back(stock_fd_path); // Append original arguments starting from argv[1] for (int i = 1; i < argc; ++i) { exec_argv.push_back(argv[i]); } // Append hooking flags to disable inline, which is our purpose of this wrapper, since we cannot // hook inlined target methods. exec_argv.push_back("--inline-max-code-units=0"); exec_argv.push_back(nullptr); // Setup Environment variables // Clear LD_LIBRARY_PATH to let the linker use internal config unsetenv("LD_LIBRARY_PATH"); // Set LD_PRELOAD to point to the hooker library FD std::string preload_val = "LD_PRELOAD=/proc/self/fd/" + std::to_string(hooker_fd); LOGD("Inject oat hook via %s", preload_val.data()); setenv("LD_PRELOAD", ("/proc/self/fd/" + std::to_string(hooker_fd)).c_str(), 1); // Pass original argv[0] as DEX2OAT_CMD if (argv[0]) { setenv("DEX2OAT_CMD", argv[0], 1); LOGD("DEX2OAT_CMD set to %s", argv[0]); } LOGI("Executing via linker: %s executing %s", linker_path, stock_fd_path); // Perform the execution execve(linker_path, const_cast(exec_argv.data()), environ); // If we reach here, execve failed PLOGE("execve failed"); return 2; } ================================================ FILE: dex2oat/src/main/cpp/include/base_macros.h ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include // for size_t #include // for TEMP_FAILURE_RETRY #include // bionic and glibc both have TEMP_FAILURE_RETRY, but eg Mac OS' libc doesn't. #ifndef TEMP_FAILURE_RETRY #define TEMP_FAILURE_RETRY(exp) \ ({ \ decltype(exp) _rc; \ do { \ _rc = (exp); \ } while (_rc == -1 && errno == EINTR); \ _rc; \ }) #endif // A macro to disallow the copy constructor and operator= functions // This must be placed in the private: declarations for a class. // // For disallowing only assign or copy, delete the relevant operator or // constructor, for example: // void operator=(const TypeName&) = delete; // Note, that most uses of DISALLOW_ASSIGN and DISALLOW_COPY are broken // semantically, one should either use disallow both or neither. Try to // avoid these in new code. #define DISALLOW_COPY_AND_ASSIGN(TypeName) \ TypeName(const TypeName &) = delete; \ void operator=(const TypeName &) = delete // A macro to disallow all the implicit constructors, namely the // default constructor, copy constructor and operator= functions. // // This should be used in the private: declarations for a class // that wants to prevent anyone from instantiating it. This is // especially useful for classes containing only static methods. #define DISALLOW_IMPLICIT_CONSTRUCTORS(TypeName) \ TypeName() = delete; \ DISALLOW_COPY_AND_ASSIGN(TypeName) // The arraysize(arr) macro returns the # of elements in an array arr. // The expression is a compile-time constant, and therefore can be // used in defining new arrays, for example. If you use arraysize on // a pointer by mistake, you will get a compile-time error. // // One caveat is that arraysize() doesn't accept any array of an // anonymous type or a type defined inside a function. In these rare // cases, you have to use the unsafe ARRAYSIZE_UNSAFE() macro below. This is // due to a limitation in C++'s template system. The limitation might // eventually be removed, but it hasn't happened yet. // This template function declaration is used in defining arraysize. // Note that the function doesn't need an implementation, as we only // use its type. template char (&ArraySizeHelper(T (&array)[N]))[N]; // NOLINT(readability/casting) #define arraysize(array) (sizeof(ArraySizeHelper(array))) #define SIZEOF_MEMBER(t, f) sizeof(std::declval().f) // Changing this definition will cause you a lot of pain. A majority of // vendor code defines LIKELY and UNLIKELY this way, and includes // this header through an indirect path. #define LIKELY(exp) (__builtin_expect((exp) != 0, true)) #define UNLIKELY(exp) (__builtin_expect((exp) != 0, false)) #define WARN_UNUSED __attribute__((warn_unused_result)) // A deprecated function to call to create a false use of the parameter, for // example: // int foo(int x) { UNUSED(x); return 10; } // to avoid compiler warnings. Going forward we prefer ATTRIBUTE_UNUSED. template void UNUSED(const T &...) {} // An attribute to place on a parameter to a function, for example: // int foo(int x ATTRIBUTE_UNUSED) { return 10; } // to avoid compiler warnings. #define ATTRIBUTE_UNUSED __attribute__((__unused__)) // The FALLTHROUGH_INTENDED macro can be used to annotate implicit fall-through // between switch labels: // switch (x) { // case 40: // case 41: // if (truth_is_out_there) { // ++x; // FALLTHROUGH_INTENDED; // Use instead of/along with annotations in // // comments. // } else { // return x; // } // case 42: // ... // // As shown in the example above, the FALLTHROUGH_INTENDED macro should be // followed by a semicolon. It is designed to mimic control-flow statements // like 'break;', so it can be placed in most places where 'break;' can, but // only if there are no statements on the execution path between it and the // next switch label. // // When compiled with clang, the FALLTHROUGH_INTENDED macro is expanded to // [[clang::fallthrough]] attribute, which is analysed when performing switch // labels fall-through diagnostic ('-Wimplicit-fallthrough'). See clang // documentation on language extensions for details: // http://clang.llvm.org/docs/LanguageExtensions.html#clang__fallthrough // // When used with unsupported compilers, the FALLTHROUGH_INTENDED macro has no // effect on diagnostics. // // In either case this macro has no effect on runtime behavior and performance // of code. #ifndef FALLTHROUGH_INTENDED #define FALLTHROUGH_INTENDED [[fallthrough]] // NOLINT #endif // Current ABI string #if defined(__arm__) #define ABI_STRING "arm" #elif defined(__aarch64__) #define ABI_STRING "arm64" #elif defined(__i386__) #define ABI_STRING "x86" #elif defined(__riscv) #define ABI_STRING "riscv64" #elif defined(__x86_64__) #define ABI_STRING "x86_64" #endif ================================================ FILE: dex2oat/src/main/cpp/include/logging.h ================================================ #pragma once #include #include #ifndef LOG_TAG #define LOG_TAG "VectorDex2Oat" #endif #ifdef LOG_DISABLED #define LOGD(...) 0 #define LOGV(...) 0 #define LOGI(...) 0 #define LOGW(...) 0 #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, \ "%s:%d#%s" \ ": " fmt, \ __FILE_NAME__, __LINE__, __PRETTY_FUNCTION__ __VA_OPT__(, ) __VA_ARGS__) #else #define LOGD(...) 0 #define LOGV(...) 0 #endif #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__) #define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) #endif ================================================ FILE: dex2oat/src/main/cpp/include/macros.h ================================================ /* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef ART_LIBARTBASE_BASE_MACROS_H_ #define ART_LIBARTBASE_BASE_MACROS_H_ #include // for size_t #include // for TEMP_FAILURE_RETRY // Declare a friend relationship in a class with a test. Used rather that FRIEND_TEST to avoid // globally importing gtest/gtest.h into the main ART header files. #define ART_FRIEND_TEST(test_set_name, individual_test) \ friend class test_set_name##_##individual_test##_Test // Declare a friend relationship in a class with a typed test. #define ART_FRIEND_TYPED_TEST(test_set_name, individual_test) \ template \ ART_FRIEND_TEST(test_set_name, individual_test) // Shorthand for formatting with compile time checking of the format string #define ART_FORMAT(str, ...) ::fmt::format(FMT_STRING(str), __VA_ARGS__) // A macro to disallow new and delete operators for a class. It goes in the private: declarations. // NOTE: Providing placement new (and matching delete) for constructing container elements. #define DISALLOW_ALLOCATION() \ public: \ NO_RETURN ALWAYS_INLINE void operator delete(void*, size_t) { UNREACHABLE(); } \ ALWAYS_INLINE void* operator new(size_t, void* ptr) noexcept { return ptr; } \ ALWAYS_INLINE void operator delete(void*, void*) noexcept {} \ \ private: \ void* operator new(size_t) = delete // NOLINT // offsetof is not defined by the spec on types with non-standard layout, // however it is implemented by compilers in practice. // (note that reinterpret_cast is not valid constexpr) // // Alternative approach would be something like: // #define OFFSETOF_HELPER(t, f) \ // (reinterpret_cast(&reinterpret_cast(16)->f) - static_cast(16u)) // #define OFFSETOF_MEMBER(t, f) \ // (__builtin_constant_p(OFFSETOF_HELPER(t,f)) ? OFFSETOF_HELPER(t,f) : OFFSETOF_HELPER(t,f)) #define OFFSETOF_MEMBER(t, f) offsetof(t, f) #define OFFSETOF_MEMBERPTR(t, f) \ (reinterpret_cast(&(reinterpret_cast(16)->*f)) - \ static_cast(16)) // NOLINT #define ALIGNED(x) __attribute__((__aligned__(x))) #define PACKED(x) __attribute__((__aligned__(x), __packed__)) // Stringify the argument. #define QUOTE(x) #x #define STRINGIFY(x) QUOTE(x) // Append tokens after evaluating. #define APPEND_TOKENS_AFTER_EVAL_2(a, b) a##b #define APPEND_TOKENS_AFTER_EVAL(a, b) APPEND_TOKENS_AFTER_EVAL_2(a, b) #ifndef NDEBUG #define ALWAYS_INLINE #define FLATTEN #else #define ALWAYS_INLINE __attribute__((always_inline)) #define FLATTEN __attribute__((flatten)) #endif #define NO_STACK_PROTECTOR __attribute__((no_stack_protector)) // clang doesn't like attributes on lambda functions. It would be nice to say: // #define ALWAYS_INLINE_LAMBDA ALWAYS_INLINE #define ALWAYS_INLINE_LAMBDA #define NO_INLINE __attribute__((noinline)) #if defined(__APPLE__) #define HOT_ATTR #define COLD_ATTR #else #define HOT_ATTR __attribute__((hot)) #define COLD_ATTR __attribute__((cold)) #endif #define PURE __attribute__((__pure__)) // Define that a position within code is unreachable, for example: // int foo () { LOG(FATAL) << "Don't call me"; UNREACHABLE(); } // without the UNREACHABLE a return statement would be necessary. #define UNREACHABLE __builtin_unreachable // Add the C++11 noreturn attribute. #define NO_RETURN [[noreturn]] // NOLINT[whitespace/braces] [5] // Annotalysis thread-safety analysis support. Things that are not in base. #define LOCKABLE CAPABILITY("mutex") #define SHARED_LOCKABLE SHARED_CAPABILITY("mutex") // Some of the libs (e.g. libarttest(d)) require more public symbols when built // in debug configuration. // Using symbol visibility only for release builds allows to reduce the list of // exported symbols and eliminates the need to check debug build configurations // when changing the exported symbols. #ifdef NDEBUG #define HIDDEN __attribute__((visibility("hidden"))) #define PROTECTED __attribute__((visibility("protected"))) #define EXPORT __attribute__((visibility("default"))) #else #define HIDDEN #define PROTECTED #define EXPORT #endif // Protected symbols must be declared with "protected" visibility attribute when // building the library and "default" visibility when referred to from external // libraries/binaries. Otherwise, the external code will expect the symbol to be // defined locally and fail to link. #ifdef BUILDING_LIBART #define LIBART_PROTECTED PROTECTED #else #define LIBART_PROTECTED EXPORT #endif // Some global variables shouldn't be visible outside libraries declaring them. // The attribute allows hiding them, so preventing direct access. #define ALWAYS_HIDDEN __attribute__((visibility("hidden"))) #endif // ART_LIBARTBASE_BASE_MACROS_H_ ================================================ FILE: dex2oat/src/main/cpp/include/oat.h ================================================ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef ART_RUNTIME_OAT_OAT_H_ #define ART_RUNTIME_OAT_OAT_H_ #include #include #include #include #include "base_macros.h" #include "macros.h" namespace art { enum class InstructionSet; class EXPORT PACKED(4) OatHeader { public: static constexpr std::array kOatMagic{{'o', 'a', 't', '\n'}}; // Last oat version changed reason: Ensure oat checksum determinism across hosts and devices. static constexpr std::array kOatVersion{{'2', '5', '9', '\0'}}; static constexpr const char* kDex2OatCmdLineKey = "dex2oat-cmdline"; static constexpr const char* kDebuggableKey = "debuggable"; static constexpr const char* kNativeDebuggableKey = "native-debuggable"; static constexpr const char* kCompilerFilter = "compiler-filter"; static constexpr const char* kClassPathKey = "classpath"; static constexpr const char* kBootClassPathKey = "bootclasspath"; static constexpr const char* kBootClassPathChecksumsKey = "bootclasspath-checksums"; static constexpr const char* kApexVersionsKey = "apex-versions"; static constexpr const char* kConcurrentCopying = "concurrent-copying"; static constexpr const char* kCompilationReasonKey = "compilation-reason"; static constexpr const char* kRequiresImage = "requires-image"; // Fields listed here are key value store fields that are deterministic across hosts and // devices, meaning they should have exactly the same value when the oat file is generated on // different hosts and devices for the same app / boot image and for the same device model with // the same compiler options. If you are adding a new field that doesn't hold this property, put // it in `kNonDeterministicFieldsAndLengths` and assign a length limit. // // When writing the oat header, the non-deterministic fields are padded to their length limits // and excluded from the oat checksum computation. This makes the oat checksum deterministic // across hosts and devices, which is important for Cloud Compilation, where we generate an oat // file on a host and use it on a device. static constexpr std::array kDeterministicFields{ kDebuggableKey, kNativeDebuggableKey, kCompilerFilter, kClassPathKey, kBootClassPathKey, kBootClassPathChecksumsKey, kConcurrentCopying, kCompilationReasonKey, kRequiresImage, }; static constexpr std::array, 2> kNonDeterministicFieldsAndLengths{ std::make_pair(kDex2OatCmdLineKey, 2048), std::make_pair(kApexVersionsKey, 1024), }; static constexpr const char kTrueValue[] = "true"; static constexpr const char kFalseValue[] = "false"; // Added helper to access the key_value_store_ field, which could be fragile across // different Android versions and compiler optimizations. const uint8_t* getKeyValueStore() const { return key_value_store_; } void ComputeChecksum(/*inout*/ uint32_t* checksum) const; private: std::array magic_; std::array version_; uint32_t oat_checksum_; InstructionSet instruction_set_; uint32_t instruction_set_features_bitmap_; uint32_t dex_file_count_; uint32_t oat_dex_files_offset_; uint32_t bcp_bss_info_offset_; // Offset of the oat header (i.e. start of the oat data) in the ELF file. // It is used to additional validation of the oat header as it is not // page-aligned in the memory. uint32_t base_oat_offset_; uint32_t executable_offset_; uint32_t jni_dlsym_lookup_trampoline_offset_; uint32_t jni_dlsym_lookup_critical_trampoline_offset_; uint32_t quick_generic_jni_trampoline_offset_; uint32_t quick_imt_conflict_trampoline_offset_; uint32_t quick_resolution_trampoline_offset_; uint32_t quick_to_interpreter_bridge_offset_; uint32_t nterp_trampoline_offset_; uint32_t key_value_store_size_; uint8_t key_value_store_[0]; // note variable width data at end DISALLOW_COPY_AND_ASSIGN(OatHeader); }; } // namespace art #endif // ART_RUNTIME_OAT_OAT_H_ ================================================ FILE: dex2oat/src/main/cpp/oat_hook.cpp ================================================ #include #include #include #include #include #include #include #include #include #include "logging.h" #include "oat.h" /** * This library is injected into dex2oat to intercept the generation of OAT headers. Our wrapper * runs dex2oat via the linker with extra flags. Without this hook, the resulting OAT file would * record the transferred fd path of wrapper and the extra flags in its "dex2oat-cmdline" key, which * can be used to detect the wrapper. */ namespace { const std::string_view kParamToRemove = "--inline-max-code-units=0"; std::string g_binary_path = getenv("DEX2OAT_CMD"); // The original binary path } // namespace /** * Sanitizes the command line string by: * 1. Replacing the first token (the linker/binary path) with the original dex2oat path. * 2. Removing the specific optimization flag we injected. */ std::string process_cmd(std::string_view sv, std::string_view new_cmd_path) { std::vector tokens; std::string current; // Simple split by space for (char c : sv) { if (c == ' ') { if (!current.empty()) { tokens.push_back(std::move(current)); current.clear(); } } else { current.push_back(c); } } if (!current.empty()) tokens.push_back(std::move(current)); // 1. Replace the command path (argv[0]) if (!tokens.empty()) { tokens[0] = std::string(new_cmd_path); } // 2. Remove the injected parameter if it exists auto it = std::remove(tokens.begin(), tokens.end(), std::string(kParamToRemove)); tokens.erase(it, tokens.end()); // 3. Join tokens back into a single string std::string result; for (size_t i = 0; i < tokens.size(); ++i) { result += tokens[i]; if (i != tokens.size() - 1) result += ' '; } return result; } /** * Re-serializes the Key-Value map back into the OAT header memory space. */ uint8_t* WriteKeyValueStore(const std::map& key_values, uint8_t* store) { LOGD("Writing KeyValueStore back to memory"); char* data_ptr = reinterpret_cast(store); for (const auto& [key, value] : key_values) { // Copy key + null terminator std::memcpy(data_ptr, key.c_str(), key.length() + 1); data_ptr += key.length() + 1; // Copy value + null terminator std::memcpy(data_ptr, value.c_str(), value.length() + 1); data_ptr += value.length() + 1; } LOGD("Written KeyValueStore with size: %zu", reinterpret_cast(data_ptr) - store); return reinterpret_cast(data_ptr); } // Helper function to test if a header field could have variable length bool IsNonDeterministic(const std::string_view& key) { auto variable_fields = art::OatHeader::kNonDeterministicFieldsAndLengths; return std::any_of(variable_fields.begin(), variable_fields.end(), [&key](const auto& pair) { return pair.first.compare(key) == 0; }); } /** * Parses the OAT KeyValueStore and spoofs the "dex2oat-cmdline" entry. * * @return true if the store was modified in-place or successfully rebuilt. */ bool SpoofKeyValueStore(uint8_t* store) { if (!store) return false; uint32_t* const store_size_ptr = reinterpret_cast(store - sizeof(uint32_t)); uint32_t const store_size = *store_size_ptr; const char* ptr = reinterpret_cast(store); const char* const store_end = ptr + store_size; std::map new_store_map; LOGI("Parsing KeyValueStore [%p - %p] of size %u", ptr, store_end, store_size); bool store_modified = false; while (ptr < store_end && *ptr != '\0') { // Find key const char* key_end = reinterpret_cast(std::memchr(ptr, 0, store_end - ptr)); if (!key_end) break; std::string_view key(ptr, key_end - ptr); // Find value const char* value_start = key_end + 1; if (value_start >= store_end) break; const char* value_end = reinterpret_cast(std::memchr(value_start, 0, store_end - value_start)); if (!value_end) break; std::string_view value(value_start, value_end - value_start); const bool has_padding = value_end + 1 < store_end && *(value_end + 1) == '\0' && IsNonDeterministic(key); if (key == art::OatHeader::kDex2OatCmdLineKey && value.find(kParamToRemove) != std::string_view::npos) { std::string cleaned_cmd = process_cmd(value, g_binary_path); LOGI("Spoofing cmdline: Original size %zu -> New size %zu", value.length(), cleaned_cmd.length()); // We can overwrite in-place if the padding is enabled if (has_padding) { LOGI("In-place spoofing dex2oat-cmdline (padding detected)"); // Zero out the entire original value range to be safe size_t original_capacity = value.length(); std::memset(const_cast(value_start), 0, original_capacity); // Write the new command. std::memcpy(const_cast(value_start), cleaned_cmd.c_str(), std::min(cleaned_cmd.length(), original_capacity)); return true; } // Standard logic: store in map and rebuild later new_store_map[std::string(key)] = std::move(cleaned_cmd); store_modified = true; } else { new_store_map[std::string(key)] = std::string(value); LOGI("Parsed item:\t[%s:%s]", key.data(), value.data()); } ptr = value_end + 1; if (has_padding) { while (*ptr == '\0') { ptr++; } } } if (store_modified) { uint8_t* const new_store_end = WriteKeyValueStore(new_store_map, store); *store_size_ptr = new_store_end - store; LOGI("Store size set to %u", *store_size_ptr); return true; } return false; } #define DCL_HOOK_FUNC(ret, func, ...) \ ret (*old_##func)(__VA_ARGS__) = nullptr; \ ret new_##func(__VA_ARGS__) // For Android version < 16 DCL_HOOK_FUNC(uint8_t*, _ZNK3art9OatHeader16GetKeyValueStoreEv, void* header) { uint8_t* const key_value_store = old__ZNK3art9OatHeader16GetKeyValueStoreEv(header); SpoofKeyValueStore(key_value_store); return key_value_store; } // For Android version 16+ : Intercept during checksum calculation DCL_HOOK_FUNC(void, _ZNK3art9OatHeader15ComputeChecksumEPj, void* header, uint32_t* checksum) { auto* oat_header = reinterpret_cast(header); uint8_t* const store = const_cast(oat_header->getKeyValueStore()); SpoofKeyValueStore(store); // Call original to compute checksum on our modified data old__ZNK3art9OatHeader15ComputeChecksumEPj(header, checksum); LOGV("OAT Checksum recalculated: 0x%08X", *checksum); } #undef DCL_HOOK_FUNC void register_hook(dev_t dev, ino_t inode, const char* symbol, void* new_func, void** old_func) { if (!lsplt::RegisterHook(dev, inode, symbol, new_func, old_func)) { LOGE("Failed to register PLT hook: %s", symbol); } } #define PLT_HOOK_REGISTER_SYM(DEV, INODE, SYM, NAME) \ register_hook(DEV, INODE, SYM, reinterpret_cast(new_##NAME), \ reinterpret_cast(&old_##NAME)) #define PLT_HOOK_REGISTER(DEV, INODE, NAME) PLT_HOOK_REGISTER_SYM(DEV, INODE, #NAME, NAME) __attribute__((constructor)) static void initialize() { dev_t dev = 0; ino_t inode = 0; // Locate the dex2oat binary in memory to get its device and inode for PLT hooking for (const auto& info : lsplt::MapInfo::Scan()) { if (info.path.find("bin/dex2oat") != std::string::npos) { dev = info.dev; inode = info.inode; if (g_binary_path.empty()) g_binary_path = std::string(info.path); LOGD("Found target: %s (dev: %ju, inode: %ju)", info.path.data(), (uintmax_t)dev, (uintmax_t)inode); break; } } if (dev == 0) { LOGE("Could not locate dex2oat memory map"); return; } // Register hook for the standard KeyValueStore getter PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader16GetKeyValueStoreEv); // If the standard store hook fails (e.g., on Android 16+), try the Checksum hook if (!lsplt::CommitHook()) { PLT_HOOK_REGISTER(dev, inode, _ZNK3art9OatHeader15ComputeChecksumEPj); lsplt::CommitHook(); } } ================================================ FILE: external/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) project(external) set(CMAKE_CXX_STANDARD 23) set(XZ_SOURCES xz_crc32.c xz_crc64.c # xz_dec_bcj.c xz_dec_lzma2.c xz_dec_stream.c) set(XZ_INCLUDES xz-embedded/linux/include/linux xz-embedded/userspace) list(TRANSFORM XZ_SOURCES PREPEND xz-embedded/linux/lib/xz/) add_library(xz_static STATIC ${XZ_SOURCES}) target_compile_options(xz_static PRIVATE -DXZ_USE_CRC64) target_include_directories(xz_static PRIVATE ${XZ_INCLUDES}) option(LSPLANT_BUILD_SHARED OFF) option(Plugin.SymbolResolver OFF) option(FMT_INSTALL OFF) add_subdirectory(dobby) add_subdirectory(fmt) add_subdirectory(lsplant/lsplant/src/main/jni) target_compile_options(lsplant_static PUBLIC -Wno-gnu-anonymous-struct) target_compile_definitions(fmt-header-only INTERFACE FMT_USE_LOCALE=0 FMT_USE_FLOAT=0 FMT_USE_DOUBLE=0 FMT_USE_LONG_DOUBLE=0 FMT_USE_BITINT=0) ================================================ FILE: external/README.md ================================================ # External Dependencies This directory contains all the external dependencies required to build the Vector framework. They are included as git submodules to ensure version consistency and timely updating. ## Native dependencies - [Dobby](https://github.com/JingMatrix/Dobby): A lightweight, multi-platform inline hooking framework. It serves as the backend for all native function hooking (`HookInline`). - [fmt](https://github.com/fmtlib/fmt): A modern formatting library used for high-performance, type-safe logging throughout the native code. - [LSPlant](https://github.com/JingMatrix/LSPlant): A hooking framework for the Android Runtime (ART). It provides the core functionality for intercepting and modifying Java methods. - [xz-embedded](https://github.com/tukaani-project/xz-embedded): A lightweight data compression library with a small footprint. It is used by the ELF parser to decompress the `.gnu_debugdata` section of stripped native libraries. - [LSPlt](https://github.com/JingMatrix/LSPlt): A library for PLT (Procedure Linkage Table) hooking. It is used in the `dex2oat` sub-project to bypass a detection point. **Note:** This is included as a submodule for project convenience but is not compiled into the `external` C++ library itself. ## Java libraries - [apache/commons-lang](https://github.com/apache/commons-lang): A package of Java utility classes for the classes that are in java.lang's hierarchy. Some classes are renamed and then used to implement the `XposedHelpers` API. - [axml/manifest-editor](https://github.com/JingMatrix/ManifestEditor): A a tool used to modify Android Manifest binary file. It is to parse manifestation files of Xposed modules. ================================================ FILE: external/apache/.gitignore ================================================ local/generated ================================================ FILE: external/apache/build.gradle.kts ================================================ val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra plugins { id("java-library") } java { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility sourceSets { main { java.srcDirs("commons-lang/src/main/java", "local") } } } val lang3Src = "commons-lang/src/main/java/org/apache/commons/lang3" val localDir = "local/generated" tasks.register("ClassUtilsX") { from("$lang3Src/ClassUtils.java") into(localDir) filter { line: String -> line.replace("ClassUtils", "ClassUtilsX") } rename("(.+).java", "$1X.java") } tasks.register("SerializationUtilsX") { from("$lang3Src/SerializationUtils.java") into(localDir) filter { line: String -> line.replace("SerializationUtils", "SerializationUtilsX") } rename("(.+).java", "$1X.java") } tasks.compileJava { dependsOn("ClassUtilsX") dependsOn("SerializationUtilsX") } ================================================ FILE: external/apache/local/MemberUtilsX.java ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.commons.lang3.reflect; import java.lang.reflect.Constructor; import java.lang.reflect.Method; public class MemberUtilsX { public static int compareConstructorFit(final Constructor left, final Constructor right, final Class[] actual) { return MemberUtils.compareConstructorFit(left, right, actual); } public static int compareMethodFit(final Method left, final Method right, final Class[] actual) { return MemberUtils.compareMethodFit(left, right, actual); } } ================================================ FILE: external/axml/build.gradle.kts ================================================ val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra plugins { id("java-library") } java { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility sourceSets { main { java.srcDirs("manifest-editor/lib/src/main/java") resources.srcDirs("manifest-editor/lib/src/main") } } } ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] agp = "8.13.1" kotlin = "2.3.10" nav = "2.9.7" appcenter = "5.0.5" glide = "5.0.5" okhttp = "5.3.2" ktfmt = "0.25.0" [plugins] agp-lib = { id = "com.android.library", version.ref = "agp" } agp-app = { id = "com.android.application", version.ref = "agp" } kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 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] rikkax-appcompat = { module = "dev.rikka.rikkax.appcompat:appcompat", version = "1.6.1" } rikkax-core = { module = "dev.rikka.rikkax.core:core", version = "1.4.1" } rikkax-insets = { module = "dev.rikka.rikkax.insets:insets", version = "1.3.0" } rikkax-layoutinflater = { module = "dev.rikka.rikkax.layoutinflater:layoutinflater", version = "1.3.0" } rikkax-material = { module = "dev.rikka.rikkax.material:material", version = "2.7.2" } rikkax-material-preference = { module = "dev.rikka.rikkax.material:material-preference", version = "2.0.0" } rikkax-parcelablelist = { module = "dev.rikka.rikkax.parcelablelist:parcelablelist", version = "2.0.1" } rikkax-recyclerview = { module = "dev.rikka.rikkax.recyclerview:recyclerview-ktx", version = "1.3.2" } rikkax-widget-borderview = { module = "dev.rikka.rikkax.widget:borderview", version = "1.1.0" } rikkax-widget-mainswitchbar = { module = "dev.rikka.rikkax.widget:mainswitchbar", version = "1.0.2" } androidx-activity = { module = "androidx.activity:activity", version = "1.12.4" } androidx-annotation = { module = "androidx.annotation:annotation", version = "1.9.1" } androidx-browser = { module = "androidx.browser:browser", version = "1.9.0" } androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.1" } androidx-core = { module = "androidx.core:core", version = "1.17.0" } androidx-fragment = { module = "androidx.fragment:fragment", version = "1.8.9" } androidx-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "nav" } androidx-navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "nav" } androidx-preference = { module = "androidx.preference:preference", version = "1.2.1" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.4.0" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version = "1.2.0" } glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } glide-compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "glide" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-dnsoverhttps = { group = "com.squareup.okhttp3", name = "okhttp-dnsoverhttps", version.ref = "okhttp" } okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } agp-apksig = { group = "com.android.tools.build", name = "apksig", version.ref = "agp" } appiconloader = { module = "me.zhanghai.android.appiconloader:appiconloader", version = "1.5.0" } 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" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ ## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx1024m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true android.useAndroidX=true android.nonFinalResIds=false ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: hiddenapi/bridge/.gitignore ================================================ /build ================================================ FILE: hiddenapi/bridge/build.gradle.kts ================================================ plugins { `java-library` } dependencies { compileOnly(projects.hiddenapi.stubs) } ================================================ FILE: hiddenapi/bridge/src/main/java/hidden/ByteBufferDexClassLoader.java ================================================ package hidden; import java.nio.ByteBuffer; import dalvik.system.BaseDexClassLoader; public class ByteBufferDexClassLoader extends BaseDexClassLoader { public ByteBufferDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) { super(dexFiles, parent); } public ByteBufferDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) { super(dexFiles, librarySearchPath, parent); } public String getLdLibraryPath() { return super.getLdLibraryPath(); } } ================================================ FILE: hiddenapi/bridge/src/main/java/hidden/HiddenApiBridge.java ================================================ /* * 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 hidden; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInstaller; import android.content.res.AssetManager; import android.content.res.CompatibilityInfo; import android.content.res.Resources; import android.content.res.ResourcesImpl; import android.os.Binder; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.UserHandle; import android.system.ErrnoException; import android.system.Int32Ref; import android.system.Os; import android.util.MutableInt; import androidx.annotation.RequiresApi; import java.io.File; import java.io.FileDescriptor; public class HiddenApiBridge { public static int AssetManager_addAssetPath(AssetManager am, String path) { return am.addAssetPath(path); } public static IBinder Binder_allowBlocking(IBinder binder) { return Binder.allowBlocking(binder); } public static void Resources_setImpl(Resources resources, ResourcesImpl impl) { resources.setImpl(impl); } public static int PackageInstaller_SessionParams_installFlags(PackageInstaller.SessionParams params) { return params.installFlags; } public static void PackageInstaller_SessionParams_installFlags(PackageInstaller.SessionParams params, int flags) { params.installFlags = flags; } public static IBinder Context_getActivityToken(Context ctx) { return ctx.getActivityToken(); } public static File Environment_getDataProfilesDePackageDirectory(int userId, String packageName) { return Environment.getDataProfilesDePackageDirectory(userId, packageName); } public static Intent Context_registerReceiverAsUser(Context ctx, BroadcastReceiver receiver, UserHandle user, IntentFilter filter, String broadcastPermission, Handler scheduler) { return ctx.registerReceiverAsUser(receiver, user, filter, broadcastPermission, scheduler); } public static UserHandle UserHandle_ALL() { return UserHandle.ALL; } public static UserHandle UserHandle(int h) { return new UserHandle(h); } public static String ApplicationInfo_credentialProtectedDataDir(ApplicationInfo applicationInfo) { return applicationInfo.credentialProtectedDataDir; } public static void ApplicationInfo_credentialProtectedDataDir(ApplicationInfo applicationInfo, String dir) { applicationInfo.credentialProtectedDataDir = dir; } public static String[] ApplicationInfo_resourceDirs(ApplicationInfo applicationInfo) { return applicationInfo.resourceDirs; } public static void ApplicationInfo_resourceDirs(ApplicationInfo applicationInfo, String[] resourceDirs) { applicationInfo.resourceDirs = resourceDirs; } @RequiresApi(31) public static String[] ApplicationInfo_overlayPaths(ApplicationInfo applicationInfo) { return applicationInfo.overlayPaths; } @RequiresApi(31) public static void ApplicationInfo_overlayPaths(ApplicationInfo applicationInfo, String[] overlayPaths) { applicationInfo.overlayPaths = overlayPaths; } public static CompatibilityInfo Resources_getCompatibilityInfo(Resources res) { return res.getCompatibilityInfo(); } public static int Os_ioctlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { return Os.ioctlInt(fd, cmd, new MutableInt(arg)); } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { return Os.ioctlInt(fd, cmd, new Int32Ref(arg)); } else { return Os.ioctlInt(fd, cmd); } } public static int ActivityManager_UID_OBSERVER_GONE() { return ActivityManager.UID_OBSERVER_GONE; } public static int ActivityManager_UID_OBSERVER_ACTIVE() { return ActivityManager.UID_OBSERVER_ACTIVE; } public static int ActivityManager_UID_OBSERVER_IDLE() { return ActivityManager.UID_OBSERVER_IDLE; } public static int ActivityManager_UID_OBSERVER_CACHED() { return ActivityManager.UID_OBSERVER_CACHED; } public static int ActivityManager_PROCESS_STATE_UNKNOWN() { return ActivityManager.PROCESS_STATE_UNKNOWN; } } ================================================ FILE: hiddenapi/stubs/.gitignore ================================================ /build ================================================ FILE: hiddenapi/stubs/build.gradle.kts ================================================ plugins { `java-library` } java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } ================================================ FILE: hiddenapi/stubs/src/main/java/android/annotation/NonNull.java ================================================ /* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.SOURCE; /** * Denotes that a parameter, field or method return value can never be null. *

* This is a marker annotation and it has no specific attributes. * * @paramDoc This value must never be {@code null}. * @returnDoc This value will never be {@code null}. * @hide */ @Retention(SOURCE) @Target({METHOD, PARAMETER, FIELD}) public @interface NonNull { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/annotation/Nullable.java ================================================ /* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.SOURCE; /** * Denotes that a parameter, field or method return value can be null. *

* When decorating a method call parameter, this denotes that the parameter can * legitimately be null and the method will gracefully deal with it. Typically * used on optional parameters. *

* When decorating a method, this denotes the method might legitimately return * null. *

* This is a marker annotation and it has no specific attributes. * * @paramDoc This value may be {@code null}. * @returnDoc This value may be {@code null}. */ @Retention(SOURCE) @Target({METHOD, PARAMETER, FIELD}) public @interface Nullable { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/ActivityManager.java ================================================ package android.app; public class ActivityManager { public static int UID_OBSERVER_GONE; public static int UID_OBSERVER_ACTIVE; public static int UID_OBSERVER_IDLE; public static int UID_OBSERVER_CACHED; public static int PROCESS_STATE_UNKNOWN; } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/ActivityThread.java ================================================ package android.app; import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; import android.os.Bundle; import android.os.IBinder; import android.os.PersistableBundle; public final class ActivityThread { public static ActivityThread currentActivityThread() { throw new UnsupportedOperationException("STUB"); } public ApplicationThread getApplicationThread() { throw new UnsupportedOperationException("STUB"); } public static Application currentApplication() { throw new UnsupportedOperationException("STUB"); } public static String currentPackageName() { throw new UnsupportedOperationException("STUB"); } public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo) { throw new UnsupportedOperationException("STUB"); } public static String currentProcessName() { throw new UnsupportedOperationException("STUB"); } public ContextImpl getSystemContext() { throw new UnsupportedOperationException("STUB"); } public static ActivityThread systemMain() { throw new UnsupportedOperationException("STUB"); } private class ApplicationThread extends IApplicationThread.Stub { @Override public IBinder asBinder() { return null; } } public static final class ActivityClientRecord { Bundle state; PersistableBundle persistentState; } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/Application.java ================================================ package android.app; public class Application { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/ContentProviderHolder.java ================================================ package android.app; import android.content.IContentProvider; public class ContentProviderHolder { public IContentProvider provider; } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/ContextImpl.java ================================================ package android.app; import android.content.Context; public class ContextImpl extends Context { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/IActivityController.java ================================================ package android.app; import android.content.Intent; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; public interface IActivityController extends IInterface { /** * The system is trying to start an activity. Return true to allow * it to be started as normal, or false to cancel/reject this activity. */ boolean activityStarting(Intent intent, String pkg); /** * The system is trying to return to an activity. Return true to allow * it to be resumed as normal, or false to cancel/reject this activity. */ boolean activityResuming(String pkg); /** * An application process has crashed (in Java). Return true for the * normal error recovery (app crash dialog) to occur, false to kill * it immediately. */ boolean appCrashed(String processName, int pid, String shortMsg, String longMsg, long timeMillis, String stackTrace); /** * Early call as soon as an ANR is detected. */ int appEarlyNotResponding(String processName, int pid, String annotation); /** * An application process is not responding. Return 0 to show the "app * not responding" dialog, 1 to continue waiting, or -1 to kill it * immediately. */ int appNotResponding(String processName, int pid, String processStats); /** * The system process watchdog has detected that the system seems to be * hung. Return 1 to continue waiting, or -1 to let it continue with its * normal kill. */ int systemNotResponding(String msg); /** * 360 phones */ boolean moveTaskToFront(String pkg, int task, int flags, Bundle options); abstract class Stub extends Binder implements IActivityController { public static IActivityController asInterface(IBinder obj) { throw new UnsupportedOperationException(); } @Override public IBinder asBinder() { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/IActivityManager.java ================================================ /* * 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 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 android.app; 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.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; import android.os.RemoteException; import androidx.annotation.RequiresApi; public interface IActivityManager extends IInterface { @RequiresApi(31) int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras, String[] requiredPermissions, String[] excludedPermissions, String[] excludePackages, int appOp, Bundle bOptions, boolean serialized, boolean sticky, int userId) throws RemoteException; @RequiresApi(31) int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras, String[] requiredPermissions, String[] excludedPermissions, int appOp, Bundle bOptions, boolean serialized, boolean sticky, int userId) throws RemoteException; @RequiresApi(30) int broadcastIntentWithFeature(IApplicationThread caller, 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; int broadcastIntent(IApplicationThread caller, 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; int startActivity(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags, ProfilerInfo profilerInfo, Bundle options) throws RemoteException; @RequiresApi(30) int startActivityWithFeature(IApplicationThread caller, String callingPackage, String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags, ProfilerInfo profilerInfo, Bundle options) throws RemoteException; int startActivityAsUser(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags, ProfilerInfo profilerInfo, Bundle options, int userId) throws RemoteException; @RequiresApi(30) int startActivityAsUserWithFeature(IApplicationThread caller, String callingPackage, String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int flags, ProfilerInfo profilerInfo, Bundle options, int userId) throws RemoteException; void forceStopPackage(String packageName, int userId) throws RemoteException; boolean startUserInBackground(int userid) throws RemoteException; Intent registerReceiver(IApplicationThread caller, String callerPackage, IIntentReceiver receiver, IntentFilter filter, String requiredPermission, int userId, int flags) throws RemoteException; void finishReceiver(IBinder caller, int resultCode, String resultData, Bundle resultExtras, boolean resultAbort, int flags) throws RemoteException; @RequiresApi(30) Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage, String callingFeatureId, IIntentReceiver receiver, IntentFilter filter, String requiredPermission, int userId, int flags) throws RemoteException; @RequiresApi(31) Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage, String callingFeatureId, String receiverId, IIntentReceiver receiver, IntentFilter filter, String requiredPermission, int userId, int flags) throws RemoteException; int bindService(IApplicationThread caller, IBinder token, Intent service, String resolvedType, IServiceConnection connection, int flags, String callingPackage, int userId) throws RemoteException; @RequiresApi(34) int bindService(IApplicationThread caller, IBinder token, Intent service, String resolvedType, IServiceConnection connection, long flags, String callingPackage, int userId) throws RemoteException; boolean unbindService(IServiceConnection connection) throws RemoteException; boolean switchUser(int userid) throws RemoteException; UserInfo getCurrentUser() throws RemoteException; void setActivityController(IActivityController watcher, boolean imAMonkey) throws RemoteException; @RequiresApi(29) ContentProviderHolder getContentProviderExternal(String name, int userId, IBinder token, String tag) throws RemoteException; ContentProviderHolder getContentProviderExternal(String name, int userId, IBinder token) throws RemoteException; Configuration getConfiguration() throws RemoteException; void registerUidObserver(IUidObserver observer, int which, int cutpoint, String callingPackage) throws RemoteException; abstract class Stub extends Binder implements IActivityManager { public static int TRANSACTION_setActivityController; public static IActivityManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/IApplicationThread.java ================================================ /* * 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 android.app; import android.os.Binder; import android.os.IBinder; import android.os.IInterface; public interface IApplicationThread extends IInterface { abstract class Stub extends Binder implements IApplicationThread { public static IApplicationThread asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/INotificationManager.java ================================================ package android.app; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.IBinder; import android.os.IInterface; import android.os.RemoteException; import androidx.annotation.RequiresApi; public interface INotificationManager extends IInterface { void enqueueNotificationWithTag(String pkg, String opPkg, String tag, int id, Notification notification, int userId) throws RemoteException; void cancelNotificationWithTag(String pkg, String tag, int id, int userId) throws RemoteException; @RequiresApi(30) void cancelNotificationWithTag(String pkg, String opPkg, String tag, int id, int userId) throws RemoteException; void createNotificationChannelsForPackage(String pkg, int uid, ParceledListSlice channelsList) throws RemoteException; void updateNotificationChannelForPackage(String pkg, int uid, NotificationChannel channel); @RequiresApi(30) NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, String conversationId, boolean includeDeleted) throws RemoteException; NotificationChannel getNotificationChannelForPackage(String pkg, int uid, String channelId, boolean includeDeleted) throws RemoteException; abstract class Stub extends Binder implements INotificationManager { public static INotificationManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/IServiceConnection.java ================================================ package android.app; import android.content.ComponentName; import android.os.Binder; import android.os.IBinder; import android.os.IInterface; public interface IServiceConnection extends IInterface { void connected(ComponentName name, IBinder service, boolean dead); abstract class Stub extends Binder implements IServiceConnection { public static IServiceConnection asInterface(IBinder obj) { throw new UnsupportedOperationException(); } @Override public IBinder asBinder() { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/IUidObserver.java ================================================ package android.app; import android.os.Binder; public interface IUidObserver { void onUidGone(int uid, boolean disabled); void onUidActive(int uid); void onUidIdle(int uid, boolean disabled); void onUidCachedChanged(int uid, boolean cached); abstract class Stub extends Binder implements IUidObserver { } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/LoadedApk.java ================================================ package android.app; import android.content.pm.ApplicationInfo; public final class LoadedApk { private ClassLoader mDefaultClassLoader; public ApplicationInfo getApplicationInfo() { throw new UnsupportedOperationException("STUB"); } public ClassLoader getClassLoader() { throw new UnsupportedOperationException("STUB"); } public String getPackageName() { throw new UnsupportedOperationException("STUB"); } public String getResDir() { throw new UnsupportedOperationException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/Notification.java ================================================ package android.app; public class Notification { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/NotificationChannel.java ================================================ package android.app; public class NotificationChannel { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/ProfilerInfo.java ================================================ /* * 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 android.app; public class ProfilerInfo { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/app/ResourcesManager.java ================================================ package android.app; public class ResourcesManager { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/AttributionSource.java ================================================ package android.content; public class AttributionSource { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/BroadcastReceiver.java ================================================ package android.content; public abstract class BroadcastReceiver { public abstract void onReceive(Context context, Intent intent); } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/ComponentName.java ================================================ package android.content; public final class ComponentName { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/Context.java ================================================ package android.content; import android.os.Handler; import android.os.IBinder; import android.os.UserHandle; public class Context { public IBinder getActivityToken() { throw new UnsupportedOperationException("STUB"); } public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, IntentFilter filter, String broadcastPermission, Handler scheduler) { throw new UnsupportedOperationException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/IContentProvider.java ================================================ package android.content; import android.os.Bundle; import android.os.IInterface; import android.os.RemoteException; import androidx.annotation.RequiresApi; public interface IContentProvider extends IInterface { Bundle call(String callingPkg, String method, String arg, Bundle extras) throws RemoteException; @RequiresApi(29) Bundle call(String callingPkg, String authority, String method, String arg, Bundle extras) throws RemoteException; @RequiresApi(30) Bundle call(String callingPkg, String attributionTag, String authority, String method, String arg, Bundle extras) throws RemoteException; @RequiresApi(31) Bundle call(AttributionSource attributionSource, String authority, String method, String arg, Bundle extras) throws RemoteException; } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/IIntentReceiver.java ================================================ /* * 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 android.content; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; public interface IIntentReceiver extends IInterface { void performReceive(Intent intent, int resultCode, String data, Bundle extras, boolean ordered, boolean sticky, int sendingUser); abstract class Stub extends Binder implements IIntentReceiver { public static IIntentReceiver asInterface(IBinder obj) { throw new UnsupportedOperationException(); } @Override public IBinder asBinder() { return this; } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/IIntentSender.java ================================================ package android.content; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; import androidx.annotation.RequiresApi; public interface IIntentSender extends IInterface { int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options); @RequiresApi(26) void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options); abstract class Stub extends Binder implements IIntentSender { public Stub() { throw new UnsupportedOperationException(); } @Override public android.os.IBinder asBinder() { throw new UnsupportedOperationException(); } public static IIntentSender asInterface(IBinder binder) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/Intent.java ================================================ /* * 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 android.content; public class Intent { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/IntentFilter.java ================================================ package android.content; public class IntentFilter { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/IntentSender.java ================================================ package android.content; public class IntentSender { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/ApplicationInfo.java ================================================ package android.content.pm; import androidx.annotation.RequiresApi; public class ApplicationInfo { public String credentialProtectedDataDir; public String[] resourceDirs; @RequiresApi(31) public String[] overlayPaths; } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/BaseParceledListSlice.java ================================================ package android.content.pm; import java.util.List; abstract class BaseParceledListSlice { public List getList() { throw new RuntimeException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/IPackageInstaller.java ================================================ package android.content.pm; import android.os.Binder; import android.os.IBinder; import android.os.IInterface; public interface IPackageInstaller extends IInterface { void uninstall(android.content.pm.VersionedPackage versionedPackage, java.lang.String callerPackageName, int flags, android.content.IntentSender statusReceiver, int userId) throws android.os.RemoteException; abstract class Stub extends Binder implements IPackageInstaller { public static IPackageInstaller asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/IPackageManager.java ================================================ package android.content.pm; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import android.os.IInterface; import android.os.RemoteException; import androidx.annotation.RequiresApi; import java.util.List; public interface IPackageManager extends IInterface { boolean isPackageAvailable(String packageName, int userId) throws RemoteException; boolean getApplicationHiddenSettingAsUser(String packageName, int userId) throws RemoteException; ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) throws RemoteException; @RequiresApi(33) ApplicationInfo getApplicationInfo(String packageName, long flags, int userId) throws RemoteException; PackageInfo getPackageInfo(String packageName, int flags, int userId) throws RemoteException; @RequiresApi(33) PackageInfo getPackageInfo(String packageName, long flags, int userId) throws RemoteException; int getPackageUid(String packageName, int flags, int userId) throws RemoteException; @RequiresApi(33) int getPackageUid(String packageName, long flags, int userId) throws RemoteException; 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; @RequiresApi(33) ParceledListSlice getInstalledApplications(long flags, int userId) throws RemoteException; int getUidForSharedUser(String sharedUserName) throws RemoteException; void grantRuntimePermission(String packageName, String permissionName, int userId) throws RemoteException; void revokeRuntimePermission(String packageName, String permissionName, int userId) throws RemoteException; int getPermissionFlags(String permissionName, String packageName, int userId) throws RemoteException; void updatePermissionFlags(String permissionName, String packageName, int flagMask, int flagValues, int userId) throws RemoteException; int checkPermission(String permName, String pkgName, int userId) throws RemoteException; int checkUidPermission(String permName, int uid) throws RemoteException; IPackageInstaller getPackageInstaller() throws RemoteException; int installExistingPackageAsUser(String packageName, int userId, int installFlags, int installReason) throws RemoteException; @RequiresApi(29) int installExistingPackageAsUser(String packageName, int userId, int installFlags, int installReason, List whiteListedPermissions) throws RemoteException; ParceledListSlice queryIntentActivities(Intent intent, String resolvedType, int flags, int userId) throws RemoteException; @RequiresApi(33) ParceledListSlice queryIntentActivities(Intent intent, String resolvedType, long flags, int userId) throws RemoteException; boolean performDexOptMode(String packageName, boolean checkProfiles, String targetCompilerFilter, boolean force, boolean bootComplete, String splitName) throws RemoteException; void clearApplicationProfileData(String packageName) throws RemoteException; abstract class Stub extends Binder implements IPackageManager { public static IPackageManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/PackageInfo.java ================================================ package android.content.pm; public class PackageInfo { public String overlayTarget; } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/PackageInstaller.java ================================================ package android.content.pm; public class PackageInstaller { public static class SessionParams { public int installFlags = 0; } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/PackageManager.java ================================================ package android.content.pm; import java.util.List; public class PackageManager { public List getInstalledPackagesAsUser(int flags, int userId) { throw new UnsupportedOperationException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/PackageParser.java ================================================ package android.content.pm; import java.io.File; public class PackageParser { public static class PackageLite { public final String packageName = null; } public final static class Package { public ApplicationInfo applicationInfo; } /** Before SDK21 */ public static PackageLite parsePackageLite(String packageFile, int flags) { throw new UnsupportedOperationException("STUB"); } /** Since SDK21 */ public static PackageLite parsePackageLite(File packageFile, int flags) throws PackageParserException { throw new UnsupportedOperationException("STUB"); } public Package parsePackage(File packageFile, int flags, boolean useCaches) throws PackageParserException { throw new UnsupportedOperationException("STUB"); } /** Since SDK21 */ public static class PackageParserException extends Exception { } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/ParceledListSlice.java ================================================ package android.content.pm; import java.util.List; public class ParceledListSlice extends BaseParceledListSlice { public ParceledListSlice(List list) { throw new IllegalArgumentException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/ResolveInfo.java ================================================ package android.content.pm; public class ResolveInfo { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/UserInfo.java ================================================ package android.content.pm; public class UserInfo { public int id; public String name; } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/pm/VersionedPackage.java ================================================ package android.content.pm; public class VersionedPackage { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/res/AssetManager.java ================================================ package android.content.res; import java.io.IOException; import java.io.InputStream; public final class AssetManager { public final int addAssetPath(String path) { throw new UnsupportedOperationException("STUB"); } public void close() { throw new UnsupportedOperationException("STUB"); } public final InputStream open(String fileName) throws IOException { throw new UnsupportedOperationException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/res/CompatibilityInfo.java ================================================ package android.content.res; public class CompatibilityInfo { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/res/Configuration.java ================================================ package android.content.res; public class Configuration { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/res/Resources.java ================================================ package android.content.res; import android.util.DisplayMetrics; public class Resources { public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) { throw new UnsupportedOperationException("STUB"); } public Resources(ClassLoader classLoader) { throw new UnsupportedOperationException("STUB"); } public void setImpl(ResourcesImpl impl) { throw new UnsupportedOperationException("STUB"); } public CompatibilityInfo getCompatibilityInfo() { throw new UnsupportedOperationException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/res/ResourcesImpl.java ================================================ package android.content.res; public class ResourcesImpl { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/res/ResourcesKey.java ================================================ package android.content.res; public class ResourcesKey { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/content/res/TypedArray.java ================================================ package android.content.res; public class TypedArray { protected TypedArray(Resources resources) { throw new UnsupportedOperationException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/ddm/DdmHandleAppName.java ================================================ package android.ddm; public class DdmHandleAppName { public static void setAppName(String name, int userId) { throw new RuntimeException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/graphics/Movie.java ================================================ package android.graphics; public class Movie { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/graphics/drawable/Drawable.java ================================================ package android.graphics.drawable; public class Drawable { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/Binder.java ================================================ package android.os; import android.annotation.NonNull; import android.annotation.Nullable; import java.io.FileDescriptor; public class Binder implements IBinder { @Override public boolean transact(int code, @NonNull Parcel data, Parcel reply, int flags) { throw new RuntimeException("STUB"); } @Override public String getInterfaceDescriptor() { throw new RuntimeException("STUB"); } public boolean pingBinder() { throw new RuntimeException("STUB"); } @Override public boolean isBinderAlive() { throw new RuntimeException("STUB"); } @Override public IInterface queryLocalInterface(@NonNull String descriptor) { throw new RuntimeException("STUB"); } @Override public void dump(@NonNull FileDescriptor fd, String[] args) { throw new RuntimeException("STUB"); } @Override public void dumpAsync(@NonNull FileDescriptor fd, String[] args) { throw new RuntimeException("STUB"); } @Override public void linkToDeath(@NonNull DeathRecipient recipient, int flags) { throw new RuntimeException("STUB"); } @Override public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { throw new RuntimeException("STUB"); } protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { throw new RuntimeException("STUB"); } public static IBinder allowBlocking(IBinder binder){ throw new RuntimeException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/Build.java ================================================ package android.os; public class Build { public static class VERSION { public final static int SDK_INT = SystemProperties.getInt( "ro.build.version.sdk", 0); } public static class VERSION_CODES { public static final int O_MR1 = 27; public static final int P = 28; public static final int Q = 29; public static final int R = 30; public static final int S = 31; } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/Bundle.java ================================================ /* * 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 android.os; public class Bundle { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/Environment.java ================================================ package android.os; import java.io.File; public class Environment { public static File getDataProfilesDePackageDirectory(int userId, String packageName) { throw new IllegalArgumentException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/Handler.java ================================================ package android.os; public class Handler { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/IBinder.java ================================================ package android.os; import android.annotation.NonNull; import android.annotation.Nullable; import java.io.FileDescriptor; public interface IBinder { boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags); @Nullable String getInterfaceDescriptor(); boolean pingBinder(); boolean isBinderAlive(); @Nullable IInterface queryLocalInterface(@NonNull String descriptor); void dump(@NonNull FileDescriptor fd, @Nullable String[] args); void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args); void linkToDeath(@NonNull DeathRecipient recipient, int flags); boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags); interface DeathRecipient { void binderDied(); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/IInterface.java ================================================ package android.os; public interface IInterface { IBinder asBinder(); } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/IPowerManager.java ================================================ package android.os; public interface IPowerManager extends IInterface { void reboot(boolean confirm, String reason, boolean wait) throws RemoteException; abstract class Stub extends Binder implements IPowerManager { public static IPowerManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/IServiceCallback.java ================================================ package android.os; public interface IServiceCallback extends IInterface { public static abstract class Stub extends android.os.Binder implements android.os.IServiceCallback { } /** * Called when a service is registered. * * @param name the service name that has been registered with * @param binder the binder that is registered */ public void onRegistration(java.lang.String name, android.os.IBinder binder) throws android.os.RemoteException; } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/IServiceManager.java ================================================ package android.os; public interface IServiceManager extends IInterface { void tryUnregisterService(java.lang.String name, android.os.IBinder service); IBinder getService(String name); public void registerForNotifications(String name, IServiceCallback cb); abstract class Stub extends Binder implements IServiceManager { public static IServiceManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/IUserManager.java ================================================ package android.os; import android.content.pm.UserInfo; import androidx.annotation.RequiresApi; import java.util.List; public interface IUserManager extends IInterface { @RequiresApi(26) boolean isUserUnlocked(int userId) throws RemoteException; List getUsers(boolean excludeDying) throws RemoteException; List getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated) throws RemoteException; UserInfo getUserInfo(int userHandle) throws RemoteException; UserInfo getProfileParent(int userId) throws RemoteException; boolean isUserUnlockingOrUnlocked(int userId) throws RemoteException; abstract class Stub extends Binder implements IUserManager { public static IUserManager asInterface(IBinder obj) { throw new RuntimeException("STUB"); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/Parcel.java ================================================ package android.os; public class Parcel { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/Parcelable.java ================================================ package android.os; public interface Parcelable { interface Creator{ public T createFromParcel(Parcel source); public T[] newArray(int size); } void writeToParcel(Parcel dest, int flags); int describeContents(); } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/PersistableBundle.java ================================================ package android.os; public class PersistableBundle { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/RemoteException.java ================================================ package android.os; public class RemoteException extends Exception { public RemoteException(String message) { throw new RuntimeException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/ResultReceiver.java ================================================ package android.os; public class ResultReceiver { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/SELinux.java ================================================ package android.os; public class SELinux { public static boolean checkSELinuxAccess(String scon, String tcon, String tclass, String perm) { throw new UnsupportedOperationException("Stub"); } public static boolean setFileContext(String path, String context) { throw new UnsupportedOperationException("Stub"); } public static String getFileContext(String path) { throw new UnsupportedOperationException("Stub"); } public static boolean setFSCreateContext(String context){ throw new UnsupportedOperationException("Stub"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/ServiceManager.java ================================================ package android.os; import android.annotation.Nullable; public class ServiceManager { /** * Returns a reference to a service with the given name. * * @param name the name of the service to get * @return a reference to the service, or null if the service doesn't exist */ @Nullable public static IBinder getService(String name) { throw new RuntimeException("STUB"); } /** * Place a new @a service called @a name into the service * manager. * * @param name the name of the new service * @param service the service object */ public static void addService(String name, IBinder service) { throw new RuntimeException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/ShellCallback.java ================================================ package android.os; public class ShellCallback implements Parcelable { public static final Parcelable.Creator CREATOR = new Creator() { @Override public ShellCallback createFromParcel(Parcel source) { throw new IllegalArgumentException("STUB"); } @Override public ShellCallback[] newArray(int size) { throw new IllegalArgumentException("STUB"); } }; @Override public void writeToParcel(Parcel dest, int flags) { throw new IllegalArgumentException("STUB"); } @Override public int describeContents() { throw new IllegalArgumentException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/ShellCommand.java ================================================ package android.os; import java.io.FileDescriptor; import java.io.InputStream; import java.io.PrintWriter; public abstract class ShellCommand { public int exec(Binder target, FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { throw new IllegalArgumentException("STUB!"); } public abstract int onCommand(String cmd); public abstract void onHelp(); public String getNextOption(){ throw new IllegalArgumentException("STUB!"); } public String getNextArgRequired() { throw new IllegalArgumentException("STUB!"); } public PrintWriter getErrPrintWriter() { throw new IllegalArgumentException("STUB!"); } public PrintWriter getOutPrintWriter() { throw new IllegalArgumentException("STUB!"); } public InputStream getRawInputStream() { throw new IllegalArgumentException("STUB!"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/SystemProperties.java ================================================ package android.os; import android.annotation.NonNull; import android.annotation.Nullable; public class SystemProperties { public static String get(@NonNull String key) { throw new UnsupportedOperationException("Stub"); } public static String get(@NonNull String key, @Nullable String def) { throw new UnsupportedOperationException("Stub"); } public static void set(@NonNull String key, @Nullable String val) { throw new UnsupportedOperationException("Stub"); } public static boolean getBoolean(@NonNull String key, boolean def) { throw new UnsupportedOperationException("Stub"); } public static int getInt(@NonNull String key, int def) { throw new UnsupportedOperationException("Stub"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/UserHandle.java ================================================ package android.os; import android.annotation.NonNull; public class UserHandle { public UserHandle(int h) { throw new RuntimeException("STUB"); } public int getIdentifier() { throw new RuntimeException("STUB"); } public static final @NonNull UserHandle ALL = null; } ================================================ FILE: hiddenapi/stubs/src/main/java/android/os/UserManager.java ================================================ package android.os; import android.content.pm.UserInfo; import java.util.List; public class UserManager { public List getUsers() { throw new UnsupportedOperationException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/permission/IPermissionManager.java ================================================ package android.permission; import java.util.List; public interface IPermissionManager { List getSplitPermissions(); } ================================================ FILE: hiddenapi/stubs/src/main/java/android/system/ErrnoException.java ================================================ package android.system; public final class ErrnoException extends Exception { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/system/Int32Ref.java ================================================ package android.system; import java.util.Objects; public class Int32Ref { public int value; public Int32Ref(int value) { this.value = value; } @Override public String toString() { return Objects.toString(this); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/system/Os.java ================================================ package android.system; import android.util.MutableInt; import androidx.annotation.RequiresApi; import java.io.FileDescriptor; public class Os { public static int ioctlInt(FileDescriptor fd, int cmd, MutableInt arg) throws ErrnoException { throw new ErrnoException(); } @RequiresApi(27) public static int ioctlInt(FileDescriptor fd, int cmd, Int32Ref arg) throws ErrnoException { throw new ErrnoException(); } @RequiresApi(31) public static int ioctlInt(FileDescriptor fd, int cmd) throws ErrnoException { throw new ErrnoException(); } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/util/DisplayMetrics.java ================================================ package android.util; public class DisplayMetrics { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/util/MutableInt.java ================================================ package android.util; public final class MutableInt { public int value; public MutableInt(int value) { this.value = value; } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/util/TypedValue.java ================================================ package android.util; public class TypedValue { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/view/IWindowManager.java ================================================ package android.view; import android.app.IActivityManager; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; public interface IWindowManager extends IInterface { void lockNow(Bundle options); abstract class Stub extends Binder implements IWindowManager { public static IWindowManager asInterface(IBinder obj) { throw new UnsupportedOperationException(); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/android/webkit/WebViewDelegate.java ================================================ package android.webkit; public class WebViewDelegate { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/webkit/WebViewFactory.java ================================================ package android.webkit; public class WebViewFactory { } ================================================ FILE: hiddenapi/stubs/src/main/java/android/webkit/WebViewFactoryProvider.java ================================================ package android.webkit; public class WebViewFactoryProvider { } ================================================ FILE: hiddenapi/stubs/src/main/java/androidx/annotation/IntRange.java ================================================ /* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.LOCAL_VARIABLE; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.CLASS; /** * Denotes that the annotated element should be an int or long in the given range *

* Example: *


 *  @IntRange(from=0,to=255)
 *  public int getAlpha() {
 *      ...
 *  }
 * 
*/ @Retention(CLASS) @Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE}) public @interface IntRange { /** * Smallest value, inclusive */ long from() default Long.MIN_VALUE; /** * Largest value, inclusive */ long to() default Long.MAX_VALUE; } ================================================ FILE: hiddenapi/stubs/src/main/java/androidx/annotation/RequiresApi.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.CONSTRUCTOR; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.SOURCE; /** * Denotes that the annotated element should only be called on the given API level * or higher. *

* This is similar in purpose to the older {@code @TargetApi} annotation, but more * clearly expresses that this is a requirement on the caller, rather than being * used to "suppress" warnings within the method that exceed the {@code minSdkVersion}. */ @Retention(SOURCE) @Target({TYPE, METHOD, CONSTRUCTOR, FIELD}) public @interface RequiresApi { /** * The API level to require. Alias for {@link #api} which allows you to leave out the * {@code api=} part. */ @IntRange(from = 1) int value() default 1; /** * The API level to require */ @IntRange(from = 1) int api() default 1; } ================================================ FILE: hiddenapi/stubs/src/main/java/com/android/internal/os/BinderInternal.java ================================================ package com.android.internal.os; import android.os.IBinder; public class BinderInternal { public static final native IBinder getContextObject(); } ================================================ FILE: hiddenapi/stubs/src/main/java/com/android/internal/os/ZygoteInit.java ================================================ package com.android.internal.os; public class ZygoteInit { } ================================================ FILE: hiddenapi/stubs/src/main/java/com/android/internal/util/XmlUtils.java ================================================ package com.android.internal.util; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; public class XmlUtils { @SuppressWarnings("rawtypes") public static final HashMap readMapXml(InputStream in) throws XmlPullParserException, IOException { throw new UnsupportedOperationException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/com/android/server/LocalServices.java ================================================ package com.android.server; public class LocalServices { public static T getService(Class type) { throw new UnsupportedOperationException("STUB"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/com/android/server/SystemService.java ================================================ package com.android.server; public abstract class SystemService { } ================================================ FILE: hiddenapi/stubs/src/main/java/com/android/server/SystemServiceManager.java ================================================ package com.android.server; import java.util.ArrayList; public class SystemServiceManager { private final ArrayList mServices = new ArrayList<>(); } ================================================ FILE: hiddenapi/stubs/src/main/java/com/android/server/am/ActivityManagerService.java ================================================ package com.android.server.am; import com.android.server.SystemService; public class ActivityManagerService { public static final class Lifecycle extends SystemService { public ActivityManagerService getService() { throw new UnsupportedOperationException("STUB"); } private ProcessRecord findProcessLocked(String process, int userId, String callName) { throw new UnsupportedOperationException("STUB"); } } } ================================================ FILE: hiddenapi/stubs/src/main/java/com/android/server/am/ProcessRecord.java ================================================ package com.android.server.am; public class ProcessRecord { String processName = null; } ================================================ FILE: hiddenapi/stubs/src/main/java/dalvik/system/BaseDexClassLoader.java ================================================ package dalvik.system; import java.nio.ByteBuffer; public class BaseDexClassLoader extends ClassLoader { public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) { throw new RuntimeException("Stub!"); } public BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) { throw new RuntimeException("Stub!"); } public String getLdLibraryPath() { throw new RuntimeException("Stub!"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/dalvik/system/VMRuntime.java ================================================ package dalvik.system; public class VMRuntime { public static VMRuntime getRuntime() { throw new RuntimeException("Stub!"); } // Use `Process.is64Bit()` instead public native boolean is64Bit(); public native String vmInstructionSet(); public native boolean isJavaDebuggable(); } ================================================ FILE: hiddenapi/stubs/src/main/java/org/xmlpull/v1/XmlPullParserException.java ================================================ package org.xmlpull.v1; public class XmlPullParserException extends Throwable { } ================================================ FILE: hiddenapi/stubs/src/main/java/sun/misc/CompoundEnumeration.java ================================================ package sun.misc; import java.util.Enumeration; import java.util.NoSuchElementException; public class CompoundEnumeration implements Enumeration { private final Enumeration[] enums; private int index = 0; public CompoundEnumeration(Enumeration[] enums) { this.enums = enums; } private boolean next() { while (index < enums.length) { if (enums[index] != null && enums[index].hasMoreElements()) { return true; } index++; } return false; } public boolean hasMoreElements() { return next(); } public E nextElement() { if (!next()) { throw new NoSuchElementException(); } return enums[index].nextElement(); } } ================================================ FILE: hiddenapi/stubs/src/main/java/sun/net/www/ParseUtil.java ================================================ package sun.net.www; public class ParseUtil { public static String encodePath(String path, boolean flag) { throw new RuntimeException("Stub!"); } } ================================================ FILE: hiddenapi/stubs/src/main/java/sun/net/www/protocol/jar/Handler.java ================================================ package sun.net.www.protocol.jar; public abstract class Handler extends java.net.URLStreamHandler { } ================================================ FILE: hiddenapi/stubs/src/main/java/xposed/dummy/XResourcesSuperClass.java ================================================ package xposed.dummy; import android.content.res.Resources; /** * This class is used as super class of XResources. * * This implementation isn't included in the .dex file. Instead, it's created on the device. * Usually, it will extend Resources, but some ROMs use their own Resources subclass. * In that case, XResourcesSuperClass will extend the ROM's subclass in an attempt to increase * compatibility. */ public class XResourcesSuperClass extends Resources { /** Dummy, will never be called (objects are transferred to this class only). */ protected XResourcesSuperClass() { super(null, null, null); throw new UnsupportedOperationException(); } protected XResourcesSuperClass(ClassLoader classLoader) { super(classLoader); throw new UnsupportedOperationException(); } } ================================================ FILE: hiddenapi/stubs/src/main/java/xposed/dummy/XTypedArraySuperClass.java ================================================ package xposed.dummy; import android.content.res.Resources; import android.content.res.TypedArray; /** * This class is used as super class of XResources.XTypedArray. * * This implementation isn't included in the .dex file. Instead, it's created on the device. * Usually, it will extend TypedArray, but some ROMs use their own TypedArray subclass. * In that case, XTypedArraySuperClass will extend the ROM's subclass in an attempt to increase * compatibility. */ public class XTypedArraySuperClass extends TypedArray { /** Dummy, will never be called (objects are transferred to this class only). */ protected XTypedArraySuperClass(Resources resources) { super(resources); throw new UnsupportedOperationException(); } } ================================================ FILE: magisk-loader/update/changelog.md ================================================ # LSPosed v1.11.0 🎐 This release brings major improvements for **Android 16 Beta** readiness, resolves specific quirks on Android 10 and OnePlus devices, and significantly reinforces overall system stability. ### 📱 Compatibility & Core * **Android 16 Beta Support:** Fixed compatibility issues with Android 16 QPR Beta 3 (specifically `UserManager` changes) and recent ART updates affecting the `dex2oat` wrapper. * **Android 10 Fixes:** Resolved `dex2oat` crashes caused by 32-bit/64-bit architecture mismatches. * **OnePlus Compatibility:** Restored `Application#attach` hooking capabilities, overcoming aggressive method inlining found in recent OOS updates. * **Dex2Oat Overhaul:** Refactored the wrapper to utilize the APEX linker directly, eliminating missing symbol errors and boosting reliability. ### 🛠️ Stability & Fixes * **Database Integrity:** Resolved critical crashes and potential corruption during database initialization and migration. * **Frida Compatibility:** Fixed `SIGSEGV` crashes when running alongside Frida by making memory mapping parsing more robust. * **SELinux:** Corrected file contexts for the modern Xposed API 100 (`openRemoteFile`) and ensured they persist across reboots. * **Injection Reliability:** Implemented retry logic for System Server injection to minimize start-up failures. ### ⚡ Internal Changes * **Kotlin Refactor:** The `DexParser` has been rewritten in Kotlin for improved performance and maintainability. * **WebUI Removal:** Removed the WebUI integration as it is no longer required. --- ## 🔮 Development Plan The current LSPosed fork is undergoing a complete refactor into a new project: **Vector**. We are in the process of rewriting the Java layer into Kotlin and adding extensive documentation for the native layer. The name **Vector** was chosen to manifest its close mathematical relationship with **Matrix**, while symbolizing the framework's role as a precise injection vector for modules. ================================================ FILE: magisk-loader/update/zygisk.json ================================================ { "version": "v1.11.0", "versionCode": 7209, "zipUrl": "https://github.com/JingMatrix/LSPosed/releases/download/v1.11.0/LSPosed-v1.11.0-7209-zygisk-release.zip", "changelog": "https://raw.githubusercontent.com/JingMatrix/LSPosed/master/magisk-loader/update/changelog.md" } ================================================ FILE: native/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) project(native) set(CMAKE_CXX_STANDARD 23) add_subdirectory(${VECTOR_ROOT}/external external) file(GLOB_RECURSE NATIVE_SOURCES "src/*.cpp") add_library(${PROJECT_NAME} STATIC ${NATIVE_SOURCES}) set(IGNORED_WARNINGS -Wno-c99-extensions -Wno-extra-semi -Wno-gnu-flexible-array-initializer -Wno-gnu-string-literal-operator-templat -Wno-gnu-zero-variadic-macro-arguments -Wno-variadic-macros -Wno-zero-length-array ) # --- Include Directories --- # Add the 'include' directory as a PUBLIC include path. This means any # target that links against vector_native will also inherit this include path. target_include_directories(${PROJECT_NAME} PUBLIC include) # Add the 'src' directory and external XZ library includes as PRIVATE. # These are only needed to build vector_native itself. target_include_directories(${PROJECT_NAME} PRIVATE src ${VECTOR_ROOT}/external/xz-embedded/linux/include) # --- Compiler Options --- # Apply the standard pedantic warnings and the ignored warnings list. target_compile_options(${PROJECT_NAME} PRIVATE -Wpedantic ${IGNORED_WARNINGS}) # --- Library Linking --- # Link against required external and system libraries. # PUBLIC libraries are propagated to targets that link against this one. # PRIVATE libraries are only used for the compilation of this target. target_link_libraries(${PROJECT_NAME} PUBLIC dobby_static lsplant_static xz_static log # Android logging library fmt-header-only ) target_link_libraries(${PROJECT_NAME} PRIVATE dex_builder_static ) ================================================ FILE: native/README.md ================================================ # Vector Native Library (`native`) ## Purpose and Design Philosophy This library provides low-level hooking and modification capabilities for the Android OS. It is not a standalone application but a collection of components designed to be integrated into a larger loading mechanism, such as a Zygisk module. ## Architectural Breakdown The library is organized into distinct modules, each with a clear responsibility. ### `core` - The Abstract Engine This module defines the central abstractions and manages the runtime state. It's the conceptual heart of the library. - **`Context`**: An abstract base class that defines the injection lifecycle. It contains pure virtual methods like `LoadDex` and `SetupEntryClass`. The consumer of this library (e.g., the Zygisk module) must inherit from `Context` and provide the concrete implementations for these steps. - **`ConfigBridge`**: A simple, native-side singleton that acts as a cache for configuration data (specifically, the obfuscation map) that is fetched and provided by the consumer. - **`native_api`**: Implements the native module support system. It works by hooking the system's `do_dlopen` function. When it detects a registered module library being loaded, it calls that library's `native_init` entry point, providing it with a set of [API](include/core/native_api.h)s for creating its own native hooks. ### `elf` - Symbol Resolution This module is responsible for runtime symbol lookups in shared libraries, a critical function for native hooking. - **`ElfImage`**: A parser for ELF files mapped into the current process's memory. It can resolve symbols in stripped binaries by locating, decompressing (using `xz-embedded`), and parsing the `.gnu_debugdata` section. It applies a cascading lookup strategy: GNU hash -> ELF hash -> linear scan of the symbol table. - **`ElfSymbolCache`**: A thread-safe, lazy-initialized cache for `ElfImage` instances, providing a safe way to access common libraries like `libart.so` and the `linker`. ### `jni` - The Business Logic Interface This is the most significant module and represents the library's primary service layer. It contains a set of JNI bridges that expose the core features to the injected Java framework code. The functionality here is the main product of the native library. - **`jni_bridge.h`**: Provides a set of helper macros (`VECTOR_NATIVE_METHOD`, `REGISTER_VECTOR_NATIVE_METHODS`, etc.) to standardize and simplify the tedious process of writing JNI boilerplate. - **`HookBridge`**: The engine for ART method hooking. It maintains a thread-safe map of all active hooks. It also includes some stability controls, such as using atomic operations to set the backup method trampoline and throwing a Java exception instead of causing a native crash if a user tries to invoke the original method of a failed hook. - **`ResourcesHook`**: Provides the functionality to intercept and rewrite Android's binary XML resources on the fly. It relies on non-public structures from `libandroidfw.so` and uses the `elf` module to find the necessary function symbols at runtime. - **`DexParserBridge`**: Exposes a native DEX parser to the Java layer using a visitor pattern. This allows for analysis of an app's bytecode without the overhead of instantiating the entire DEX structure as Java objects. - **`NativeApiBridge`**: The JNI counterpart to the `core/native_api`. It exposes a method for the Java framework to register the filenames of third-party native modules. ### `common` & `framework` - **`common`**: A collection of basic utilities, including a `fmt`-based logging system, global constants, and helper functions. - **`framework`**: Contains minimal C++ structure definitions that mirror those inside Android's internal `libandroidfw.so`. These are necessary to correctly interpret resource data pointers. ## 3. Build System The library is configured with CMake to be built as a **static library (`libnative.a`)**. All external dependencies are also linked statically for maximum portability. ================================================ FILE: native/include/common/config.h ================================================ #pragma once /** * @file config.h * @brief Compile-time constants, version information, and platform-specific configurations. */ namespace vector::native { [[nodiscard]] constexpr bool IsDebugBuild() { #ifdef NDEBUG return false; #else return true; #endif } /// A compile-time constant indicating if this is a debug build. inline constexpr bool kIsDebugBuild = IsDebugBuild(); /** * @def LP_SELECT(lp32, lp64) * @brief A preprocessor macro to select a value based on the architecture. * @param lp32 The value to use on 32-bit platforms. * @param lp64 The value to use on 64-bit platforms. */ #if defined(__LP64__) #define LP_SELECT(lp32, lp64) lp64 #else #define LP_SELECT(lp32, lp64) lp32 #endif /// The filename of the core Android Runtime (ART) library. inline constexpr auto kArtLibraryName = "libart.so"; /// The filename of the Android Binder library. inline constexpr auto kBinderLibraryName = "libbinder.so"; /// The filename of the Android Framework library. inline constexpr auto kFrameworkLibraryName = "libandroidfw.so"; /// The path to the dynamic linker. inline constexpr auto kLinkerPath = "/linker"; /// The version code of the library, populated by the build system. const int kVersionCode = VERSION_CODE; /// The version name of the library, populated by the build system. const char *const kVersionName = VERSION_NAME; } // namespace vector::native ================================================ FILE: native/include/common/logging.h ================================================ #pragma once #include #include #include /** * @file logging.h * @brief Provides a lightweight logging framework using fmt. * */ /// The tag used for all log messages from this library. #ifndef LOG_TAG #define LOG_TAG "VectorNative" #endif /** * @def LOGV(fmt, ...) * @brief Logs a verbose message. Compiled out in release builds. * Includes file, line, and function information. */ /** * @def LOGD(fmt, ...) * @brief Logs a debug message. Compiled out in release builds. * Includes file, line, and function information. */ /** * @def LOGI(fmt, ...) * @brief Logs an informational message. */ /** * @def LOGW(fmt, ...) * @brief Logs a warning message. */ /** * @def LOGE(fmt, ...) * @brief Logs an error message. */ /** * @def LOGF(fmt, ...) * @brief Logs a fatal error message. */ /** * @def PLOGE(fmt, ...) * @brief Logs an error message and appends the string representation of the * current `errno` value. */ #ifdef LOG_DISABLED #define LOGV(...) ((void)0) #define LOGD(...) ((void)0) #define LOGI(...) ((void)0) #define LOGW(...) ((void)0) #define LOGE(...) ((void)0) #define LOGF(...) ((void)0) #define PLOGE(...) ((void)0) #else namespace vector::native::detail { template inline void LogToAndroid(int prio, const char *tag, fmt::format_string fmt, T &&...args) { // Using a stack-allocated buffer for performance. std::array buf{}; // format_to_n is safe against buffer overflows. auto result = fmt::format_to_n(buf.data(), buf.size() - 1, fmt, std::forward(args)...); buf[result.size] = '\0'; __android_log_write(prio, tag, buf.data()); } } // 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__) #else #define LOGV(...) ((void)0) #define LOGD(...) ((void)0) #endif #define LOGI(fmt, ...) \ ::vector::native::detail::LogToAndroid(ANDROID_LOG_INFO, LOG_TAG, \ fmt __VA_OPT__(, ) __VA_ARGS__) #define LOGW(fmt, ...) \ ::vector::native::detail::LogToAndroid(ANDROID_LOG_WARN, LOG_TAG, \ fmt __VA_OPT__(, ) __VA_ARGS__) #define LOGE(fmt, ...) \ ::vector::native::detail::LogToAndroid(ANDROID_LOG_ERROR, LOG_TAG, \ fmt __VA_OPT__(, ) __VA_ARGS__) #define LOGF(fmt, ...) \ ::vector::native::detail::LogToAndroid(ANDROID_LOG_FATAL, LOG_TAG, \ fmt __VA_OPT__(, ) __VA_ARGS__) #define PLOGE(fmt, ...) LOGE(fmt " failed with error {}: {}", ##__VA_ARGS__, errno, strerror(errno)) #endif // LOG_DISABLED ================================================ FILE: native/include/core/config_bridge.h ================================================ #pragma once #include #include #include /** * @file config_bridge.h * @brief A native-side cache for configuration data, currently only the obfuscation map. */ namespace vector::native { /** * @class ConfigBridge * @brief A singleton that holds configuration data. */ class ConfigBridge { public: virtual ~ConfigBridge() = default; ConfigBridge(const ConfigBridge &) = delete; ConfigBridge &operator=(const ConfigBridge &) = delete; /** * @brief Gets the singleton instance of the ConfigBridge. */ static ConfigBridge *GetInstance() { return instance_.get(); } /** * @brief Releases ownership of the singleton instance. */ static std::unique_ptr ReleaseInstance() { return std::move(instance_); } /// Gets a reference to the obfuscation map. virtual std::map &obfuscation_map() = 0; /// Sets the obfuscation map. virtual void obfuscation_map(std::map map) = 0; protected: ConfigBridge() = default; /// The singleton instance, managed alongside the main Context. static std::unique_ptr instance_; }; } // namespace vector::native ================================================ FILE: native/include/core/context.h ================================================ #pragma once #include #include #include #include #include #include "common/logging.h" /** * @file context.h * @brief Defines the core runtime context for the Vector native environment. * * The Context class is a singleton that holds essential runtime information, such as * the injected class loader, and provides core functionalities like class finding and DEX loading. * It serves as the central hub for native operations. */ namespace vector::native { // Forward declaration from another module. class ConfigBridge; /** * @class Context * @brief Manages the global state and core operations of the native library. * * This singleton is responsible for initializing hooks, managing DEX files, * and providing access to the application's class loader. * It orchestrates the setup process when the library is loaded into the target application. */ class Context { public: Context(const Context &) = delete; Context &operator=(const Context &) = delete; /** * @brief Gets the singleton instance of the Context. * @return A pointer to the global Context instance. */ static Context *GetInstance(); /** * @brief Releases ownership of the singleton instance. * * This is typically used during shutdown to clean up resources. * After this call, GetInstance() will return nullptr until a new instance is created. * * @return A unique_ptr owning the Context instance. */ static std::unique_ptr ReleaseInstance(); /** * @brief Gets the class loader used for injecting framework classes. * @return A global JNI reference to the class loader. */ [[nodiscard]] jobject GetCurrentClassLoader() const { return inject_class_loader_; } /** * @brief Finds a class using the framework's injected class loader. * * This is the primary method for looking up classes that are part of the * Vector framework's Java components. * * @param env The JNI environment. * @param class_name The fully qualified name of the class to find (dot-separated). * @return A ScopedLocalRef containing the jclass object, or nullptr if not found. */ [[nodiscard]] lsplant::ScopedLocalRef FindClassFromCurrentLoader( JNIEnv *env, std::string_view class_name) const { return FindClassFromLoader(env, GetCurrentClassLoader(), class_name); } virtual ~Context() = default; protected: /** * @class PreloadedDex * @brief Manages a memory-mapped DEX file. * * This helper class handles the mapping of a DEX file from a file descriptor * into memory and ensures it is unmapped upon destruction. */ class PreloadedDex { public: PreloadedDex() : addr_(nullptr), size_(0) {} PreloadedDex(const PreloadedDex &) = delete; PreloadedDex &operator=(const PreloadedDex &) = delete; /** * @brief Memory-maps a DEX file from a file descriptor. * @param fd The file descriptor of the DEX file. * @param size The size of the file. */ PreloadedDex(int fd, size_t size); PreloadedDex(PreloadedDex &&other) noexcept : addr_(other.addr_), size_(other.size_) { other.addr_ = nullptr; other.size_ = 0; } PreloadedDex &operator=(PreloadedDex &&other) noexcept { if (this != &other) { if (addr_) { munmap(addr_, size_); } addr_ = other.addr_; size_ = other.size_; other.addr_ = nullptr; other.size_ = 0; } return *this; } ~PreloadedDex(); /// Checks if the DEX file was successfully mapped. explicit operator bool() const { return addr_ != nullptr && size_ > 0; } /// Returns the size of the mapped DEX data. [[nodiscard]] auto size() const { return size_; } /// Returns a pointer to the beginning of the mapped DEX data. [[nodiscard]] auto data() const { return addr_; } private: void *addr_; size_t size_; }; Context() = default; /** * @brief Finds a class from a specific class loader instance. * @param env The JNI environment. * @param class_loader The class loader to use for the lookup. * @param class_name The name of the class to find. * @return A ScopedLocalRef containing the jclass, or nullptr if not found. */ static lsplant::ScopedLocalRef FindClassFromLoader(JNIEnv *env, jobject class_loader, std::string_view class_name); /** * @brief Finds and calls a static void method on the framework's entry class. * * A utility for internal communication between the native and Java layers. * * @tparam Args Argument types for the method call. * @param env The JNI environment. * @param method_name The name of the static method. * @param method_sig The JNI signature of the method. * @param args The arguments to pass to the method. */ template void FindAndCall(JNIEnv *env, std::string_view method_name, std::string_view method_sig, Args &&...args) const { if (!entry_class_) { LOGE("Cannot call method '{}', entry class is null", method_name.data()); return; } jmethodID mid = lsplant::JNI_GetStaticMethodID(env, entry_class_, method_name, method_sig); if (mid) { env->CallStaticVoidMethod(entry_class_, mid, lsplant::UnwrapScope(std::forward(args))...); } else { LOGE("Static method '{}' with signature '{}' not found", method_name.data(), method_sig.data()); } } // --- Virtual methods for platform-specific implementations --- /// Initializes the ART hooking framework (LSPlant). virtual void InitArtHooker(JNIEnv *env, const lsplant::InitInfo &initInfo); /// Registers all necessary JNI bridges and native hooks. virtual void InitHooks(JNIEnv *env); /// Loads a DEX file into the target application. virtual void LoadDex(JNIEnv *env, PreloadedDex &&dex) = 0; /// Sets up the main entry class for native-to-Java calls. virtual void SetupEntryClass(JNIEnv *env) = 0; protected: /// The singleton instance of the Context. static std::unique_ptr instance_; /// Global reference to the classloader used to load the framework. jobject inject_class_loader_ = nullptr; /// Global reference to the primary entry point class in the Java framework. jclass entry_class_ = nullptr; }; } // namespace vector::native ================================================ FILE: native/include/core/native_api.h ================================================ #pragma once #include #include #include #include #include "common/config.h" #include "common/logging.h" /** * @file native_api.h * @brief Manages the native module ecosystem and provides a stable API for them. * * This component is responsible for hooking the dynamic library loader (`dlopen`) to * detect when registered native modules are loaded. * It then provides these modules with a set of function pointers for * interacting with the Vector core, primarily for creating native hooks. */ // NOTE: The following type definitions form a public ABI for native modules. // Do not change them without careful consideration for backward compatibility. /* * ========================================================================================= * Vector Native API Interface * ========================================================================================= * * This following function types and data structures allow a native library (your module) to * interface with the Vector framework. * The core idea is that Vector provides a set of powerful tools (like function hooking), * and your module consumes these tools through a well-defined entry point. * * The interaction flow is as follows: * * 1. Vector intercepts the loading of your native library (e.g., libnative.so). * 2. Vector looks for and calls the `native_init` function within your library. * 3. Vector passes a `NativeAPIEntries` struct to your `native_init`, * which contains function pointers to Vector's hooking * and unhooking implementations (powered by Dobby). * 4. Your `native_init` function saves these function pointers for later use * and returns a callback function (`NativeOnModuleLoaded`). * 5. Vector will then invoke your returned callback every time * a new native library is loaded into the target process, * allowing you to perform "late" hooks on specific libraries. * * * Initialization Flow * * Vector Framework Your Native Module (e.g., libnative.so) * ----------------- ------------------------------------- * * | | * [ Intercepts dlopen("libnative.so") ] | * | | * |----------> [ Finds & Calls native_init() ] | * | | * [ Passes NativeAPIEntries* ] ---> [ Stores function pointers ] * (Contains hook/unhook funcs) | * | | * | | * | <-----------[ Returns `NativeOnModuleLoaded` callback ] * | | * | | * [ Stores your callback ] | * | | * */ // Function pointer type for a native hooking implementation. using HookFunType = int (*)(void *func, void *replace, void **backup); // Function pointer type for a native unhooking implementation. using UnhookFunType = int (*)(void *func); // Callback function pointer that modules receive, invoked when any library is loaded. using NativeOnModuleLoaded = void (*)(const char *name, void *handle); /** * @struct NativeAPIEntries * @brief A struct containing function pointers exposed to native modules. */ struct NativeAPIEntries { uint32_t version; // The version of this API struct. HookFunType hookFunc; // Pointer to the function for inline hooking. UnhookFunType unhookFunc; // Pointer to the function for unhooking. }; // NOTE: Module developers should not include the following INTERNAL definitions. namespace vector::native { // The entry point function that native modules must export (`native_init`). using NativeInit = NativeOnModuleLoaded (*)(const NativeAPIEntries *entries); /** * @brief Installs the hooks required for the native API to function. * @param handler The LSPlant hook handler. * @return True on success, false on failure. */ bool InstallNativeAPI(const lsplant::HookHandler &handler); /** * @brief Registers a native library by its filename for module initialization. * * When a library with a matching filename is loaded via `dlopen`, the runtime will attempt to * initialize it as a native module by calling its `native_init` function. * * @param library_name The filename of the native module's .so file (e.g., "libmymodule.so"). */ void RegisterNativeLib(const std::string &library_name); /** * @brief A wrapper around DobbyHook. */ inline int HookInline(void *original, void *replace, void **backup) { if constexpr (kIsDebugBuild) { Dl_info info; if (dladdr(original, &info)) { LOGD("Dobby hooking {} ({}) from {} ({})", info.dli_sname ? info.dli_sname : "(unknown symbol)", info.dli_saddr ? info.dli_saddr : original, info.dli_fname ? info.dli_fname : "(unknown file)", info.dli_fbase); } } return DobbyHook(original, reinterpret_cast(replace), reinterpret_cast(backup)); } /** * @brief A wrapper around DobbyDestroy. */ inline int UnhookInline(void *original) { if constexpr (kIsDebugBuild) { Dl_info info; if (dladdr(original, &info)) { LOGD("Dobby unhooking {} ({}) from {} ({})", info.dli_sname ? info.dli_sname : "(unknown symbol)", info.dli_saddr ? info.dli_saddr : original, info.dli_fname ? info.dli_fname : "(unknown file)", info.dli_fbase); } } return DobbyDestroy(original); } } // namespace vector::native ================================================ FILE: native/include/elf/elf_image.h ================================================ #pragma once #include #include #include #include #include #include /** * @file elf_image.h * @brief Defines the ElfImage class for parsing ELF files from memory. * * This utility can find the base address of a loaded shared library, parse its ELF headers, and * look up symbol addresses using various methods (GNU hash, ELF hash, and linear search). * * It handles stripped ELF files by decompressing and parsing the `.gnu_debugdata` section. */ namespace vector::native { /** * @class ElfImage * @brief Represents a loaded ELF shared library in the current process. * * An ElfImage instance is created with the filename of a library (e.g., "libart.so"). * It automatically finds the library's base address in memory by parsing `/proc/self/maps` and * then memory-maps the ELF file from disk to parse its headers. */ class ElfImage { public: /** * @brief Constructs an ElfImage for a given shared library. * @param lib_name The filename of the library (e.g., "libart.so", "/linker"). */ explicit ElfImage(std::string_view lib_name); ~ElfImage(); // Disable copy and assignment to prevent accidental slicing and resource mismanagement. ElfImage(const ElfImage &) = delete; ElfImage &operator=(const ElfImage &) = delete; /** * @brief Finds the memory address of a symbol by its name. * * This method attempts to resolve a symbol's address using, in order: * 1. The GNU hash table (.gnu.hash) for fast lookups. * 2. The standard ELF hash table (.hash) as a fallback. * 3. A linear search through the full symbol table (.symtab). * * @tparam T The desired pointer type (e.g., `void*`, `int (*)(...)`). * @param name The name of the symbol to find. * @return The absolute memory address of the symbol, or nullptr if not found. */ template requires(std::is_pointer_v) const T getSymbAddress(std::string_view name) const { // Pre-calculate hashes for efficiency. auto gnu_hash = GnuHash(name); auto elf_hash = ElfHash(name); auto offset = getSymbOffset(name, gnu_hash, elf_hash); if (offset > 0 && base_ != nullptr) { // The final address is: base_address + symbol_offset - load_bias return reinterpret_cast(reinterpret_cast(base_) + offset - bias_); } return nullptr; } /** * @brief Finds the first symbol whose name starts with the given prefix. * * This is useful for finding symbols when the exact name is unknown, such as mangled C++ * symbols. This search is performed only on the full symbol table (.symtab) and may be slow. * * @tparam T The desired pointer type. * @param prefix The prefix to search for. * @return The address of the first matching symbol, or nullptr if none is found. */ template requires(std::is_pointer_v) const T getSymbPrefixFirstAddress(std::string_view prefix) const { auto offset = prefixLookupFirst(prefix); if (offset > 0 && base_ != nullptr) { return reinterpret_cast(reinterpret_cast(base_) + offset - bias_); } return nullptr; } /** * @brief Checks if the ELF image was successfully loaded and parsed. * @return True if the image is valid, false otherwise. */ [[nodiscard]] bool IsValid() const { return base_ != nullptr; } /** * @brief Returns the canonical path of the loaded library, as found in /proc/self/maps. */ [[nodiscard]] const std::string &GetPath() const { return path_; } private: // Finds the base address of the library in the current process's memory map. bool findModuleBase(); // Parses the main ELF headers from a given header pointer. void parseHeaders(ElfW(Ehdr) * header); // Decompresses the .gnu_debugdata section if it exists. bool decompressGnuDebugData(); // Looks up a symbol offset using the ELF hash table. ElfW(Addr) elfLookup(std::string_view name, uint32_t hash) const; // Looks up a symbol offset using the GNU hash table. ElfW(Addr) gnuLookup(std::string_view name, uint32_t hash) const; // Looks up a symbol offset via a linear scan of the .symtab section. ElfW(Addr) linearLookup(std::string_view name) const; // Finds all symbol offsets with a given name via a linear scan. std::vector linearRangeLookup(std::string_view name) const; // Finds the first symbol offset whose name starts with the given prefix. ElfW(Addr) prefixLookupFirst(std::string_view prefix) const; // Gets a symbol's offset from the start of the file. ElfW(Addr) getSymbOffset(std::string_view name, uint32_t gnu_hash, uint32_t elf_hash) const; // Lazily initializes the map for linear symbol lookups from the .symtab section. void ensureLinearMapInitialized() const; // Calculates the standard ELF hash for a symbol name. [[nodiscard]] static constexpr uint32_t ElfHash(std::string_view name); // Calculates the GNU hash for a symbol name. [[nodiscard]] static constexpr uint32_t GnuHash(std::string_view name); // --- Member Variables --- std::string path_; void *base_ = nullptr; void *file_map_ = nullptr; size_t file_size_ = 0; ElfW(Addr) bias_ = 0; bool bias_calculated_ = false; // Pointers into the mapped ELF file data. ElfW(Ehdr) *header_ = nullptr; ElfW(Shdr) *dynsym_ = nullptr; ElfW(Sym) *dynsym_start_ = nullptr; const char *strtab_start_ = nullptr; // Note: const char* is safer. // ELF hash section fields uint32_t nbucket_ = 0; uint32_t *bucket_ = nullptr; uint32_t *chain_ = nullptr; // GNU hash section fields uint32_t gnu_nbucket_ = 0; uint32_t gnu_symndx_ = 0; uint32_t gnu_bloom_size_ = 0; uint32_t gnu_shift2_ = 0; uintptr_t *gnu_bloom_filter_ = nullptr; uint32_t *gnu_bucket_ = nullptr; uint32_t *gnu_chain_ = nullptr; // For stripped binaries with .gnu_debugdata std::string elf_debugdata_; ElfW(Ehdr) *header_debugdata_ = nullptr; ElfW(Sym) *symtab_start_ = nullptr; ElfW(Off) symtab_count_ = 0; const char *symtab_str_start_ = nullptr; // Lazily-initialized map for fast linear lookups. // `mutable` allows init in const methods. mutable std::map symtabs_; }; // --- Inlined Hash Function Implementations --- constexpr uint32_t ElfImage::ElfHash(std::string_view name) { uint32_t h = 0, g; for (unsigned char p : name) { h = (h << 4) + p; if ((g = h & 0xf0000000) != 0) { h ^= g >> 24; } h &= ~g; } return h; } constexpr uint32_t ElfImage::GnuHash(std::string_view name) { uint32_t h = 5381; for (unsigned char p : name) { h = (h << 5) + h + p; // h * 33 + p } return h; } } // namespace vector::native ================================================ FILE: native/include/elf/symbol_cache.h ================================================ #pragma once /** * @file symbol_cache.h * @brief Provides a thread-safe, lazy-initialized cache for commonly used ElfImage objects. * * This avoids the cost of repeatedly parsing the ELF files for libart, libbinder, * and the linker during runtime. */ namespace vector::native { // Forward declaration class ElfImage; /** * @class ElfSymbolCache * @brief A singleton cache for frequently accessed system library ELF images. * * All methods are static and guarantee thread-safe, one-time initialization * of the underlying ElfImage objects. */ class ElfSymbolCache { public: /** * @brief Gets the cached ElfImage for the ART library (libart.so). * @return A const pointer to the ElfImage, or nullptr if it could not be loaded. */ static const ElfImage *GetArt(); /** * @brief Gets the cached ElfImage for the Binder library (libbinder.so). * @return A const pointer to the ElfImage, or nullptr if it could not be loaded. */ static const ElfImage *GetLibBinder(); /** * @brief Gets the cached ElfImage for the dynamic linker. * @return A const pointer to the ElfImage, or nullptr if it could not be loaded. */ static const ElfImage *GetLinker(); /** * @brief Clears the cache for a specific ElfImage object. * * If the provided pointer matches one of the cached images, that specific cache entry will be cleared, * forcing a reload on the next `Get...()` call for that library. * If the pointer does not match any cached image, this function does nothing. * * @param image_to_clear A pointer to the cached ElfImage to be removed. */ static bool ClearCache(const ElfImage *image_to_clear); /** * @brief Clears the cache, releasing all ElfImage objects. * * This is primarily for testing or specific shutdown scenarios. * After this call, the next call to a Get...() method will reload the library from scratch. */ static void ClearCache(); }; } // namespace vector::native ================================================ FILE: native/include/framework/android_types.h ================================================ #pragma once #include #include #include "utils/hook_helper.hpp" using lsplant::operator""_sym; // References: // https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h namespace android { typedef int32_t status_t; template struct unexpected { E val_; }; template struct expected { using value_type = T; using error_type = E; using unexpected_type = unexpected; std::variant var_; constexpr bool has_value() const noexcept { return var_.index() == 0; } constexpr const T &value() const & { return std::get(var_); } constexpr T &value() & { return std::get(var_); } constexpr const T *operator->() const { return std::addressof(value()); } constexpr T *operator->() { return std::addressof(value()); } }; enum class IOError { // Used when reading a file residing on an IncFs file-system times out. PAGES_MISSING = -1, }; template struct BasicStringPiece { const TChar *data_; size_t length_; }; using NullOrIOError = std::variant; using StringPiece16 = BasicStringPiece; enum { RES_NULL_TYPE = 0x0000, RES_STRING_POOL_TYPE = 0x0001, RES_TABLE_TYPE = 0x0002, RES_XML_TYPE = 0x0003, // Chunk types in RES_XML_TYPE RES_XML_FIRST_CHUNK_TYPE = 0x0100, RES_XML_START_NAMESPACE_TYPE = 0x0100, RES_XML_END_NAMESPACE_TYPE = 0x0101, RES_XML_START_ELEMENT_TYPE = 0x0102, RES_XML_END_ELEMENT_TYPE = 0x0103, RES_XML_CDATA_TYPE = 0x0104, RES_XML_LAST_CHUNK_TYPE = 0x017f, // This contains a uint32_t array mapping strings in the string // pool back to resource identifiers. It is optional. RES_XML_RESOURCE_MAP_TYPE = 0x0180, // Chunk types in RES_TABLE_TYPE RES_TABLE_PACKAGE_TYPE = 0x0200, RES_TABLE_TYPE_TYPE = 0x0201, RES_TABLE_TYPE_SPEC_TYPE = 0x0202, RES_TABLE_LIBRARY_TYPE = 0x0203 }; struct ResXMLTree_node { void *header; // Line number in original source file at which this element appeared. uint32_t lineNumber; // Optional XML comment that was associated with this element; -1 if none. void *comment; }; class ResXMLTree; class ResXMLParser { public: enum event_code_t { BAD_DOCUMENT = -1, START_DOCUMENT = 0, END_DOCUMENT = 1, FIRST_CHUNK_CODE = RES_XML_FIRST_CHUNK_TYPE, START_NAMESPACE = RES_XML_START_NAMESPACE_TYPE, END_NAMESPACE = RES_XML_END_NAMESPACE_TYPE, START_TAG = RES_XML_START_ELEMENT_TYPE, END_TAG = RES_XML_END_ELEMENT_TYPE, TEXT = RES_XML_CDATA_TYPE }; const ResXMLTree &mTree; event_code_t mEventCode; const ResXMLTree_node *mCurNode; const void *mCurExt; }; class ResStringPool { public: status_t mError; void *mOwnedData; const void *mHeader; size_t mSize; mutable pthread_mutex_t mDecodeLock; const uint32_t *mEntries; const uint32_t *mEntryStyles; const void *mStrings; char16_t mutable **mCache; uint32_t mStringPoolSize; // number of uint16_t const uint32_t *mStyles; uint32_t mStylePoolSize; // number of uint32_t using stringAtRet = expected; inline static auto stringAtS_ = ("_ZNK7android13ResStringPool8stringAtEjPj"_sym | "_ZNK7android13ResStringPool8stringAtEmPm"_sym) .as; inline static auto stringAt_ = ("_ZNK7android13ResStringPool8stringAtEj"_sym | "_ZNK7android13ResStringPool8stringAtEm"_sym) .as; StringPiece16 stringAt(size_t idx) const { if (stringAt_) { size_t len; const char16_t *str = stringAt_(const_cast(this), idx, &len); return {str, len}; } else if (stringAtS_) { auto str = stringAtS_(const_cast(this), idx); if (str.has_value()) { return {str->data_, str->length_}; } } return {nullptr, 0u}; } static bool setup(const lsplant::HookHandler &handler) { return handler(stringAt_) || handler(stringAtS_); } }; class ResXMLTree : public ResXMLParser { public: void *mDynamicRefTable; status_t mError; void *mOwnedData; const void *mHeader; size_t mSize; const uint8_t *mDataEnd; ResStringPool mStrings; const uint32_t *mResIds; size_t mNumResIds; const ResXMLTree_node *mRootNode; const void *mRootExt; event_code_t mRootCode; }; struct ResStringPool_ref { // Index into the string pool table at which // to find the location of the string data in the pool. // (uint32_t-offset from the indices immediately after ResStringPool_header) uint32_t index; }; struct ResXMLTree_attrExt { // String of the full namespace of this element. struct ResStringPool_ref ns; // String name of this node if it is an ELEMENT; the raw character data if this is a CDATA node. struct ResStringPool_ref name; // Byte offset from the start of this structure where the attributes start. uint16_t attributeStart; // Size of the ResXMLTree_attribute structures that follow. uint16_t attributeSize; // Number of attributes associated with an ELEMENT. // These are available as an array of ResXMLTree_attribute structures // immediately following this node. uint16_t attributeCount; // Index (1-based) of the "id" attribute. 0 if none. uint16_t idIndex; // Index (1-based) of the "class" attribute. 0 if none. uint16_t classIndex; // Index (1-based) of the "style" attribute. 0 if none. uint16_t styleIndex; }; struct Res_value { // Number of bytes in this structure. uint16_t size; // Always set to 0. uint8_t res0; // Type of the data value. enum : uint8_t { // The 'data' is either 0 or 1, specifying this resource is // either undefined or empty, respectively. TYPE_NULL = 0x00, // The 'data' holds a ResTable_ref, a reference to another resource table entry. TYPE_REFERENCE = 0x01, // The 'data' holds an attribute resource identifier. TYPE_ATTRIBUTE = 0x02, // The 'data' holds an index into the containing resource table's global value string pool. TYPE_STRING = 0x03, // The 'data' holds a single-precision floating point number. TYPE_FLOAT = 0x04, // The 'data' holds a complex number encoding a dimension value, such as "100in". TYPE_DIMENSION = 0x05, // The 'data' holds a complex number encoding a fraction of a container. TYPE_FRACTION = 0x06, // The 'data' holds a dynamic ResTable_ref, // which needs to be resolved before it can be used like a TYPE_REFERENCE. TYPE_DYNAMIC_REFERENCE = 0x07, // The 'data' holds an attribute resource identifier, // which needs to be resolved before it can be used like a TYPE_ATTRIBUTE. TYPE_DYNAMIC_ATTRIBUTE = 0x08, // Beginning of integer flavors... TYPE_FIRST_INT = 0x10, // The 'data' is a raw integer value of the form n..n. TYPE_INT_DEC = 0x10, // The 'data' is a raw integer value of the form 0xn..n. TYPE_INT_HEX = 0x11, // The 'data' is either 0 or 1, for input "false" or "true" respectively. TYPE_INT_BOOLEAN = 0x12, // Beginning of color integer flavors... TYPE_FIRST_COLOR_INT = 0x1c, // The 'data' is a raw integer value of the form #aarrggbb. TYPE_INT_COLOR_ARGB8 = 0x1c, // The 'data' is a raw integer value of the form #rrggbb. TYPE_INT_COLOR_RGB8 = 0x1d, // The 'data' is a raw integer value of the form #argb. TYPE_INT_COLOR_ARGB4 = 0x1e, // The 'data' is a raw integer value of the form #rgb. TYPE_INT_COLOR_RGB4 = 0x1f, // ...end of integer flavors. TYPE_LAST_COLOR_INT = 0x1f, // ...end of integer flavors. TYPE_LAST_INT = 0x1f }; uint8_t dataType; // Structure of complex data values (TYPE_UNIT and TYPE_FRACTION) enum { // Where the unit type information is. // This gives us 16 possible types, as defined below. COMPLEX_UNIT_SHIFT = 0, COMPLEX_UNIT_MASK = 0xf, // TYPE_DIMENSION: Value is raw pixels. COMPLEX_UNIT_PX = 0, // TYPE_DIMENSION: Value is Device Independent Pixels. COMPLEX_UNIT_DIP = 1, // TYPE_DIMENSION: Value is a Scaled device independent Pixels. COMPLEX_UNIT_SP = 2, // TYPE_DIMENSION: Value is in points. COMPLEX_UNIT_PT = 3, // TYPE_DIMENSION: Value is in inches. COMPLEX_UNIT_IN = 4, // TYPE_DIMENSION: Value is in millimeters. COMPLEX_UNIT_MM = 5, // TYPE_FRACTION: A basic fraction of the overall size. COMPLEX_UNIT_FRACTION = 0, // TYPE_FRACTION: A fraction of the parent size. COMPLEX_UNIT_FRACTION_PARENT = 1, // Where the radix information is, telling where the decimal place appears in the mantissa. // This give us 4 possible fixed point representations as defined below. COMPLEX_RADIX_SHIFT = 4, COMPLEX_RADIX_MASK = 0x3, // The mantissa is an integral number -- i.e., 0xnnnnnn.0 COMPLEX_RADIX_23p0 = 0, // The mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn COMPLEX_RADIX_16p7 = 1, // The mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn COMPLEX_RADIX_8p15 = 2, // The mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn COMPLEX_RADIX_0p23 = 3, // Where the actual value is. // This gives us 23 bits of precision. // The top bit is the sign. COMPLEX_MANTISSA_SHIFT = 8, COMPLEX_MANTISSA_MASK = 0xffffff }; // Possible data values for TYPE_NULL. enum { // The value is not defined. DATA_NULL_UNDEFINED = 0, // The value is explicitly defined as empty. DATA_NULL_EMPTY = 1 }; // The data for this item, as interpreted according to dataType. typedef uint32_t data_type; data_type data; }; struct ResXMLTree_attribute { // Namespace of this attribute. struct ResStringPool_ref ns; // Name of this attribute. struct ResStringPool_ref name; // The original raw string value of this attribute. struct ResStringPool_ref rawValue; // Processesd typed value of this attribute. struct Res_value typedValue; }; } // namespace android ================================================ FILE: native/include/jni/jni_bridge.h ================================================ #pragma once #include #include "common/logging.h" #include "core/config_bridge.h" #include "core/context.h" /** * @file jni_bridge.h * @brief Provides essential macros and helper functions for creating JNI bridges. * */ namespace vector::native::jni { /** * @brief Returns the number of elements in a statically-allocated C-style array. * * This is a compile-time constant. * Attempting to use this on a pointer will result in a compilation error, * preventing common mistakes. * * @tparam T The type of the array elements. * @tparam N The size of the array. * @param arr A reference to the array. * @return The number of elements in the array. */ template [[nodiscard]] constexpr inline size_t ArraySize(T (&)[N]) { return N; } /** * @brief A helper function to get the obfuscated native bridge class signature prefix. * * It reads the obfuscation map to find the correct, potentially obfuscated, * package name for the native bridge classes. * * @return The JNI signature prefix (e.g., "org/matrix/vector/nativebridge/"). */ inline std::string GetNativeBridgeSignature() { auto *bridge = ConfigBridge::GetInstance(); if (bridge) { const auto &obfs_map = bridge->obfuscation_map(); // The key must match what the Java build script places in the map. auto it = obfs_map.find("org.matrix.vector.nativebridge."); if (it != obfs_map.end()) { return it->second; } } // Fallback or default value if not found. return "org/matrix/vector/nativebridge/"; } /** * @brief Internal implementation for registering native methods. * * Finds the target class using the framework's class loader and calls JNI's RegisterNatives. */ [[gnu::always_inline]] inline bool RegisterNativeMethodsInternal(JNIEnv *env, std::string_view class_name, const JNINativeMethod *methods, jint method_count) { auto *context = Context::GetInstance(); if (!context) { LOGF("Cannot register natives for '{}', Context is null.", class_name.data()); return false; } auto clazz = context->FindClassFromCurrentLoader(env, class_name); if (clazz.get() == nullptr) { LOGF("JNI class not found: {}", class_name.data()); return false; } return env->RegisterNatives(clazz.get(), methods, method_count) == JNI_OK; } // A helper cast for the native method function pointers. #define VECTOR_JNI_CAST(to) reinterpret_cast /** * @def VECTOR_NATIVE_METHOD(className, functionName, signature) * @brief Defines a JNINativeMethod entry. * * This macro constructs a JNINativeMethod struct, automatically * creating the mangled C-style function name that JNI expects. * * @param className The simple name of the Java class (e.g., "HookBridge"). * @param functionName The name of the Java method (e.g., "hookMethod"). * @param signature The JNI signature of the method (e.g., "(I)V"). */ #define VECTOR_NATIVE_METHOD(className, functionName, signature) \ {#functionName, signature, \ VECTOR_JNI_CAST(void *)(Java_org_matrix_vector_nativebridge_##className##_##functionName)} /** * @def JNI_START * @brief Defines the standard first two arguments for any JNI native method implementation. */ #define JNI_START [[maybe_unused]] JNIEnv *env, [[maybe_unused]] jclass clazz /** * @def VECTOR_DEF_NATIVE_METHOD(ret, className, functionName, ...) * @brief Defines the function signature for a JNI native method implementation. * * This macro creates the full C++ function definition with * the correct JNI name-mangling convention. */ #define VECTOR_DEF_NATIVE_METHOD(ret, className, functionName, ...) \ extern "C" JNIEXPORT ret JNICALL \ Java_org_matrix_vector_nativebridge_##className##_##functionName(JNI_START, ##__VA_ARGS__) /** * @def REGISTER_VECTOR_NATIVE_METHODS(class_name) * @brief Registers all methods defined in the `gMethods` array for a given class. * * This is the final step in linking the C++ implementations to the Java native methods. */ #define REGISTER_VECTOR_NATIVE_METHODS(class_name) \ RegisterNativeMethodsInternal(env, GetNativeBridgeSignature() + #class_name, gMethods, \ ArraySize(gMethods)) } // namespace vector::native::jni ================================================ FILE: native/include/jni/jni_hooks.h ================================================ #pragma once #include /** * @file jni_hooks.h * @brief Declares the registration functions for all JNI bridge modules. */ namespace vector::native::jni { /// Registers the JNI methods for the DexParserBridge. void RegisterDexParserBridge(JNIEnv *env); /// Registers the JNI methods for the HookBridge. void RegisterHookBridge(JNIEnv *env); /// Registers the JNI methods for the NativeApiBridge. void RegisterNativeApiBridge(JNIEnv *env); /// Registers the JNI methods for the ResourcesHook bridge. void RegisterResourcesHook(JNIEnv *env); } // namespace vector::native::jni ================================================ FILE: native/src/core/context.cpp ================================================ #include "core/context.h" #include "core/config_bridge.h" #include "jni/jni_hooks.h" namespace vector::native { // Instantiate the singleton pointers for Context and ConfigBridge. std::unique_ptr Context::instance_; std::unique_ptr ConfigBridge::instance_; Context *Context::GetInstance() { return instance_.get(); } std::unique_ptr Context::ReleaseInstance() { return std::move(instance_); } Context::PreloadedDex::PreloadedDex(int fd, size_t size) { LOGD("Mapping PreloadedDex: fd={}, size={}", fd, size); void *addr = mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0); if (addr != MAP_FAILED) { addr_ = addr; size_ = size; } else { addr_ = nullptr; size_ = 0; PLOGE("Failed to mmap dex file"); } } Context::PreloadedDex::~PreloadedDex() { if (addr_ && size_ > 0) { munmap(addr_, size_); } } void Context::InitArtHooker(JNIEnv *env, const lsplant::InitInfo &initInfo) { if (!lsplant::Init(env, initInfo)) { LOGE("Failed to initialize LSPlant hooking framework."); } } void Context::InitHooks(JNIEnv *env) { // ------------------------------------------------------------------------- // DEX Privilege Elevation // ------------------------------------------------------------------------- // We traverse the DexPathList of the injected ClassLoader to access the // underlying 'mCookie' for every loaded DEX file. // The cookie provides a handle to the native C++ DexFile object in ART memory. // Retrieve the DexPathList object (holds the array of DEX elements). auto path_list = lsplant::JNI_GetObjectFieldOf(env, inject_class_loader_, "pathList", "Ldalvik/system/DexPathList;"); if (!path_list) { LOGE("InitHooks: Failed to retrieve 'pathList' from the injected class loader."); return; } // Retrieve the 'dexElements' array, which contains the actual DEX files and resources. auto elements = lsplant::JNI_Cast(lsplant::JNI_GetObjectFieldOf( env, path_list, "dexElements", "[Ldalvik/system/DexPathList$Element;")); if (!elements) { LOGE("InitHooks: Failed to retrieve 'dexElements' from DexPathList."); return; } // Iterate over every element in the DexPathList to process individual DEX files. for (auto &element : elements) { if (element.get() == nullptr) continue; // extract the DexFile Java object from the element. auto java_dex_file = lsplant::JNI_GetObjectFieldOf(env, element, "dexFile", "Ldalvik/system/DexFile;"); if (!java_dex_file) { // Not all elements are guaranteed to have a valid DexFile // (e.g., resource-only elements). LOGW("InitHooks: Encountered a dexElement with no associated DexFile."); continue; } // Retrieve the 'mCookie'. In ART, this field stores the pointer (as a long or object) // to the internal native DexFile structure. auto cookie = lsplant::JNI_GetObjectFieldOf(env, java_dex_file, "mCookie", "Ljava/lang/Object;"); if (!cookie) { LOGW("InitHooks: Could not retrieve 'mCookie' (native handle) from DexFile."); continue; } // Attempt to modify the internal ART flags for this DEX file. // This effectively whitelists the DEX file, treating it as if it were part of // the BootClassPath, thereby bypassing Hidden API enforcement policies. if (lsplant::MakeDexFileTrusted(env, cookie.get())) { LOGD("InitHooks: Successfully elevated trust privileges for DexFile."); } else { LOGW("InitHooks: Failed to elevate trust privileges for DexFile."); } } // ------------------------------------------------------------------------- // JNI Bridge Registration // ------------------------------------------------------------------------- jni::RegisterResourcesHook(env); jni::RegisterHookBridge(env); jni::RegisterNativeApiBridge(env); jni::RegisterDexParserBridge(env); } lsplant::ScopedLocalRef Context::FindClassFromLoader(JNIEnv *env, jobject class_loader, std::string_view class_name) { if (class_loader == nullptr) { return {env, nullptr}; } static const auto dex_class_loader_class = lsplant::JNI_NewGlobalRef(env, lsplant::JNI_FindClass(env, "dalvik/system/DexClassLoader")); static jmethodID load_class_mid = lsplant::JNI_GetMethodID( env, dex_class_loader_class, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); if (!load_class_mid) { load_class_mid = lsplant::JNI_GetMethodID(env, dex_class_loader_class, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); } if (load_class_mid) { auto name_str = lsplant::JNI_NewStringUTF(env, class_name.data()); auto result = lsplant::JNI_CallObjectMethod(env, class_loader, load_class_mid, name_str); if (result) { return result; } } else { LOGE("Could not find DexClassLoader.loadClass / .findClass method ID."); } // Log clearly on failure. if (env->ExceptionCheck()) { env->ExceptionClear(); // Clear exception to prevent app crash } LOGE("Class '{}' not found using the provided class loader.", class_name.data()); return {env, nullptr}; } } // namespace vector::native ================================================ FILE: native/src/core/native_api.cpp ================================================ #include "core/native_api.h" #include #include #include #include #include #include "common/logging.h" #include "elf/elf_image.h" #include "elf/symbol_cache.h" /** * @file native_api.cpp * @brief Implementation of the native module loading and API provisioning system. */ using lsplant::operator""_sym; /* * =========================================================================================== * LSPLANT HOOKING DSL (DOMAIN SPECIFIC LANGUAGE) DOCUMENTATION * =========================================================================================== * * This source file utilizes the 'lsplant' library, which implements a C++20 Hooking DSL. * Unlike traditional C-style hooking (which relies on void* casting, manual trampolines, * and global function pointers), this DSL uses compile-time metaprogramming to ensure * type safety and encapsulate hooking logic. * * ------------------------------------------------------------------------------------------- * 1. SYNTAX ANATOMY * ------------------------------------------------------------------------------------------- * The hooking syntax follows this pattern: * "SYMBOL_NAME"_sym .hook ->* [] (args...) { ...body... }; * * A. "SYMBOL_NAME"_sym * - This is a C++ User-Defined Literal (UDL). It converts the string literal into a * compile-time 'Symbol' type. * - For C++ mangled names (common in Android system libs), you must provide the full * mangled signature (e.g., "__dl__Z9do_dlopen..."). * * B. Multi-Architecture Support (| Operator) * - Android often requires different symbol names for 32-bit (ARM) and 64-bit (ARM64). * - The DSL supports the pipe operator '|' to select the correct symbol at compile time: * ("Sym32"_sym | "Sym64"_sym) * * C. .hook ->* * - '.hook' accesses the hook injection mechanism. * - '->*' (Member Pointer Operator) is overloaded to bind the symbol to the lambda. * * D. The Template Lambda (The Replacement) * - Syntax: [] (Type arg1, Type arg2...) { ... } * - This is a C++20 Template Lambda. * - 'backup': Represents the ORIGINAL function (trampoline). * You call this to execute the original system logic. * - 'args...': Must match the signature of the target function exactly. * * ------------------------------------------------------------------------------------------- * 2. EXAMPLE USAGE * ------------------------------------------------------------------------------------------- * inline static auto my_hook = * "__open"_sym.hook ->* [](const char* path, int flags) { * // 1. Pre-processing (Before original) * LOGD("Opening file: %s", path); * * // 2. Call Original (The "Backup") * int result = backup(path, flags); * * // 3. Post-processing (After original) * return result; * }; * * ------------------------------------------------------------------------------------------- * 3. REGISTRATION * ------------------------------------------------------------------------------------------- * Defining the hook variable does not apply it. * You must pass the variable to the HookHandler to modify memory: handler(my_hook). * =========================================================================================== */ namespace vector::native { namespace { // Mutex to protect access to the global module lists. std::mutex g_module_registry_mutex; // List of callback functions provided by loaded native modules. std::list g_module_loaded_callbacks; // List of native library filenames that are registered as modules. std::list g_module_native_libs; // A smart pointer to a memory page that will hold the NativeAPIEntries struct. std::unique_ptr> g_api_page( mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0), [](void *ptr) { if (ptr != MAP_FAILED) { munmap(ptr, 4096); } }); } // namespace // The read-only, statically available Native API entry points for modules. const NativeAPIEntries *g_native_api_entries = nullptr; /** * @brief Initializes the Native API entries struct and makes it read-only. */ void InitializeApiEntries() { if (g_api_page.get() == MAP_FAILED) { LOGF("Failed to allocate memory for native API entries."); LOGD("Release the memory page pointer %p", g_api_page.release()); return; } auto *entries = new (g_api_page.get()) NativeAPIEntries{ .version = 2, .hookFunc = &HookInline, .unhookFunc = &UnhookInline, }; if (mprotect(g_api_page.get(), 4096, PROT_READ) != 0) { PLOGE("Failed to mprotect API page to read-only"); } g_native_api_entries = entries; LOGI("Native API entries initialized and protected."); } void RegisterNativeLib(const std::string &library_name) { static bool is_initialized = []() { InitializeApiEntries(); return InstallNativeAPI(lsplant::InitInfo{ .inline_hooker = [](void *target, void *replacement) { void *backup = nullptr; return HookInline(target, replacement, &backup) == 0 ? backup : nullptr; }, .art_symbol_resolver = [](auto symbol) { return ElfSymbolCache::GetLinker()->getSymbAddress(symbol); }, }); }(); if (!is_initialized) { LOGE("Cannot register module '{}' because native API failed to initialize.", library_name.c_str()); return; } std::lock_guard lock(g_module_registry_mutex); g_module_native_libs.push_back(library_name); LOGD("Native module library '{}' has been registered.", library_name.c_str()); } bool HasEnding(std::string_view fullString, std::string_view ending) { if (fullString.length() >= ending.length()) { return (fullString.compare(fullString.length() - ending.length(), std::string_view::npos, ending) == 0); } return false; } inline static auto do_dlopen_hook = "__dl__Z9do_dlopenPKciPK17android_dlextinfoPKv"_sym.hook->* [](const char *name, int flags, const void *extinfo, const void *caller_addr) static -> void * { void *handle = backup(name, flags, extinfo, caller_addr); const std::string lib_name = (name != nullptr) ? name : "null"; LOGV("do_dlopen hook triggered for library: '{}'", lib_name.c_str()); if (handle == nullptr) return nullptr; std::lock_guard lock(g_module_registry_mutex); for (std::string_view module_lib : g_module_native_libs) { if (HasEnding(lib_name, module_lib)) { LOGI("Detected registered native module being loaded: '{}'", lib_name.c_str()); void *init_sym = dlsym(handle, "native_init"); if (init_sym == nullptr) { LOGW("Library '{}' matches a module name but does not export 'native_init'.", lib_name.c_str()); break; } auto native_init = reinterpret_cast(init_sym); if (auto callback = native_init(g_native_api_entries)) { g_module_loaded_callbacks.push_back(callback); LOGI("Initialized native module '{}' and registered its callback.", lib_name.c_str()); } break; } } for (const auto &callback : g_module_loaded_callbacks) { callback(name, handle); } return handle; }; bool InstallNativeAPI(const lsplant::HookHandler &handler) { return handler(do_dlopen_hook); } } // namespace vector::native ================================================ FILE: native/src/elf/elf_image.cpp ================================================ #include "elf/elf_image.h" #include #include // For decompressing .gnu_debugdata #include #include #include #include #include // For std::move #include "common/logging.h" namespace vector::native { namespace { // Helper to safely cast an offset from a base pointer. template inline T PtrOffset(void *base, ptrdiff_t offset) { return reinterpret_cast(reinterpret_cast(base) + offset); } } // namespace ElfImage::ElfImage(std::string_view lib_name) : path_(lib_name) { if (!findModuleBase()) { base_ = nullptr; // Ensure base_ is null on failure. return; } int fd = open(path_.c_str(), O_RDONLY | O_CLOEXEC); if (fd < 0) { PLOGE("Failed to open ELF file: {}", path_.c_str()); return; } struct stat file_info; if (fstat(fd, &file_info) < 0) { PLOGE("fstat failed for {}", path_.c_str()); close(fd); return; } file_size_ = file_info.st_size; file_map_ = mmap(nullptr, file_size_, PROT_READ, MAP_SHARED, fd, 0); close(fd); if (file_map_ == MAP_FAILED) { PLOGE("mmap failed for {}", path_.c_str()); file_map_ = nullptr; return; } header_ = static_cast(file_map_); parseHeaders(header_); // Check for and handle compressed debug symbols. if (decompressGnuDebugData()) { header_debugdata_ = PtrOffset(elf_debugdata_.data(), 0); // Re-parse to find the .symtab and its .strtab from the debug data. parseHeaders(header_debugdata_); } } ElfImage::~ElfImage() { if (file_map_ != nullptr) { munmap(file_map_, file_size_); } } void ElfImage::parseHeaders(ElfW(Ehdr) * header) { if (!header) return; ElfW(Shdr) *section_headers = PtrOffset(header, header->e_shoff); const char *section_str_table = PtrOffset(header, section_headers[header->e_shstrndx].sh_offset); for (int i = 0; i < header->e_shnum; ++i) { ElfW(Shdr) *section_h = §ion_headers[i]; const char *sname = section_str_table + section_h->sh_name; switch (section_h->sh_type) { case SHT_DYNSYM: // We only care about the first .dynsym found in the original ELF file. if (dynsym_ == nullptr) { dynsym_ = section_h; dynsym_start_ = PtrOffset(header, section_h->sh_offset); } break; case SHT_SYMTAB: if (strcmp(sname, ".symtab") == 0) { symtab_start_ = PtrOffset(header, section_h->sh_offset); symtab_count_ = section_h->sh_size / section_h->sh_entsize; } break; case SHT_STRTAB: // The string table for .dynsym is usually the first SHT_STRTAB after .dynsym. // We identify it by checking if dynsym is found but its strtab is not. if (dynsym_ != nullptr && strtab_start_ == nullptr) { strtab_start_ = PtrOffset(header, section_h->sh_offset); } // The string table for .symtab is explicitly named ".strtab". if (strcmp(sname, ".strtab") == 0) { symtab_str_start_ = PtrOffset(header, section_h->sh_offset); } break; case SHT_PROGBITS: // The load bias is the difference between // the virtual address of a loaded segment and its offset in the file. // Ensure we skip early sections like .interp or .note // by waiting until after dynsym and strtab are found. if (dynsym_ == nullptr || strtab_start_ == nullptr) break; if (!bias_calculated_ && section_h->sh_flags & SHF_ALLOC && section_h->sh_addr > 0) { bias_ = section_h->sh_addr - section_h->sh_offset; bias_calculated_ = true; } break; case SHT_HASH: // Standard ELF hash table. if (nbucket_ == 0) { uint32_t *hash_data = PtrOffset(header, section_h->sh_offset); nbucket_ = hash_data[0]; // nchain is hash_data[1] bucket_ = &hash_data[2]; chain_ = bucket_ + nbucket_; } break; case SHT_GNU_HASH: // GNU-style hash table. if (gnu_nbucket_ == 0) { uint32_t *hash_data = PtrOffset(header, section_h->sh_offset); gnu_nbucket_ = hash_data[0]; gnu_symndx_ = hash_data[1]; gnu_bloom_size_ = hash_data[2]; gnu_shift2_ = hash_data[3]; gnu_bloom_filter_ = reinterpret_cast(&hash_data[4]); gnu_bucket_ = reinterpret_cast(gnu_bloom_filter_ + gnu_bloom_size_); gnu_chain_ = gnu_bucket_ + gnu_nbucket_; } break; } } } bool ElfImage::decompressGnuDebugData() { ElfW(Shdr) *section_headers = PtrOffset(header_, header_->e_shoff); const char *section_str_table = PtrOffset(header_, section_headers[header_->e_shstrndx].sh_offset); ElfW(Off) debugdata_offset = 0; ElfW(Off) debugdata_size = 0; for (int i = 0; i < header_->e_shnum; ++i) { if (strcmp(section_str_table + section_headers[i].sh_name, ".gnu_debugdata") == 0) { debugdata_offset = section_headers[i].sh_offset; debugdata_size = section_headers[i].sh_size; break; } } if (debugdata_offset == 0 || debugdata_size == 0) { return false; // Section not found. } LOGD("Found .gnu_debugdata section in {} ({} bytes). Decompressing...", path_.c_str(), debugdata_size); xz_crc32_init(); struct xz_dec *dec = xz_dec_init(XZ_DYNALLOC, 1 << 26); if (!dec) return false; struct xz_buf buf; buf.in = PtrOffset(header_, debugdata_offset); buf.in_pos = 0; buf.in_size = debugdata_size; elf_debugdata_.resize(debugdata_size * 8); // Initial guess buf.out = reinterpret_cast(elf_debugdata_.data()); buf.out_pos = 0; buf.out_size = elf_debugdata_.size(); while (true) { enum xz_ret ret = xz_dec_run(dec, &buf); if (ret == XZ_STREAM_END) { elf_debugdata_.resize(buf.out_pos); xz_dec_end(dec); LOGD("Successfully decompressed .gnu_debugdata ({} bytes)", elf_debugdata_.size()); return true; } if (ret != XZ_OK) { LOGE("XZ decompression failed with code {}", (int)ret); xz_dec_end(dec); return false; } if (buf.out_pos == buf.out_size) { elf_debugdata_.resize(elf_debugdata_.size() * 2); // Reset pointer to the potentially new base address buf.out = reinterpret_cast(elf_debugdata_.data()); // Update the total capacity buf.out_size = elf_debugdata_.size(); } } } ElfW(Addr) ElfImage::getSymbOffset(std::string_view name, uint32_t gnu_hash, uint32_t elf_hash) const { if (auto offset = gnuLookup(name, gnu_hash); offset > 0) { return offset; } else if (offset = elfLookup(name, elf_hash); offset > 0) { return offset; } else if (offset = linearLookup(name); offset > 0) { return offset; } else { return 0; } } ElfW(Addr) ElfImage::gnuLookup(std::string_view name, uint32_t hash) const { if (gnu_nbucket_ == 0) return 0; constexpr auto bloom_mask_bits = sizeof(ElfW(Addr)) * 8; auto bloom_word = gnu_bloom_filter_[(hash / bloom_mask_bits) % gnu_bloom_size_]; uintptr_t mask = (1ULL << (hash % bloom_mask_bits)) | (1ULL << ((hash >> gnu_shift2_) % bloom_mask_bits)); if ((bloom_word & mask) != mask) { return 0; // Not in bloom filter, definitely not here. } uint32_t sym_idx = gnu_bucket_[hash % gnu_nbucket_]; if (sym_idx < gnu_symndx_) return 0; do { ElfW(Sym) *sym = dynsym_start_ + sym_idx; if (((gnu_chain_[sym_idx - gnu_symndx_] ^ hash) >> 1) == 0) { if (std::string_view(strtab_start_ + sym->st_name) == name) { return sym->st_value; } } } while ((gnu_chain_[sym_idx++ - gnu_symndx_] & 1) == 0); return 0; } ElfW(Addr) ElfImage::elfLookup(std::string_view name, uint32_t hash) const { if (nbucket_ == 0) return 0; for (uint32_t n = bucket_[hash % nbucket_]; n != 0; n = chain_[n]) { ElfW(Sym) *sym = dynsym_start_ + n; if (std::string_view(strtab_start_ + sym->st_name) == name) { return sym->st_value; } } return 0; } void ElfImage::ensureLinearMapInitialized() const { // Lazily parse the .symtab section and build a map for faster lookups. if (!symtabs_.empty() || !symtab_start_ || !symtab_str_start_) { return; } for (ElfW(Off) i = 0; i < symtab_count_; ++i) { auto *sym = &symtab_start_[i]; unsigned int st_type = ELF_ST_TYPE(sym->st_info); // We only care about function or object symbols that have a size. if ((st_type == STT_FUNC || st_type == STT_OBJECT) && sym->st_size > 0) { const char *st_name = symtab_str_start_ + sym->st_name; symtabs_.emplace(st_name, sym); } } } ElfW(Addr) ElfImage::linearLookup(std::string_view name) const { ensureLinearMapInitialized(); auto it = symtabs_.find(name); if (it != symtabs_.end()) { return it->second->st_value; } return 0; } std::vector ElfImage::linearRangeLookup(std::string_view name) const { ensureLinearMapInitialized(); std::vector res; for (auto [it, end] = symtabs_.equal_range(name); it != end; ++it) { res.emplace_back(it->second->st_value); } return res; } ElfW(Addr) ElfImage::prefixLookupFirst(std::string_view prefix) const { ensureLinearMapInitialized(); // lower_bound finds the first element not less than the prefix. auto it = symtabs_.lower_bound(prefix); if (it != symtabs_.end() && it->first.starts_with(prefix)) { return it->second->st_value; } return 0; } bool ElfImage::findModuleBase() { // A helper struct to hold parsed map entry data. struct MapEntry { uintptr_t start_addr; char perms[5] = {0}; std::string pathname; }; FILE *maps = fopen("/proc/self/maps", "r"); if (!maps) { PLOGE("Failed to open /proc/self/maps"); return false; } char line_buffer[512]; std::vector filtered_list; // Filter all entries containing the library name. while (fgets(line_buffer, sizeof(line_buffer), maps)) { if (strstr(line_buffer, path_.c_str())) { unsigned long long temp_start; char path_buffer[256] = {0}; char p[5] = {0}; int items_parsed = sscanf(line_buffer, "%llx-%*x %4s %*x %*s %*d %255s", &temp_start, p, path_buffer); if (items_parsed >= 2) { MapEntry entry; entry.start_addr = static_cast(temp_start); strncpy(entry.perms, p, 4); if (items_parsed == 3) entry.pathname = path_buffer; filtered_list.push_back(std::move(entry)); LOGD("Found module entry: {}", line_buffer); } } } fclose(maps); if (filtered_list.empty()) { LOGE("Could not find any mappings for {}", path_.c_str()); return false; } const MapEntry *found_block = nullptr; // Search for the first `r--p` whose next entry is `r-xp`. // This is the most reliable pattern for `libart.so`. for (size_t i = 0; i + 1 < filtered_list.size(); ++i) { if (strcmp(filtered_list[i].perms, "r--p") == 0 && strcmp(filtered_list[i + 1].perms, "r-xp") == 0) { found_block = &filtered_list[i]; break; } } // If the pattern was not found, find the first `r-xp` entry. if (!found_block) { for (const auto &entry : filtered_list) { if (strcmp(entry.perms, "r-xp") == 0) { found_block = &entry; break; } } } // If still no match, take the very first entry found. if (!found_block) { found_block = &filtered_list[0]; } // Use the starting address of the found block as the base address. base_ = reinterpret_cast(found_block->start_addr); // Update path to the canonical one from the maps file. if (!found_block->pathname.empty()) { path_ = found_block->pathname; } LOGD("Found base for {} at {:#x}", path_.c_str(), found_block->start_addr); return true; } } // namespace vector::native ================================================ FILE: native/src/elf/symbol_cache.cpp ================================================ #include "elf/symbol_cache.h" #include #include "common/config.h" #include "elf/elf_image.h" namespace vector::native { namespace { // Each cached ElfImage gets its own unique_ptr and a mutex to guard its // initialization. std::unique_ptr g_art_image = nullptr; std::mutex g_art_mutex; std::unique_ptr g_binder_image = nullptr; std::mutex g_binder_mutex; std::unique_ptr g_linker_image = nullptr; std::mutex g_linker_mutex; } // namespace const ElfImage *ElfSymbolCache::GetArt() { // Double-checked locking pattern for performance. // The first check is lock-free. if (g_art_image) { return g_art_image.get(); } // If it's null, acquire the lock to perform the initialization safely. std::lock_guard lock(g_art_mutex); // Check again inside the lock in case another thread initialized it // while we were waiting for the lock. if (!g_art_image) { g_art_image = std::make_unique(kArtLibraryName); if (!g_art_image->IsValid()) { g_art_image.reset(); // Release if invalid. } } return g_art_image.get(); } const ElfImage *ElfSymbolCache::GetLibBinder() { if (g_binder_image) { return g_binder_image.get(); } std::lock_guard lock(g_binder_mutex); if (!g_binder_image) { g_binder_image = std::make_unique(kBinderLibraryName); if (!g_binder_image->IsValid()) { g_binder_image.reset(); } } return g_binder_image.get(); } const ElfImage *ElfSymbolCache::GetLinker() { if (g_linker_image) { return g_linker_image.get(); } std::lock_guard lock(g_linker_mutex); if (!g_linker_image) { g_linker_image = std::make_unique(kLinkerPath); if (!g_linker_image->IsValid()) { g_linker_image.reset(); } } return g_linker_image.get(); } bool ElfSymbolCache::ClearCache(const ElfImage *image_to_clear) { if (!image_to_clear) { return false; } // This "lock, check, then reset" pattern must be atomic for each cache entry. // We check each cache one by one. // Check ART cache { std::lock_guard lock(g_art_mutex); if (image_to_clear == g_art_image.get()) { g_art_image.reset(); return true; // Found and cleared, no need to check others. } } // Check Binder cache { std::lock_guard lock(g_binder_mutex); if (image_to_clear == g_binder_image.get()) { g_binder_image.reset(); return true; } } // Check Linker cache { std::lock_guard lock(g_linker_mutex); if (image_to_clear == g_linker_image.get()) { g_linker_image.reset(); return true; } } return false; } void ElfSymbolCache::ClearCache() { // Acquire all locks to ensure no other thread is currently initializing. std::lock_guard art_lock(g_art_mutex); std::lock_guard binder_lock(g_binder_mutex); std::lock_guard linker_lock(g_linker_mutex); g_art_image.reset(); g_binder_image.reset(); g_linker_image.reset(); } } // namespace vector::native ================================================ FILE: native/src/jni/dex_parser_bridge.cpp ================================================ #include #include #include #include "jni/jni_bridge.h" #include "jni/jni_hooks.h" /** * @file dex_parser_bridge.cpp * @brief Implements a JNI bridge to a native DEX file parser. * * This bridge provides a memory-efficient way for Java code to parse Android DEX files. * It avoids creating a complete object representation of the DEX file in memory, * which can be very large. * * It employs a visitor pattern: * 1. The `openDex` method performs an initial parse of the DEX file's main sections * (strings, types, fields, methods, classes) and returns them * to the Java caller as primitive arrays. * It stores the detailed parsed data in a native `DexParser` object. * 2. The `visitClass` method then iterates through the parsed classes and * invokes callback methods on a Java "visitor" object for * each class, field, and method. * * This approach minimizes JNI overhead and memory consumption by processing data * in a streaming fashion and only creating Java objects as needed for the visitor callbacks. */ namespace { // Type aliases for representing DEX encoded values and annotations. // These structures temporarily hold parsed annotation data before it's converted to Java objects. // A DEX encoded value, represented as a tuple of its type and raw byte data. using Value = std::tuple /*data*/>; // A DEX encoded array, which is a vector of encoded values. using Array = std::vector; // A list of encoded arrays. A list is used because its elements won't be // reallocated, which is important when indices are stored. using ArrayList = std::list; // An element of an annotation, consisting of a name (index into string table) and a value. using Element = std::tuple; // A list of annotation elements. using ElementList = std::vector; // A DEX annotation, containing its visibility, type, and a list of its elements. using Annotation = std::tuple; // A list of annotations. using AnnotationList = std::vector; /** * @class DexParser * @brief Extends slicer's dex::Reader to hold parsed class, method, and annotation data. * * This class serves as the main native handle for a parsed DEX file. * It stores structured data that has been read from the DEX file, * making it readily available for the `visitClass` function. */ class DexParser : public dex::Reader { public: DexParser(const dex::u1 *data, size_t size) : dex::Reader(data, size, nullptr, 0) {} /** * @struct ClassData * @brief Holds all relevant information for a single class definition. * * This structure is populated during the `openDex` phase and contains indices * pointing to the DEX file's various data pools (types, fields, methods). */ struct ClassData { std::vector interfaces; std::vector static_fields; std::vector static_fields_access_flags; std::vector instance_fields; std::vector instance_fields_access_flags; std::vector direct_methods; std::vector direct_methods_access_flags; std::vector direct_methods_code; // Pointers to method bytecode std::vector virtual_methods; std::vector virtual_methods_access_flags; std::vector virtual_methods_code; // Pointers to method bytecode std::vector annotations; }; /** * @struct MethodBody * @brief Lazily-parsed information from a method's bytecode. * * This data is only computed when a method is visited in `visitClass`, * saving significant processing time if the caller is not interested in method body details. */ struct MethodBody { bool loaded = false; // Flag to indicate if this body has been parsed yet. std::vector referred_strings; std::vector accessed_fields; // Fields read from (iget/sget) std::vector assigned_fields; // Fields written to (iput/sput) std::vector invoked_methods; std::vector opcodes; }; // Parsed data storage std::vector class_data; // One entry per ClassDef in the DEX file. // Mappings from an item's index to a list of annotation indices. // Using phmap::flat_hash_map for fast lookups. phmap::flat_hash_map> field_annotations; phmap::flat_hash_map> method_annotations; phmap::flat_hash_map> parameter_annotations; // Lazily populated map of method index to its parsed body. phmap::flat_hash_map method_bodies; }; /** * @brief Parses a variable-length integer from the DEX byte stream. * @tparam T The integral type to parse (e.g., int8_t, int32_t). * @param pptr Pointer to the current position in the byte stream. * @param size The number of bytes to read (1 to sizeof(T)). * @return A vector of bytes containing the parsed value. */ template static std::vector ParseIntValue(const dex::u1 **pptr, size_t size) { static_assert(std::is_integral::value, "must be an integral type"); std::vector ret(sizeof(T)); // Use reinterpret_cast to type-pun the byte vector's data into the target integer type. T &value = *reinterpret_cast(ret.data()); value = 0; // Ensure starting from a clean state. for (size_t i = 0; i < size; ++i) { value |= T(*(*pptr)++) << (i * 8); } // If the type is signed and we read fewer bytes than its full size, // we need to manually sign-extend the value. if constexpr (std::is_signed_v) { size_t shift = (sizeof(T) - size) * 8; if (shift > 0) { value = T(value << shift) >> shift; } } return ret; } /** * @brief Parses a variable-length float from the DEX byte stream. * @tparam T The floating-point type to parse (float or double). * @param pptr Pointer to the current position in the byte stream. * @param size The number of bytes to read. * @return A vector of bytes containing the parsed value. */ template static std::vector ParseFloatValue(const dex::u1 **pptr, size_t size) { std::vector ret(sizeof(T), 0); T &value = *reinterpret_cast(ret.data()); // The value is right-padded with zero bytes, so we copy into the higher-order bytes. int start_byte = sizeof(T) - size; for (dex::u1 *p = reinterpret_cast(&value) + start_byte; size > 0; --size) { *p++ = *(*pptr)++; } return ret; } // Forward declarations for recursive parsing functions. Annotation ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, ArrayList &array_list); Array ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list); /** * @brief Parses a single `encoded_value` from the byte stream. * This is the core of the annotation parsing logic and * handles all possible value types recursively. */ Value ParseValue(const dex::u1 **value, AnnotationList &annotation_list, ArrayList &array_list) { Value res; auto &[type, value_content] = res; auto header = *(*value)++; type = header & dex::kEncodedValueTypeMask; dex::u1 arg = header >> dex::kEncodedValueArgShift; switch (type) { // For numeric types, `arg` is `size - 1`. case dex::kEncodedByte: value_content = ParseIntValue(value, arg + 1); break; case dex::kEncodedShort: value_content = ParseIntValue(value, arg + 1); break; case dex::kEncodedChar: value_content = ParseIntValue(value, arg + 1); break; case dex::kEncodedInt: value_content = ParseIntValue(value, arg + 1); break; case dex::kEncodedLong: value_content = ParseIntValue(value, arg + 1); break; case dex::kEncodedFloat: value_content = ParseFloatValue(value, arg + 1); break; case dex::kEncodedDouble: value_content = ParseFloatValue(value, arg + 1); break; // For index types, the value is the index itself. case dex::kEncodedMethodType: case dex::kEncodedMethodHandle: case dex::kEncodedString: case dex::kEncodedType: case dex::kEncodedField: case dex::kEncodedMethod: case dex::kEncodedEnum: value_content = ParseIntValue(value, arg + 1); break; // For complex types, we parse them recursively and store an index to the // parsed object. case dex::kEncodedArray: value_content.resize(sizeof(jint)); *reinterpret_cast(value_content.data()) = static_cast(array_list.size()); array_list.emplace_back(ParseArray(value, annotation_list, array_list)); break; case dex::kEncodedAnnotation: value_content.resize(sizeof(jint)); *reinterpret_cast(value_content.data()) = static_cast(annotation_list.size()); annotation_list.emplace_back(ParseAnnotation(value, annotation_list, array_list)); break; case dex::kEncodedNull: // No value content needed. break; case dex::kEncodedBoolean: // The boolean value is stored in the `arg` part of the header. value_content = {static_cast(arg == 1)}; break; default: // This should never be reached for a valid DEX file. __builtin_unreachable(); } return res; } /** * @brief Parses an `encoded_annotation` structure. */ Annotation ParseAnnotation(const dex::u1 **annotation, AnnotationList &annotation_list, ArrayList &array_list) { Annotation ret = {dex::kVisibilityEncoded, dex::ReadULeb128(annotation), ElementList{}}; auto &[vis, type, element_list] = ret; auto size = dex::ReadULeb128(annotation); element_list.resize(size); for (size_t j = 0; j < size; ++j) { auto &[name, value] = element_list[j]; name = static_cast(dex::ReadULeb128(annotation)); value = ParseValue(annotation, annotation_list, array_list); } return ret; } /** * @brief Parses an `encoded_array` structure. */ Array ParseArray(const dex::u1 **array, AnnotationList &annotation_list, ArrayList &array_list) { auto size = dex::ReadULeb128(array); Array ret; ret.reserve(size); for (size_t i = 0; i < size; ++i) { ret.emplace_back(ParseValue(array, annotation_list, array_list)); } return ret; } /** * @brief Parses an `AnnotationSetItem`, which is a collection of annotations. */ void ParseAnnotationSet(dex::Reader &dex, AnnotationList &annotation_list, ArrayList &array_list, std::vector &indices, const dex::AnnotationSetItem *annotation_set) { if (annotation_set == nullptr) { return; } for (size_t i = 0; i < annotation_set->size; ++i) { auto *item = dex.dataPtr(annotation_set->entries[i]); auto *annotation_data = item->annotation; // Store the index of the new annotation in the output list. indices.emplace_back(annotation_list.size()); // Parse the annotation and add it to the global list. auto &[visibility, type, element_list] = annotation_list.emplace_back( ParseAnnotation(&annotation_data, annotation_list, array_list)); // The visibility is stored in the item, not the encoded annotation itself. visibility = item->visibility; } } } // namespace namespace vector::native::jni { /** * @brief JNI method to open a DEX file and perform initial parsing. * @param data A direct java.nio.ByteBuffer containing the DEX file. * @param args A jlongArray used for passing arguments. * args[0] is an output parameter to store the native DexParser pointer (cookie). * args[1] is an input flag to control whether to parse annotations. * @return A java.lang.Object[] array containing the top-level DEX structures. */ VECTOR_DEF_NATIVE_METHOD(jobject, DexParserBridge, openDex, jobject data, jlongArray args) { auto dex_size = env->GetDirectBufferCapacity(data); if (dex_size == -1) { env->ThrowNew(env->FindClass("java/io/IOException"), "DEX data must be in a direct ByteBuffer"); return nullptr; } auto *dex_data = env->GetDirectBufferAddress(data); if (dex_data == nullptr) { env->ThrowNew(env->FindClass("java/io/IOException"), "Failed to get direct buffer address"); return nullptr; } // Create the native parser object. // This will be the handle for subsequent calls. auto *dex_reader = new DexParser(reinterpret_cast(dex_data), dex_size); auto *args_ptr = env->GetLongArrayElements(args, nullptr); auto include_annotations = args_ptr[1]; env->ReleaseLongArrayElements(args, args_ptr, JNI_ABORT); // Store the pointer to the native object in the first element of the args array. // This "cookie" will be passed back to other native methods. env->SetLongArrayRegion(args, 0, 1, reinterpret_cast(&dex_reader)); auto &dex = *dex_reader; if (dex.IsCompact()) { env->ThrowNew(env->FindClass("java/io/IOException"), "Compact dex is not supported"); delete dex_reader; // Clean up before returning. return nullptr; } // Find classes needed for creating Java objects. auto object_class = env->FindClass("java/lang/Object"); auto string_class = env->FindClass("java/lang/String"); auto int_array_class = env->FindClass("[I"); // This is the main output array that will be returned to Java. auto out = env->NewObjectArray(8, object_class, nullptr); // 1. Parse String IDs auto out0 = env->NewObjectArray(static_cast(dex.StringIds().size()), string_class, nullptr); auto strings = dex.StringIds(); for (size_t i = 0; i < strings.size(); ++i) { const auto *ptr = dex.dataPtr(strings[i].string_data_off); // The string data is MUTF-8 encoded. We skip the length prefix. [[maybe_unused]] size_t len = dex::ReadULeb128(&ptr); auto str = env->NewStringUTF(reinterpret_cast(ptr)); env->SetObjectArrayElement(out0, static_cast(i), str); env->DeleteLocalRef(str); } env->SetObjectArrayElement(out, 0, out0); env->DeleteLocalRef(out0); // 2. Parse Type IDs auto types = dex.TypeIds(); auto out1 = env->NewIntArray(static_cast(types.size())); auto *out1_ptr = env->GetIntArrayElements(out1, nullptr); for (size_t i = 0; i < types.size(); ++i) { out1_ptr[i] = static_cast(types[i].descriptor_idx); // Index into String table } env->ReleaseIntArrayElements(out1, out1_ptr, 0); env->SetObjectArrayElement(out, 1, out1); env->DeleteLocalRef(out1); // 3. Parse Proto IDs (Method Prototypes) auto protos = dex.ProtoIds(); auto out2 = env->NewObjectArray(static_cast(protos.size()), int_array_class, nullptr); auto empty_type_list = dex::TypeList{.size = 0, .list = {}}; for (size_t i = 0; i < protos.size(); ++i) { auto &proto = protos[i]; const auto ¶ms = proto.parameters_off ? *dex.dataPtr(proto.parameters_off) : empty_type_list; auto out2i = env->NewIntArray(static_cast(2 + params.size)); auto *out2i_ptr = env->GetIntArrayElements(out2i, nullptr); out2i_ptr[0] = static_cast(proto.shorty_idx); out2i_ptr[1] = static_cast(proto.return_type_idx); for (size_t j = 0; j < params.size; ++j) { out2i_ptr[2 + j] = static_cast(params.list[j].type_idx); } env->ReleaseIntArrayElements(out2i, out2i_ptr, 0); env->SetObjectArrayElement(out2, static_cast(i), out2i); env->DeleteLocalRef(out2i); } env->SetObjectArrayElement(out, 2, out2); env->DeleteLocalRef(out2); // 4. Parse Field IDs auto fields = dex.FieldIds(); auto out3 = env->NewIntArray(static_cast(3 * fields.size())); auto *out3_ptr = env->GetIntArrayElements(out3, nullptr); for (size_t i = 0; i < fields.size(); ++i) { auto &field = fields[i]; out3_ptr[3 * i] = static_cast(field.class_idx); // Defining class type index out3_ptr[3 * i + 1] = static_cast(field.type_idx); // Field type index out3_ptr[3 * i + 2] = static_cast(field.name_idx); // Field name string index } env->ReleaseIntArrayElements(out3, out3_ptr, 0); env->SetObjectArrayElement(out, 3, out3); env->DeleteLocalRef(out3); // 5. Parse Method IDs auto methods = dex.MethodIds(); auto out4 = env->NewIntArray(static_cast(3 * methods.size())); auto *out4_ptr = env->GetIntArrayElements(out4, nullptr); for (size_t i = 0; i < methods.size(); ++i) { out4_ptr[3 * i] = static_cast(methods[i].class_idx); // Defining class type index out4_ptr[3 * i + 1] = static_cast(methods[i].proto_idx); // Method prototype index out4_ptr[3 * i + 2] = static_cast(methods[i].name_idx); // Method name string index } env->ReleaseIntArrayElements(out4, out4_ptr, 0); env->SetObjectArrayElement(out, 4, out4); env->DeleteLocalRef(out4); // 6. Parse Class Definitions and their data auto classes = dex.ClassDefs(); dex.class_data.resize(classes.size()); // These lists will store all annotations found in the DEX file. AnnotationList annotation_list; ArrayList array_list; for (size_t i = 0; i < classes.size(); ++i) { auto &class_def = classes[i]; // Pointers to various parts of the class data. Initialize to safe defaults. dex::u4 static_fields_count = 0; dex::u4 instance_fields_count = 0; dex::u4 direct_methods_count = 0; dex::u4 virtual_methods_count = 0; const dex::u1 *class_data_ptr = nullptr; const dex::AnnotationsDirectoryItem *annotations = nullptr; const dex::AnnotationSetItem *class_annotation = nullptr; dex::u4 field_annotations_count = 0; dex::u4 method_annotations_count = 0; dex::u4 parameter_annotations_count = 0; auto &class_data = dex.class_data[i]; // Parse implemented interfaces. if (class_def.interfaces_off) { auto defined_interfaces = dex.dataPtr(class_def.interfaces_off); class_data.interfaces.resize(defined_interfaces->size); for (size_t k = 0; k < class_data.interfaces.size(); ++k) { class_data.interfaces[k] = defined_interfaces->list[k].type_idx; } } // Locate the annotations directory for this class, if it exists. if (class_def.annotations_off != 0) { annotations = dex.dataPtr(class_def.annotations_off); if (annotations->class_annotations_off != 0) { class_annotation = dex.dataPtr(annotations->class_annotations_off); } field_annotations_count = annotations->fields_size; method_annotations_count = annotations->methods_size; parameter_annotations_count = annotations->parameters_size; } // Read the core class data: fields and methods. if (class_def.class_data_off != 0) { class_data_ptr = dex.dataPtr(class_def.class_data_off); static_fields_count = dex::ReadULeb128(&class_data_ptr); instance_fields_count = dex::ReadULeb128(&class_data_ptr); direct_methods_count = dex::ReadULeb128(&class_data_ptr); virtual_methods_count = dex::ReadULeb128(&class_data_ptr); // Pre-allocate vectors to improve performance. class_data.static_fields.resize(static_fields_count); class_data.static_fields_access_flags.resize(static_fields_count); class_data.instance_fields.resize(instance_fields_count); class_data.instance_fields_access_flags.resize(instance_fields_count); class_data.direct_methods.resize(direct_methods_count); class_data.direct_methods_access_flags.resize(direct_methods_count); class_data.direct_methods_code.resize(direct_methods_count); class_data.virtual_methods.resize(virtual_methods_count); class_data.virtual_methods_access_flags.resize(virtual_methods_count); class_data.virtual_methods_code.resize(virtual_methods_count); } // Now, decode the field and method lists. if (class_data_ptr) { // Static fields for (size_t k = 0, field_idx = 0; k < static_fields_count; ++k) { field_idx += dex::ReadULeb128(&class_data_ptr); // field_idx is a diff from previous class_data.static_fields[k] = static_cast(field_idx); class_data.static_fields_access_flags[k] = static_cast(dex::ReadULeb128(&class_data_ptr)); } // Instance fields for (size_t k = 0, field_idx = 0; k < instance_fields_count; ++k) { field_idx += dex::ReadULeb128(&class_data_ptr); class_data.instance_fields[k] = static_cast(field_idx); class_data.instance_fields_access_flags[k] = static_cast(dex::ReadULeb128(&class_data_ptr)); } // Direct methods (static, private, constructors) for (size_t k = 0, method_idx = 0; k < direct_methods_count; ++k) { method_idx += dex::ReadULeb128(&class_data_ptr); class_data.direct_methods[k] = static_cast(method_idx); class_data.direct_methods_access_flags[k] = static_cast(dex::ReadULeb128(&class_data_ptr)); auto code_off = dex::ReadULeb128(&class_data_ptr); class_data.direct_methods_code[k] = code_off ? dex.dataPtr(code_off) : nullptr; } // Virtual methods for (size_t k = 0, method_idx = 0; k < virtual_methods_count; ++k) { method_idx += dex::ReadULeb128(&class_data_ptr); class_data.virtual_methods[k] = static_cast(method_idx); class_data.virtual_methods_access_flags[k] = static_cast(dex::ReadULeb128(&class_data_ptr)); auto code_off = dex::ReadULeb128(&class_data_ptr); class_data.virtual_methods_code[k] = code_off ? dex.dataPtr(code_off) : nullptr; } } // Optionally skip the expensive annotation parsing. if (!include_annotations) continue; // Parse annotations for the class, its fields, methods, and parameters. ParseAnnotationSet(dex, annotation_list, array_list, class_data.annotations, class_annotation); auto *field_annotations = annotations ? reinterpret_cast(annotations + 1) : nullptr; for (size_t k = 0; k < field_annotations_count; ++k) { auto *field_annotation = dex.dataPtr(field_annotations[k].annotations_off); ParseAnnotationSet( dex, annotation_list, array_list, dex.field_annotations[static_cast(field_annotations[k].field_idx)], field_annotation); } auto *method_annotations = field_annotations ? reinterpret_cast( field_annotations + field_annotations_count) : nullptr; for (size_t k = 0; k < method_annotations_count; ++k) { auto *method_annotation = dex.dataPtr(method_annotations[k].annotations_off); ParseAnnotationSet( dex, annotation_list, array_list, dex.method_annotations[static_cast(method_annotations[k].method_idx)], method_annotation); } auto *parameter_annotations = method_annotations ? reinterpret_cast( method_annotations + method_annotations_count) : nullptr; for (size_t k = 0; k < parameter_annotations_count; ++k) { auto *parameter_annotation = dex.dataPtr(parameter_annotations[k].annotations_off); auto &indices = dex.parameter_annotations[static_cast(parameter_annotations[k].method_idx)]; for (size_t l = 0; l < parameter_annotation->size; ++l) { if (parameter_annotation->list[l].annotations_off != 0) { auto *parameter_annotation_item = dex.dataPtr( parameter_annotation->list[l].annotations_off); ParseAnnotationSet(dex, annotation_list, array_list, indices, parameter_annotation_item); } // A kNoIndex entry serves as a separator between parameter annotation sets. indices.emplace_back(dex::kNoIndex); } } } // If annotations were skipped, we are done. if (!include_annotations) return out; // 7. Convert parsed C++ annotation structures to Java objects. auto out5 = env->NewIntArray(static_cast(2 * annotation_list.size())); auto out6 = env->NewObjectArray(static_cast(2 * annotation_list.size()), object_class, nullptr); auto out5_ptr = env->GetIntArrayElements(out5, nullptr); size_t i = 0; for (auto &[visibility, type, items] : annotation_list) { auto out6i0 = env->NewIntArray(static_cast(2 * items.size())); auto out6i0_ptr = env->GetIntArrayElements(out6i0, nullptr); auto out6i1 = env->NewObjectArray(static_cast(items.size()), object_class, nullptr); size_t j = 0; for (auto &[name, value] : items) { auto &[value_type, value_data] = value; // The raw value data is passed in a direct ByteBuffer. auto java_value = value_data.empty() ? nullptr : env->NewDirectByteBuffer(value_data.data(), value_data.size()); env->SetObjectArrayElement(out6i1, static_cast(j), java_value); out6i0_ptr[2 * j] = name; out6i0_ptr[2 * j + 1] = value_type; env->DeleteLocalRef(java_value); ++j; } env->ReleaseIntArrayElements(out6i0, out6i0_ptr, 0); env->SetObjectArrayElement(out6, static_cast(2 * i), out6i0); env->SetObjectArrayElement(out6, static_cast(2 * i + 1), out6i1); out5_ptr[2 * i] = visibility; out5_ptr[2 * i + 1] = type; env->DeleteLocalRef(out6i0); env->DeleteLocalRef(out6i1); ++i; } env->ReleaseIntArrayElements(out5, out5_ptr, 0); env->SetObjectArrayElement(out, 5, out5); env->SetObjectArrayElement(out, 6, out6); env->DeleteLocalRef(out5); env->DeleteLocalRef(out6); // 8. Convert parsed C++ array values to Java objects. auto out7 = env->NewObjectArray(static_cast(2 * array_list.size()), object_class, nullptr); i = 0; for (auto &array : array_list) { auto out7i0 = env->NewIntArray(static_cast(array.size())); auto out7i0_ptr = env->GetIntArrayElements(out7i0, nullptr); auto out7i1 = env->NewObjectArray(static_cast(array.size()), object_class, nullptr); size_t j = 0; for (auto &value : array) { auto &[value_type, value_data] = value; auto java_value = value_data.empty() ? nullptr : env->NewDirectByteBuffer(value_data.data(), value_data.size()); out7i0_ptr[j] = value_type; env->SetObjectArrayElement(out7i1, static_cast(j), java_value); env->DeleteLocalRef(java_value); ++j; } env->ReleaseIntArrayElements(out7i0, out7i0_ptr, 0); env->SetObjectArrayElement(out7, static_cast(2 * i), out7i0); env->SetObjectArrayElement(out7, static_cast(2 * i + 1), out7i1); env->DeleteLocalRef(out7i0); env->DeleteLocalRef(out7i1); ++i; } env->SetObjectArrayElement(out, 7, out7); env->DeleteLocalRef(out7); return out; } /** * @brief JNI method to release the native DexParser object. * @param cookie The pointer to the DexParser object created by `openDex`. */ VECTOR_DEF_NATIVE_METHOD(void, DexParserBridge, closeDex, jlong cookie) { if (cookie != 0) delete reinterpret_cast(cookie); } /** * @brief Iterates through classes, fields, and methods, calling back to a Java * visitor. * @param cookie The pointer to the DexParser object. * @param visitor The main Java visitor object. * @param ...visitor_class/.._method Java reflection objects used to * get method IDs and perform type checks. */ VECTOR_DEF_NATIVE_METHOD(void, DexParserBridge, visitClass, jlong cookie, jobject visitor, jclass field_visitor_class, jclass method_visitor_class, jobject class_visit_method, jobject field_visit_method, jobject method_visit_method, jobject method_body_visit_method, jobject stop_method) { // Constants for DEX opcodes used in method body parsing. static constexpr dex::u1 kOpcodeMask = 0xff; static constexpr dex::u1 kOpcodeNoOp = 0x00; static constexpr dex::u1 kOpcodeConstString = 0x1a; static constexpr dex::u1 kOpcodeConstStringJumbo = 0x1b; static constexpr dex::u1 kOpcodeIGetStart = 0x52; static constexpr dex::u1 kOpcodeIGetEnd = 0x58; static constexpr dex::u1 kOpcodeSGetStart = 0x60; static constexpr dex::u1 kOpcodeSGetEnd = 0x66; static constexpr dex::u1 kOpcodeIPutStart = 0x59; static constexpr dex::u1 kOpcodeIPutEnd = 0x5f; static constexpr dex::u1 kOpcodeSPutStart = 0x67; static constexpr dex::u1 kOpcodeSPutEnd = 0x6d; static constexpr dex::u1 kOpcodeInvokeStart = 0x6e; static constexpr dex::u1 kOpcodeInvokeEnd = 0x72; static constexpr dex::u1 kOpcodeInvokeRangeStart = 0x74; static constexpr dex::u1 kOpcodeInvokeRangeEnd = 0x78; // Constants for special "payload" opcodes that follow a NOP instruction. static constexpr dex::u2 kInstPackedSwitchPlayLoad = 0x0100; static constexpr dex::u2 kInstSparseSwitchPlayLoad = 0x0200; static constexpr dex::u2 kInstFillArrayDataPlayLoad = 0x0300; if (cookie == 0) { return; } auto &dex = *reinterpret_cast(cookie); // Get jmethodIDs from the reflected java.lang.reflect.Method objects. auto *visit_class = env->FromReflectedMethod(class_visit_method); auto *visit_field = env->FromReflectedMethod(field_visit_method); auto *visit_method = env->FromReflectedMethod(method_visit_method); auto *visit_method_body = env->FromReflectedMethod(method_body_visit_method); auto *stop = env->FromReflectedMethod(stop_method); auto classes = dex.ClassDefs(); for (size_t i = 0; i < classes.size(); ++i) { auto &class_def = classes[i]; auto &class_data = dex.class_data[i]; // --- Prepare arguments for the visit_class callback --- // This involves converting C++ vectors of integers into Java int arrays. auto interfaces = env->NewIntArray(static_cast(class_data.interfaces.size())); env->SetIntArrayRegion(interfaces, 0, static_cast(class_data.interfaces.size()), class_data.interfaces.data()); auto static_fields = env->NewIntArray(static_cast(class_data.static_fields.size())); env->SetIntArrayRegion(static_fields, 0, static_cast(class_data.static_fields.size()), class_data.static_fields.data()); auto static_fields_access_flags = env->NewIntArray(static_cast(class_data.static_fields_access_flags.size())); env->SetIntArrayRegion(static_fields_access_flags, 0, static_cast(class_data.static_fields_access_flags.size()), class_data.static_fields_access_flags.data()); auto instance_fields = env->NewIntArray(static_cast(class_data.instance_fields.size())); env->SetIntArrayRegion(instance_fields, 0, static_cast(class_data.instance_fields.size()), class_data.instance_fields.data()); auto instance_fields_access_flags = env->NewIntArray(static_cast(class_data.instance_fields_access_flags.size())); env->SetIntArrayRegion(instance_fields_access_flags, 0, static_cast(class_data.instance_fields_access_flags.size()), class_data.instance_fields_access_flags.data()); auto direct_methods = env->NewIntArray(static_cast(class_data.direct_methods.size())); env->SetIntArrayRegion(direct_methods, 0, static_cast(class_data.direct_methods.size()), class_data.direct_methods.data()); auto direct_methods_access_flags = env->NewIntArray(static_cast(class_data.direct_methods_access_flags.size())); env->SetIntArrayRegion(direct_methods_access_flags, 0, static_cast(class_data.direct_methods_access_flags.size()), class_data.direct_methods_access_flags.data()); auto virtual_methods = env->NewIntArray(static_cast(class_data.virtual_methods.size())); env->SetIntArrayRegion(virtual_methods, 0, static_cast(class_data.virtual_methods.size()), class_data.virtual_methods.data()); auto virtual_methods_access_flags = env->NewIntArray(static_cast(class_data.virtual_methods_access_flags.size())); env->SetIntArrayRegion(virtual_methods_access_flags, 0, static_cast(class_data.virtual_methods_access_flags.size()), class_data.virtual_methods_access_flags.data()); auto class_annotations = env->NewIntArray(static_cast(class_data.annotations.size())); env->SetIntArrayRegion(class_annotations, 0, static_cast(class_data.annotations.size()), class_data.annotations.data()); // --- Call back to the Java visitor for the class --- jobject member_visitor = env->CallObjectMethod( visitor, visit_class, static_cast(class_def.class_idx), static_cast(class_def.access_flags), static_cast(class_def.superclass_idx), interfaces, static_cast(class_def.source_file_idx), static_fields, static_fields_access_flags, instance_fields, instance_fields_access_flags, direct_methods, direct_methods_access_flags, virtual_methods, virtual_methods_access_flags, class_annotations); // --- Clean up local JNI references --- env->DeleteLocalRef(interfaces); env->DeleteLocalRef(static_fields); env->DeleteLocalRef(static_fields_access_flags); env->DeleteLocalRef(instance_fields); env->DeleteLocalRef(instance_fields_access_flags); env->DeleteLocalRef(direct_methods); env->DeleteLocalRef(direct_methods_access_flags); env->DeleteLocalRef(virtual_methods); env->DeleteLocalRef(virtual_methods_access_flags); env->DeleteLocalRef(class_annotations); // --- Visit fields --- if (member_visitor && env->IsInstanceOf(member_visitor, field_visitor_class)) { jboolean stopped = JNI_FALSE; // This structured binding provides a clean way to iterate over both // static and instance field collections. for (auto &[fields, fields_access_flags] : {std::tie(class_data.static_fields, class_data.static_fields_access_flags), std::tie(class_data.instance_fields, class_data.instance_fields_access_flags)}) { for (size_t j = 0; j < fields.size(); j++) { auto field_idx = fields[j]; auto access_flags = fields_access_flags[j]; auto &field_annotations = dex.field_annotations[field_idx]; auto annotations = env->NewIntArray(static_cast(field_annotations.size())); env->SetIntArrayRegion(annotations, 0, static_cast(field_annotations.size()), field_annotations.data()); // Call back to Java for this field. env->CallVoidMethod(member_visitor, visit_field, field_idx, access_flags, annotations); env->DeleteLocalRef(annotations); // Check if the visitor wants to stop iteration. stopped = env->CallBooleanMethod(member_visitor, stop); if (stopped == JNI_TRUE) break; } if (stopped == JNI_TRUE) break; } } // --- Visit methods --- if (member_visitor && env->IsInstanceOf(member_visitor, method_visitor_class)) { jboolean stopped = JNI_FALSE; // Iterate over both direct and virtual methods. for (auto &[methods, methods_access_flags, methods_code] : {std::tie(class_data.direct_methods, class_data.direct_methods_access_flags, class_data.direct_methods_code), std::tie(class_data.virtual_methods, class_data.virtual_methods_access_flags, class_data.virtual_methods_code)}) { for (size_t j = 0; j < methods.size(); j++) { auto method_idx = methods[j]; auto access_flags = methods_access_flags[j]; auto code = methods_code[j]; auto method_annotation = dex.method_annotations[method_idx]; auto method_annotations = env->NewIntArray(static_cast(method_annotation.size())); env->SetIntArrayRegion(method_annotations, 0, static_cast(method_annotation.size()), method_annotation.data()); auto parameter_annotation = dex.parameter_annotations[method_idx]; auto parameter_annotations = env->NewIntArray(static_cast(parameter_annotation.size())); env->SetIntArrayRegion(parameter_annotations, 0, static_cast(parameter_annotation.size()), parameter_annotation.data()); // Call back to Java for this method. // This may return a "body visitor". auto body_visitor = env->CallObjectMethod( member_visitor, visit_method, method_idx, access_flags, code != nullptr, method_annotations, parameter_annotations); env->DeleteLocalRef(method_annotations); env->DeleteLocalRef(parameter_annotations); // --- Lazily parse the method body if requested --- if (body_visitor && code != nullptr) { auto &body = dex.method_bodies[method_idx]; if (!body.loaded) { // Using hash sets for efficient collection of unique indices. phmap::flat_hash_set referred_strings; phmap::flat_hash_set assigned_fields; phmap::flat_hash_set accessed_fields; phmap::flat_hash_set invoked_methods; const dex::u2 *inst = code->insns; const dex::u2 *end = inst + code->insns_size; // Iterate through the bytecode instructions. while (inst < end) { dex::u1 opcode = *inst & kOpcodeMask; body.opcodes.push_back(static_cast(opcode)); // Check for opcodes of interest. if (opcode == kOpcodeConstString) { auto str_idx = inst[1]; referred_strings.emplace(str_idx); } if (opcode == kOpcodeConstStringJumbo) { auto str_idx = *reinterpret_cast(&inst[1]); referred_strings.emplace(static_cast(str_idx)); } if ((opcode >= kOpcodeIGetStart && opcode <= kOpcodeIGetEnd) || (opcode >= kOpcodeSGetStart && opcode <= kOpcodeSGetEnd)) { auto field_idx = inst[1]; accessed_fields.emplace(field_idx); } if ((opcode >= kOpcodeIPutStart && opcode <= kOpcodeIPutEnd) || (opcode >= kOpcodeSPutStart && opcode <= kOpcodeSPutEnd)) { auto field_idx = inst[1]; assigned_fields.emplace(field_idx); } if ((opcode >= kOpcodeInvokeStart && opcode <= kOpcodeInvokeEnd) || (opcode >= kOpcodeInvokeRangeStart && opcode <= kOpcodeInvokeRangeEnd)) { auto callee = inst[1]; invoked_methods.emplace(callee); } // Handle special payload instructions which have variable // length. if (opcode == kOpcodeNoOp) { if (*inst == kInstPackedSwitchPlayLoad) { inst += inst[1] * 2 + 3; } else if (*inst == kInstSparseSwitchPlayLoad) { inst += inst[1] * 4 + 1; } else if (*inst == kInstFillArrayDataPlayLoad) { inst += (*reinterpret_cast(&inst[2]) * inst[1] + 1) / 2 + 3; } } // Advance instruction pointer by the known length of // the current opcode. inst += dex::opcode_len[opcode]; } // Copy the collected unique indices into the body's vectors. body.referred_strings.assign(referred_strings.begin(), referred_strings.end()); body.assigned_fields.assign(assigned_fields.begin(), assigned_fields.end()); body.accessed_fields.assign(accessed_fields.begin(), accessed_fields.end()); body.invoked_methods.assign(invoked_methods.begin(), invoked_methods.end()); body.loaded = true; } // --- Prepare arguments and call back for the method body --- auto referred_strings = env->NewIntArray(static_cast(body.referred_strings.size())); env->SetIntArrayRegion(referred_strings, 0, static_cast(body.referred_strings.size()), body.referred_strings.data()); auto accessed_fields = env->NewIntArray(static_cast(body.accessed_fields.size())); env->SetIntArrayRegion(accessed_fields, 0, static_cast(body.accessed_fields.size()), body.accessed_fields.data()); auto assigned_fields = env->NewIntArray(static_cast(body.assigned_fields.size())); env->SetIntArrayRegion(assigned_fields, 0, static_cast(body.assigned_fields.size()), body.assigned_fields.data()); auto invoked_methods = env->NewIntArray(static_cast(body.invoked_methods.size())); env->SetIntArrayRegion(invoked_methods, 0, static_cast(body.invoked_methods.size()), body.invoked_methods.data()); auto opcodes = env->NewByteArray(static_cast(body.opcodes.size())); env->SetByteArrayRegion(opcodes, 0, static_cast(body.opcodes.size()), body.opcodes.data()); env->CallVoidMethod(body_visitor, visit_method_body, referred_strings, invoked_methods, accessed_fields, assigned_fields, opcodes); } stopped = env->CallBooleanMethod(member_visitor, stop); if (stopped == JNI_TRUE) break; } if (stopped == JNI_TRUE) break; } } // Check if the top-level visitor wants to stop. if (env->CallBooleanMethod(visitor, stop) == JNI_TRUE) break; } } // Array of native method descriptors for JNI registration. static JNINativeMethod gMethods[] = { VECTOR_NATIVE_METHOD(DexParserBridge, openDex, "(Ljava/nio/ByteBuffer;[J)Ljava/lang/Object;"), VECTOR_NATIVE_METHOD(DexParserBridge, closeDex, "(J)V"), VECTOR_NATIVE_METHOD(DexParserBridge, visitClass, "(JLjava/lang/Object;Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/" "reflect/Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/" "Method;Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"), }; /** * @brief Registers the native methods with the JVM. */ void RegisterDexParserBridge(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(DexParserBridge); } } // namespace vector::native::jni ================================================ FILE: native/src/jni/hook_bridge.cpp ================================================ #include #include #include #include #include #include "jni/jni_bridge.h" #include "jni/jni_hooks.h" namespace { /** * @struct ModuleCallback * @brief Stores the jmethodIDs for the "modern" callback API. * This API separates the logic that runs before and after the original method. */ struct ModuleCallback { jmethodID before_method; jmethodID after_method; }; /** * @struct HookItem * @brief Holds all state associated with a single hooked method. * * This includes lists of all registered callback functions * (both modern and legacy), sorted by priority. * * It also manages a thread-safe "backup" object, * which is a handle to the original, un-hooked method. */ struct HookItem { // Callbacks are stored in multimaps, keyed by priority. // std::greater<> ensures that higher priority numbers are processed first. std::multimap> legacy_callbacks; std::multimap> modern_callbacks; private: // The backup is an atomic jobject. // This is crucial for thread safety during the initial hooking process. // It can be in one of three states: // - nullptr: The hook has not been initialized yet. // - FAILED: The hook attempt failed. // - A valid jobject: A handle to the original method. std::atomic backup{nullptr}; static_assert(decltype(backup)::is_always_lock_free); // A sentinel value to indicate that the hooking process failed. inline static jobject FAILED = reinterpret_cast(std::numeric_limits::max()); public: /** * @brief Atomically and safely retrieves the backup method handle. * If another thread is currently setting up the hook, this method will wait until * the process is complete, to prevent race conditions. */ jobject GetBackup() { // Wait until the 'backup' atomic is no longer nullptr. backup.wait(nullptr, std::memory_order_acquire); if (auto bk = backup.load(std::memory_order_relaxed); bk != FAILED) { return bk; } else { return nullptr; } } /** * @brief Atomically sets the backup method handle once after hooking. * This method uses compare_exchange_strong to ensure it only sets the value once. * After setting, it notifies any waiting threads. */ void SetBackup(jobject newBackup) { jobject null = nullptr; // Attempt to transition from nullptr to the new backup (or FAILED). // memory_order_acq_rel ensures memory synchronization // with both waiting threads (acquire) and subsequent reads (release). backup.compare_exchange_strong(null, newBackup ? newBackup : FAILED, std::memory_order_acq_rel, std::memory_order_relaxed); // Wake up all threads that were waiting in GetBackup(). backup.notify_all(); } }; // A type alias for a thread-safe parallel hash map. // This map is the central registry, mapping a method's ID to its HookItem. // It uses a std::shared_mutex to allow concurrent reads but exclusive writes. template , class Eq = phmap::priv::hash_default_eq, class Alloc = phmap::priv::Allocator>, size_t N = 4> using SharedHashMap = phmap::parallel_flat_hash_map; // The global map of all hooked methods. SharedHashMap> hooked_methods; // Cached JNI method and field IDs for performance. // Looking these up frequently is slow, so they are cached on first use. jmethodID invoke = nullptr; jmethodID callback_ctor = nullptr; jfieldID before_method_field = nullptr; jfieldID after_method_field = nullptr; } // namespace namespace vector::native::jni { /** * @brief JNI method to install a hook on a given method or constructor. * @param useModernApi Distinguishes between the legacy and modern callback * types. * @param hookMethod The java.lang.reflect.Executable to be hooked. * @param hooker The Java class that acts as the hook trampoline. * @param priority The priority of this callback. * @param callback The Java callback object. * @return JNI_TRUE on success, JNI_FALSE on failure. */ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, hookMethod, jboolean useModernApi, jobject hookMethod, jclass hooker, jint priority, jobject callback) { bool newHook = false; #ifndef NDEBUG // Simple RAII struct for performance timing in debug builds. struct finally { std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now(); bool &newHook; ~finally() { auto finish = std::chrono::steady_clock::now(); if (newHook) { LOGV("New hook took {}us", std::chrono::duration_cast(finish - start).count()); } } } finally{.newHook = newHook}; #endif auto target = env->FromReflectedMethod(hookMethod); HookItem *hook_item = nullptr; // Atomically find or create an entry for the target method. // This is a highly concurrent operation. hooked_methods.lazy_emplace_l( target, // Lambda for existing element: just get the pointer. [&hook_item](auto &it) { hook_item = it.second.get(); }, // Lambda for new element: create the HookItem and mark it as a new hook. [&hook_item, &target, &newHook](const auto &ctor) { auto ptr = std::make_unique(); hook_item = ptr.get(); ctor(target, std::move(ptr)); newHook = true; }); // If this is the first time this method is being hooked, // we need to perform the actual native hook using lsplant. if (newHook) { auto init = env->GetMethodID(hooker, "", "(Ljava/lang/reflect/Executable;)V"); auto callback_method = env->ToReflectedMethod( hooker, env->GetMethodID(hooker, "callback", "([Ljava/lang/Object;)Ljava/lang/Object;"), false); auto hooker_object = env->NewObject(hooker, init, hookMethod); // Use lsplant to replace the target method with our trampoline. // The returned jobject is a handle to the original method. hook_item->SetBackup(lsplant::Hook(env, hookMethod, hooker_object, callback_method)); env->DeleteLocalRef(hooker_object); } // Wait for the backup to become available (it might be set by another thread). jobject backup = hook_item->GetBackup(); if (!backup) return JNI_FALSE; // Use an RAII monitor to lock the backup object, // ensuring thread-safe modification of the callback lists. lsplant::JNIMonitor monitor(env, backup); if (useModernApi) { // Lazy initialization of JNI IDs for the modern API. if (before_method_field == nullptr) { auto callback_class = env->GetObjectClass(callback); callback_ctor = env->GetMethodID(callback_class, "", "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V"); before_method_field = env->GetFieldID(callback_class, "beforeInvocation", "Ljava/lang/reflect/Method;"); after_method_field = env->GetFieldID(callback_class, "afterInvocation", "Ljava/lang/reflect/Method;"); } // Extract the before/after methods from the Java callback object. auto before_method = env->GetObjectField(callback, before_method_field); auto after_method = env->GetObjectField(callback, after_method_field); auto callback_type = ModuleCallback{ .before_method = env->FromReflectedMethod(before_method), .after_method = env->FromReflectedMethod(after_method), }; hook_item->modern_callbacks.emplace(priority, callback_type); } else { // For the legacy API, store a global reference to the callback object itself. hook_item->legacy_callbacks.emplace(priority, env->NewGlobalRef(callback)); } return JNI_TRUE; } /** * @brief JNI method to remove a previously installed hook callback. */ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, unhookMethod, jboolean useModernApi, jobject hookMethod, jobject callback) { auto target = env->FromReflectedMethod(hookMethod); HookItem *hook_item = nullptr; // Find the HookItem for the target method. hooked_methods.if_contains(target, [&hook_item](const auto &it) { hook_item = it.second.get(); }); if (!hook_item) return JNI_FALSE; jobject backup = hook_item->GetBackup(); if (!backup) return JNI_FALSE; // Lock to safely modify the callback list. lsplant::JNIMonitor monitor(env, backup); if (useModernApi) { auto before_method = env->GetObjectField(callback, before_method_field); auto before = env->FromReflectedMethod(before_method); // Find the callback by comparing the before_method's ID. for (auto i = hook_item->modern_callbacks.begin(); i != hook_item->modern_callbacks.end(); ++i) { if (before == i->second.before_method) { hook_item->modern_callbacks.erase(i); return JNI_TRUE; } } } else { // Find the callback by comparing the jobject directly. for (auto i = hook_item->legacy_callbacks.begin(); i != hook_item->legacy_callbacks.end(); ++i) { if (env->IsSameObject(i->second, callback)) { env->DeleteGlobalRef(i->second); // Clean up the global reference. hook_item->legacy_callbacks.erase(i); return JNI_TRUE; } } } return JNI_FALSE; } /** * @brief JNI method to request de-optimization of a method. * This can be necessary for some types of hooks to work correctly on JIT-compiled methods. */ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, deoptimizeMethod, jobject hookMethod) { return lsplant::Deoptimize(env, hookMethod); } /** * @brief JNI method to invoke the original, un-hooked method. */ VECTOR_DEF_NATIVE_METHOD(jobject, HookBridge, invokeOriginalMethod, jobject hookMethod, jobject thiz, jobjectArray args) { auto target = env->FromReflectedMethod(hookMethod); HookItem *hook_item = nullptr; hooked_methods.if_contains(target, [&hook_item](const auto &it) { hook_item = it.second.get(); }); // If a hook item exists, invoke its backup. Otherwise, invoke the method directly // (though this case should be rare if called from a hook callback). jobject method_to_invoke = hook_item ? hook_item->GetBackup() : hookMethod; if (!method_to_invoke) { // Hooking might have failed or is not complete. return nullptr; } return env->CallObjectMethod(method_to_invoke, invoke, thiz, args); } /** * @brief JNI wrapper around AllocObject. */ VECTOR_DEF_NATIVE_METHOD(jobject, HookBridge, allocateObject, jclass cls) { return env->AllocObject(cls); } /** * Core JNI backend for non-virtual method invocation and special object initialization. * * Implementation details: * 1. Dispatches using JNI CallNonvirtualMethodA. * 2. Employs stack allocation (alloca) for JNI argument mapping. * 3. Safely mirrors standard Java reflection (NPEs on null primitives/receivers). * 4. Prevents JNI Type Confusion and memory leaks by caching primitive wrappers globally, * while leveraging java.lang.Number for fast implicit widening/narrowing. * 5. Accurately catches and wraps target method exceptions into InvocationTargetException. */ VECTOR_DEF_NATIVE_METHOD(jobject, HookBridge, invokeSpecialMethod, jobject method, jcharArray shorty, jclass cls, jobject thiz, jobjectArray args) { // --- JNI Global Reference Caching --- // Cached once per process lifecycle to maintain extreme performance and prevent JNI aborts. static jclass cls_Number = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Number")); static jclass cls_Boolean = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Boolean")); static jclass cls_Character = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Character")); // Globally cache primitive wrapper classes for safe return value boxing static jclass cls_Integer = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Integer")); static jclass cls_Double = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Double")); static jclass cls_Long = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Long")); static jclass cls_Float = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Float")); static jclass cls_Short = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Short")); static jclass cls_Byte = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Byte")); static jclass cls_ITE = (jclass)env->NewGlobalRef(env->FindClass("java/lang/reflect/InvocationTargetException")); static auto *const ctor_ite = env->GetMethodID(cls_ITE, "", "(Ljava/lang/Throwable;)V"); static auto *const get_int = env->GetMethodID(cls_Number, "intValue", "()I"); static auto *const get_double = env->GetMethodID(cls_Number, "doubleValue", "()D"); static auto *const get_long = env->GetMethodID(cls_Number, "longValue", "()J"); static auto *const get_float = env->GetMethodID(cls_Number, "floatValue", "()F"); static auto *const get_short = env->GetMethodID(cls_Number, "shortValue", "()S"); static auto *const get_byte = env->GetMethodID(cls_Number, "byteValue", "()B"); static auto *const get_char = env->GetMethodID(cls_Character, "charValue", "()C"); static auto *const get_boolean = env->GetMethodID(cls_Boolean, "booleanValue", "()Z"); static auto *const set_int = env->GetStaticMethodID(cls_Integer, "valueOf", "(I)Ljava/lang/Integer;"); static auto *const set_double = env->GetStaticMethodID(cls_Double, "valueOf", "(D)Ljava/lang/Double;"); static auto *const set_long = env->GetStaticMethodID(cls_Long, "valueOf", "(J)Ljava/lang/Long;"); static auto *const set_float = env->GetStaticMethodID(cls_Float, "valueOf", "(F)Ljava/lang/Float;"); static auto *const set_short = env->GetStaticMethodID(cls_Short, "valueOf", "(S)Ljava/lang/Short;"); static auto *const set_byte = env->GetStaticMethodID(cls_Byte, "valueOf", "(B)Ljava/lang/Byte;"); static auto *const set_char = env->GetStaticMethodID(cls_Character, "valueOf", "(C)Ljava/lang/Character;"); static auto *const set_boolean = env->GetStaticMethodID(cls_Boolean, "valueOf", "(Z)Ljava/lang/Boolean;"); auto target = env->FromReflectedMethod(method); auto param_len = env->GetArrayLength(shorty) - 1; // --- Argument & Receiver Validation --- auto args_len = args != nullptr ? env->GetArrayLength(args) : 0; if (args_len != param_len) { env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "args.length does not match parameter count"); return nullptr; } if (thiz == nullptr) { env->ThrowNew(env->FindClass("java/lang/NullPointerException"), "null receiver"); return nullptr; } // Allocate jvalue array on the stack jvalue *a = param_len > 0 ? static_cast(alloca(param_len * sizeof(jvalue))) : nullptr; auto *const shorty_char = env->GetCharArrayElements(shorty, nullptr); if (shorty_char == nullptr) { return nullptr; // JVM already threw OutOfMemoryError } // RAII/Helper for clean JNI array exits auto abort_and_return = [&]() { env->ReleaseCharArrayElements(shorty, shorty_char, JNI_ABORT); return nullptr; }; // --- Safe Unboxing --- for (jint i = 0; i != param_len; ++i) { jobject element = env->GetObjectArrayElement(args, i); if (env->ExceptionCheck()) return abort_and_return(); char type = shorty_char[i + 1]; if (element == nullptr) { if (type != 'L' && type != '[') { env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "null primitive argument"); return abort_and_return(); } a[i].l = nullptr; } else { if (type == 'Z') { if (!env->IsInstanceOf(element, cls_Boolean)) { env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "Expected Boolean"); return abort_and_return(); } a[i].z = env->CallBooleanMethod(element, get_boolean); } else if (type == 'C') { if (!env->IsInstanceOf(element, cls_Character)) { env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "Expected Character"); return abort_and_return(); } a[i].c = env->CallCharMethod(element, get_char); } else if (type != 'L' && type != '[') { bool is_number = env->IsInstanceOf(element, cls_Number) == JNI_TRUE; bool is_character = !is_number && (env->IsInstanceOf(element, cls_Character) == JNI_TRUE); if (!is_number && !is_character) { env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), "Expected Number or Character"); return abort_and_return(); } // If a Character is passed to a numeric parameter, extract its value for widening jchar c_val = 0; if (is_character) { c_val = env->CallCharMethod(element, get_char); if (env->ExceptionCheck()) return abort_and_return(); } switch (type) { case 'I': a[i].i = env->CallIntMethod(element, get_int); break; case 'D': a[i].d = env->CallDoubleMethod(element, get_double); break; case 'J': a[i].j = env->CallLongMethod(element, get_long); break; case 'F': a[i].f = env->CallFloatMethod(element, get_float); break; case 'S': a[i].s = env->CallShortMethod(element, get_short); break; case 'B': a[i].b = env->CallByteMethod(element, get_byte); break; } } else { a[i].l = element; element = nullptr; // Transferred ownership to jvalue array; will be freed on return } } if (element) env->DeleteLocalRef(element); if (env->ExceptionCheck()) return abort_and_return(); } // --- Non-virtual Invocation --- jvalue ret_val; switch (shorty_char[0]) { case 'I': ret_val.i = env->CallNonvirtualIntMethodA(thiz, cls, target, a); break; case 'D': ret_val.d = env->CallNonvirtualDoubleMethodA(thiz, cls, target, a); break; case 'J': ret_val.j = env->CallNonvirtualLongMethodA(thiz, cls, target, a); break; case 'F': ret_val.f = env->CallNonvirtualFloatMethodA(thiz, cls, target, a); break; case 'S': ret_val.s = env->CallNonvirtualShortMethodA(thiz, cls, target, a); break; case 'B': ret_val.b = env->CallNonvirtualByteMethodA(thiz, cls, target, a); break; case 'C': ret_val.c = env->CallNonvirtualCharMethodA(thiz, cls, target, a); break; case 'Z': ret_val.z = env->CallNonvirtualBooleanMethodA(thiz, cls, target, a); break; case 'L': ret_val.l = env->CallNonvirtualObjectMethodA(thiz, cls, target, a); break; default: env->CallNonvirtualVoidMethodA(thiz, cls, target, a); break; } // --- Exception Wrapping --- jthrowable target_exception = env->ExceptionOccurred(); if (target_exception) { env->ExceptionClear(); jobject ite = env->NewObject(cls_ITE, ctor_ite, target_exception); // Ensure NewObject didn't fail due to OOM before throwing if (ite) { env->Throw(static_cast(ite)); } return abort_and_return(); } // --- Box Return Value --- jobject value = nullptr; switch (shorty_char[0]) { case 'I': value = env->CallStaticObjectMethod(cls_Integer, set_int, ret_val.i); break; case 'D': value = env->CallStaticObjectMethod(cls_Double, set_double, ret_val.d); break; case 'J': value = env->CallStaticObjectMethod(cls_Long, set_long, ret_val.j); break; case 'F': value = env->CallStaticObjectMethod(cls_Float, set_float, ret_val.f); break; case 'S': value = env->CallStaticObjectMethod(cls_Short, set_short, ret_val.s); break; case 'B': value = env->CallStaticObjectMethod(cls_Byte, set_byte, ret_val.b); break; case 'C': value = env->CallStaticObjectMethod(cls_Character, set_char, ret_val.c); break; case 'Z': value = env->CallStaticObjectMethod(cls_Boolean, set_boolean, ret_val.z); break; case 'L': value = ret_val.l; break; case 'V': value = nullptr; break; } env->ReleaseCharArrayElements(shorty, shorty_char, JNI_ABORT); return value; } /** * @brief JNI wrapper around IsInstanceOf. */ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, instanceOf, jobject object, jclass expected_class) { return env->IsInstanceOf(object, expected_class); } /** * @brief JNI wrapper to mark a DEX file loaded from memory as trusted. */ VECTOR_DEF_NATIVE_METHOD(jboolean, HookBridge, setTrusted, jobject cookie) { return lsplant::MakeDexFileTrusted(env, cookie); } /** * @brief Creates a snapshot of all registered callbacks for a given method. * This is useful for debugging and introspection from the Java side. * @return An Object[2][] array where index 0 contains modern callbacks and * index 1 contains legacy callbacks. */ VECTOR_DEF_NATIVE_METHOD(jobjectArray, HookBridge, callbackSnapshot, jclass callback_class, jobject method) { auto target = env->FromReflectedMethod(method); HookItem *hook_item = nullptr; hooked_methods.if_contains(target, [&hook_item](const auto &it) { hook_item = it.second.get(); }); if (!hook_item) return nullptr; jobject backup = hook_item->GetBackup(); if (!backup) return nullptr; // Lock to ensure a consistent snapshot of the callback lists. lsplant::JNIMonitor monitor(env, backup); auto res = env->NewObjectArray(2, env->FindClass("[Ljava/lang/Object;"), nullptr); auto modern = env->NewObjectArray((jsize)hook_item->modern_callbacks.size(), env->FindClass("java/lang/Object"), nullptr); auto legacy = env->NewObjectArray((jsize)hook_item->legacy_callbacks.size(), env->FindClass("java/lang/Object"), nullptr); jsize i = 0; for (const auto &callback_pair : hook_item->modern_callbacks) { // The clazz argument refers to the Java class where the native method is // declared, provided by the macro VECTOR_DEF_NATIVE_METHOD. auto before_method = env->ToReflectedMethod(clazz, callback_pair.second.before_method, JNI_FALSE); auto after_method = env->ToReflectedMethod(clazz, callback_pair.second.after_method, JNI_FALSE); // Re-create the Java callback object from the stored method IDs. auto callback_object = env->NewObject(callback_class, callback_ctor, before_method, after_method); env->SetObjectArrayElement(modern, i++, callback_object); // Clean up local references created during object construction. env->DeleteLocalRef(before_method); env->DeleteLocalRef(after_method); env->DeleteLocalRef(callback_object); } i = 0; for (const auto &callback_pair : hook_item->legacy_callbacks) { // The legacy list already stores a global ref to the callback object. env->SetObjectArrayElement(legacy, i++, callback_pair.second); } env->SetObjectArrayElement(res, 0, modern); env->SetObjectArrayElement(res, 1, legacy); env->DeleteLocalRef(modern); env->DeleteLocalRef(legacy); return res; } /** * @brief Retrieves the static initializer () of a class as a Method object. * @param target_class The class to inspect. * @return A Method object for the static initializer, or null if it doesn't exist. */ VECTOR_DEF_NATIVE_METHOD(jobject, HookBridge, getStaticInitializer, jclass target_class) { // is the internal name for a static initializer. // Its signature is always ()V (no arguments, void return). jmethodID mid = env->GetStaticMethodID(target_class, "", "()V"); if (!mid) { // If GetStaticMethodID fails, it throws an exception. // We clear it and return null to let the Java side handle it gracefully. env->ExceptionClear(); return nullptr; } // Convert the method ID to a java.lang.reflect.Method object. // The last parameter must be JNI_TRUE because it's a static method. return env->ToReflectedMethod(target_class, mid, JNI_TRUE); } // Array of native method descriptors for JNI registration. static JNINativeMethod gMethods[] = { VECTOR_NATIVE_METHOD(HookBridge, hookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Class;ILjava/" "lang/Object;)Z"), VECTOR_NATIVE_METHOD(HookBridge, unhookMethod, "(ZLjava/lang/reflect/Executable;Ljava/lang/Object;)Z"), VECTOR_NATIVE_METHOD(HookBridge, deoptimizeMethod, "(Ljava/lang/reflect/Executable;)Z"), VECTOR_NATIVE_METHOD(HookBridge, invokeOriginalMethod, "(Ljava/lang/reflect/Executable;Ljava/lang/Object;[Ljava/" "lang/Object;)Ljava/lang/Object;"), VECTOR_NATIVE_METHOD(HookBridge, invokeSpecialMethod, "(Ljava/lang/reflect/Executable;[CLjava/lang/Class;Ljava/" "lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"), VECTOR_NATIVE_METHOD(HookBridge, allocateObject, "(Ljava/lang/Class;)Ljava/lang/Object;"), VECTOR_NATIVE_METHOD(HookBridge, instanceOf, "(Ljava/lang/Object;Ljava/lang/Class;)Z"), VECTOR_NATIVE_METHOD(HookBridge, setTrusted, "(Ljava/lang/Object;)Z"), VECTOR_NATIVE_METHOD(HookBridge, callbackSnapshot, "(Ljava/lang/Class;Ljava/lang/reflect/" "Executable;)[[Ljava/lang/Object;"), VECTOR_NATIVE_METHOD(HookBridge, getStaticInitializer, "(Ljava/lang/Class;)Ljava/lang/reflect/Method;"), }; /** * @brief Registers all native methods with the JVM when the library is loaded. */ void RegisterHookBridge(JNIEnv *env) { // Cache the Method.invoke methodID for use in invokeOriginalMethod. jclass method = env->FindClass("java/lang/reflect/Method"); invoke = env->GetMethodID(method, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"); env->DeleteLocalRef(method); REGISTER_VECTOR_NATIVE_METHODS(HookBridge); } } // namespace vector::native::jni ================================================ FILE: native/src/jni/native_api_bridge.cpp ================================================ #include "core/native_api.h" #include "jni/jni_bridge.h" #include "jni/jni_hooks.h" namespace vector::native::jni { VECTOR_DEF_NATIVE_METHOD(void, NativeAPI, recordNativeEntrypoint, jstring jstr) { lsplant::JUTFString str(env, jstr); vector::native::RegisterNativeLib(str); } static JNINativeMethod gMethods[] = { VECTOR_NATIVE_METHOD(NativeAPI, recordNativeEntrypoint, "(Ljava/lang/String;)V")}; void RegisterNativeApiBridge(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(NativeAPI); } } // namespace vector::native::jni ================================================ FILE: native/src/jni/resources_hook.cpp ================================================ #include #include #include #include #include "common/config.h" #include "elf/elf_image.h" #include "elf/symbol_cache.h" #include "framework/android_types.h" #include "jni/jni_bridge.h" #include "jni/jni_hooks.h" namespace vector::native::jni { // --- Type Aliases for Native Android Framework Functions --- // Signature for android::ResXMLParser::getAttributeNameID(int) using TYPE_GET_ATTR_NAME_ID = int32_t (*)(void *, int); // Signature for android::ResStringPool::stringAt(int, size_t*) using TYPE_STRING_AT = char16_t *(*)(const void *, int32_t, size_t *); // Signature for android::ResXMLParser::restart() using TYPE_RESTART = void (*)(void *); // Signature for android::ResXMLParser::next() using TYPE_NEXT = int32_t (*)(void *); // --- JNI Globals & Cached IDs --- static jclass classXResources; static jmethodID methodXResourcesTranslateAttrId; static jmethodID methodXResourcesTranslateResId; // --- Native Function Pointers --- // To store the memory addresses of the private Android framework functions. static TYPE_NEXT ResXMLParser_next = nullptr; static TYPE_RESTART ResXMLParser_restart = nullptr; static TYPE_GET_ATTR_NAME_ID ResXMLParser_getAttributeNameID = nullptr; /** * @brief Constructs the class name for the XResources class at runtime. */ static std::string GetXResourcesClassName() { // Use a static local variable to ensure this lookup and string manipulation // only happens once. static std::string name = []() { auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); if (obfs_map.empty()) { LOGW("GetXResourcesClassName: obfuscation_map is empty."); } // The key is the original, unobfuscated class name prefix. // The value is the new, obfuscated prefix. auto it = obfs_map.find("android.content.res.XRes"); if (it == obfs_map.end()) { LOGE("Could not find obfuscated name for XResources."); return std::string(); } std::string jni_name = it->second + "ources"; LOGD("Resolved XResources class name to: {}", jni_name.c_str()); return jni_name; }(); return name; } /** * @brief Finds and caches the addresses of private functions in libframework.so. * * It uses the ElfImage utility to parse the Android framework's shared library in memory, * find functions by their C++ mangled names, and * store their addresses in our global function pointers. * * @return True if all required symbols were found, false otherwise. */ static bool PrepareSymbols() { ElfImage fw(kFrameworkLibraryName); if (!fw.IsValid()) { LOGE("Failed to open Android framework library."); return false; }; // The mangled names are specific to the compiler and architecture. // This is a very fragile part of the hook. // Find android::ResXMLParser::next() if (!(ResXMLParser_next = fw.getSymbAddress("_ZN7android12ResXMLParser4nextEv"))) { LOGE("Failed to find symbol: ResXMLParser::next"); return false; } // Find android::ResXMLParser::restart() if (!(ResXMLParser_restart = fw.getSymbAddress("_ZN7android12ResXMLParser7restartEv"))) { LOGE("Failed to find symbol: ResXMLParser::restart"); return false; }; // Find android::ResXMLParser::getAttributeNameID(unsigned int/long) if (!(ResXMLParser_getAttributeNameID = fw.getSymbAddress( LP_SELECT("_ZNK7android12ResXMLParser18getAttributeNameIDEj", "_ZNK7android12ResXMLParser18getAttributeNameIDEm")))) { LOGE("Failed to find symbol: ResXMLParser::getAttributeNameID"); return false; } // Initialize another part of the resource framework that we depend on. return android::ResStringPool::setup(lsplant::InitInfo{ .art_symbol_resolver = [&](auto s) { return fw.template getSymbAddress<>(s); }}); } /** * @brief JNI entry point to initialize the entire native resources hook. */ VECTOR_DEF_NATIVE_METHOD(jboolean, ResourcesHook, initXResourcesNative) { const auto x_resources_class_name = GetXResourcesClassName(); if (x_resources_class_name.empty()) { return JNI_FALSE; } if (auto classXResources_ = Context::GetInstance()->FindClassFromCurrentLoader(env, x_resources_class_name)) { classXResources = JNI_NewGlobalRef(env, classXResources_); } else { LOGE("Error while loading XResources class '{}'", x_resources_class_name.c_str()); return JNI_FALSE; } // Dynamically build the method signature using the (possibly obfuscated) class name. std::string x_resources_jni_name = "L" + x_resources_class_name + ";"; std::replace(x_resources_jni_name.begin(), x_resources_jni_name.end(), '.', '/'); methodXResourcesTranslateResId = env->GetStaticMethodID( classXResources, "translateResId", fmt::format("(I{}Landroid/content/res/Resources;)I", x_resources_jni_name).c_str()); if (!methodXResourcesTranslateResId) { LOGE("Failed to find method: XResources.translateResId"); return JNI_FALSE; } methodXResourcesTranslateAttrId = env->GetStaticMethodID( classXResources, "translateAttrId", fmt::format("(Ljava/lang/String;{})I", x_resources_jni_name).c_str()); if (!methodXResourcesTranslateAttrId) { LOGE("Failed to find method: XResources.translateAttrId"); return JNI_FALSE; } if (!PrepareSymbols()) { LOGE("Failed to prepare native symbols for resource hooking."); return JNI_FALSE; } return JNI_TRUE; } /** * @brief Removes the 'final' modifier from a Java class at runtime. * This allows the framework to create subclasses of what are normally final classes. */ VECTOR_DEF_NATIVE_METHOD(jboolean, ResourcesHook, makeInheritable, jclass target_class) { if (lsplant::MakeClassInheritable(env, target_class)) { return JNI_TRUE; } return JNI_FALSE; } /** * @brief Builds a new ClassLoader in memory containing dynamically generated classes. * * This function creates a DEX file on-the-fly. * The DEX file contains dummy classes that inherit from key Android resource classes. * This allows the framework to inject its own logic by later creating classes that * inherit from these dummies. * * @return A new dalvik.system.InMemoryDexClassLoader instance. */ VECTOR_DEF_NATIVE_METHOD(jobject, ResourcesHook, buildDummyClassLoader, jobject parent, jstring resource_super_class, jstring typed_array_super_class) { using namespace startop::dex; // Cache the class and constructor for InMemoryDexClassLoader. static auto in_memory_classloader = (jclass)env->NewGlobalRef(env->FindClass("dalvik/system/InMemoryDexClassLoader")); static jmethodID initMid = env->GetMethodID(in_memory_classloader, "", "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); DexBuilder dex_file; // Create a class named "xposed.dummy.XResourcesSuperClass". ClassBuilder xresource_builder{dex_file.MakeClass("xposed/dummy/XResourcesSuperClass")}; // Set its superclass to the one specified by the Java caller. xresource_builder.setSuperClass( TypeDescriptor::FromClassname(lsplant::JUTFString(env, resource_super_class).get())); // Create a class named "xposed.dummy.XTypedArraySuperClass". ClassBuilder xtypearray_builder{dex_file.MakeClass("xposed/dummy/XTypedArraySuperClass")}; // Set its superclass. xtypearray_builder.setSuperClass( TypeDescriptor::FromClassname(lsplant::JUTFString(env, typed_array_super_class).get())); // Finalize the DEX file into a memory buffer. slicer::MemView image{dex_file.CreateImage()}; // Wrap the memory buffer in a Java ByteBuffer. auto dex_buffer = env->NewDirectByteBuffer(const_cast(image.ptr()), image.size()); // Create and return a new InMemoryDexClassLoader instance. return env->NewObject(in_memory_classloader, initMid, dex_buffer, parent); } /** * @brief The core resource rewriting function. * * This method iterates through a binary XML file as it's being parsed by the Android framework. * For each attribute and value, it calls back to Java to see * if the resource ID should be replaced with a different one. * * @param parserPtr A raw pointer to the native android::ResXMLParser object. * @param origRes The original XResources object. * @param repRes The replacement Resources object. */ VECTOR_DEF_NATIVE_METHOD(void, ResourcesHook, rewriteXmlReferencesNative, jlong parserPtr, jobject origRes, jobject repRes) { // Cast the long from Java back to a native C++ pointer. // This is dangerous and assumes the Java code provides a valid pointer. auto parser = (android::ResXMLParser *)parserPtr; if (parser == nullptr) return; const android::ResXMLTree &mTree = parser->mTree; auto mResIds = (uint32_t *)mTree.mResIds; android::ResXMLTree_attrExt *tag; int attrCount; // This loop iterates through all tokens in the binary XML file. do { // Call the native android::ResXMLParser::next() function via our pointer. switch (ResXMLParser_next(parser)) { case android::ResXMLParser::START_TAG: tag = (android::ResXMLTree_attrExt *)parser->mCurExt; attrCount = tag->attributeCount; // Loop through all attributes of the current XML tag. for (int idx = 0; idx < attrCount; idx++) { auto attr = (android::ResXMLTree_attribute *)(((const uint8_t *)tag) + tag->attributeStart + tag->attributeSize * idx); // Translate the attribute name's resource ID --- // e.g., for 'android:textColor', translate the ID for 'textColor'. int32_t attrNameID = ResXMLParser_getAttributeNameID(parser, idx); // Only replace IDs that belong to the app's package (0x7f...). if (attrNameID >= 0 && (size_t)attrNameID < mTree.mNumResIds && mResIds[attrNameID] >= 0x7f000000) { auto attrName = mTree.mStrings.stringAt(attrNameID); jstring attrNameStr = env->NewString((const jchar *)attrName.data_, attrName.length_); if (env->ExceptionCheck()) goto leave; // Critical check // Call back to Java: XResources.translateAttrId(String name, ...) jint attrResID = env->CallStaticIntMethod( classXResources, methodXResourcesTranslateAttrId, attrNameStr, origRes); env->DeleteLocalRef(attrNameStr); if (env->ExceptionCheck()) goto leave; // Directly modify the resource ID table in the parser's memory. mResIds[attrNameID] = attrResID; } // Translate the attribute's value if it's a reference --- // e.g., for 'android:textColor="@color/my_text"', translate the ID for // '@color/my_text'. if (attr->typedValue.dataType != android::Res_value::TYPE_REFERENCE) continue; jint oldValue = attr->typedValue.data; if (oldValue < 0x7f000000) continue; // Call back to Java: XResources.translateResId(int id, ...) jint newValue = env->CallStaticIntMethod( classXResources, methodXResourcesTranslateResId, oldValue, origRes, repRes); if (env->ExceptionCheck()) goto leave; // If the ID was changed, update the value directly in the parser's // memory. if (newValue != oldValue) attr->typedValue.data = newValue; } continue; case android::ResXMLParser::END_DOCUMENT: case android::ResXMLParser::BAD_DOCUMENT: goto leave; // Exit the loop. default: continue; // Process next XML token. } } while (true); // A single exit point for the function. leave: // Reset the parser to its initial state so it can be read again. ResXMLParser_restart(parser); } // JNI method registration table. static JNINativeMethod gMethods[] = { VECTOR_NATIVE_METHOD(ResourcesHook, initXResourcesNative, "()Z"), VECTOR_NATIVE_METHOD(ResourcesHook, makeInheritable, "(Ljava/lang/Class;)Z"), VECTOR_NATIVE_METHOD(ResourcesHook, buildDummyClassLoader, "(Ljava/lang/ClassLoader;Ljava/lang/String;Ljava/lang/" "String;)Ljava/lang/ClassLoader;"), VECTOR_NATIVE_METHOD(ResourcesHook, rewriteXmlReferencesNative, "(JLxposed/dummy/XResourcesSuperClass;Landroid/content/res/Resources;)V")}; void RegisterResourcesHook(JNIEnv *env) { REGISTER_VECTOR_NATIVE_METHODS(ResourcesHook); } } // namespace vector::native::jni ================================================ FILE: services/daemon-service/.gitignore ================================================ /build ================================================ FILE: services/daemon-service/build.gradle.kts ================================================ plugins { alias(libs.plugins.agp.lib) } android { buildFeatures { aidl = true } buildTypes { release { isMinifyEnabled = false } } sourceSets { named("main") { java.srcDirs("src/main/java", "../libxposed/service/src/main") aidl.srcDirs("src/main/aidl", "../libxposed/interface/src/main/aidl") } } aidlPackagedList += "org/lsposed/lspd/models/Module.aidl" namespace = "org.lsposed.lspd.daemonservice" } dependencies { compileOnly(libs.androidx.annotation) compileOnly(projects.hiddenapi.stubs) } ================================================ FILE: services/daemon-service/src/main/AndroidManifest.xml ================================================ ================================================ FILE: services/daemon-service/src/main/aidl/org/lsposed/lspd/models/Module.aidl ================================================ package org.lsposed.lspd.models; import org.lsposed.lspd.models.PreLoadedApk; import org.lsposed.lspd.service.ILSPInjectedModuleService; parcelable Module { String packageName; int appId; String apkPath; PreLoadedApk file; ApplicationInfo applicationInfo; ILSPInjectedModuleService service; } ================================================ FILE: services/daemon-service/src/main/aidl/org/lsposed/lspd/models/PreLoadedApk.aidl ================================================ package org.lsposed.lspd.models; parcelable PreLoadedApk { List preLoadedDexes; List moduleClassNames; List moduleLibraryNames; boolean legacy; } ================================================ FILE: services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPApplicationService.aidl ================================================ package org.lsposed.lspd.service; import org.lsposed.lspd.models.Module; interface ILSPApplicationService { boolean isLogMuted(); List getLegacyModulesList(); List getModulesList(); String getPrefsPath(String packageName); ParcelFileDescriptor requestInjectedManagerBinder(out List binder); } ================================================ FILE: services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPInjectedModuleService.aidl ================================================ package org.lsposed.lspd.service; import org.lsposed.lspd.service.IRemotePreferenceCallback; interface ILSPInjectedModuleService { int getFrameworkPrivilege(); Bundle requestRemotePreferences(String group, IRemotePreferenceCallback callback); ParcelFileDescriptor openRemoteFile(String path); String[] getRemoteFileList(); } ================================================ FILE: services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPSystemServerService.aidl ================================================ package org.lsposed.lspd.service; import org.lsposed.lspd.service.ILSPApplicationService; interface ILSPSystemServerService { ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat); } ================================================ FILE: services/daemon-service/src/main/aidl/org/lsposed/lspd/service/ILSPosedService.aidl ================================================ package org.lsposed.lspd.service; import org.lsposed.lspd.service.ILSPApplicationService; interface ILSPosedService { ILSPApplicationService requestApplicationService(int uid, int pid, String processName, IBinder heartBeat); oneway void dispatchSystemServerContext(in IBinder activityThread, in IBinder activityToken, String api); boolean preStartManager(); boolean setManagerEnabled(boolean enabled); } ================================================ FILE: services/daemon-service/src/main/aidl/org/lsposed/lspd/service/IRemotePreferenceCallback.aidl ================================================ package org.lsposed.lspd.service; interface IRemotePreferenceCallback { oneway void onUpdate(in Bundle map); } ================================================ FILE: services/daemon-service/src/main/java/org/lsposed/lspd/util/Utils.java ================================================ /* * 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 */ package org.lsposed.lspd.util; import android.os.SystemProperties; import android.text.TextUtils; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.zone.ZoneRulesException; public class Utils { public static final String LOG_TAG = "LSPosed"; public static final boolean isMIUI = !TextUtils.isEmpty(SystemProperties.get("ro.miui.ui.version.name")); public static final boolean isLENOVO = !TextUtils.isEmpty(SystemProperties.get("ro.lenovo.region")); public class Log { public static boolean muted = false; public static String getStackTraceString(Throwable tr) { return android.util.Log.getStackTraceString(tr); } public static void d(String tag, String msg) { if (muted) return; android.util.Log.d(tag, msg); } public static void d(String tag, String msg, Throwable tr) { android.util.Log.d(tag, msg, tr); } public static void v(String tag, String msg) { if (muted) return; android.util.Log.v(tag, msg); } public static void v(String tag, String msg, Throwable tr) { android.util.Log.v(tag, msg, tr); } public static void i(String tag, String msg) { if (muted) return; android.util.Log.i(tag, msg); } public static void i(String tag, String msg, Throwable tr) { android.util.Log.i(tag, msg, tr); } public static void w(String tag, String msg) { if (muted) return; android.util.Log.w(tag, msg); } public static void w(String tag, String msg, Throwable tr) { if (muted) return; android.util.Log.w(tag, msg, tr); } public static void e(String tag, String msg) { android.util.Log.e(tag, msg); } public static void e(String tag, String msg, Throwable tr) { android.util.Log.e(tag, msg, tr); } } public static void logD(Object msg) { Log.d(LOG_TAG, msg.toString()); } public static void logD(String msg, Throwable throwable) { Log.d(LOG_TAG, msg, throwable); } public static void logW(String msg) { Log.w(LOG_TAG, msg); } public static void logW(String msg, Throwable throwable) { Log.w(LOG_TAG, msg, throwable); } public static void logI(String msg) { Log.i(LOG_TAG, msg); } public static void logI(String msg, Throwable throwable) { Log.i(LOG_TAG, msg, throwable); } public static void logE(String msg) { Log.e(LOG_TAG, msg); } public static void logE(String msg, Throwable throwable) { Log.e(LOG_TAG, msg, throwable); } public static ZoneId getZoneId() { var timezone = SystemProperties.get("persist.sys.timezone", "GMT"); try { return ZoneId.of(timezone); } catch (ZoneRulesException e) { return ZoneOffset.UTC; } } } ================================================ FILE: services/manager-service/.gitignore ================================================ /build ================================================ FILE: services/manager-service/build.gradle.kts ================================================ plugins { alias(libs.plugins.agp.lib) } android { buildFeatures { aidl = true } buildTypes { release { isMinifyEnabled = false } } namespace = "org.lsposed.lspd.managerservice" } dependencies { api(libs.rikkax.parcelablelist) } ================================================ FILE: services/manager-service/src/main/AndroidManifest.xml ================================================ ================================================ FILE: services/manager-service/src/main/aidl/org/lsposed/lspd/ILSPManagerService.aidl ================================================ package org.lsposed.lspd; import rikka.parcelablelist.ParcelableListSlice; import org.lsposed.lspd.models.UserInfo; import org.lsposed.lspd.models.Application; interface ILSPManagerService { const int DEX2OAT_OK = 0; const int DEX2OAT_CRASHED = 1; const int DEX2OAT_MOUNT_FAILED = 2; 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; boolean enableModule(String packageName) = 4; boolean disableModule(String packageName) = 5; boolean setModuleScope(String packageName, in List scope) = 6; List getModuleScope(String packageName) = 7; boolean isVerboseLog() = 11; void setVerboseLog(boolean enabled) = 12; ParcelFileDescriptor getVerboseLog() = 16; ParcelFileDescriptor getModulesLog() = 17; int getXposedVersionCode() = 18; String getXposedVersionName() = 19; int getXposedApiVersion() = 20; boolean clearLogs(boolean verbose) = 21; PackageInfo getPackageInfo(String packageName, int flags, int uid) = 22; void forceStopPackage(String packageName, int userId) = 23; void reboot() = 24; boolean uninstallPackage(String packageName, int userId) = 25; boolean isSepolicyLoaded() = 26; List getUsers() = 27; int installExistingPackageAsUser(String packageName, int userId) = 28; boolean systemServerRequested() = 29; int startActivityAsUserWithFeature(in Intent intent, int userId) = 30; ParcelableListSlice queryIntentActivitiesAsUser(in Intent intent, int flags, int userId) = 31; boolean dex2oatFlagsLoaded() = 32; void setHiddenIcon(boolean hide) = 33; void getLogs(in ParcelFileDescriptor zipFd) = 34; void restartFor(in Intent intent) = 35; oneway void flashZip(String zipPath, in ParcelFileDescriptor outputStream) = 39; boolean performDexOptMode(String packageName) = 40; List getDenyListPackages() = 41; boolean getDexObfuscate() = 42; void setDexObfuscate(boolean enable) = 43; int getDex2OatWrapperCompatibility() = 44; void clearApplicationProfileData(in String packageName) = 45; boolean enableStatusNotification() = 47; 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; } ================================================ FILE: services/manager-service/src/main/aidl/org/lsposed/lspd/models/Application.aidl ================================================ package org.lsposed.lspd.models; parcelable Application { String packageName; int userId; } ================================================ FILE: services/manager-service/src/main/aidl/org/lsposed/lspd/models/UserInfo.aidl ================================================ package org.lsposed.lspd.models; parcelable UserInfo { int id; String name; } ================================================ FILE: settings.gradle.kts ================================================ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS repositories { google() mavenCentral() } } rootProject.name = "Vector" include( ":app", ":core", ":daemon", ":dex2oat", ":external:axml", ":external:apache", ":hiddenapi:stubs", ":hiddenapi:bridge", ":magisk-loader", ":services:manager-service", ":services:daemon-service", ":xposed", ":zygisk", ) ================================================ FILE: xposed/README.md ================================================ # Xposed API implementation of the Vector framework LSPosed is being refactored into a new project `Vector`. This sub-project `xposed`, written in Kotlin, will be refactored from the `core` sub-project written in `Java`. ================================================ FILE: xposed/build.gradle.kts ================================================ plugins { alias(libs.plugins.agp.lib) alias(libs.plugins.kotlin) alias(libs.plugins.ktfmt) } ktfmt { kotlinLangStyle() } android { namespace = "org.matrix.vector.xposed" androidResources { enable = false } sourceSets { named("main") { java.srcDirs("src/main/kotlin", "libxposed/api/src/main/java") } } } dependencies { compileOnly(libs.androidx.annotation) compileOnly(projects.hiddenapi.stubs) } ================================================ FILE: xposed/src/main/kotlin/org/matrix/vector/impl/utils/VectorDexParser.kt ================================================ package org.matrix.vector.impl.utils import io.github.libxposed.api.utils.DexParser import io.github.libxposed.api.utils.DexParser.* import java.io.IOException import java.nio.ByteBuffer import org.matrix.vector.nativebridge.DexParserBridge /** * Kotlin implementation of [DexParser] for Vector. * * This class acts as a high-level wrapper around the native C++ DexParser. It maps raw JNI data * structures (integer arrays, flat buffers) into usable object graphs (StringId, TypeId, MethodId, * etc.). */ @Suppress("UNCHECKED_CAST") class VectorDexParser(buffer: ByteBuffer, includeAnnotations: Boolean) : DexParser { private var cookie: Long = 0 private val data: ByteBuffer // Internal storage for parsed DEX structures. // We use private properties and explicit getter methods as requested. private val internalStrings: Array private val internalTypeIds: Array private val internalProtoIds: Array private val internalFieldIds: Array private val internalMethodIds: Array private val internalAnnotations: Array private val internalArrays: Array init { // Ensure the buffer is Direct and accessible by native code data = if (!buffer.isDirect || !buffer.asReadOnlyBuffer().hasArray()) { ByteBuffer.allocateDirect(buffer.capacity()).apply { put(buffer) // Ensure position is reset for reading if needed, // though native uses address flip() } } else { buffer } try { val args = LongArray(2) args[1] = if (includeAnnotations) 1 else 0 // Call Native Bridge // Returns a raw Object[] containing headers and pools val out = DexParserBridge.openDex(data, args) as Array cookie = args[0] // --- Parse Strings (Index 0) --- val rawStrings = out[0] as Array internalStrings = Array(rawStrings.size) { i -> VectorStringId(i, rawStrings[i]) } // --- Parse Type IDs (Index 1) --- val rawTypeIds = out[1] as IntArray internalTypeIds = Array(rawTypeIds.size) { i -> VectorTypeId(i, rawTypeIds[i]) } // --- Parse Proto IDs (Index 2) --- val rawProtoIds = out[2] as Array internalProtoIds = Array(rawProtoIds.size) { i -> VectorProtoId(i, rawProtoIds[i]) } // --- Parse Field IDs (Index 3) --- val rawFieldIds = out[3] as IntArray // Each field is represented by 3 integers (class_idx, type_idx, name_idx) internalFieldIds = Array(rawFieldIds.size / 3) { i -> VectorFieldId( i, rawFieldIds[3 * i], rawFieldIds[3 * i + 1], rawFieldIds[3 * i + 2], ) } // --- Parse Method IDs (Index 4) --- val rawMethodIds = out[4] as IntArray // Each method is represented by 3 integers (class_idx, proto_idx, name_idx) internalMethodIds = Array(rawMethodIds.size / 3) { i -> VectorMethodId( i, rawMethodIds[3 * i], rawMethodIds[3 * i + 1], rawMethodIds[3 * i + 2], ) } // --- Parse Annotations (Index 5 & 6) --- val rawAnnotationMetadata = out[5] as? IntArray val rawAnnotationValues = out[6] as? Array internalAnnotations = if (rawAnnotationMetadata != null && rawAnnotationValues != null) { Array(rawAnnotationMetadata.size / 2) { i -> // Metadata: [visibility, type_idx] // Values: [name_indices[], values[]] val elementsMeta = rawAnnotationValues[2 * i] as IntArray val elementsData = rawAnnotationValues[2 * i + 1] as Array VectorAnnotation( rawAnnotationMetadata[2 * i], rawAnnotationMetadata[2 * i + 1], elementsMeta, elementsData, ) } } else { emptyArray() } // --- Parse Arrays (Index 7) --- val rawArrays = out[7] as? Array internalArrays = if (rawArrays != null) { Array(rawArrays.size / 2) { i -> val types = rawArrays[2 * i] as IntArray val values = rawArrays[2 * i + 1] as Array VectorArray(types, values) } } else { emptyArray() } } catch (e: Throwable) { throw IOException("Invalid dex file", e) } } @Synchronized override fun close() { if (cookie != 0L) { DexParserBridge.closeDex(cookie) cookie = 0 } } override fun getStringId(): Array = internalStrings override fun getTypeId(): Array = internalTypeIds override fun getFieldId(): Array = internalFieldIds override fun getMethodId(): Array = internalMethodIds override fun getProtoId(): Array = internalProtoIds override fun getAnnotations(): Array = internalAnnotations override fun getArrays(): Array = internalArrays override fun visitDefinedClasses(visitor: ClassVisitor) { if (cookie == 0L) { throw IllegalStateException("Closed") } // Accessing [0] is fragile val classVisitMethod = ClassVisitor::class.java.declaredMethods[0] val fieldVisitMethod = FieldVisitor::class.java.declaredMethods[0] val methodVisitMethod = MethodVisitor::class.java.declaredMethods[0] val methodBodyVisitMethod = MethodBodyVisitor::class.java.declaredMethods[0] val stopMethod = EarlyStopVisitor::class.java.declaredMethods[0] DexParserBridge.visitClass( cookie, visitor, FieldVisitor::class.java, MethodVisitor::class.java, classVisitMethod, fieldVisitMethod, methodVisitMethod, methodBodyVisitMethod, stopMethod, ) } /** Base implementation for all Dex IDs. */ private open class VectorId>(private val id: Int) : Id { override fun getId(): Int = id override fun compareTo(other: Self): Int = id - other.id } private inner class VectorStringId(id: Int, private val string: String) : VectorId(id), StringId { override fun getString(): String = string } private inner class VectorTypeId(id: Int, descriptorIdx: Int) : VectorId(id), TypeId { private val descriptor: StringId = internalStrings[descriptorIdx] override fun getDescriptor(): StringId = descriptor } private inner class VectorProtoId(id: Int, protoData: IntArray) : VectorId(id), ProtoId { private val shorty: StringId = internalStrings[protoData[0]] private val returnType: TypeId = internalTypeIds[protoData[1]] private val parameters: Array? init { if (protoData.size > 2) { // protoData format: [shorty_idx, return_type_idx, param1_idx, param2_idx...] parameters = Array(protoData.size - 2) { i -> internalTypeIds[protoData[i + 2]] } } else { parameters = null } } override fun getShorty(): StringId = shorty override fun getReturnType(): TypeId = returnType override fun getParameters(): Array? = parameters } private inner class VectorFieldId(id: Int, classIdx: Int, typeIdx: Int, nameIdx: Int) : VectorId(id), FieldId { private val declaringClass: TypeId = internalTypeIds[classIdx] private val type: TypeId = internalTypeIds[typeIdx] private val name: StringId = internalStrings[nameIdx] override fun getType(): TypeId = type override fun getDeclaringClass(): TypeId = declaringClass override fun getName(): StringId = name } private inner class VectorMethodId(id: Int, classIdx: Int, protoIdx: Int, nameIdx: Int) : VectorId(id), MethodId { private val declaringClass: TypeId = internalTypeIds[classIdx] private val prototype: ProtoId = internalProtoIds[protoIdx] private val name: StringId = internalStrings[nameIdx] override fun getDeclaringClass(): TypeId = declaringClass override fun getPrototype(): ProtoId = prototype override fun getName(): StringId = name } private class VectorArray(elementsTypes: IntArray, valuesData: Array) : DexParser.Array { private val values: Array init { values = Array(valuesData.size) { i -> VectorValue(elementsTypes[i], valuesData[i] as? ByteBuffer) } } override fun getValues(): Array = values } private inner class VectorAnnotation( private val visibility: Int, typeIdx: Int, elementNameIndices: IntArray, elementValues: Array, ) : DexParser.Annotation { private val type: TypeId = internalTypeIds[typeIdx] private val elements: Array init { elements = Array(elementValues.size) { i -> // Flattened structure from JNI: names are at 2*i, types at 2*i+1 VectorElement( elementNameIndices[i * 2], elementNameIndices[i * 2 + 1], // valueType elementValues[i] as? ByteBuffer, ) } } override fun getVisibility(): Int = visibility override fun getType(): TypeId = type override fun getElements(): Array = elements } private open class VectorValue(private val valueType: Int, buffer: ByteBuffer?) : Value { private val value: ByteArray? init { if (buffer != null) { value = ByteArray(buffer.remaining()) buffer.get(value) } else { value = null } } override fun getValue(): ByteArray? = value override fun getValueType(): Int = valueType } private inner class VectorElement(nameIdx: Int, valueType: Int, value: ByteBuffer?) : VectorValue(valueType, value), Element { private val name: StringId = internalStrings[nameIdx] override fun getName(): StringId = name } } ================================================ FILE: xposed/src/main/kotlin/org/matrix/vector/nativebridge/DexParserBridge.kt ================================================ package org.matrix.vector.nativebridge import dalvik.annotation.optimization.FastNative import io.github.libxposed.api.utils.DexParser import java.io.IOException import java.lang.reflect.Method import java.nio.ByteBuffer object DexParserBridge { @JvmStatic @FastNative @Throws(IOException::class) external fun openDex(data: ByteBuffer, args: LongArray): Any @JvmStatic @FastNative external fun closeDex(cookie: Long) @JvmStatic @FastNative external fun visitClass( cookie: Long, visitor: Any, fieldVisitorClass: Class, methodVisitorClass: Class, classVisitMethod: Method, fieldVisitMethod: Method, methodVisitMethod: Method, methodBodyVisitMethod: Method, stopMethod: Method, ) } ================================================ FILE: xposed/src/main/kotlin/org/matrix/vector/nativebridge/HookBridge.kt ================================================ package org.matrix.vector.nativebridge import dalvik.annotation.optimization.FastNative import java.lang.reflect.Executable import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method object HookBridge { @JvmStatic external fun hookMethod( useModernApi: Boolean, hookMethod: Executable, hooker: Class<*>, priority: Int, callback: Any?, ): Boolean @JvmStatic external fun unhookMethod( useModernApi: Boolean, hookMethod: Executable, callback: Any?, ): Boolean @JvmStatic external fun deoptimizeMethod(method: Executable): Boolean @JvmStatic @Throws(InstantiationException::class) external fun allocateObject(clazz: Class): T @JvmStatic @Throws( IllegalAccessException::class, IllegalArgumentException::class, InvocationTargetException::class, ) external fun invokeOriginalMethod(method: Executable, thisObject: Any?, vararg args: Any?): Any? @JvmStatic @Throws( IllegalAccessException::class, IllegalArgumentException::class, InvocationTargetException::class, ) external fun invokeSpecialMethod( method: Executable, shorty: CharArray, clazz: Class, thisObject: Any?, vararg args: Any?, ): Any? @JvmStatic @FastNative external fun instanceOf(obj: Any?, clazz: Class<*>): Boolean @JvmStatic @FastNative external fun setTrusted(cookie: Any?): Boolean @JvmStatic external fun callbackSnapshot(hooker_callback: Class<*>, method: Executable): Array> @JvmStatic external fun getStaticInitializer(clazz: Class<*>): Method } ================================================ FILE: xposed/src/main/kotlin/org/matrix/vector/nativebridge/NativeAPI.kt ================================================ package org.matrix.vector.nativebridge object NativeAPI { @JvmStatic external fun recordNativeEntrypoint(library_name: String) } ================================================ FILE: xposed/src/main/kotlin/org/matrix/vector/nativebridge/ResourcesHook.kt ================================================ package org.matrix.vector.nativebridge import android.content.res.Resources import dalvik.annotation.optimization.FastNative import xposed.dummy.XResourcesSuperClass object ResourcesHook { @JvmStatic external fun initXResourcesNative(): Boolean @JvmStatic external fun makeInheritable(clazz: Class<*>): Boolean @JvmStatic external fun buildDummyClassLoader( parent: ClassLoader, resourceSuperClass: String, typedArraySuperClass: String, ): ClassLoader @JvmStatic @FastNative external fun rewriteXmlReferencesNative( parserPtr: Long, origRes: XResourcesSuperClass, repRes: Resources, ) } ================================================ FILE: zygisk/.gitignore ================================================ /build /release /.cxx ================================================ FILE: zygisk/README.md ================================================ # Vector Zygisk Module & 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. 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. 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. --- ## Part 1: The Native Zygisk Layer 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. ### 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. ### 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. --- ## Part 2: The Kotlin Framework Loader 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. ### 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. ### 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. --- ## Injection & Execution Flow 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. ================================================ FILE: zygisk/build.gradle.kts ================================================ import java.security.MessageDigest import org.apache.commons.codec.binary.Hex import org.apache.tools.ant.filters.ReplaceTokens plugins { alias(libs.plugins.agp.app) alias(libs.plugins.kotlin) alias(libs.plugins.ktfmt) } ktfmt { kotlinLangStyle() } val versionCodeProvider: Provider by rootProject.extra val versionNameProvider: Provider by rootProject.extra val injectedPackageName: String by rootProject.extra val injectedPackageUid: Int by rootProject.extra val defaultManagerPackageName: String by rootProject.extra android { namespace = "org.matrix.vector" defaultConfig { multiDexEnabled = false buildConfigField("String", "InjectedPackageName", """"${injectedPackageName}"""") buildConfigField("String", "ManagerPackageName", """"${defaultManagerPackageName}"""") val flags = listOf( "-DINJECTED_PACKAGE_NAME='\"${injectedPackageName}\"'", "-DINJECTED_PACKAGE_UID=${injectedPackageUid}", "-DMANAGER_PACKAGE_NAME='\"${defaultManagerPackageName}\"'", ) externalNativeBuild { cmake { cFlags.addAll(flags) cppFlags.addAll(flags) } } } buildTypes { release { isMinifyEnabled = true proguardFiles("proguard-rules.pro") } } externalNativeBuild { cmake { path("src/main/cpp/CMakeLists.txt") } } } abstract class Injected @Inject constructor(val moduleDir: String) { @get:Inject abstract val factory: ObjectFactory } dependencies { implementation(projects.core) implementation(projects.hiddenapi.bridge) implementation(projects.services.managerService) implementation(projects.services.daemonService) compileOnly(libs.androidx.annotation) compileOnly(projects.hiddenapi.stubs) } val zipAll = tasks.register("zipAll") { group = "Vector" } androidComponents { onVariants(selector().all()) { variant -> val variantCapped = variant.name.replaceFirstChar { it.uppercase() } val variantLowered = variant.name.lowercase() // --- Define output locations and file names --- // Stage all files in a temporary directory inside 'build' before zipping val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}") val zipFileName = "Vector-v${versionNameProvider.get()}-${versionCodeProvider.get()}-$variantCapped.zip" // Using Sync ensures that stale files from previous runs are removed. val prepareModuleFilesTask = tasks.register("prepareModuleFiles$variantCapped") { group = "Vector Module Packaging" dependsOn( "assemble$variantCapped", ":app:package$variantCapped", ":daemon:package$variantCapped", ":dex2oat:externalNativeBuild$variantCapped", ) into(tempModuleDir) from("${rootProject.projectDir}/README.md") from("$projectDir/module") { exclude("module.prop", "action.sh", "daemon") } from("$projectDir/module") { include("module.prop") expand( "versionName" to "v${versionNameProvider.get()}", "versionCode" to versionCodeProvider.get(), ) } from("$projectDir/module") { include("action.sh") val tokens = mapOf( "MANAGER_PACKAGE_NAME" to defaultManagerPackageName, "INJECTED_PACKAGE_NAME" to injectedPackageName, ) filter("tokens" to tokens) } from("$projectDir/module") { include("daemon") val tokens = mapOf("DEBUG" to if (variantLowered == "debug") "true" else "false") filter("tokens" to tokens) } from(project(":app").tasks.getByName("package$variantCapped").outputs) { include("*.apk") rename(".*\\.apk", "manager.apk") } from(project(":daemon").tasks.getByName("package$variantCapped").outputs) { include("*.apk") rename(".*\\.apk", "daemon.apk") } into("lib") { val libDir = variantLowered + "/strip${variantCapped}DebugSymbols" from( layout.buildDirectory.dir( "intermediates/stripped_native_libs/$libDir/out/lib" ) ) { include("**/libzygisk.so") } } into("bin") { from( project(":dex2oat") .layout .buildDirectory .dir("intermediates/cmake/$variantLowered/obj") ) { include("**/dex2oat") include("**/liboat_hook.so") } } val dexOutPath = if (variantLowered == "release") layout.buildDirectory.dir( "intermediates/dex/$variantLowered/minify${variantCapped}WithR8" ) else layout.buildDirectory.dir( "intermediates/dex/$variantLowered/mergeDex$variantCapped" ) into("framework") { from(dexOutPath) rename("classes.dex", "lspd.dex") } val injected = objects.newInstance(tempModuleDir.get().asFile.path) doLast { injected.factory.fileTree().from(injected.moduleDir).visit { if (isDirectory) return@visit val md = MessageDigest.getInstance("SHA-256") file.forEachBlock(4096) { bytes, size -> md.update(bytes, 0, size) } File(file.path + ".sha256").writeText(Hex.encodeHexString(md.digest())) } } } val zipTask = tasks.register("zip${variantCapped}") { group = "Vector Module Packaging" dependsOn(prepareModuleFilesTask) archiveFileName = zipFileName destinationDirectory = file("$projectDir/release") from(tempModuleDir) } zipAll.configure { dependsOn(zipTask) } // A helper function to create installation tasks for different root providers. fun createInstallTasks(rootProvider: String, installCli: String) { val pushTask = tasks.register("push${rootProvider}Module${variantCapped}") { group = "Zygisk Module Installation" description = "Pushes the ${variant.name} build to the device for $rootProvider." dependsOn(zipTask) commandLine( "adb", "push", zipTask.get().archiveFile.get().asFile, "/data/local/tmp", ) } val installTask = tasks.register("install${rootProvider}${variantCapped}") { group = "Zygisk Module Installation" description = "Installs the ${variant.name} build via $rootProvider." dependsOn(pushTask) commandLine( "adb", "shell", "su", "-c", "$installCli /data/local/tmp/$zipFileName", ) } tasks.register("install${rootProvider}AndReboot${variantCapped}") { group = "Zygisk Module Installation" description = "Installs the ${variant.name} build via $rootProvider and reboots." dependsOn(installTask) commandLine("adb", "reboot") } } createInstallTasks("Magisk", "magisk --install-module") createInstallTasks("Ksu", "ksud module install") createInstallTasks("Apatch", "/data/adb/apd module install") } } evaluationDependsOn(":app") evaluationDependsOn(":daemon") ================================================ FILE: zygisk/module/META-INF/com/google/android/update-binary ================================================ ################# # Initialization ################# umask 022 # echo before loading util_functions ui_print() { echo "$1"; } require_new_magisk() { ui_print "*******************************" ui_print " Please install Magisk v20.4+! " ui_print "*******************************" exit 1 } ######################### # Load util_functions.sh ######################### OUTFD=$2 ZIPFILE=$3 [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk . /data/adb/magisk/util_functions.sh [ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk install_module exit 0 ================================================ FILE: zygisk/module/META-INF/com/google/android/updater-script ================================================ #MAGISK ================================================ FILE: zygisk/module/action.sh ================================================ MANAGER_PACKAGE_NAME="@MANAGER_PACKAGE_NAME@" INJECTED_PACKAGE_NAME="@INJECTED_PACKAGE_NAME@" am start -c "${MANAGER_PACKAGE_NAME}.LAUNCH_MANAGER" "${INJECTED_PACKAGE_NAME}/.BugreportWarningActivity" ================================================ FILE: zygisk/module/customize.sh ================================================ SKIPUNZIP=1 # ========================================================= # Utils functions to extract and verify installation package TMPDIR_FOR_VERIFY="$TMPDIR/.vunzip" mkdir "$TMPDIR_FOR_VERIFY" abort_verify() { ui_print "*********************************************************" ui_print "! $1" ui_print "! This zip may be corrupted, please try downloading again" abort "*********************************************************" } # Usage: extract [junk_paths: true|false] extract() { local zip="$1" local file="$2" local dir="$3" local junk_paths="${4:-false}" # Defaults to false if not provided local opts="-o" local file_path hash_path file_basename file_basename=$(basename "$file") if [ "$junk_paths" = "true" ]; then opts="-oj" file_path="$dir/$file_basename" hash_path="${TMPDIR_FOR_VERIFY}/$file_basename.sha256" else file_path="$dir/$file" hash_path="${TMPDIR_FOR_VERIFY}/$file.sha256" fi # Extract the file and its hash unzip $opts "$zip" "$file" -d "$dir" >/dev/null 2>&1 [ -f "$file_path" ] || abort_verify "Extracted $file does not exist" unzip $opts "$zip" "$file.sha256" -d "${TMPDIR_FOR_VERIFY}" >/dev/null 2>&1 [ -f "$hash_path" ] || abort_verify "Hash file $file.sha256 does not exist" # Read the expected hash and verify it local expected_hash read -r expected_hash < "$hash_path" expected_hash="${expected_hash%% *}" # Strip anything after the actual hash string if ! echo "$expected_hash $file_path" | sha256sum -c -s -; then abort_verify "Failed to verify $file" fi ui_print "- Verified $file" } # ========================================================= VERSION=$(grep_prop version "${TMPDIR}/module.prop") ui_print "- Vector version ${VERSION}" # Disable existing LSPosed installation LSPOSED_DIR="/data/adb/modules/zygisk_lsposed" if [ -d "$LSPOSED_DIR" ]; then ui_print "*********************************************************" ui_print "LSPosed installation detected, disabling it for Vector" touch "$LSPOSED_DIR/disable" ui_print "*********************************************************" fi # 1. Map architecture to standard ABI paths, eliminating duplicate logic case "$ARCH" in arm|arm64) ABI32="armeabi-v7a" ABI64="arm64-v8a" ;; x86|x64) ABI32="x86" ABI64="x86_64" ;; *) abort "! Unsupported platform: $ARCH" ;; 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 extract "$ZIPFILE" "$file" "$MODPATH" done ui_print "- Extracting Zygisk libraries" mkdir -p "$MODPATH/zygisk" # Extract 32-bit lib extract "$ZIPFILE" "lib/$ABI32/libzygisk.so" "$MODPATH/zygisk" true mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/${ABI32}.so" # Extract 64-bit lib if supported if [ "$IS64BIT" = true ]; then extract "$ZIPFILE" "lib/$ABI64/libzygisk.so" "$MODPATH/zygisk" true mv "$MODPATH/zygisk/libzygisk.so" "$MODPATH/zygisk/${ABI64}.so" fi if [ "$API" -ge 29 ]; then ui_print "- Extracting dex2oat binaries" mkdir -p "$MODPATH/bin" # Extract 32-bit binaries extract "$ZIPFILE" "bin/$ABI32/dex2oat" "$MODPATH/bin" true extract "$ZIPFILE" "bin/$ABI32/liboat_hook.so" "$MODPATH/bin" true mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat32" mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook32.so" # Extract 64-bit binaries if [ "$IS64BIT" = true ]; then extract "$ZIPFILE" "bin/$ABI64/dex2oat" "$MODPATH/bin" true extract "$ZIPFILE" "bin/$ABI64/liboat_hook.so" "$MODPATH/bin" true mv "$MODPATH/bin/dex2oat" "$MODPATH/bin/dex2oat64" mv "$MODPATH/bin/liboat_hook.so" "$MODPATH/bin/liboat_hook64.so" fi ui_print "- Patching binaries for anti-detection" DEV_PATH=$(tr -dc 'a-z0-9' >"$MODPATH/system.prop" fi ui_print "- Welcome to Vector!" ================================================ FILE: zygisk/module/daemon ================================================ #!/system/bin/sh dir="${0%/*}" tmpDaemonApk="/data/local/tmp/daemon.apk" debug="@DEBUG@" # Safely check for debug APK and set classpath if [ -r "$tmpDaemonApk" ]; then java_options="-Djava.class.path=$tmpDaemonApk" debug="true" else java_options="-Djava.class.path=$dir/daemon.apk" fi # Apply debug options based on Android SDK version. # Reference: https://developer.android.com/ndk/guides/wrap-script#debugging_when_using_wrapsh if [ "$debug" = "true" ]; then os_version=$(getprop ro.build.version.sdk) if [ "$os_version" -eq "27" ]; then java_options="$java_options -Xrunjdwp:transport=dt_android_adb,suspend=n,server=y -Xcompiler-option --debuggable" elif [ "$os_version" -eq "28" ]; then java_options="$java_options -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y -Xcompiler-option --debuggable" else java_options="$java_options -XjdwpProvider:adbconnection -XjdwpOptions:suspend=n,server=y" fi fi # Mount an empty temporary file system over /data/resource-cache to avoid possible overlay conflicts. # Note that this script runs inside an isolated mount namespace (via unshare). mount tmpfs -t tmpfs /data/resource-cache # Wait for Zygote socket using a polling loop if [ ! -S "/dev/socket/zygote" ]; then # Wait up to ~10 seconds to avoid an infinite hang if something breaks wait_count=0 while [ ! -S "/dev/socket/zygote" ] &&[ "$wait_count" -lt 100 ]; do sleep 0.1 wait_count=$((wait_count + 1)) done [ "$debug" = "true" ] && log -p v -t "Vector" "zygote started" 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 ================================================ FILE: zygisk/module/module.prop ================================================ id=zygisk_vector name=Vector version=${versionName} (${versionCode}) versionCode=${versionCode} author=JingMatrix description=A modern, Xposed-compatible framework for Android application hooking. (Android 8.1 ~ 16) updateJson=https://raw.githubusercontent.com/JingMatrix/Vector/master/zygisk/update.json ================================================ FILE: zygisk/module/sepolicy.rule ================================================ allow dex2oat dex2oat_exec file execute_no_trans allow dex2oat system_linker_exec file execute_no_trans allow shell shell dir write type xposed_file file_type typeattribute xposed_file mlstrustedobject allow {dex2oat installd isolated_app shell} xposed_file {file dir} * allow dex2oat {unlabeled tmpfs} file * type xposed_data file_type typeattribute xposed_data mlstrustedobject allow * xposed_data {file dir} * ================================================ FILE: zygisk/module/service.sh ================================================ # Extract the directory path and change directory MODDIR="${0%/*}" cd "$MODDIR" || exit 1 # Start the daemon directly in the background within a private mount namespace unshare --propagation slave -m "$MODDIR/daemon" --system-server-max-retry=3 "$@" & ================================================ FILE: zygisk/module/system.prop ================================================ dalvik.vm.dex2oat-flags=--inline-max-code-units=0 ================================================ FILE: zygisk/module/uninstall.sh ================================================ rm -rf /data/adb/lspd ================================================ FILE: zygisk/proguard-rules.pro ================================================ -keepclasseswithmembers class org.matrix.vector.core.Main { public static void forkCommon(boolean, boolean, java.lang.String, java.lang.String, android.os.IBinder); } -keepclasseswithmembers,includedescriptorclasses class * { native ; } -keepclasseswithmembers class org.matrix.vector.service.BridgeService { public static boolean *(android.os.IBinder, int, long, long, int); } -assumenosideeffects class android.util.Log { public static *** v(...); public static *** d(...); } -repackageclasses -allowaccessmodification -dontwarn org.lsposed.lspd.core.* -dontwarn org.lsposed.lspd.util.Hookers ================================================ FILE: zygisk/src/main/AndroidManifest.xml ================================================ ================================================ FILE: zygisk/src/main/cpp/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.10) project(zygisk) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_subdirectory(${VECTOR_ROOT}/native native) add_library(${PROJECT_NAME} SHARED module.cpp ipc_bridge.cpp) target_include_directories(${PROJECT_NAME} PUBLIC include) target_link_libraries(${PROJECT_NAME} native log) if (DEFINED DEBUG_SYMBOLS_PATH) message(STATUS "Debug symbols will be placed at ${DEBUG_SYMBOLS_PATH}") add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI} COMMAND ${CMAKE_OBJCOPY} --only-keep-debug $ ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug COMMAND ${CMAKE_STRIP} --strip-all $ COMMAND ${CMAKE_OBJCOPY} --add-gnu-debuglink ${DEBUG_SYMBOLS_PATH}/${ANDROID_ABI}/${PROJECT_NAME}.debug $) endif() ================================================ FILE: zygisk/src/main/cpp/include/ipc_bridge.h ================================================ #pragma once #include #include #include #include // This module is a client of the 'native' library. // We include the JNI bridge from 'native' to leverage its helper functions. #include namespace vector::native::module { /** * @class IPCBridge * @brief Manages Binder IPC communication with the Vector host service. * * This singleton class is the communication arm of the Zygisk module. Its key responsibilities are: * 1. Discovering and connecting to the central host service (the "manager"). * 2. Requesting the framework's DEX file and obfuscation map from the service. * 3. Caching all necessary JNI class and method IDs for efficient reuse. */ class IPCBridge { public: // Enforce singleton pattern. IPCBridge(const IPCBridge &) = delete; IPCBridge &operator=(const IPCBridge &) = delete; /** * @brief Gets the singleton instance of the IPCBridge. */ static IPCBridge &GetInstance(); /** * @brief Caches JNI class and method IDs needed for Binder communication. * @param env A valid JNI environment pointer. */ void Initialize(JNIEnv *env); /** * @brief Requests an application-specific Binder from the host service. * @param env JNI environment pointer. * @param nice_name The process name. * @return A ScopedLocalRef to the Binder object, or nullptr on failure. */ lsplant::ScopedLocalRef RequestAppBinder(JNIEnv *env, jstring nice_name); /** * @brief Requests the system_server's dedicated Binder from the host service. * @param env JNI environment pointer. * @param bridgeServiceName rendezvous point used by the system_server * @return A ScopedLocalRef to the Binder object, or nullptr on failure. */ lsplant::ScopedLocalRef RequestSystemServerBinder(JNIEnv *env, std::string bridgeServiceName); /** * @brief Asks the system_server binder for the application manager binder. * @param env JNI environment pointer. * @param system_server_binder The binder connected to the system server service. * @return A ScopedLocalRef to the application manager Binder, or nullptr on failure. */ lsplant::ScopedLocalRef RequestManagerBinderFromSystemServer( JNIEnv *env, jobject system_server_binder); /** * @brief Fetches the framework DEX file via the provided Binder connection. * @param env JNI environment pointer. * @param binder A live Binder connection to the host service. * @return A tuple containing the file descriptor and size of the DEX file. * Returns {-1, 0} on failure. */ std::tuple FetchFrameworkDex(JNIEnv *env, jobject binder); /** * @brief Fetches the framework's obfuscation map via the provided Binder. * @param env JNI environment pointer. * @param binder A live Binder connection to the host service. * @return A map of original names to obfuscated names. */ std::map FetchObfuscationMap(JNIEnv *env, jobject binder); /** * @brief Sets up the JNI hook to intercept Binder transactions. * * This is the core of the IPC interception mechanism. * It replaces the JNI function pointer for CallBooleanMethodV to * inspect calls to Binder.execTransact, allowing the framework to * handle its own custom transaction codes directly. * @param env JNI environment pointer. */ void HookBridge(JNIEnv *env); private: /** * @class ParcelWrapper * @brief A private RAII wrapper to ensure Parcel objects are always recycled. * * As a private nested class, it has access to the private members of IPCBridge * (like parcel_class_ and recycle_method_). */ class ParcelWrapper { public: ParcelWrapper(JNIEnv *env, IPCBridge *bridge); ~ParcelWrapper(); // Disable copy operations ParcelWrapper(const ParcelWrapper &) = delete; ParcelWrapper &operator=(const ParcelWrapper &) = delete; lsplant::ScopedLocalRef data; lsplant::ScopedLocalRef reply; private: JNIEnv *env_; IPCBridge *bridge_; }; // Private constructor for singleton. IPCBridge() = default; bool initialized_ = false; // --- Cached JNI References --- // These are initialized once and stored as global references for performance. // android.os.ServiceManager jclass service_manager_class_ = nullptr; jmethodID get_service_method_ = nullptr; // android.os.IBinder jmethodID transact_method_ = nullptr; // android.os.Binder jclass binder_class_ = nullptr; jmethodID binder_ctor_ = nullptr; // android.os.Parcel jclass parcel_class_ = nullptr; jmethodID obtain_method_ = nullptr; jmethodID recycle_method_ = nullptr; jmethodID write_interface_token_method_ = nullptr; jmethodID write_int_method_ = nullptr; jmethodID write_string_method_ = nullptr; jmethodID write_strong_binder_method_ = nullptr; jmethodID read_exception_method_ = nullptr; jmethodID read_strong_binder_method_ = nullptr; jmethodID read_file_descriptor_method_ = nullptr; jmethodID read_int_method_ = nullptr; jmethodID read_long_method_ = nullptr; jmethodID read_string_method_ = nullptr; // android.os.ParcelFileDescriptor jclass parcel_fd_class_ = nullptr; jmethodID detach_fd_method_ = nullptr; // --- JNI Hooking Members --- // These are required to store the state for our JNI function table override. // The C++ hook function that will replace the original CallBooleanMethodV. static jboolean JNICALL CallBooleanMethodV_Hook(JNIEnv *env, jobject obj, jmethodID methodId, va_list args); // The helper function that handles our specific transaction code. static jboolean ExecTransact_Replace(jboolean *res, JNIEnv *env, jobject obj, va_list args); // A complete copy of the original JNI function table, with our hook installed. JNINativeInterface native_interface_hook_{}; // The original jmethodID for android.os.Binder.execTransact(). jmethodID exec_transact_backup_method_id_ = nullptr; // A function pointer to the original CallBooleanMethodV implementation. jboolean (*call_boolean_method_v_backup_)(JNIEnv *, jobject, jmethodID, va_list) = nullptr; // A global reference to the framework's Java BridgeService class. jclass bridge_service_class_ = nullptr; // The jmethodID for the static Java method that handles the intercepted transaction. jmethodID exec_transact_replace_method_id_ = nullptr; }; } // namespace vector::native::module ================================================ FILE: zygisk/src/main/cpp/include/zygisk.hpp ================================================ /* Copyright 2022-2023 John "topjohnwu" Wu * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ // This is the public API for Zygisk modules. // DO NOT MODIFY ANY CODE IN THIS HEADER. #pragma once #include #include #define ZYGISK_API_VERSION 4 /* *************** * Introduction *************** On Android, all app processes are forked from a special daemon called "Zygote". For each new app process, zygote will fork a new process and perform "specialization". This specialization operation enforces the Android security sandbox on the newly forked process to make sure that 3rd party application code is only loaded after it is being restricted within a sandbox. On Android, there is also this special process called "system_server". This single process hosts a significant portion of system services, which controls how the Android operating system and apps interact with each other. The Zygisk framework provides a way to allow developers to build modules and run custom code before and after system_server and any app processes' specialization. This enable developers to inject code and alter the behavior of system_server and app processes. Please note that modules will only be loaded after zygote has forked the child process. THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON! ********************* * Development Guide ********************* Define a class and inherit zygisk::ModuleBase to implement the functionality of your module. Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk. Example code: static jint (*orig_logger_entry_max)(JNIEnv *env); static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); } class ExampleModule : public zygisk::ModuleBase { public: void onLoad(zygisk::Api *api, JNIEnv *env) override { this->api = api; this->env = env; } void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { JNINativeMethod methods[] = { { "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max }, }; api->hookJniNativeMethods(env, "android/util/Log", methods, 1); *(void **) &orig_logger_entry_max = methods[0].fnPtr; } private: zygisk::Api *api; JNIEnv *env; }; REGISTER_ZYGISK_MODULE(ExampleModule) ----------------------------------------------------------------------------------------- Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize, or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class never runs in a true superuser environment. If your module require access to superuser permissions, you can create and register a root companion handler function. This function runs in a separate root companion daemon process, and an Unix domain socket is provided to allow you to perform IPC between your target process and the root companion process. Example code: static void example_handler(int socket) { ... } REGISTER_ZYGISK_COMPANION(example_handler) */ namespace zygisk { struct Api; struct AppSpecializeArgs; struct ServerSpecializeArgs; class ModuleBase { public: // This method is called as soon as the module is loaded into the target process. // A Zygisk API handle will be passed as an argument. virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {} // This method is called before the app process is specialized. // At this point, the process just got forked from zygote, but no app specific specialization // is applied. This means that the process does not have any sandbox restrictions and // still runs with the same privilege of zygote. // // All the arguments that will be sent and used for app specialization is passed as a single // AppSpecializeArgs object. You can read and overwrite these arguments to change how the app // process will be specialized. // // If you need to run some operations as superuser, you can call Api::connectCompanion() to // get a socket to do IPC calls with a root companion process. // See Api::connectCompanion() for more info. virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {} // This method is called after the app process is specialized. // At this point, the process has all sandbox restrictions enabled for this application. // This means that this method runs with the same privilege of the app's own code. virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {} // This method is called before the system server process is specialized. // See preAppSpecialize(args) for more info. virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {} // This method is called after the system server process is specialized. // At this point, the process runs with the privilege of system_server. virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {} }; struct AppSpecializeArgs { // Required arguments. These arguments are guaranteed to exist on all Android versions. jint &uid; jint &gid; jintArray &gids; jint &runtime_flags; jobjectArray &rlimits; jint &mount_external; jstring &se_info; jstring &nice_name; jstring &instruction_set; jstring &app_data_dir; // Optional arguments. Please check whether the pointer is null before de-referencing jintArray *const fds_to_ignore; jboolean *const is_child_zygote; jboolean *const is_top_app; jobjectArray *const pkg_data_info_list; jobjectArray *const whitelisted_data_info_list; jboolean *const mount_data_dirs; jboolean *const mount_storage_dirs; AppSpecializeArgs() = delete; }; struct ServerSpecializeArgs { jint &uid; jint &gid; jintArray &gids; jint &runtime_flags; jlong &permitted_capabilities; jlong &effective_capabilities; ServerSpecializeArgs() = delete; }; namespace internal { struct api_table; template void entry_impl(api_table *, JNIEnv *); } // namespace internal // These values are used in Api::setOption(Option) enum Option : int { // Force Magisk's denylist unmount routines to run on this process. // // Setting this option only makes sense in preAppSpecialize. // The actual unmounting happens during app process specialization. // // Set this option to force all Magisk and modules' files to be unmounted from the // mount namespace of the process, regardless of the denylist enforcement status. FORCE_DENYLIST_UNMOUNT = 0, // When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize. // Be aware that after dlclose-ing your module, all of your code will be unmapped from memory. // YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS. DLCLOSE_MODULE_LIBRARY = 1, }; // Bit masks of the return value of Api::getFlags() enum StateFlag : uint32_t { // The user has granted root access to the current process PROCESS_GRANTED_ROOT = (1u << 0), // The current process was added on the denylist PROCESS_ON_DENYLIST = (1u << 1), }; // All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded // from the specialized process afterwards. struct Api { // Connect to a root companion process and get a Unix domain socket for IPC. // // This API only works in the pre[XXX]Specialize methods due to SELinux restrictions. // // The pre[XXX]Specialize methods run with the same privilege of zygote. // If you would like to do some operations with superuser permissions, register a handler // function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func). // Another good use case for a companion process is that if you want to share some resources // across multiple processes, hold the resources in the companion process and pass it over. // // The root companion process is ABI aware; that is, when calling this method from a 32-bit // process, you will be connected to a 32-bit companion process, and vice versa for 64-bit. // // Returns a file descriptor to a socket that is connected to the socket passed to your // module's companion request handler. Returns -1 if the connection attempt failed. int connectCompanion(); // Get the file descriptor of the root folder of the current module. // // This API only works in the pre[XXX]Specialize methods. // Accessing the directory returned is only possible in the pre[XXX]Specialize methods // or in the root companion process (assuming that you sent the fd over the socket). // Both restrictions are due to SELinux and UID. // // Returns -1 if errors occurred. int getModuleDir(); // Set various options for your module. // Please note that this method accepts one single option at a time. // Check zygisk::Option for the full list of options available. void setOption(Option opt); // Get information about the current process. // Returns bitwise-or'd zygisk::StateFlag values. uint32_t getFlags(); // Exempt the provided file descriptor from being automatically closed. // // This API only make sense in preAppSpecialize; calling this method in any other situation // is either a no-op (returns true) or an error (returns false). // // When false is returned, the provided file descriptor will eventually be closed by zygote. bool exemptFd(int fd); // Hook JNI native methods for a class // // Lookup all registered JNI native methods and replace it with your own methods. // The original function pointer will be saved in each JNINativeMethod's fnPtr. // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr // will be set to nullptr. void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); // Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory. // // Parsing /proc/[PID]/maps will give you the memory map of a process. As an example: // //

// 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64 // (More details: https://man7.org/linux/man-pages/man5/proc.5.html) // // The `dev` and `inode` pair uniquely identifies a file being mapped into memory. // For matching ELFs loaded in memory, replace function `symbol` with `newFunc`. // If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`. void pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc); // Commit all the hooks that was previously registered. // Returns false if an error occurred. bool pltHookCommit(); private: internal::api_table *tbl; template friend void internal::entry_impl(internal::api_table *, JNIEnv *); }; // Register a class as a Zygisk module #define REGISTER_ZYGISK_MODULE(clazz) \ void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ zygisk::internal::entry_impl(table, env); \ } // Register a root companion request handler function for your module // // The function runs in a superuser daemon process and handles a root companion request from // your module running in a target process. The function has to accept an integer value, // which is a Unix domain socket that is connected to the target process. // See Api::connectCompanion() for more info. // // NOTE: the function can run concurrently on multiple threads. // Be aware of race conditions if you have globally shared resources. #define REGISTER_ZYGISK_COMPANION(func) \ void zygisk_companion_entry(int client) { func(client); } /********************************************************* * The following is internal ABI implementation detail. * You do not have to understand what it is doing. *********************************************************/ namespace internal { struct module_abi { long api_version; ModuleBase *impl; void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *); void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *); void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *); void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *); module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) { preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); }; postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); }; preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); }; postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); }; } }; struct api_table { // Base void *impl; bool (*registerModule)(api_table *, module_abi *); void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int); void (*pltHookRegister)(dev_t, ino_t, const char *, void *, void **); bool (*exemptFd)(int); bool (*pltHookCommit)(); int (*connectCompanion)(void * /* impl */); void (*setOption)(void * /* impl */, Option); int (*getModuleDir)(void * /* impl */); uint32_t (*getFlags)(void * /* impl */); }; template void entry_impl(api_table *table, JNIEnv *env) { static Api api; api.tbl = table; static T module; ModuleBase *m = &module; static module_abi abi(m); if (!table->registerModule(table, &abi)) return; m->onLoad(&api, env); } } // namespace internal inline int Api::connectCompanion() { return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1; } inline int Api::getModuleDir() { return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1; } inline void Api::setOption(Option opt) { if (tbl->setOption) tbl->setOption(tbl->impl, opt); } inline uint32_t Api::getFlags() { return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0; } inline bool Api::exemptFd(int fd) { return tbl->exemptFd != nullptr && tbl->exemptFd(fd); } inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods); } inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc) { if (tbl->pltHookRegister) tbl->pltHookRegister(dev, inode, symbol, newFunc, oldFunc); } inline bool Api::pltHookCommit() { return tbl->pltHookCommit != nullptr && tbl->pltHookCommit(); } } // namespace zygisk extern "C" { [[gnu::visibility("default"), maybe_unused]] void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); [[gnu::visibility("default"), maybe_unused]] void zygisk_companion_entry(int); } // extern "C" ================================================ FILE: zygisk/src/main/cpp/ipc_bridge.cpp ================================================ #include "ipc_bridge.h" #include #include #include #include #include #include using namespace std::literals::string_view_literals; #include namespace vector::native::module { // Store the ID of the last caller whose framework transaction failed. // It's initialized to a value that won't match. static std::atomic g_last_failed_id = ~0; /** * @class BinderCaller * @brief A helper to get the UID and PID of the current Binder caller. * * This class encapsulates the logic for finding and calling private functions * within libbinder.so to identify the origin of an IPC transaction. */ class BinderCaller { public: /** * @brief Initializes the helper by finding the required function pointers. * This must be called once before GetId() is used. */ static void Initialize() { // Use the native library's symbol cache to find symbols in the loaded libbinder.so auto libbinder = ElfSymbolCache::GetLibBinder(); if (!libbinder) { LOGW("libbinder.so not found in cache, cannot get caller ID."); return; } s_self_or_null_fn = (IPCThreadState * (*)()) libbinder->getSymbAddress( "_ZN7android14IPCThreadState10selfOrNullEv"); s_get_calling_pid_fn = (pid_t(*)(IPCThreadState *))libbinder->getSymbAddress( "_ZNK7android14IPCThreadState13getCallingPidEv"); s_get_calling_uid_fn = (uid_t(*)(IPCThreadState *))libbinder->getSymbAddress( "_ZNK7android14IPCThreadState13getCallingUidEv"); if (!s_self_or_null_fn || !s_get_calling_pid_fn || !s_get_calling_uid_fn) { LOGW("Could not resolve all IPCThreadState symbols. Caller ID check will be disabled."); } else { LOGV("IPCThreadState symbols resolved successfully."); } } /** * @brief Gets the unique 64-bit ID of the current Binder caller. * @return A combined UID and PID, or 0 if symbols are not available. */ static uint64_t GetId() { if (!s_self_or_null_fn) [[unlikely]] return 0; IPCThreadState *self = s_self_or_null_fn(); if (!self) return 0; auto uid = s_get_calling_uid_fn(self); auto pid = s_get_calling_pid_fn(self); return (static_cast(uid) << 32) | pid; } private: // Forward declare the opaque struct from libbinder. struct IPCThreadState; inline static IPCThreadState *(*s_self_or_null_fn)() = nullptr; inline static pid_t (*s_get_calling_pid_fn)(IPCThreadState *) = nullptr; inline static uid_t (*s_get_calling_uid_fn)(IPCThreadState *) = nullptr; }; // --- Binder IPC Protocol Constants --- // These are the "secret handshakes" used to communicate with the Vector manager service. // The name of the system service we use as a rendezvous point to find our manager service. // Using "activity" is a common technique as it's always available. constexpr auto kBridgeServiceName = "activity"sv; // Transaction codes for specific actions. constexpr jint kBridgeTransactionCode = ('_' << 24) | ('V' << 16) | ('E' << 8) | 'C'; constexpr jint kDexTransactionCode = ('_' << 24) | ('D' << 16) | ('E' << 8) | 'X'; constexpr jint kObfuscationMapTransactionCode = ('_' << 24) | ('O' << 16) | ('B' << 8) | 'F'; // Action codes sent within a kBridgeTransactionCode transaction. constexpr jint kActionGetBinder = 2; // ========================================================================================= // Implementation of IPCBridge::ParcelWrapper (Private Nested Class) // ========================================================================================= /** * @brief Constructs the ParcelWrapper, obtaining two new Parcel objects from the pool. * @param env A valid JNI environment pointer. * @param bridge A pointer to the parent IPCBridge instance to access its cached JNI IDs. */ IPCBridge::ParcelWrapper::ParcelWrapper(JNIEnv *env, IPCBridge *bridge) : data(lsplant::JNI_CallStaticObjectMethod(env, bridge->parcel_class_, bridge->obtain_method_)), reply( lsplant::JNI_CallStaticObjectMethod(env, bridge->parcel_class_, bridge->obtain_method_)), env_(env), bridge_(bridge) {} /** * @brief Destructs the ParcelWrapper, ensuring both Parcel objects are recycled. * * This is the core of the RAII pattern for Parcels. * This destructor guarantees that recycle() is called, preventing resource leaks even if * errors occur during the transaction. */ IPCBridge::ParcelWrapper::~ParcelWrapper() { // Check if the parcel was successfully obtained before trying to recycle it. if (data) { lsplant::JNI_CallVoidMethod(env_, data.get(), bridge_->recycle_method_); } if (reply) { lsplant::JNI_CallVoidMethod(env_, reply.get(), bridge_->recycle_method_); } } // ========================================================================================= // IPCBridge Implementation // ========================================================================================= IPCBridge &IPCBridge::GetInstance() { static IPCBridge instance; return instance; } void IPCBridge::Initialize(JNIEnv *env) { if (initialized_) { return; } // --- Cache JNI Classes and Method IDs --- // Caching these at startup is more efficient and robust than looking them up on every IPC call. // If any of these fail, the IPC bridge is unusable. // ServiceManager auto sm_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/ServiceManager")); if (!sm_class) { LOGE("IPCBridge: ServiceManager class not found!"); return; } service_manager_class_ = (jclass)env->NewGlobalRef(sm_class.get()); get_service_method_ = lsplant::JNI_GetStaticMethodID( env, service_manager_class_, "getService", "(Ljava/lang/String;)Landroid/os/IBinder;"); if (!get_service_method_) { LOGE("IPCBridge: ServiceManager.getService method not found!"); return; } // IBinder auto ibinder_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/IBinder")); if (!ibinder_class) { LOGE("IPCBridge: IBinder class not found!"); return; } transact_method_ = lsplant::JNI_GetMethodID(env, ibinder_class.get(), "transact", "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z"); if (!transact_method_) { LOGE("IPCBridge: IBinder.transact method not found!"); return; } // Binder auto binder_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/Binder")); if (!binder_class) { LOGE("IPCBridge: Binder class not found!"); return; } binder_class_ = (jclass)env->NewGlobalRef(binder_class.get()); binder_ctor_ = lsplant::JNI_GetMethodID(env, binder_class_, "", "()V"); if (!binder_ctor_) { LOGE("IPCBridge: Binder constructor not found!"); return; } // Parcel auto parcel_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/Parcel")); if (!parcel_class) { LOGE("IPCBridge: Parcel class not found!"); return; } parcel_class_ = (jclass)env->NewGlobalRef(parcel_class.get()); obtain_method_ = lsplant::JNI_GetStaticMethodID(env, parcel_class_, "obtain", "()Landroid/os/Parcel;"); recycle_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "recycle", "()V"); write_interface_token_method_ = lsplant::JNI_GetMethodID( env, parcel_class_, "writeInterfaceToken", "(Ljava/lang/String;)V"); write_int_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "writeInt", "(I)V"); write_string_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "writeString", "(Ljava/lang/String;)V"); write_strong_binder_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "writeStrongBinder", "(Landroid/os/IBinder;)V"); read_exception_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readException", "()V"); read_strong_binder_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readStrongBinder", "()Landroid/os/IBinder;"); read_file_descriptor_method_ = lsplant::JNI_GetMethodID( env, parcel_class_, "readFileDescriptor", "()Landroid/os/ParcelFileDescriptor;"); read_int_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readInt", "()I"); read_long_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readLong", "()J"); read_string_method_ = lsplant::JNI_GetMethodID(env, parcel_class_, "readString", "()Ljava/lang/String;"); // ParcelFileDescriptor auto pfd_class = lsplant::ScopedLocalRef(env, env->FindClass("android/os/ParcelFileDescriptor")); if (!pfd_class) { LOGE("IPCBridge: ParcelFileDescriptor class not found!"); return; } parcel_fd_class_ = (jclass)env->NewGlobalRef(pfd_class.get()); detach_fd_method_ = lsplant::JNI_GetMethodID(env, parcel_fd_class_, "detachFd", "()I"); if (!detach_fd_method_) { LOGE("IPCBridge: ParcelFileDescriptor.detachFd method not found!"); return; } LOGV("IPCBridge initialized successfully."); initialized_ = true; } lsplant::ScopedLocalRef IPCBridge::RequestAppBinder(JNIEnv *env, jstring nice_name) { if (!initialized_) { LOGE("RequestAppBinder failed: IPCBridge not initialized."); return {env, nullptr}; } // Get the rendezvous service from the Android ServiceManager. auto service_name = lsplant::ScopedLocalRef(env, env->NewStringUTF(kBridgeServiceName.data())); auto bridge_service = lsplant::JNI_CallStaticObjectMethod( env, service_manager_class_, get_service_method_, service_name.get()); if (!bridge_service) { LOGE("Could not get rendezvous service '{}'. Manager not available?", kBridgeServiceName.data()); return {env, nullptr}; } // Prepare the IPC transaction. ParcelWrapper parcels(env, this); if (!parcels.data || !parcels.reply) { LOGE("Failed to obtain parcels for IPC."); return {env, nullptr}; } // This is a "heartbeat" binder. // If our process dies, the manager service will be notified that this binder has died, // allowing it to clean up resources. auto heartbeat_binder = lsplant::ScopedLocalRef(env, env->NewObject(binder_class_, binder_ctor_)); if (!heartbeat_binder) { LOGE("Failed to create heartbeat binder."); return {env, nullptr}; } // Write the request data to the 'data' parcel. lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, kActionGetBinder); lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_string_method_, nice_name); lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, heartbeat_binder.get()); // Perform the transaction. bool success = lsplant::JNI_CallBooleanMethod(env, bridge_service.get(), transact_method_, kBridgeTransactionCode, parcels.data.get(), parcels.reply.get(), 0); lsplant::ScopedLocalRef result_binder = {env, nullptr}; if (success) { // Read the reply. CRITICAL: must call readException first. lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); if (env->ExceptionCheck()) { LOGW("Remote exception received while requesting app binder."); env->ExceptionClear(); } else { result_binder = lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_strong_binder_method_); if (result_binder) { // IMPORTANT: Keep the heartbeat binder alive by making it a global ref. // If we don't do this, it gets garbage collected and the remote service // thinks our process has died. env->NewGlobalRef(heartbeat_binder.get()); } } } else { LOGD("Transact call to request app binder failed."); } return result_binder; } lsplant::ScopedLocalRef IPCBridge::RequestSystemServerBinder( JNIEnv *env, std::string bridgeServiceName) { if (!initialized_) { LOGE("RequestSystemServerBinder failed: IPCBridge not initialized."); return {env, nullptr}; } auto service_name = lsplant::ScopedLocalRef(env, env->NewStringUTF(bridgeServiceName.data())); lsplant::ScopedLocalRef binder = {env, nullptr}; // The system_server might start its services slightly after Zygisk injects us. // We retry a few times to give it a chance to register. for (int i = 0; i < 3; ++i) { binder = lsplant::JNI_CallStaticObjectMethod(env, service_manager_class_, get_service_method_, service_name.get()); if (binder) { LOGI("Got system server binder via {} on attempt {}.", bridgeServiceName.data(), i + 1); return binder; } if (i < 2) { LOGW("Failed to get system server binder via {}, will retry in 1 second...", bridgeServiceName.data()); std::this_thread::sleep_for(std::chrono::seconds(1)); } } LOGE("Failed to get system server binder after 3 attempts. Aborting."); return {env, nullptr}; } lsplant::ScopedLocalRef IPCBridge::RequestManagerBinderFromSystemServer( JNIEnv *env, jobject system_server_binder) { ParcelWrapper parcels(env, this); auto heartbeat_binder = lsplant::ScopedLocalRef(env, env->NewObject(binder_class_, binder_ctor_)); auto system_name = lsplant::ScopedLocalRef(env, env->NewStringUTF("system")); // Populate the request lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, getuid()); lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_int_method_, getpid()); lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_string_method_, system_name.get()); lsplant::JNI_CallVoidMethod(env, parcels.data.get(), write_strong_binder_method_, heartbeat_binder.get()); // Transact bool success = lsplant::JNI_CallBooleanMethod(env, system_server_binder, transact_method_, kBridgeTransactionCode, parcels.data.get(), parcels.reply.get(), 0); lsplant::ScopedLocalRef result_binder = {env, nullptr}; if (success) { lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); if (env->ExceptionCheck()) { LOGW("Remote exception while getting manager binder from system_server."); env->ExceptionClear(); } else { result_binder = lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_strong_binder_method_); if (result_binder) { env->NewGlobalRef(heartbeat_binder.get()); // Keep heartbeat alive } } } LOGD("Manager binder from system_server: {}", static_cast(result_binder.get())); return result_binder; } std::tuple IPCBridge::FetchFrameworkDex(JNIEnv *env, jobject binder) { if (!initialized_ || !binder) { return {-1, 0}; } ParcelWrapper parcels(env, this); bool success = lsplant::JNI_CallBooleanMethod(env, binder, transact_method_, kDexTransactionCode, parcels.data.get(), parcels.reply.get(), 0); if (!success) { LOGE("DEX fetch transaction failed."); return {-1, 0}; } lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); if (env->ExceptionCheck()) { LOGE("Remote exception received while fetching DEX."); env->ExceptionClear(); return {-1, 0}; } auto pfd = lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_file_descriptor_method_); if (!pfd) { LOGE("Received null ParcelFileDescriptor for DEX."); return {-1, 0}; } int fd = lsplant::JNI_CallIntMethod(env, pfd.get(), detach_fd_method_); size_t size = static_cast( lsplant::JNI_CallLongMethod(env, parcels.reply.get(), read_long_method_)); LOGV("Fetched framework DEX: fd={}, size={}", fd, size); return {fd, size}; } std::map IPCBridge::FetchObfuscationMap(JNIEnv *env, jobject binder) { std::map result_map; if (!initialized_ || !binder) { return result_map; } ParcelWrapper parcels(env, this); bool success = lsplant::JNI_CallBooleanMethod(env, binder, transact_method_, kObfuscationMapTransactionCode, parcels.data.get(), parcels.reply.get(), 0); if (!success) { LOGE("Obfuscation map fetch transaction failed."); return result_map; } lsplant::JNI_CallVoidMethod(env, parcels.reply.get(), read_exception_method_); if (env->ExceptionCheck()) { LOGE("Remote exception received while fetching obfuscation map."); env->ExceptionClear(); return result_map; } int size = lsplant::JNI_CallIntMethod(env, parcels.reply.get(), read_int_method_); if (size < 0 || (size % 2 != 0)) { LOGE("Invalid size for obfuscation map received: %d", size); return result_map; } for (int i = 0; i < size / 2; ++i) { auto key_jstr = lsplant::JNI_Cast( lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_string_method_)); auto val_jstr = lsplant::JNI_Cast( lsplant::JNI_CallObjectMethod(env, parcels.reply.get(), read_string_method_)); if (env->ExceptionCheck() || !key_jstr || !val_jstr) { LOGE("Error reading string from parcel for obfuscation map."); env->ExceptionClear(); result_map.clear(); // Return an empty map on error return result_map; } lsplant::JUTFString key_str(env, key_jstr.get()); lsplant::JUTFString val_str(env, val_jstr.get()); result_map[key_str.get()] = val_str.get(); } LOGV("Fetched obfuscation map with {} entries.", result_map.size()); return result_map; } jboolean IPCBridge::ExecTransact_Replace(jboolean *res, JNIEnv *env, jobject obj, va_list args) { va_list copy; va_copy(copy, args); // Extract arguments from the va_list for Binder.execTransact(int, long, long, int) auto code = va_arg(copy, jint); auto data_obj = va_arg(copy, jlong); auto reply_obj = va_arg(copy, jlong); auto flags = va_arg(copy, jint); va_end(copy); // If the transaction code matches our special code, intercept it. if (code == kBridgeTransactionCode) { // Call the static Java method in our framework's BridgeService to handle the call. *res = env->CallStaticBooleanMethod(GetInstance().bridge_service_class_, GetInstance().exec_transact_replace_method_id_, obj, code, data_obj, reply_obj, flags); if (env->ExceptionCheck()) { LOGW("Exception in Java BridgeService.execTransact handler."); env->ExceptionClear(); } if (*res == JNI_FALSE) { uint64_t caller_id = BinderCaller::GetId(); if (caller_id != 0) { g_last_failed_id.store(caller_id, std::memory_order_relaxed); } } return true; // Return true to indicate we handled the call. } return false; // Not our transaction, let the original method run. } jboolean JNICALL IPCBridge::CallBooleanMethodV_Hook(JNIEnv *env, jobject obj, jmethodID methodId, va_list args) { uint64_t current_caller_id = BinderCaller::GetId(); if (current_caller_id != 0) { uint64_t last_failed = g_last_failed_id.load(std::memory_order_relaxed); // If this caller is the one that just failed, // skip interception and go straight to the original function. if (current_caller_id == last_failed) { // We "consume" the failed state by resetting it, so the *next* call is not skipped. g_last_failed_id.store(~0, std::memory_order_relaxed); return GetInstance().call_boolean_method_v_backup_(env, obj, methodId, args); } } // Check if the method being called is the one we want to intercept: Binder.execTransact() if (methodId == GetInstance().exec_transact_backup_method_id_) { jboolean res = false; // Attempt to handle the transaction with our replacement logic. if (ExecTransact_Replace(&res, env, obj, args)) { return res; // If we handled it, return the result directly. } // If not handled, fall through to call the original method. } // Call the original, real CallBooleanMethodV function. return GetInstance().call_boolean_method_v_backup_(env, obj, methodId, args); } void IPCBridge::HookBridge(JNIEnv *env) { if (!initialized_) { LOGE("Cannot hook bridge: IPCBridge is not initialized."); return; } // Get framework-specific Java classes and methods --- const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); std::string bridge_service_class_name; bridge_service_class_name = obfs_map.at("org.matrix.vector.service.") + "BridgeService"; auto bridge_class_ref = Context::GetInstance()->FindClassFromCurrentLoader(env, bridge_service_class_name); if (!bridge_class_ref) { LOGE("Failed to find BridgeService class '{}'", bridge_service_class_name.c_str()); return; } bridge_service_class_ = lsplant::JNI_NewGlobalRef(env, bridge_class_ref); exec_transact_replace_method_id_ = lsplant::JNI_GetStaticMethodID( env, bridge_service_class_, "execTransact", "(Landroid/os/IBinder;IJJI)Z"); if (!exec_transact_replace_method_id_) { LOGE("Failed to find static method BridgeService.execTransact!"); return; } // --- Prepare the JNI hook --- // Get the original method ID for android.os.Binder.execTransact exec_transact_backup_method_id_ = lsplant::JNI_GetMethodID(env, binder_class_, "execTransact", "(IJJI)Z"); if (!exec_transact_backup_method_id_) { LOGE("Failed to find original method Binder.execTransact!"); return; } // Use the native library's API to get the JNI table override function. auto set_table_override_func = (void (*)(const JNINativeInterface *))ElfSymbolCache::GetArt()->getSymbAddress( "_ZN3art9JNIEnvExt16SetTableOverrideEPK18JNINativeInterface"); if (!set_table_override_func) { LOGE("Failed to find ART symbol SetTableOverride!"); return; } // --- Step 3: Install the hook --- // Make a full copy of the existing JNI function table. memcpy(&native_interface_hook_, env->functions, sizeof(JNINativeInterface)); // Store a pointer to the original function we are about to replace. call_boolean_method_v_backup_ = env->functions->CallBooleanMethodV; // Overwrite the function pointer in our copy with our hook. native_interface_hook_.CallBooleanMethodV = &CallBooleanMethodV_Hook; // Atomically swap the process's JNI function table with our modified one. set_table_override_func(&native_interface_hook_); BinderCaller::Initialize(); LOGI("IPC Bridge JNI hook installed successfully."); } } // namespace vector::native::module ================================================ FILE: zygisk/src/main/cpp/module.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include "ipc_bridge.h" namespace vector::native::module { // --- Process UID Constants --- // Values used to identify special Android processes to avoid injection. // https://android.googlesource.com/platform/system/core/+/master/libcutils/include/private/android_filesystem_config.h // The range of UIDs used for isolated processes (e.g., web renderers, WebView). constexpr int FIRST_ISOLATED_UID = 99000; constexpr int LAST_ISOLATED_UID = 99999; // The range of UIDs used for application zygotes, which are also not targets. constexpr int FIRST_APP_ZYGOTE_ISOLATED_UID = 90000; constexpr int LAST_APP_ZYGOTE_ISOLATED_UID = 98999; // UID for the process responsible for creating shared RELRO files. constexpr int SHARED_RELRO_UID = 1037; // Android uses this to separate users. UID = AppID + UserID * 10000. constexpr int PER_USER_RANGE = 100000; // Defined via CMake generated marcos constexpr uid_t kHostPackageUid = INJECTED_PACKAGE_UID; const char *const kHostPackageName = INJECTED_PACKAGE_NAME; const char *const kManagePackageName = MANAGER_PACKAGE_NAME; constexpr uid_t GID_INET = 3003; // Android's Internet group ID. enum RuntimeFlags : uint32_t { // Flags defined by NeoZygisk LATE_INJECT = 1 << 30, }; // A simply ConfigBridge implemnetation holding obfuscation maps in memory using obfuscation_map_t = std::map; class ConfigImpl : public ConfigBridge { public: inline static void Init() { instance_ = std::make_unique(); } virtual obfuscation_map_t &obfuscation_map() override { return obfuscation_map_; } virtual void obfuscation_map(obfuscation_map_t m) override { obfuscation_map_ = std::move(m); } private: ConfigImpl() = default; friend std::unique_ptr std::make_unique(); obfuscation_map_t obfuscation_map_; }; /** * @class VectorModule * @brief The core implementation of the Zygisk module for the Vector framework. * * This class is the main entry point for Zygisk. It inherits from: * - zygisk::ModuleBase: To receive lifecycle callbacks from the Zygisk loader. * - vector::native::Context: To gain the core injection capabilities (DEX loading, ART hooking) * from the 'native' library. * * It orchestrates the injection process by deciding which processes to target, * using the IPCBridge to fetch the framework from the manager service, and then * using the Context base to perform the actual injection. */ class VectorModule : public zygisk::ModuleBase, public vector::native::Context { public: void onLoad(zygisk::Api *api, JNIEnv *env) override; void preAppSpecialize(zygisk::AppSpecializeArgs *args) override; void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override; void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override; void postServerSpecialize(const zygisk::ServerSpecializeArgs *args) override; protected: /** * @brief Provides the concrete implementation for loading the framework DEX. * * This method is a pure virtual in the native::core::Context base class and * must be implemented here. * It uses an InMemoryDexClassLoader to load our framework into the target process. */ void LoadDex(JNIEnv *env, PreloadedDex &&dex) override; /** * @brief Provides the concrete implementation for finding the Java entry * class. * * This method is also a pure virtual in the base class. * It uses the obfuscation map to determine the real entry class name and * finds it in the ClassLoader we created in LoadDex. */ void SetupEntryClass(JNIEnv *env) override; private: /** * @brief Encapsulates the logic for telling Zygisk whether to unload our library. * * If we don't inject into a process, we allow Zygisk to dlclose our .so. * Otherwise, we MUST prevent this. * @param unload True to allow unloading, false to prevent it. */ void SetAllowUnload(bool unload); zygisk::Api *api_ = nullptr; JNIEnv *env_ = nullptr; // --- ART Hooker Configuration --- const lsplant::InitInfo init_info_{ .inline_hooker = [](auto target, auto replace) { void *backup = nullptr; return HookInline(target, replace, &backup) == 0 ? backup : nullptr; }, .inline_unhooker = [](auto target) { return UnhookInline(target) == 0; }, .art_symbol_resolver = [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbAddress(symbol); }, .art_symbol_prefix_resolver = [](auto symbol) { return ElfSymbolCache::GetArt()->getSymbPrefixFirstAddress(symbol); }, }; // State managed within the class instance for each forked process. bool should_inject_ = false; bool is_manager_app_ = false; }; // ========================================================================================= // Implementation of VectorModule // ========================================================================================= void VectorModule::LoadDex(JNIEnv *env, PreloadedDex &&dex) { LOGV("Loading framework DEX into memory (size: {}).", dex.size()); // Get the system ClassLoader. This will be the parent of our new loader. auto classloader_class = lsplant::JNI_FindClass(env, "java/lang/ClassLoader"); if (!classloader_class) { LOGE("Failed to find java.lang.ClassLoader"); return; } auto getsyscl_mid = lsplant::JNI_GetStaticMethodID( env, classloader_class.get(), "getSystemClassLoader", "()Ljava/lang/ClassLoader;"); auto system_classloader = lsplant::JNI_CallStaticObjectMethod(env, classloader_class.get(), getsyscl_mid); if (!system_classloader) { LOGE("Failed to get SystemClassLoader"); return; } // Create a Java ByteBuffer wrapping our in-memory DEX data. auto byte_buffer_class = lsplant::JNI_FindClass(env, "java/nio/ByteBuffer"); if (!byte_buffer_class) { LOGE("Failed to find java.nio.ByteBuffer"); return; } auto dex_buffer = lsplant::ScopedLocalRef(env, env->NewDirectByteBuffer(dex.data(), dex.size())); if (!dex_buffer) { LOGE("Failed to create DirectByteBuffer for DEX."); return; } // Create an InMemoryDexClassLoader instance. auto in_memory_cl_class = lsplant::JNI_FindClass(env, "dalvik/system/InMemoryDexClassLoader"); if (!in_memory_cl_class) { LOGE("Failed to find InMemoryDexClassLoader."); return; } auto init_mid = lsplant::JNI_GetMethodID(env, in_memory_cl_class.get(), "", "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); if (!init_mid) { LOGE("Failed to find InMemoryDexClassLoader constructor."); return; } auto new_cl = lsplant::ScopedLocalRef(env, env->NewObject(in_memory_cl_class.get(), init_mid, dex_buffer.get(), system_classloader.get())); if (env->ExceptionCheck() || !new_cl) { LOGE("Failed to create InMemoryDexClassLoader instance."); env->ExceptionClear(); return; } // Store a global reference to our new ClassLoader. inject_class_loader_ = env->NewGlobalRef(new_cl.get()); LOGV("Framework ClassLoader created successfully."); } void VectorModule::SetupEntryClass(JNIEnv *env) { if (!inject_class_loader_) { LOGE("Cannot setup entry class: ClassLoader is null."); return; } // Use the obfuscation map from the config to get the real class name. const auto &obfs_map = ConfigBridge::GetInstance()->obfuscation_map(); std::string entry_class_name; entry_class_name = obfs_map.at("org.matrix.vector.core.") + "Main"; // We must find the class through our custom ClassLoader. auto entry_class = this->FindClassFromLoader(env, inject_class_loader_, entry_class_name); if (!entry_class) { LOGE("Failed to find entry class '{}' in the loaded DEX.", entry_class_name.c_str()); return; } // Store a global reference to the entry class. entry_class_ = lsplant::JNI_NewGlobalRef(env, entry_class); LOGV("Framework entry class '{}' located.", entry_class_name.c_str()); } void VectorModule::onLoad(zygisk::Api *api, JNIEnv *env) { this->api_ = api; this->env_ = env; // Create two singlton instances for classes Context and ConfigBridge instance_.reset(this); ConfigImpl::Init(); LOGD("Vector Zygisk module loaded"); } void VectorModule::preAppSpecialize(zygisk::AppSpecializeArgs *args) { // Reset state for this new process fork. should_inject_ = false; is_manager_app_ = false; // --- Manager App Special Handling --- // We identify our manager app by a special UID and // grant it internet permissions by adding it to the INET group. if (args->uid == kHostPackageUid) { lsplant::JUTFString nice_name_str(env_, args->nice_name); if (nice_name_str.get() == std::string(kManagePackageName)) { LOGI("Manager app detected. Granting internet permissions."); is_manager_app_ = true; // Add GID_INET to the GID list. int original_gids_count = env_->GetArrayLength(args->gids); jintArray new_gids = env_->NewIntArray(original_gids_count + 1); if (env_->ExceptionCheck()) { LOGE("Failed to create new GID array for manager."); env_->ExceptionClear(); // Clear exception to prevent a crash. return; } jint *gids_array = env_->GetIntArrayElements(args->gids, nullptr); env_->SetIntArrayRegion(new_gids, 0, original_gids_count, gids_array); env_->ReleaseIntArrayElements(args->gids, gids_array, JNI_ABORT); jint inet_gid = GID_INET; env_->SetIntArrayRegion(new_gids, original_gids_count, 1, &inet_gid); args->nice_name = env_->NewStringUTF(INJECTED_PACKAGE_NAME); args->gids = new_gids; } } IPCBridge::GetInstance().Initialize(env_); // --- Injection Decision Logic --- // Determine if the current process is a valid target for injection. lsplant::JUTFString nice_name_str(env_, args->nice_name); // An app without a data directory cannot be a target. if (!args->app_data_dir) { LOGD("Skipping injection for '{}': no app_data_dir.", nice_name_str.get()); return; } // Child Zygotes are specialized zygotes for apps like WebView and are not targets. if (args->is_child_zygote && *args->is_child_zygote) { LOGD("Skipping injection for '{}': is a child zygote.", nice_name_str.get()); return; } // Skip isolated processes, which are heavily sandboxed. const uid_t app_id = args->uid % PER_USER_RANGE; if ((app_id >= FIRST_ISOLATED_UID && app_id <= LAST_ISOLATED_UID) || (app_id >= FIRST_APP_ZYGOTE_ISOLATED_UID && app_id <= LAST_APP_ZYGOTE_ISOLATED_UID) || app_id == SHARED_RELRO_UID) { LOGV("Skipping injection for '{}': is an isolated process (UID: %d).", nice_name_str.get(), app_id); return; } // If we passed all checks, mark this process for injection. should_inject_ = true; LOGV("Process '{}' (UID: {}) is marked for injection.", nice_name_str.get(), args->uid); } void VectorModule::postAppSpecialize(const zygisk::AppSpecializeArgs *args) { if (!should_inject_) { SetAllowUnload(true); // Not a target, allow module to be unloaded. return; } if (is_manager_app_) { args->nice_name = env_->NewStringUTF(kManagePackageName); } // --- Framework Injection --- lsplant::JUTFString nice_name_str(env_, args->nice_name); LOGD("Attempting injection into '{}'.", nice_name_str.get()); auto &ipc_bridge = IPCBridge::GetInstance(); auto binder = ipc_bridge.RequestAppBinder(env_, args->nice_name); if (!binder) { LOGD("No IPC binder obtained for '{}'. Skipping injection.", nice_name_str.get()); SetAllowUnload(true); return; } // Fetch resources from the manager service. auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, binder.get()); if (dex_fd < 0) { LOGE("Failed to fetch framework DEX for '{}'.", nice_name_str.get()); SetAllowUnload(true); return; } auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, binder.get()); ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); { PreloadedDex dex(dex_fd, dex_size); this->LoadDex(env_, std::move(dex)); } close(dex_fd); // The FD is duplicated by mmap, we can close it now. // Initialize ART hooks via the native library. this->InitArtHooker(env_, init_info_); // Initialize JNI hooks via the native library. this->InitHooks(env_); // Find the Java entrypoint. this->SetupEntryClass(env_); // Hand off control to the Java side of the framework. this->FindAndCall( env_, "forkCommon", "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_FALSE, JNI_FALSE, args->nice_name, args->app_data_dir, binder.get(), is_manager_app_); LOGV("Injected Vector framework into '{}'.", nice_name_str.get()); SetAllowUnload(false); // We are injected, PREVENT module unloading. } void VectorModule::preServerSpecialize(zygisk::ServerSpecializeArgs *args) { // The system server is always a target for injection. should_inject_ = true; LOGI("System server process detected. Marking for injection."); // Initialize our IPC bridge singleton. IPCBridge::GetInstance().Initialize(env_); } void VectorModule::postServerSpecialize(const zygisk::ServerSpecializeArgs *args) { if (!should_inject_) { SetAllowUnload(true); return; } LOGD("Attempting injection into system_server."); // --- Device-Specific Workaround --- // Some ZTE devices require argv[0] to be explicitly set to "system_server" // for certain services to function correctly after modification. if (__system_property_find("ro.vendor.product.ztename")) { LOGV("Applying ZTE-specific workaround: setting argv[0] to system_server."); auto process_class = lsplant::ScopedLocalRef(env_, env_->FindClass("android/os/Process")); if (process_class) { auto set_argv0_mid = env_->GetStaticMethodID(process_class.get(), "setArgV0", "(Ljava/lang/String;)V"); auto name_str = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system_server")); if (set_argv0_mid && name_str) { env_->CallStaticVoidMethod(process_class.get(), set_argv0_mid, name_str.get()); } } if (env_->ExceptionCheck()) { LOGW("Exception occurred during ZTE workaround."); env_->ExceptionClear(); } } // --- Framework Injection for System Server --- auto &ipc_bridge = IPCBridge::GetInstance(); std::string bridgeServiceName = "serial"; bool is_late_inject = (args->runtime_flags & RuntimeFlags::LATE_INJECT) != 0; if (is_late_inject) bridgeServiceName = "serial_vector"; auto system_binder = ipc_bridge.RequestSystemServerBinder(env_, bridgeServiceName); if (!system_binder) { LOGE("Failed to get system server IPC binder. Aborting injection."); SetAllowUnload(true); // Allow unload on failure. return; } auto manager_binder = ipc_bridge.RequestManagerBinderFromSystemServer(env_, system_binder.get()); // Use either the direct manager binder if available, // otherwise proxy through the system binder. jobject effective_binder = manager_binder ? manager_binder.get() : system_binder.get(); auto [dex_fd, dex_size] = ipc_bridge.FetchFrameworkDex(env_, effective_binder); if (dex_fd < 0) { LOGE("Failed to fetch framework DEX for system_server."); SetAllowUnload(true); return; } auto obfs_map = ipc_bridge.FetchObfuscationMap(env_, effective_binder); ConfigBridge::GetInstance()->obfuscation_map(std::move(obfs_map)); { PreloadedDex dex(dex_fd, dex_size); this->LoadDex(env_, std::move(dex)); } close(dex_fd); ipc_bridge.HookBridge(env_); this->InitArtHooker(env_, init_info_); this->InitHooks(env_); this->SetupEntryClass(env_); auto system_name = lsplant::ScopedLocalRef(env_, env_->NewStringUTF("system")); this->FindAndCall(env_, "forkCommon", "(ZZLjava/lang/String;Ljava/lang/String;Landroid/os/IBinder;)V", JNI_TRUE, is_late_inject, system_name.get(), nullptr, manager_binder.get(), is_manager_app_); LOGI("Injected Vector framework into system_server."); SetAllowUnload(false); // We are injected, PREVENT module unloading. } void VectorModule::SetAllowUnload(bool unload) { if (api_ && unload) { LOGD("Allowing Zygisk to unload module library."); api_->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); // Release the pointer from the unique_ptr's control. This prevents the // static unique_ptr's destructor from calling delete on our object, which // would cause a double-free when the Zygisk framework cleans up. if (instance_.release() != nullptr) { LOGD("Module context singleton released."); } } else { LOGD("Preventing Zygisk from unloading module library."); } } } // namespace vector::native::module // ========================================================================================= // Zygisk Module Registration // ========================================================================================= REGISTER_ZYGISK_MODULE(vector::native::module::VectorModule); ================================================ FILE: zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerHooker.kt ================================================ package org.matrix.vector import android.annotation.SuppressLint import android.app.ActivityThread import android.app.LoadedApk import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.* import android.os.* import android.util.AndroidRuntimeException import android.util.ArrayMap import android.webkit.WebViewDelegate import android.webkit.WebViewFactory import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers import hidden.HiddenApiBridge import java.io.FileInputStream import java.io.FileOutputStream import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap import org.lsposed.lspd.ILSPManagerService import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient import org.lsposed.lspd.util.Hookers import org.lsposed.lspd.util.Utils /** The "Parasite" logic. Injects the LSPosed Manager APK into a host process (shell). */ @SuppressLint("StaticFieldLeak") object ParasiticManagerHooker { private const val CHROMIUM_WEBVIEW_FACTORY_METHOD = "create" private var managerPkgInfo: PackageInfo? = null private var managerFd: Int = -1 // Manually track Activity states since the system is unaware of our spoofed activities private val states = ConcurrentHashMap() private val persistentStates = ConcurrentHashMap() /** Constructs a hybrid PackageInfo. Combines the Manager's code with the Host's environment. */ @Synchronized private fun getManagerPkgInfo(appInfo: ApplicationInfo?): PackageInfo? { if (managerPkgInfo == null && appInfo != null) { runCatching { val ctx: Context = ActivityThread.currentActivityThread().systemContext var sourcePath = "/proc/self/fd/$managerFd" // SDK <= 28 (Android 9) cannot reliably parse APKs via FD paths in all // contexts. // We copy the APK to the host's cache as a workaround. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { val dstPath = "${appInfo.dataDir}/cache/lsposed.apk" runCatching { FileInputStream(sourcePath).use { input -> FileOutputStream(dstPath).use { output -> input.channel.transferTo( 0, input.channel.size(), output.channel, ) } } sourcePath = dstPath } .onFailure { Hookers.logE("Failed to copy parasitic APK", it) } } val pkgInfo = ctx.packageManager.getPackageArchiveInfo( sourcePath, PackageManager.GET_ACTIVITIES, ) ?: throw RuntimeException("PackageManager failed to parse $sourcePath") // Transplant identity: Keep host's paths and UID, swap the code source pkgInfo.applicationInfo!!.apply { sourceDir = sourcePath publicSourceDir = sourcePath nativeLibraryDir = appInfo.nativeLibraryDir packageName = appInfo.packageName dataDir = HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(appInfo) deviceProtectedDataDir = appInfo.deviceProtectedDataDir processName = appInfo.processName uid = appInfo.uid // A14 QPR3 Fix: Ensure the flag for code existence is set flags = flags or ApplicationInfo.FLAG_HAS_CODE HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir( this, HiddenApiBridge.ApplicationInfo_credentialProtectedDataDir(appInfo), ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { HiddenApiBridge.ApplicationInfo_overlayPaths( this, HiddenApiBridge.ApplicationInfo_overlayPaths(appInfo), ) } HiddenApiBridge.ApplicationInfo_resourceDirs( this, HiddenApiBridge.ApplicationInfo_resourceDirs(appInfo), ) } managerPkgInfo = pkgInfo } .onFailure { Utils.logE("Failed to construct manager PkgInfo", it) } } return managerPkgInfo } /** * Passes the IPC binder to the Manager's internal [Constants] class so it can communicate back * to the LSPosed system service. */ private fun sendBinderToManager(classLoader: ClassLoader, binder: IBinder) { runCatching { val clazz = XposedHelpers.findClass( BuildConfig.ManagerPackageName + ".Constants", classLoader, ) val ok = XposedHelpers.callStaticMethod( clazz, "setBinder", arrayOf(IBinder::class.java), binder, ) as Boolean if (!ok) throw RuntimeException("setBinder returned false") } .onFailure { Utils.logW("Could not send binder to LSPosed Manager", it) } } private fun hookForManager(managerService: ILSPManagerService) { // Hook 1: Swap ApplicationInfo during host binding XposedHelpers.findAndHookMethod( ActivityThread::class.java, "handleBindApplication", "android.app.ActivityThread\$AppBindData", object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam<*>) { Hookers.logD("ActivityThread#handleBindApplication() starts") val bindData = param.args[0] val hostAppInfo = XposedHelpers.getObjectField(bindData, "appInfo") as ApplicationInfo val parasiticInfo = getManagerPkgInfo(hostAppInfo)?.applicationInfo XposedHelpers.setObjectField(bindData, "appInfo", parasiticInfo) } }, ) // Hook 2: Inject APK path into the ClassLoader var classLoaderUnhook: XC_MethodHook.Unhook? = null val classLoaderHook = object : XC_MethodHook() { override fun afterHookedMethod(param: MethodHookParam<*>) { val pkgInfo = getManagerPkgInfo(null) ?: return val mAppInfo = XposedHelpers.getObjectField(param.thisObject, "mApplicationInfo") val managerAppInfo = pkgInfo.applicationInfo!! if (mAppInfo == managerAppInfo) { val dexPath = managerAppInfo.sourceDir val pathClassLoader = param.result as ClassLoader Hookers.logD("Injecting DEX into LoadedApk ClassLoader: $pathClassLoader") val pathList = XposedHelpers.getObjectField(pathClassLoader, "pathList") val dexPaths = XposedHelpers.callMethod(pathList, "getDexPaths") as List<*> if (!dexPaths.contains(dexPath)) { Utils.logW("Manager APK not found in ClassLoader, adding manually...") XposedHelpers.callMethod(pathClassLoader, "addDexPath", dexPath) } sendBinderToManager(pathClassLoader, managerService.asBinder()) classLoaderUnhook!!.unhook() // Only need to inject once } } } classLoaderUnhook = XposedHelpers.findAndHookMethod( LoadedApk::class.java, "getClassLoader", classLoaderHook, ) // Hook 3: Activity Lifecycle & Intent Redirection val activityClientRecordClass = XposedHelpers.findClass( "android.app.ActivityThread\$ActivityClientRecord", ActivityThread::class.java.classLoader, ) val activityHooker = object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam<*>) { param.args.forEachIndexed { i, arg -> if (arg is ActivityInfo) { val pkgInfo = getManagerPkgInfo(arg.applicationInfo) ?: return@forEachIndexed pkgInfo.activities ?.find { it.name == BuildConfig.ManagerPackageName + ".ui.activity.MainActivity" } ?.let { it.applicationInfo = pkgInfo.applicationInfo param.args[i] = it } } if (arg is Intent) { arg.component = ComponentName( arg.component!!.packageName, BuildConfig.ManagerPackageName + ".ui.activity.MainActivity", ) } } // Captured State Injection if (param.method.getName() == "scheduleLaunchActivity") { var currentAInfo: ActivityInfo? = null val types = (param.method as Method).parameterTypes types.forEachIndexed { idx, type -> when (type) { ActivityInfo::class.java -> currentAInfo = param.args[idx] as ActivityInfo Bundle::class.java -> currentAInfo?.let { info -> states[info.name]?.let { param.args[idx] = it } } PersistableBundle::class.java -> currentAInfo?.let { info -> persistentStates[info.name]?.let { param.args[idx] = it } } } } } } override fun afterHookedMethod(param: MethodHookParam<*>) { if (!activityClientRecordClass.isInstance(param.thisObject)) return param.args.filterIsInstance().forEach { aInfo -> Hookers.logD("Restoring state for Activity: ${aInfo.name}") states[aInfo.name]?.let { XposedHelpers.setObjectField(param.thisObject, "state", it) } persistentStates[aInfo.name]?.let { XposedHelpers.setObjectField(param.thisObject, "persistentState", it) } } } } XposedBridge.hookAllConstructors(activityClientRecordClass, activityHooker) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { val appThreadClass = XposedHelpers.findClass( "android.app.ActivityThread\$ApplicationThread", ActivityThread::class.java.classLoader, ) XposedBridge.hookAllMethods(appThreadClass, "scheduleLaunchActivity", activityHooker) } // Hook 4: Ignore Receivers (Manager doesn't need to handle host receivers) XposedBridge.hookAllMethods( ActivityThread::class.java, "handleReceiver", object : XC_MethodReplacement() { override fun replaceHookedMethod(param: MethodHookParam<*>): Any? { param.args.filterIsInstance().forEach { it.finish() } return null } }, ) // Hook 5: Provider Context Spoofing XposedBridge.hookAllMethods( ActivityThread::class.java, "installProvider", object : XC_MethodHook() { private var originalContext: Context? = null override fun beforeHookedMethod(param: MethodHookParam<*>) { var ctx: Context? = null var info: ProviderInfo? = null var ctxIdx = -1 param.args.forEachIndexed { i, arg -> when (arg) { is Context -> { ctx = arg ctxIdx = i } is ProviderInfo -> info = arg } } val pkgInfo = getManagerPkgInfo(null) if (ctx != null && info != null && pkgInfo != null) { val managerPackage = pkgInfo.applicationInfo!!.packageName if (info.applicationInfo.packageName != managerPackage) return if (originalContext == null) { // Create a fake original context to satisfy internal package checks info.applicationInfo.packageName = "$managerPackage.origin" val compatibilityInfo = HiddenApiBridge.Resources_getCompatibilityInfo(ctx!!.resources) val originalPkgInfo = ActivityThread.currentActivityThread() .getPackageInfoNoCheck(info.applicationInfo, compatibilityInfo) XposedHelpers.setObjectField( originalPkgInfo, "mPackageName", managerPackage, ) val contextImplClass = XposedHelpers.findClass("android.app.ContextImpl", null) originalContext = XposedHelpers.callStaticMethod( contextImplClass, "createAppContext", ActivityThread.currentActivityThread(), originalPkgInfo, ) as Context info.applicationInfo.packageName = managerPackage } param.args[ctxIdx] = originalContext } } }, ) // Hook 6: WebView initialization within Parasitic process XposedHelpers.findAndHookMethod( WebViewFactory::class.java, "getProvider", object : XC_MethodReplacement() { override fun replaceHookedMethod(param: MethodHookParam<*>): Any? { val existing = XposedHelpers.getStaticObjectField( WebViewFactory::class.java, "sProviderInstance", ) if (existing != null) return existing val providerClass = XposedHelpers.callStaticMethod( WebViewFactory::class.java, "getProviderClass", ) as Class<*> return try { val staticFactory = providerClass.getMethod( CHROMIUM_WEBVIEW_FACTORY_METHOD, WebViewDelegate::class.java, ) val delegateCtor = WebViewDelegate::class.java.getDeclaredConstructor().apply { isAccessible = true } val instance = staticFactory.invoke(null, delegateCtor.newInstance()) XposedHelpers.setStaticObjectField( WebViewFactory::class.java, "sProviderInstance", instance, ) Hookers.logD("WebView provider initialized: $instance") instance } catch (e: Exception) { Hookers.logE("WebView initialization failed", e) throw AndroidRuntimeException(e) } } }, ) // Hook 7: State Capture on Stop val stateCaptureHooker = object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam<*>) { runCatching { var record = param.args[0] if (record is IBinder) { val activities = XposedHelpers.getObjectField(param.thisObject, "mActivities") as ArrayMap<*, *> record = activities[record] ?: return } val saveMethod = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) "callActivityOnSaveInstanceState" else "callCallActivityOnSaveInstanceState" XposedHelpers.callMethod(param.thisObject, saveMethod, record) val state = XposedHelpers.getObjectField(record, "state") as? Bundle val pState = XposedHelpers.getObjectField(record, "persistentState") as? PersistableBundle val aInfo = XposedHelpers.getObjectField(record, "activityInfo") as ActivityInfo state?.let { states[aInfo.name] = it } pState?.let { persistentStates[aInfo.name] = it } Hookers.logD("Saved state for ${aInfo.name}") } .onFailure { Hookers.logE("Failed to save activity state", it) } } } XposedBridge.hookAllMethods( ActivityThread::class.java, "performStopActivityInner", stateCaptureHooker, ) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { XposedHelpers.findAndHookMethod( ActivityThread::class.java, "performDestroyActivity", IBinder::class.java, Boolean::class.javaPrimitiveType, Int::class.javaPrimitiveType, Boolean::class.javaPrimitiveType, stateCaptureHooker, ) } } /** Entry point. Checks if the current process should host the parasitic manager. */ @JvmStatic fun start(): Boolean { val binderList = mutableListOf() return try { serviceClient.requestInjectedManagerBinder(binderList).use { pfd -> managerFd = pfd.detachFd() val managerService = ILSPManagerService.Stub.asInterface(binderList[0]) hookForManager(managerService) Utils.logD("Vector manager injected successfully into process.") true } } catch (e: Throwable) { Utils.logE("Parasitic injection failed", e) false } } } ================================================ FILE: zygisk/src/main/kotlin/org/matrix/vector/ParasiticManagerSystemHooker.kt ================================================ package org.matrix.vector import android.annotation.SuppressLint import android.app.ProfilerInfo import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.ResolveInfo import io.github.libxposed.api.XposedInterface import org.lsposed.lspd.hooker.HandleSystemServerProcessHooker import org.lsposed.lspd.impl.LSPosedHelper import org.lsposed.lspd.util.Utils import org.matrix.vector.service.BridgeService /** * Handles System-Server side logic for the Parasitic Manager. * * When a user tries to open the LSPosed Manager, the system normally wouldn't know how to handle it * because it isn't "installed." This class intercepts the activity resolution and tells the system * to launch it in a special process. */ class ParasiticManagerSystemHooker : HandleSystemServerProcessHooker.Callback { companion object { @JvmStatic fun start() { // Register this class as the handler for system_server initialization HandleSystemServerProcessHooker.callback = ParasiticManagerSystemHooker() } } /** Intercepts Activity resolution in the System Server. */ object Hooker : XposedInterface.Hooker { @JvmStatic fun after(callback: XposedInterface.AfterHookCallback) { val intent = callback.args[0] as? Intent ?: return // Check if this intent is meant for the LSPosed Manager if (!intent.hasCategory(BuildConfig.ManagerPackageName + ".LAUNCH_MANAGER")) return val result = callback.result as? ActivityInfo ?: return // We only intercept if it's currently resolving to the shell/fallback if (result.packageName != BuildConfig.InjectedPackageName) return // --- Redirection Logic --- // We create a copy of the ActivityInfo to avoid polluting the system's cache. val redirectedInfo = ActivityInfo(result).apply { // Force the manager to run in its own dedicated process name processName = BuildConfig.ManagerPackageName // Set a standard theme so transition animations work correctly theme = android.R.style.Theme_DeviceDefault_Settings // Ensure the activity isn't excluded from recents by host flags flags = flags and (ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS or ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS) .inv() } // Notify the bridge service that we are about to start the manager BridgeService.getService()?.preStartManager() // Replace the original ResolveInfo with our parasitic one callback.result = redirectedInfo } } @SuppressLint("PrivateApi") override fun onSystemServerLoaded(classLoader: ClassLoader) { runCatching { // Android versions change the name of the internal class responsible for activity // tracking. // We check the most likely candidates based on API levels (9.0 through 14+). val supervisorClass = try { // Android 12.0 - 14+ Class.forName( "com.android.server.wm.ActivityTaskSupervisor", false, classLoader, ) } catch (e: ClassNotFoundException) { try { // Android 10 - 11 Class.forName( "com.android.server.wm.ActivityStackSupervisor", false, classLoader, ) } catch (e2: ClassNotFoundException) { // Android 8.1 - 9 Class.forName( "com.android.server.am.ActivityStackSupervisor", false, classLoader, ) } } // Hook the resolution method to inject our redirection logic LSPosedHelper.hookMethod( Hooker::class.java, supervisorClass, "resolveActivity", Intent::class.java, ResolveInfo::class.java, Int::class.javaPrimitiveType, ProfilerInfo::class.java, ) Utils.logD("Successfully hooked Activity Supervisor for Manager redirection.") } .onFailure { Utils.logE("Failed to hook system server activity resolution", it) } } } ================================================ FILE: zygisk/src/main/kotlin/org/matrix/vector/core/Main.kt ================================================ package org.matrix.vector.core import android.os.IBinder import android.os.Process import org.lsposed.lspd.core.ApplicationServiceClient.serviceClient import org.lsposed.lspd.core.Startup import org.lsposed.lspd.service.ILSPApplicationService import org.lsposed.lspd.util.Utils import org.matrix.vector.BuildConfig import org.matrix.vector.ParasiticManagerHooker import org.matrix.vector.ParasiticManagerSystemHooker /** Main entry point for the Java-side loader, invoked via JNI from the Vector Zygisk module. */ object Main { /** * Shared initialization logic for both System Server and Application processes. * * @param isSystem True if this is the system_server process. * @param isLateInject True if Zygisk APIs are not invoked via hooks * @param niceName The process name (e.g., package name or "system"). * @param appDir The application's data directory. * @param binder The Binder token associated with the application service. */ @JvmStatic fun forkCommon( isSystem: Boolean, isLateInject: Boolean, niceName: String, appDir: String?, binder: IBinder, ) { // Initialize system-specific resolution hooks if in system_server if (isSystem) { ParasiticManagerSystemHooker.start() } // Initialize Xposed bridge components val appService = ILSPApplicationService.Stub.asInterface(binder) Startup.initXposed(isSystem, niceName, appDir, appService) // Configure logging levels from the service client runCatching { Utils.Log.muted = serviceClient.isLogMuted } .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 } // Standard Xposed module loading for third-party apps Utils.logI("Loading Vector/Xposed for $niceName (UID: ${Process.myUid()})") Startup.bootstrapXposed(isSystem && isLateInject) } } ================================================ FILE: zygisk/src/main/kotlin/org/matrix/vector/service/BridgeService.kt ================================================ package org.matrix.vector.service import android.app.ActivityThread 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.util.Utils.Log /** * Manages manual Binder transactions for the Vector framework. * * This service is not registered in ServiceManager. Instead, the Zygisk native module intercepts * [Binder.execTransact] and redirects calls with the [TRANSACTION_CODE] to this class. */ 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" /** 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 /** Cleans up service references if the remote LSPosed process crashes. */ private val serviceRecipient: DeathRecipient = DeathRecipient { Log.e(TAG, "LSPosed system service died.") serviceBinder?.unlinkToDeath(this.serviceRecipient, 0) serviceBinder = null service = null } /** Returns the active LSPosed system service interface. */ @JvmStatic fun getService(): ILSPosedService? = service /** * Initializes the client-side connection to the LSPosed system service. * * @param binder The raw binder for [ILSPosedService]. */ private fun receiveFromBridge(binder: IBinder?) { if (binder == null) { Log.e(TAG, "Received null binder from bridge.") return } // Cleanup old death recipient if we are re-initializing val token = Binder.clearCallingIdentity() try { serviceBinder?.unlinkToDeath(serviceRecipient, 0) } finally { Binder.restoreCallingIdentity(token) } // Allow blocking calls since we are often in a synchronous fork path val blockingBinder = Binder_allowBlocking(binder) serviceBinder = blockingBinder service = ILSPosedService.Stub.asInterface(blockingBinder) runCatching { blockingBinder.linkToDeath(serviceRecipient, 0) } .onFailure { Log.e(TAG, "Failed to link to service death", it) } // Provide the system context to the service so it can manage system-wide states runCatching { val activityThread = ActivityThread.currentActivityThread() val at = activityThread.applicationThread as android.app.IApplicationThread val atBinder = at.asBinder() val systemCtx = activityThread.systemContext service?.dispatchSystemServerContext( atBinder, Context_getActivityToken(systemCtx), "Zygisk", ) } .onFailure { Log.e(TAG, "Failed to dispatch system context", it) } Log.i(TAG, "LSPosed system service binder linked.") } /** Handles manual parcel transactions. Called via reflection/JNI from the native hook. */ @JvmStatic fun onTransact(data: Parcel, reply: Parcel?, flags: Int): Boolean { return try { val actionIdx = data.readInt() val action = Action.values().getOrElse(actionIdx) { Action.UNKNOWN } Log.d(TAG, "onTransact: action=$action, callerUid=${Binder.getCallingUid()}") when (action) { Action.SEND_BINDER -> { // Only allow root (UID 0) to push the initial service binder if (Binder.getCallingUid() == 0) { receiveFromBridge(data.readStrongBinder()) reply?.writeNoException() true } else false } Action.GET_BINDER -> { val processName = data.readString() val heartBeat = data.readStrongBinder() val appService = service?.requestApplicationService( Binder.getCallingUid(), Binder.getCallingPid(), processName, heartBeat, ) if (appService != null && reply != null) { reply.writeNoException() reply.writeStrongBinder(appService.asBinder()) 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) { Log.e(TAG, "Error handling bridge transaction", e) false } } /** * Entry point for the JNI hook in [IPCBridge.cpp]. * * @param obj The Binder object being called. * @param code The transaction code. * @param dataObj Native pointer to the data Parcel. * @param replyObj Native pointer to the reply Parcel. * @param flags Transaction flags. * @return True if the transaction was handled. */ @JvmStatic fun execTransact(obj: IBinder, code: Int, dataObj: Long, replyObj: Long, flags: Int): Boolean { if (code != TRANSACTION_CODE) return false val data = dataObj.asParcel() val reply = replyObj.asParcel() if (data == null || reply == null) { Log.w(TAG, "Transaction dropped: null parcel pointers.") return false } return try { onTransact(data, reply, flags) } catch (e: Exception) { if (flags and IBinder.FLAG_ONEWAY == 0) { reply.setDataPosition(0) reply.writeException(e) } Log.e(TAG, "Exception during execTransact", e) true // We handled it, even if by returning an exception } finally { data.recycle() reply.recycle() } } } ================================================ FILE: zygisk/src/main/kotlin/org/matrix/vector/service/ParcelUtils.kt ================================================ package org.matrix.vector.service import android.os.Parcel import java.lang.reflect.Method /** * Internal utilities for raw [Parcel] manipulation. Used primarily for IPC transactions that bypass * standard AIDL. */ object ParcelUtils { private val obtainMethod: Method by lazy { Parcel::class.java.getDeclaredMethod("obtain", Long::class.java).apply { isAccessible = true } } /** * Reconstructs a Java [Parcel] object from a native C++ parcel pointer. Required for manual * Binder transaction interception in [BridgeService]. * * @param ptr The native pointer address (long). * @return A Java Parcel instance wrapping the native pointer, or null if pointer is 0. */ @JvmStatic fun fromNativePointer(ptr: Long): Parcel? { if (ptr == 0L) return null return try { obtainMethod.invoke(null, ptr) as? Parcel } catch (e: Throwable) { throw RuntimeException("Failed to obtain Parcel from native pointer: $ptr", e) } } } /** Extension to allow [Long] native pointers to be treated as Parcels. */ fun Long.asParcel(): Parcel? = ParcelUtils.fromNativePointer(this) ================================================ FILE: zygisk/zygisk.json ================================================ { }