Repository: topjohnwu/Magisk
Branch: master
Commit: 9035a9480440
Files: 740
Total size: 3.4 MB
Directory structure:
gitextract_zn19wufn/
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── config.yml
│ ├── actions/
│ │ └── setup/
│ │ ├── action.yml
│ │ └── sccache.sh
│ ├── ci.prop
│ ├── kvm.sh
│ └── workflows/
│ └── build.yml
├── .gitignore
├── .gitmodules
├── LICENSE
├── README.MD
├── app/
│ ├── .gitignore
│ ├── apk/
│ │ ├── build.gradle.kts
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── topjohnwu/
│ │ │ └── magisk/
│ │ │ ├── arch/
│ │ │ │ ├── AsyncLoadViewModel.kt
│ │ │ │ ├── BaseFragment.kt
│ │ │ │ ├── BaseViewModel.kt
│ │ │ │ ├── NavigationActivity.kt
│ │ │ │ ├── UIActivity.kt
│ │ │ │ ├── ViewEvent.kt
│ │ │ │ └── ViewModelHolder.kt
│ │ │ ├── databinding/
│ │ │ │ ├── DataBindingAdapters.kt
│ │ │ │ ├── DiffObservableList.kt
│ │ │ │ ├── MergeObservableList.kt
│ │ │ │ ├── ObservableHost.kt
│ │ │ │ ├── RecyclerViewItems.kt
│ │ │ │ └── RvItemAdapter.kt
│ │ │ ├── dialog/
│ │ │ │ ├── DarkThemeDialog.kt
│ │ │ │ ├── EnvFixDialog.kt
│ │ │ │ ├── LocalModuleInstallDialog.kt
│ │ │ │ ├── ManagerInstallDialog.kt
│ │ │ │ ├── MarkDownDialog.kt
│ │ │ │ ├── OnlineModuleInstallDialog.kt
│ │ │ │ ├── SecondSlotWarningDialog.kt
│ │ │ │ ├── SuperuserRevokeDialog.kt
│ │ │ │ └── UninstallDialog.kt
│ │ │ ├── events/
│ │ │ │ └── ViewEvents.kt
│ │ │ ├── ui/
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── deny/
│ │ │ │ │ ├── AppProcessInfo.kt
│ │ │ │ │ ├── DenyListFragment.kt
│ │ │ │ │ ├── DenyListRvItem.kt
│ │ │ │ │ └── DenyListViewModel.kt
│ │ │ │ ├── flash/
│ │ │ │ │ ├── ConsoleItem.kt
│ │ │ │ │ ├── FlashFragment.kt
│ │ │ │ │ └── FlashViewModel.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── DeveloperItem.kt
│ │ │ │ │ ├── HomeFragment.kt
│ │ │ │ │ ├── HomeViewModel.kt
│ │ │ │ │ └── RebootMenu.kt
│ │ │ │ ├── install/
│ │ │ │ │ ├── InstallFragment.kt
│ │ │ │ │ └── InstallViewModel.kt
│ │ │ │ ├── log/
│ │ │ │ │ ├── LogFragment.kt
│ │ │ │ │ ├── LogRvItem.kt
│ │ │ │ │ ├── LogViewModel.kt
│ │ │ │ │ └── SuLogRvItem.kt
│ │ │ │ ├── module/
│ │ │ │ │ ├── ActionFragment.kt
│ │ │ │ │ ├── ActionViewModel.kt
│ │ │ │ │ ├── ModuleFragment.kt
│ │ │ │ │ ├── ModuleRvItem.kt
│ │ │ │ │ └── ModuleViewModel.kt
│ │ │ │ ├── settings/
│ │ │ │ │ ├── BaseSettingsItem.kt
│ │ │ │ │ ├── SettingsFragment.kt
│ │ │ │ │ ├── SettingsItems.kt
│ │ │ │ │ └── SettingsViewModel.kt
│ │ │ │ ├── superuser/
│ │ │ │ │ ├── PolicyRvItem.kt
│ │ │ │ │ ├── SuperuserFragment.kt
│ │ │ │ │ └── SuperuserViewModel.kt
│ │ │ │ ├── surequest/
│ │ │ │ │ ├── SuRequestActivity.kt
│ │ │ │ │ └── SuRequestViewModel.kt
│ │ │ │ └── theme/
│ │ │ │ ├── Theme.kt
│ │ │ │ ├── ThemeFragment.kt
│ │ │ │ └── ThemeViewModel.kt
│ │ │ ├── utils/
│ │ │ │ ├── AccessibilityUtils.kt
│ │ │ │ ├── MotionRevealHelper.kt
│ │ │ │ └── TextHolder.kt
│ │ │ ├── view/
│ │ │ │ ├── MagiskDialog.kt
│ │ │ │ ├── TappableHeadlineItem.kt
│ │ │ │ └── TextItem.kt
│ │ │ └── widget/
│ │ │ └── ConcealableBottomNavigationView.java
│ │ └── res/
│ │ ├── anim/
│ │ │ ├── fragment_enter.xml
│ │ │ ├── fragment_enter_pop.xml
│ │ │ ├── fragment_exit.xml
│ │ │ └── fragment_exit_pop.xml
│ │ ├── color/
│ │ │ ├── color_card_background_color_selector.xml
│ │ │ ├── color_error_transient.xml
│ │ │ ├── color_menu_tint.xml
│ │ │ ├── color_on_primary_transient.xml
│ │ │ ├── color_primary_error_transient.xml
│ │ │ ├── color_primary_transient.xml
│ │ │ ├── color_state_primary_transient.xml
│ │ │ └── color_text_transient.xml
│ │ ├── drawable/
│ │ │ ├── avd_bug_from_filled.xml
│ │ │ ├── avd_bug_to_filled.xml
│ │ │ ├── avd_circle_check_from_filled.xml
│ │ │ ├── avd_circle_check_to_filled.xml
│ │ │ ├── avd_home_from_filled.xml
│ │ │ ├── avd_home_to_filled.xml
│ │ │ ├── avd_module_from_filled.xml
│ │ │ ├── avd_module_to_filled.xml
│ │ │ ├── avd_settings_from_filled.xml
│ │ │ ├── avd_settings_to_filled.xml
│ │ │ ├── avd_superuser_from_filled.xml
│ │ │ ├── avd_superuser_to_filled.xml
│ │ │ ├── bg_line_bottom_rounded.xml
│ │ │ ├── bg_line_top_rounded.xml
│ │ │ ├── bg_selection_circle_green.xml
│ │ │ ├── ic_action_md2.xml
│ │ │ ├── ic_back_md2.xml
│ │ │ ├── ic_bug_filled_md2.xml
│ │ │ ├── ic_bug_md2.xml
│ │ │ ├── ic_bug_outlined_md2.xml
│ │ │ ├── ic_check_circle_checked_md2.xml
│ │ │ ├── ic_check_circle_md2.xml
│ │ │ ├── ic_check_circle_unchecked_md2.xml
│ │ │ ├── ic_check_md2.xml
│ │ │ ├── ic_close_md2.xml
│ │ │ ├── ic_day.xml
│ │ │ ├── ic_day_night.xml
│ │ │ ├── ic_delete_md2.xml
│ │ │ ├── ic_download_md2.xml
│ │ │ ├── ic_folder_list.xml
│ │ │ ├── ic_forth_md2.xml
│ │ │ ├── ic_home_filled_md2.xml
│ │ │ ├── ic_home_md2.xml
│ │ │ ├── ic_home_outlined_md2.xml
│ │ │ ├── ic_install.xml
│ │ │ ├── ic_manager.xml
│ │ │ ├── ic_module_filled_md2.xml
│ │ │ ├── ic_module_md2.xml
│ │ │ ├── ic_module_outlined_md2.xml
│ │ │ ├── ic_module_storage_md2.xml
│ │ │ ├── ic_night.xml
│ │ │ ├── ic_notifications_md2.xml
│ │ │ ├── ic_paint.xml
│ │ │ ├── ic_restart.xml
│ │ │ ├── ic_save_md2.xml
│ │ │ ├── ic_search_md2.xml
│ │ │ ├── ic_settings_filled_md2.xml
│ │ │ ├── ic_settings_md2.xml
│ │ │ ├── ic_settings_outlined_md2.xml
│ │ │ ├── ic_superuser_filled_md2.xml
│ │ │ ├── ic_superuser_md2.xml
│ │ │ ├── ic_superuser_outlined_md2.xml
│ │ │ └── ic_update_md2.xml
│ │ ├── layout/
│ │ │ ├── activity_main_md2.xml
│ │ │ ├── activity_request.xml
│ │ │ ├── dialog_magisk_base.xml
│ │ │ ├── dialog_settings_app_name.xml
│ │ │ ├── dialog_settings_download_path.xml
│ │ │ ├── dialog_settings_update_channel.xml
│ │ │ ├── fragment_action_md2.xml
│ │ │ ├── fragment_deny_md2.xml
│ │ │ ├── fragment_flash_md2.xml
│ │ │ ├── fragment_home_md2.xml
│ │ │ ├── fragment_install_md2.xml
│ │ │ ├── fragment_log_md2.xml
│ │ │ ├── fragment_module_md2.xml
│ │ │ ├── fragment_settings_md2.xml
│ │ │ ├── fragment_superuser_md2.xml
│ │ │ ├── fragment_theme_md2.xml
│ │ │ ├── include_home_magisk.xml
│ │ │ ├── include_home_manager.xml
│ │ │ ├── include_log_magisk.xml
│ │ │ ├── include_log_superuser.xml
│ │ │ ├── item_console_md2.xml
│ │ │ ├── item_developer.xml
│ │ │ ├── item_hide_md2.xml
│ │ │ ├── item_hide_process_md2.xml
│ │ │ ├── item_icon_link.xml
│ │ │ ├── item_list_single_line.xml
│ │ │ ├── item_log_access_md2.xml
│ │ │ ├── item_log_textview.xml
│ │ │ ├── item_log_track_md2.xml
│ │ │ ├── item_module_download.xml
│ │ │ ├── item_module_md2.xml
│ │ │ ├── item_policy_md2.xml
│ │ │ ├── item_settings.xml
│ │ │ ├── item_settings_section.xml
│ │ │ ├── item_spinner.xml
│ │ │ ├── item_tappable_headline.xml
│ │ │ ├── item_text.xml
│ │ │ ├── item_theme.xml
│ │ │ ├── item_theme_container.xml
│ │ │ └── markdown_window_md2.xml
│ │ ├── menu/
│ │ │ ├── menu_bottom_nav.xml
│ │ │ ├── menu_deny_md2.xml
│ │ │ ├── menu_flash.xml
│ │ │ ├── menu_home_md2.xml
│ │ │ ├── menu_log_md2.xml
│ │ │ └── menu_reboot.xml
│ │ ├── navigation/
│ │ │ └── main.xml
│ │ ├── values/
│ │ │ ├── attrs.xml
│ │ │ ├── dimens.xml
│ │ │ ├── ids.xml
│ │ │ ├── styles_md2.xml
│ │ │ ├── styles_md2_appearance.xml
│ │ │ ├── styles_md2_impl.xml
│ │ │ ├── styles_view_md2.xml
│ │ │ ├── theme_overlay.xml
│ │ │ ├── themes.xml
│ │ │ ├── themes_md2.xml
│ │ │ └── themes_override.xml
│ │ ├── values-night/
│ │ │ ├── styles_md2.xml
│ │ │ └── themes_md2.xml
│ │ └── values-v27/
│ │ └── themes.xml
│ ├── apk-ng/
│ │ ├── build.gradle.kts
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── topjohnwu/
│ │ │ └── magisk/
│ │ │ ├── arch/
│ │ │ │ ├── AsyncLoadViewModel.kt
│ │ │ │ ├── BaseViewModel.kt
│ │ │ │ └── ViewModelFactory.kt
│ │ │ ├── terminal/
│ │ │ │ ├── TerminalBuffer.kt
│ │ │ │ ├── TerminalEmulator.kt
│ │ │ │ ├── TerminalProcess.kt
│ │ │ │ ├── TerminalRow.kt
│ │ │ │ ├── TerminalStyle.kt
│ │ │ │ └── WcWidth.kt
│ │ │ ├── ui/
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── MainScreen.kt
│ │ │ │ ├── component/
│ │ │ │ │ ├── Dialog.kt
│ │ │ │ │ └── MenuPositionProvider.kt
│ │ │ │ ├── deny/
│ │ │ │ │ ├── AppProcessInfo.kt
│ │ │ │ │ ├── DenyListScreen.kt
│ │ │ │ │ └── DenyListViewModel.kt
│ │ │ │ ├── flash/
│ │ │ │ │ ├── FlashScreen.kt
│ │ │ │ │ ├── FlashUtils.kt
│ │ │ │ │ └── FlashViewModel.kt
│ │ │ │ ├── home/
│ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ └── HomeViewModel.kt
│ │ │ │ ├── install/
│ │ │ │ │ └── InstallViewModel.kt
│ │ │ │ ├── log/
│ │ │ │ │ ├── LogScreen.kt
│ │ │ │ │ ├── LogViewModel.kt
│ │ │ │ │ └── MagiskLogParser.kt
│ │ │ │ ├── module/
│ │ │ │ │ ├── ActionScreen.kt
│ │ │ │ │ ├── ActionViewModel.kt
│ │ │ │ │ ├── ModuleScreen.kt
│ │ │ │ │ └── ModuleViewModel.kt
│ │ │ │ ├── navigation/
│ │ │ │ │ ├── CollectNavEvents.kt
│ │ │ │ │ ├── Navigator.kt
│ │ │ │ │ └── Routes.kt
│ │ │ │ ├── settings/
│ │ │ │ │ ├── SettingsScreen.kt
│ │ │ │ │ └── SettingsViewModel.kt
│ │ │ │ ├── superuser/
│ │ │ │ │ ├── SuperuserDetailScreen.kt
│ │ │ │ │ ├── SuperuserScreen.kt
│ │ │ │ │ └── SuperuserViewModel.kt
│ │ │ │ ├── surequest/
│ │ │ │ │ ├── SuRequestActivity.kt
│ │ │ │ │ ├── SuRequestScreen.kt
│ │ │ │ │ └── SuRequestViewModel.kt
│ │ │ │ ├── terminal/
│ │ │ │ │ ├── TerminalRenderer.kt
│ │ │ │ │ └── TerminalScreen.kt
│ │ │ │ ├── theme/
│ │ │ │ │ ├── MagiskTheme.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ └── util/
│ │ │ │ └── DrawablePainter.kt
│ │ │ └── utils/
│ │ │ └── AccessibilityUtils.kt
│ │ └── res/
│ │ ├── color/
│ │ │ ├── color_card_background_color_selector.xml
│ │ │ ├── color_error_transient.xml
│ │ │ ├── color_menu_tint.xml
│ │ │ ├── color_on_primary_transient.xml
│ │ │ ├── color_primary_error_transient.xml
│ │ │ ├── color_primary_transient.xml
│ │ │ ├── color_state_primary_transient.xml
│ │ │ └── color_text_transient.xml
│ │ ├── drawable/
│ │ │ ├── avd_bug_from_filled.xml
│ │ │ ├── avd_bug_to_filled.xml
│ │ │ ├── avd_circle_check_from_filled.xml
│ │ │ ├── avd_circle_check_to_filled.xml
│ │ │ ├── avd_home_from_filled.xml
│ │ │ ├── avd_home_to_filled.xml
│ │ │ ├── avd_module_from_filled.xml
│ │ │ ├── avd_module_to_filled.xml
│ │ │ ├── avd_settings_from_filled.xml
│ │ │ ├── avd_settings_to_filled.xml
│ │ │ ├── avd_superuser_from_filled.xml
│ │ │ ├── avd_superuser_to_filled.xml
│ │ │ ├── ic_bug_filled_md2.xml
│ │ │ ├── ic_bug_md2.xml
│ │ │ ├── ic_bug_outlined_md2.xml
│ │ │ ├── ic_check_circle_checked_md2.xml
│ │ │ ├── ic_check_circle_md2.xml
│ │ │ ├── ic_check_circle_unchecked_md2.xml
│ │ │ ├── ic_check_md2.xml
│ │ │ ├── ic_delete_md2.xml
│ │ │ ├── ic_download_md2.xml
│ │ │ ├── ic_home_filled_md2.xml
│ │ │ ├── ic_home_md2.xml
│ │ │ ├── ic_home_outlined_md2.xml
│ │ │ ├── ic_install.xml
│ │ │ ├── ic_manager.xml
│ │ │ ├── ic_module_filled_md2.xml
│ │ │ ├── ic_module_md2.xml
│ │ │ ├── ic_module_outlined_md2.xml
│ │ │ ├── ic_module_storage_md2.xml
│ │ │ ├── ic_notifications_md2.xml
│ │ │ ├── ic_restart.xml
│ │ │ ├── ic_save_md2.xml
│ │ │ ├── ic_search_md2.xml
│ │ │ ├── ic_settings_filled_md2.xml
│ │ │ ├── ic_settings_md2.xml
│ │ │ ├── ic_settings_outlined_md2.xml
│ │ │ ├── ic_superuser_filled_md2.xml
│ │ │ ├── ic_superuser_md2.xml
│ │ │ ├── ic_superuser_outlined_md2.xml
│ │ │ └── ic_update_md2.xml
│ │ ├── values/
│ │ │ ├── attrs.xml
│ │ │ ├── dimens.xml
│ │ │ ├── styles_md2.xml
│ │ │ ├── styles_md2_appearance.xml
│ │ │ ├── styles_md2_impl.xml
│ │ │ ├── themes.xml
│ │ │ ├── themes_md2.xml
│ │ │ └── themes_override.xml
│ │ ├── values-night/
│ │ │ ├── styles_md2.xml
│ │ │ └── themes_md2.xml
│ │ └── values-v27/
│ │ └── themes.xml
│ ├── build.gradle.kts
│ ├── buildSrc/
│ │ ├── build.gradle.kts
│ │ ├── settings.gradle.kts
│ │ └── src/
│ │ └── main/
│ │ └── java/
│ │ ├── AddCommentTask.kt
│ │ ├── DesugarClassVisitorFactory.kt
│ │ ├── Plugin.kt
│ │ ├── Setup.kt
│ │ └── Stub.kt
│ ├── core/
│ │ ├── build.gradle.kts
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── aidl/
│ │ │ └── com/
│ │ │ └── topjohnwu/
│ │ │ └── magisk/
│ │ │ └── core/
│ │ │ └── utils/
│ │ │ └── IRootUtils.aidl
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── topjohnwu/
│ │ │ └── magisk/
│ │ │ ├── core/
│ │ │ │ ├── App.kt
│ │ │ │ ├── AppContext.kt
│ │ │ │ ├── Config.kt
│ │ │ │ ├── Const.kt
│ │ │ │ ├── Hacks.kt
│ │ │ │ ├── Info.kt
│ │ │ │ ├── JobService.kt
│ │ │ │ ├── Provider.kt
│ │ │ │ ├── Receiver.kt
│ │ │ │ ├── Service.kt
│ │ │ │ ├── base/
│ │ │ │ │ ├── BaseActivity.kt
│ │ │ │ │ ├── BaseJobService.kt
│ │ │ │ │ ├── BaseProvider.kt
│ │ │ │ │ ├── BaseReceiver.kt
│ │ │ │ │ ├── BaseService.kt
│ │ │ │ │ └── SplashScreen.kt
│ │ │ │ ├── data/
│ │ │ │ │ ├── RetrofitInterfaces.kt
│ │ │ │ │ ├── SuLogDao.kt
│ │ │ │ │ └── magiskdb/
│ │ │ │ │ ├── MagiskDB.kt
│ │ │ │ │ ├── PolicyDao.kt
│ │ │ │ │ ├── SettingsDao.kt
│ │ │ │ │ └── StringDao.kt
│ │ │ │ ├── di/
│ │ │ │ │ ├── Networking.kt
│ │ │ │ │ └── ServiceLocator.kt
│ │ │ │ ├── download/
│ │ │ │ │ ├── DownloadEngine.kt
│ │ │ │ │ ├── DownloadProcessor.kt
│ │ │ │ │ ├── Interfaces.kt
│ │ │ │ │ └── Subject.kt
│ │ │ │ ├── ktx/
│ │ │ │ │ ├── XAndroid.kt
│ │ │ │ │ ├── XJVM.kt
│ │ │ │ │ └── XSU.kt
│ │ │ │ ├── model/
│ │ │ │ │ ├── UpdateInfo.kt
│ │ │ │ │ ├── module/
│ │ │ │ │ │ ├── LocalModule.kt
│ │ │ │ │ │ ├── Module.kt
│ │ │ │ │ │ └── OnlineModule.kt
│ │ │ │ │ └── su/
│ │ │ │ │ ├── SuLog.kt
│ │ │ │ │ └── SuPolicy.kt
│ │ │ │ ├── repository/
│ │ │ │ │ ├── DBConfig.kt
│ │ │ │ │ ├── LogRepository.kt
│ │ │ │ │ ├── NetworkService.kt
│ │ │ │ │ └── PreferenceConfig.kt
│ │ │ │ ├── signing/
│ │ │ │ │ ├── ApkSignerV2.java
│ │ │ │ │ ├── ByteArrayStream.java
│ │ │ │ │ ├── JarMap.java
│ │ │ │ │ ├── SignApk.java
│ │ │ │ │ └── ZipUtils.java
│ │ │ │ ├── su/
│ │ │ │ │ ├── SuCallbackHandler.kt
│ │ │ │ │ ├── SuEvents.kt
│ │ │ │ │ └── SuRequestHandler.kt
│ │ │ │ ├── tasks/
│ │ │ │ │ ├── AppMigration.kt
│ │ │ │ │ ├── FlashZip.kt
│ │ │ │ │ └── MagiskInstaller.kt
│ │ │ │ ├── utils/
│ │ │ │ │ ├── AXML.kt
│ │ │ │ │ ├── Desugar.java
│ │ │ │ │ ├── DummyList.kt
│ │ │ │ │ ├── Keygen.kt
│ │ │ │ │ ├── LocaleSetting.kt
│ │ │ │ │ ├── MediaStoreUtils.kt
│ │ │ │ │ ├── NetworkObserver.kt
│ │ │ │ │ ├── ProgressInputStream.kt
│ │ │ │ │ ├── RequestAuthentication.kt
│ │ │ │ │ ├── RequestInstall.kt
│ │ │ │ │ ├── RootUtils.kt
│ │ │ │ │ └── ShellInit.kt
│ │ │ │ └── view/
│ │ │ │ ├── Notifications.kt
│ │ │ │ └── Shortcuts.kt
│ │ │ └── test/
│ │ │ ├── AdditionalTest.kt
│ │ │ ├── BaseTest.kt
│ │ │ ├── Environment.kt
│ │ │ └── MagiskAppTest.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── ic_extension.xml
│ │ │ ├── ic_favorite.xml
│ │ │ ├── ic_fingerprint.xml
│ │ │ ├── ic_github.xml
│ │ │ ├── ic_logo.xml
│ │ │ ├── ic_magisk.xml
│ │ │ ├── ic_magisk_outline.xml
│ │ │ ├── ic_magisk_padded.xml
│ │ │ ├── ic_more.xml
│ │ │ ├── ic_patreon.xml
│ │ │ ├── ic_paypal.xml
│ │ │ ├── ic_superuser.xml
│ │ │ ├── ic_twitter.xml
│ │ │ ├── sc_extension.xml
│ │ │ └── sc_superuser.xml
│ │ ├── drawable-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ ├── sc_extension.xml
│ │ │ └── sc_superuser.xml
│ │ ├── values/
│ │ │ ├── arrays.xml
│ │ │ ├── colors.xml
│ │ │ ├── resources.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ ├── values-ar/
│ │ │ └── strings.xml
│ │ ├── values-ast/
│ │ │ └── strings.xml
│ │ ├── values-az/
│ │ │ └── strings.xml
│ │ ├── values-b+sr+Latn/
│ │ │ └── strings.xml
│ │ ├── values-be/
│ │ │ └── strings.xml
│ │ ├── values-bg/
│ │ │ └── strings.xml
│ │ ├── values-bn/
│ │ │ └── strings.xml
│ │ ├── values-ca/
│ │ │ └── strings.xml
│ │ ├── values-cs/
│ │ │ └── strings.xml
│ │ ├── values-de/
│ │ │ └── strings.xml
│ │ ├── values-el/
│ │ │ └── strings.xml
│ │ ├── values-es/
│ │ │ └── strings.xml
│ │ ├── values-et/
│ │ │ └── strings.xml
│ │ ├── values-fa/
│ │ │ └── strings.xml
│ │ ├── values-fr/
│ │ │ └── strings.xml
│ │ ├── values-hi/
│ │ │ └── strings.xml
│ │ ├── values-hn/
│ │ │ └── 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-ka/
│ │ │ └── strings.xml
│ │ ├── values-kk/
│ │ │ └── strings.xml
│ │ ├── values-ko/
│ │ │ └── strings.xml
│ │ ├── values-ku/
│ │ │ └── strings.xml
│ │ ├── values-lt/
│ │ │ └── strings.xml
│ │ ├── values-mk/
│ │ │ └── strings.xml
│ │ ├── values-ml/
│ │ │ └── strings.xml
│ │ ├── values-nb/
│ │ │ └── strings.xml
│ │ ├── values-night/
│ │ │ └── colors.xml
│ │ ├── values-nl/
│ │ │ └── strings.xml
│ │ ├── values-pa/
│ │ │ └── strings.xml
│ │ ├── values-pl/
│ │ │ └── strings.xml
│ │ ├── values-pt-rBR/
│ │ │ └── strings.xml
│ │ ├── values-pt-rPT/
│ │ │ └── strings.xml
│ │ ├── values-ro/
│ │ │ └── strings.xml
│ │ ├── values-ru/
│ │ │ └── strings.xml
│ │ ├── values-sk/
│ │ │ └── strings.xml
│ │ ├── values-sq/
│ │ │ └── strings.xml
│ │ ├── values-sr/
│ │ │ └── strings.xml
│ │ ├── values-sv/
│ │ │ └── strings.xml
│ │ ├── values-sw/
│ │ │ └── strings.xml
│ │ ├── values-ta/
│ │ │ └── strings.xml
│ │ ├── values-th/
│ │ │ └── strings.xml
│ │ ├── values-tr/
│ │ │ └── strings.xml
│ │ ├── values-uk/
│ │ │ └── strings.xml
│ │ ├── values-ur/
│ │ │ └── strings.xml
│ │ ├── values-v31/
│ │ │ └── themes.xml
│ │ ├── values-v34/
│ │ │ └── resources.xml
│ │ ├── values-vi/
│ │ │ └── strings.xml
│ │ ├── values-zh-rCN/
│ │ │ └── strings.xml
│ │ ├── values-zh-rTW/
│ │ │ └── strings.xml
│ │ └── xml/
│ │ └── locale_config.xml
│ ├── gradle/
│ │ ├── libs.versions.toml
│ │ └── wrapper/
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── settings.gradle.kts
│ ├── shared/
│ │ ├── build.gradle.kts
│ │ └── src/
│ │ ├── debug/
│ │ │ └── AndroidManifest.xml
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ └── java/
│ │ └── com/
│ │ └── topjohnwu/
│ │ └── magisk/
│ │ ├── ProviderInstaller.java
│ │ ├── StubApk.java
│ │ └── utils/
│ │ ├── APKInstall.java
│ │ ├── CompoundEnumeration.java
│ │ └── DynamicClassLoader.java
│ ├── stub/
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── proguard-rules.pro
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── topjohnwu/
│ │ │ └── magisk/
│ │ │ ├── ClassLoaders.java
│ │ │ ├── DelegateComponentFactory.java
│ │ │ ├── DownloadActivity.java
│ │ │ ├── DynLoad.java
│ │ │ ├── StubApplication.java
│ │ │ ├── StubRootService.java
│ │ │ ├── dummy/
│ │ │ │ ├── DummyProvider.java
│ │ │ │ ├── DummyReceiver.java
│ │ │ │ └── DummyService.java
│ │ │ └── net/
│ │ │ ├── BadRequest.java
│ │ │ ├── ErrorHandler.java
│ │ │ ├── Networking.java
│ │ │ ├── Request.java
│ │ │ └── ResponseListener.java
│ │ └── res/
│ │ ├── values/
│ │ │ └── strings.xml
│ │ ├── values-ar/
│ │ │ └── strings.xml
│ │ ├── values-ast/
│ │ │ └── strings.xml
│ │ ├── values-az/
│ │ │ └── strings.xml
│ │ ├── values-b+sr+Latn/
│ │ │ └── strings.xml
│ │ ├── values-be/
│ │ │ └── strings.xml
│ │ ├── values-bg/
│ │ │ └── strings.xml
│ │ ├── values-ca/
│ │ │ └── strings.xml
│ │ ├── values-cs/
│ │ │ └── strings.xml
│ │ ├── values-de/
│ │ │ └── strings.xml
│ │ ├── values-el/
│ │ │ └── strings.xml
│ │ ├── values-es/
│ │ │ └── strings.xml
│ │ ├── values-et/
│ │ │ └── strings.xml
│ │ ├── values-fa/
│ │ │ └── strings.xml
│ │ ├── values-fr/
│ │ │ └── strings.xml
│ │ ├── values-hi/
│ │ │ └── strings.xml
│ │ ├── values-hn/
│ │ │ └── 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-ka/
│ │ │ └── strings.xml
│ │ ├── values-kk/
│ │ │ └── strings.xml
│ │ ├── values-ko/
│ │ │ └── strings.xml
│ │ ├── values-ku/
│ │ │ └── strings.xml
│ │ ├── values-lt/
│ │ │ └── strings.xml
│ │ ├── values-mk/
│ │ │ └── strings.xml
│ │ ├── values-ml/
│ │ │ └── strings.xml
│ │ ├── values-nb/
│ │ │ └── strings.xml
│ │ ├── values-nl/
│ │ │ └── strings.xml
│ │ ├── values-pa/
│ │ │ └── strings.xml
│ │ ├── values-pl/
│ │ │ └── strings.xml
│ │ ├── values-pt-rBR/
│ │ │ └── strings.xml
│ │ ├── values-pt-rPT/
│ │ │ └── strings.xml
│ │ ├── values-ro/
│ │ │ └── strings.xml
│ │ ├── values-ru/
│ │ │ └── strings.xml
│ │ ├── values-sk/
│ │ │ └── strings.xml
│ │ ├── values-sq/
│ │ │ └── strings.xml
│ │ ├── values-sr/
│ │ │ └── strings.xml
│ │ ├── values-sv/
│ │ │ └── strings.xml
│ │ ├── values-sw/
│ │ │ └── strings.xml
│ │ ├── values-ta/
│ │ │ └── strings.xml
│ │ ├── values-th/
│ │ │ └── strings.xml
│ │ ├── values-tr/
│ │ │ └── strings.xml
│ │ ├── values-uk/
│ │ │ └── strings.xml
│ │ ├── values-vi/
│ │ │ └── strings.xml
│ │ ├── values-zh-rCN/
│ │ │ └── strings.xml
│ │ └── values-zh-rTW/
│ │ └── strings.xml
│ └── test/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ └── topjohnwu/
│ └── magisk/
│ └── test/
│ ├── AppMigrationTest.kt
│ └── Runners.kt
├── build.py
├── config.prop.sample
├── docs/
│ ├── README.md
│ ├── app_changes.md
│ ├── boot.md
│ ├── build.md
│ ├── changes.md
│ ├── details.md
│ ├── faq.md
│ ├── guides.md
│ ├── install.md
│ ├── ota.md
│ └── tools.md
├── native/
│ ├── .gitignore
│ └── src/
│ ├── .cargo/
│ │ └── config.toml
│ ├── Android-rs.mk
│ ├── Android.mk
│ ├── Application.mk
│ ├── Cargo.toml
│ ├── base/
│ │ ├── Android.mk
│ │ ├── Cargo.toml
│ │ ├── argh.rs
│ │ ├── base.cpp
│ │ ├── build.rs
│ │ ├── cstr.rs
│ │ ├── cxx_extern.rs
│ │ ├── derive/
│ │ │ ├── Cargo.toml
│ │ │ ├── argh/
│ │ │ │ ├── errors.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── parse_attrs.rs
│ │ │ ├── decodable.rs
│ │ │ └── lib.rs
│ │ ├── dir.rs
│ │ ├── files.rs
│ │ ├── include/
│ │ │ └── base.hpp
│ │ ├── lib.rs
│ │ ├── logging.rs
│ │ ├── misc.rs
│ │ ├── mount.rs
│ │ ├── result.rs
│ │ └── xwrap.rs
│ ├── boot/
│ │ ├── .gitignore
│ │ ├── Cargo.toml
│ │ ├── bootimg.cpp
│ │ ├── bootimg.hpp
│ │ ├── build.rs
│ │ ├── cli.rs
│ │ ├── compress.rs
│ │ ├── cpio.rs
│ │ ├── dtb.rs
│ │ ├── format.rs
│ │ ├── lib.rs
│ │ ├── magiskboot.hpp
│ │ ├── patch.rs
│ │ ├── payload.rs
│ │ ├── proto/
│ │ │ └── update_metadata.proto
│ │ └── sign.rs
│ ├── core/
│ │ ├── Cargo.toml
│ │ ├── applet_stub.cpp
│ │ ├── applets.cpp
│ │ ├── bootstages.rs
│ │ ├── build.rs
│ │ ├── daemon.rs
│ │ ├── db.rs
│ │ ├── deny/
│ │ │ ├── cli.cpp
│ │ │ ├── deny.hpp
│ │ │ ├── logcat.cpp
│ │ │ └── utils.cpp
│ │ ├── include/
│ │ │ ├── core.hpp
│ │ │ └── sqlite.hpp
│ │ ├── lib.rs
│ │ ├── logging.rs
│ │ ├── magisk.rs
│ │ ├── module.rs
│ │ ├── mount.rs
│ │ ├── package.rs
│ │ ├── resetprop/
│ │ │ ├── .gitignore
│ │ │ ├── cli.rs
│ │ │ ├── mod.rs
│ │ │ ├── persist.rs
│ │ │ ├── proto/
│ │ │ │ └── persistent_properties.proto
│ │ │ └── sys.cpp
│ │ ├── scripting.cpp
│ │ ├── selinux.rs
│ │ ├── socket.rs
│ │ ├── sqlite.cpp
│ │ ├── su/
│ │ │ ├── connect.rs
│ │ │ ├── daemon.rs
│ │ │ ├── db.rs
│ │ │ ├── mod.rs
│ │ │ ├── pts.rs
│ │ │ └── su.cpp
│ │ ├── thread.rs
│ │ ├── utils.cpp
│ │ └── zygisk/
│ │ ├── api.hpp
│ │ ├── daemon.rs
│ │ ├── entry.cpp
│ │ ├── gen_jni_hooks.py
│ │ ├── hook.cpp
│ │ ├── jni_hooks.hpp
│ │ ├── mod.rs
│ │ ├── module.cpp
│ │ ├── module.hpp
│ │ └── zygisk.hpp
│ ├── exported_sym.txt
│ ├── external/
│ │ ├── Android.mk
│ │ ├── lz4-sys/
│ │ │ ├── Cargo.toml
│ │ │ └── src/
│ │ │ ├── lib.rs
│ │ │ └── wasm_shim.rs
│ │ ├── xz-embedded/
│ │ │ ├── xz.h
│ │ │ ├── xz_config.h
│ │ │ ├── xz_crc32.c
│ │ │ ├── xz_dec_lzma2.c
│ │ │ ├── xz_dec_stream.c
│ │ │ ├── xz_lzma2.h
│ │ │ ├── xz_private.h
│ │ │ └── xz_stream.h
│ │ └── xz_config/
│ │ └── config.h
│ ├── include/
│ │ ├── codegen.rs
│ │ ├── consts.hpp
│ │ └── consts.rs
│ ├── init/
│ │ ├── Cargo.toml
│ │ ├── build.rs
│ │ ├── getinfo.cpp
│ │ ├── getinfo.rs
│ │ ├── init.hpp
│ │ ├── init.rs
│ │ ├── lib.rs
│ │ ├── logging.rs
│ │ ├── mount.cpp
│ │ ├── mount.rs
│ │ ├── preload.c
│ │ ├── rootdir.cpp
│ │ ├── rootdir.rs
│ │ ├── selinux.rs
│ │ └── twostage.rs
│ ├── rustfmt.toml
│ └── sepolicy/
│ ├── Cargo.toml
│ ├── api.cpp
│ ├── build.rs
│ ├── cli.rs
│ ├── include/
│ │ └── sepolicy.hpp
│ ├── lib.rs
│ ├── policy.hpp
│ ├── policydb.cpp
│ ├── rules.rs
│ ├── sepolicy.cpp
│ └── statement.rs
├── scripts/
│ ├── addon.d.sh
│ ├── app_functions.sh
│ ├── avd.sh
│ ├── boot_patch.sh
│ ├── cuttlefish.sh
│ ├── flash_script.sh
│ ├── host_patch.sh
│ ├── live_setup.sh
│ ├── module_installer.sh
│ ├── release.sh
│ ├── test_common.sh
│ ├── uninstaller.sh
│ ├── update_binary.sh
│ └── util_functions.sh
└── tools/
├── bootctl
├── bootctl.patch
├── elf-cleaner/
│ ├── .gitignore
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
├── futility
├── keys/
│ ├── kernel.keyblock
│ ├── kernel_data_key.vbprivk
│ ├── verity.pk8
│ └── verity.x509.pem
└── rustup-wrapper/
├── .gitignore
├── Cargo.toml
└── src/
└── main.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
# Set the default behavior, in case people don't have core.autocrlf set.
* text eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
# *.c text
# *.h text
# 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.
tools/** binary
tools/rustup-wrapper/** -binary
tools/elf-cleaner/** -binary
*.jar binary
*.exe binary
*.apk binary
*.png binary
*.jpg binary
*.ttf binary
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: ""
assignees: ""
---
Device:
Android version:
Magisk version name:
Magisk version code:
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: XDA Community Support
url: https://forum.xda-developers.com/f/magisk.5903/
about: Please ask and answer questions here.
================================================
FILE: .github/actions/setup/action.yml
================================================
name: Magisk Setup
description: Set up the build environment for Magisk
inputs:
is-asset-build:
required: false
default: false
runs:
using: "composite"
steps:
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: "temurin"
java-version: "21"
- name: Set up Python 3
uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Install GNU make
if: runner.os == 'macOS'
shell: bash
run: |
brew install make
echo 'GNUMAKE=gmake' >> "$GITHUB_ENV"
- name: Cache sccache
uses: actions/cache@v5
if: ${{ github.event_name != 'pull_request' }}
with:
path: .sccache
key: sccache-${{ runner.os }}-${{ github.sha }}
restore-keys: sccache-${{ runner.os }}-
- name: Restore sccache
uses: actions/cache/restore@v5
if: ${{ github.event_name == 'pull_request' }}
with:
path: .sccache
key: sccache-${{ runner.os }}-${{ github.sha }}
restore-keys: sccache-${{ runner.os }}-
- name: Set up sccache
shell: bash
env:
SCCACHE_DIRECT: false
SCCACHE_DIR: ${{ github.workspace }}/.sccache
SCCACHE_CACHE_SIZE: ${{ inputs.is-asset-build == 'true' && '2G' || '300M' }}
SCCACHE_IDLE_TIMEOUT: 0
run: |
bash $GITHUB_ACTION_PATH/sccache.sh
sccache --start-server
sccache -z
- name: Show sccache stats
uses: gacts/run-and-post-run@v1
with:
run: sccache -s
post: sccache -s
- name: Set GRADLE_USER_HOME
shell: bash
run: echo "GRADLE_USER_HOME=$GITHUB_WORKSPACE/.gradle" >> "$GITHUB_ENV"
- name: Cache Gradle dependencies
uses: actions/cache@v5
if: ${{ inputs.is-asset-build == 'true' && github.event_name != 'pull_request' }}
with:
path: |
.gradle/caches
.gradle/wrapper
!.gradle/caches/build-cache-*
key: gradle-cache-${{ hashFiles('app/gradle/**') }}
restore-keys: gradle-cache-
- name: Restore Gradle dependencies
uses: actions/cache/restore@v5
if: ${{ inputs.is-asset-build == 'false' || github.event_name == 'pull_request' }}
with:
path: |
.gradle/caches
.gradle/wrapper
!.gradle/caches/build-cache-*
key: gradle-cache-${{ hashFiles('gradle/**') }}
restore-keys: gradle-cache-
enableCrossOsArchive: true
- name: Cache Gradle build cache
uses: actions/cache@v5
if: ${{ inputs.is-asset-build == 'true' && github.event_name != 'pull_request' }}
with:
path: .gradle/caches/build-cache-*
key: gradle-build-cache-${{ github.sha }}
restore-keys: gradle-build-cache-
- name: Restore Gradle build cache
uses: actions/cache/restore@v5
if: ${{ inputs.is-asset-build == 'false' || github.event_name == 'pull_request' }}
with:
path: .gradle/caches/build-cache-*
key: gradle-build-cache-${{ github.sha }}
restore-keys: gradle-build-cache-
enableCrossOsArchive: true
- name: Set up NDK
shell: bash
run: python build.py -v ndk
================================================
FILE: .github/actions/setup/sccache.sh
================================================
#!/usr/bin/env bash
# Get latest sccache version
get_sccache_ver() {
curl -sL 'https://api.github.com/repos/mozilla/sccache/releases/latest' | jq -r .name
}
# $1=variant
# $2=install_dir
# $3=exe
install_from_gh() {
local ver=$(curl -sL 'https://api.github.com/repos/mozilla/sccache/releases/latest' | jq -r .name)
local url="https://github.com/mozilla/sccache/releases/download/${ver}/sccache-${ver}-$1.tar.gz"
local dest="$2/$3"
curl -L "$url" | tar xz -O --wildcards "*/$3" > $dest
chmod +x $dest
}
if [ $RUNNER_OS = "macOS" ]; then
brew install sccache
elif [ $RUNNER_OS = "Linux" ]; then
install_from_gh x86_64-unknown-linux-musl /usr/local/bin sccache
elif [ $RUNNER_OS = "Windows" ]; then
install_from_gh x86_64-pc-windows-msvc $USERPROFILE/.cargo/bin sccache.exe
fi
================================================
FILE: .github/ci.prop
================================================
abiList=arm64-v8a
================================================
FILE: .github/kvm.sh
================================================
#!/usr/bin/env bash
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
================================================
FILE: .github/workflows/build.yml
================================================
name: Magisk Build
on:
push:
branches: [master]
paths:
- "app/**"
- "native/**"
- "build.py"
- ".github/workflows/build.yml"
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build:
name: Build Magisk artifacts
runs-on: macos-26
strategy:
fail-fast: false
steps:
- name: Check out
uses: actions/checkout@v6
with:
submodules: "recursive"
- name: Setup environment
uses: ./.github/actions/setup
with:
is-asset-build: true
- name: Build release
run: ./build.py -vr all
- name: Build debug
run: ./build.py -v all
- name: Stop gradle daemon
run: ./app/gradlew --stop
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: ${{ github.sha }}
path: out
compression-level: 9
- name: Upload mapping and native debug symbols
uses: actions/upload-artifact@v7
with:
name: ${{ github.sha }}-symbols
path: app/apk/build/outputs
compression-level: 9
test-build:
name: Test building on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-2025, ubuntu-24.04]
steps:
- name: Check out
uses: actions/checkout@v6
with:
submodules: "recursive"
- name: Setup environment
uses: ./.github/actions/setup
- name: Test build
run: python build.py -v -c .github/ci.prop all
- name: Stop gradle daemon
run: ./app/gradlew --stop
avd-test:
name: Test API ${{ matrix.version }} (x86_64)
runs-on: ubuntu-24.04
needs: build
if: ${{ github.event_name != 'push' }}
strategy:
fail-fast: false
matrix:
version: [23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 36.1, "CANARY"]
type: [""]
include:
- version: "CinnamonBun"
type: "google_apis_ps16k"
steps:
- name: Check out
uses: actions/checkout@v6
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
name: ${{ github.sha }}
path: out
- name: Enable KVM group perms
run: .github/kvm.sh
- name: Run AVD test
timeout-minutes: 18
run: scripts/avd.sh test -l -v ${{ matrix.version }} -t ${{ matrix.type }}
- name: Upload logs on error
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: "avd-logs-${{ matrix.version }}"
path: |
kernel.log
logcat.log
avd-test-32:
name: Test API ${{ matrix.version }} (x86)
runs-on: ubuntu-24.04
needs: build
if: ${{ github.event_name != 'push' }}
strategy:
fail-fast: false
matrix:
version: [23, 24, 25, 26, 27, 28, 29, 30]
steps:
- name: Check out
uses: actions/checkout@v6
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
name: ${{ github.sha }}
path: out
- name: Enable KVM group perms
run: .github/kvm.sh
- name: Run AVD test
timeout-minutes: 18
env:
FORCE_32_BIT: 1
run: scripts/avd.sh test -l -v ${{ matrix.version }}
- name: Upload logs on error
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: "avd32-logs-${{ matrix.version }}"
path: |
kernel.log
logcat.log
cf-test:
name: Test ${{ matrix.device }}
runs-on: ubuntu-24.04
needs: build
if: ${{ github.event_name != 'push' }}
env:
CF_HOME: /home/runner/aosp_cf_phone
strategy:
fail-fast: false
matrix:
include:
- branch: "aosp-android-latest-release"
device: "aosp_cf_x86_64_only_phone"
steps:
- name: Check out
uses: actions/checkout@v6
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
name: ${{ github.sha }}
path: out
- name: Enable KVM group perms
run: .github/kvm.sh
- name: Setup Cuttlefish environment
run: |
scripts/cuttlefish.sh setup
scripts/cuttlefish.sh download ${{ matrix.branch }} ${{ matrix.device }}
- name: Run Cuttlefish test
timeout-minutes: 18
run: sudo -E -u $USER scripts/cuttlefish.sh test
- name: Upload logs on error
if: ${{ failure() }}
uses: actions/upload-artifact@v7
with:
name: "cvd-logs-${{ matrix.device }}"
path: |
/home/runner/aosp_cf_phone/cuttlefish/instances/cvd-1/logs
/home/runner/aosp_cf_phone/cuttlefish/instances/cvd-1/cuttlefish_config.json
================================================
FILE: .gitignore
================================================
out
*.zip
*.jks
*.apk
*.log
/config.prop
/notes.md
# Built binaries
native/out
# Android Studio
*.iml
.idea
.cursor
ramdisk.img
app/core/src/debug
app/core/src/release
================================================
FILE: .gitmodules
================================================
[submodule "selinux"]
path = native/src/external/selinux
url = https://github.com/topjohnwu/selinux.git
[submodule "lz4"]
path = native/src/external/lz4
url = https://github.com/lz4/lz4.git
[submodule "libcxx"]
path = native/src/external/libcxx
url = https://github.com/topjohnwu/libcxx.git
[submodule "cxx-rs"]
path = native/src/external/cxx-rs
url = https://github.com/topjohnwu/cxx.git
[submodule "lsplt"]
path = native/src/external/lsplt
url = https://github.com/LSPosed/LSPlt.git
[submodule "system_properties"]
path = native/src/external/system_properties
url = https://github.com/topjohnwu/system_properties.git
[submodule "crt0"]
path = native/src/external/crt0
url = https://github.com/topjohnwu/crt0.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.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
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:
{project} Copyright (C) {year} {fullname}
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
================================================

[](https://raw.githubusercontent.com/topjohnwu/magisk-files/count/count.json)
#### This is not an officially supported Google product
## Introduction
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 6.0.
Some highlight features:
- **MagiskSU**: Provide root access for applications
- **Magisk Modules**: Modify read-only partitions by installing modules
- **MagiskBoot**: The most complete tool for unpacking and repacking Android boot images
- **Zygisk**: Run code in every Android applications' processes
## Downloads
[Github](https://github.com/topjohnwu/Magisk/releases) is the only source where you can get official Magisk information and downloads.
## Useful Links
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
- [Building and Development](https://topjohnwu.github.io/Magisk/build.html)
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
- [Zygisk module sample](https://github.com/topjohnwu/zygisk-module-sample)
## Bug Reports
**Only bug reports from Debug builds will be accepted.**
For installation issues, upload both boot image and install logs.
For Magisk issues, upload boot logcat or dmesg.
For Magisk app crashes, record and upload the logcat when the crash occurs.
## Translation Contributions
Default string resources for the Magisk app and its stub APK are located here:
- `app/core/src/main/res/values/strings.xml`
- `app/stub/src/main/res/values/strings.xml`
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
## License
Magisk, including all git submodules are 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 .
================================================
FILE: app/.gitignore
================================================
/dict.txt
# Gradle
.gradle
.kotlin
build
/local.properties
================================================
FILE: app/apk/build.gradle.kts
================================================
plugins {
id("com.android.application")
kotlin("plugin.parcelize")
alias(libs.plugins.legacy.kapt)
alias(libs.plugins.navigation.safeargs)
}
setupMainApk()
kapt {
correctErrorTypes = true
useBuildCache = true
mapDiagnosticLocations = true
javacOptions {
option("-Xmaxerrs", "1000")
}
}
android {
buildFeatures {
dataBinding = true
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
defaultConfig {
proguardFile("proguard-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
}
}
}
dependencies {
implementation(project(":core"))
coreLibraryDesugaring(libs.jdk.libs)
implementation(libs.indeterminate.checkbox)
implementation(libs.rikka.layoutinflater)
implementation(libs.rikka.insets)
implementation(libs.rikka.recyclerview)
implementation(libs.navigation.fragment.ktx)
implementation(libs.navigation.ui.ktx)
implementation(libs.constraintlayout)
implementation(libs.swiperefreshlayout)
implementation(libs.recyclerview)
implementation(libs.transition)
implementation(libs.fragment.ktx)
implementation(libs.appcompat)
implementation(libs.material)
// Make sure kapt runs with a proper kotlin-stdlib
kapt(kotlin("stdlib"))
}
================================================
FILE: app/apk/proguard-rules.pro
================================================
# Excessive obfuscation
-flattenpackagehierarchy
-allowaccessmodification
================================================
FILE: app/apk/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/arch/AsyncLoadViewModel.kt
================================================
package com.topjohnwu.magisk.arch
import androidx.annotation.MainThread
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
abstract class AsyncLoadViewModel : BaseViewModel() {
private var loadingJob: Job? = null
@MainThread
fun startLoading() {
if (loadingJob?.isActive == true) {
// Prevent multiple jobs from running at the same time
return
}
loadingJob = viewModelScope.launch { doLoadWork() }
}
protected abstract suspend fun doLoadWork()
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/arch/BaseFragment.kt
================================================
package com.topjohnwu.magisk.arch
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.MenuProvider
import androidx.databinding.DataBindingUtil
import androidx.databinding.OnRebindCallback
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR
abstract class BaseFragment : Fragment(), ViewModelHolder {
val activity get() = getActivity() as? NavigationActivity<*>
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
private val navigation get() = activity?.navigation
open val snackbarView: View? get() = null
open val snackbarAnchorView: View? get() = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveLiveData()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, layoutRes, container, false).also {
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = viewLifecycleOwner
}
if (this is MenuProvider) {
activity?.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
}
savedInstanceState?.let { viewModel.onRestoreState(it) }
return binding.root
}
override fun onSaveInstanceState(outState: Bundle) {
viewModel.onSaveState(outState)
}
override fun onStart() {
super.onStart()
activity?.supportActionBar?.subtitle = null
}
override fun onEventDispatched(event: ViewEvent) = when(event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> activity?.let { event(it) } ?: Unit
is FragmentExecutor -> event(this)
else -> Unit
}
open fun onKeyEvent(event: KeyEvent): Boolean {
return false
}
open fun onBackPressed(): Boolean = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.addOnRebindCallback(object : OnRebindCallback() {
override fun onPreBind(binding: Binding): Boolean {
this@BaseFragment.onPreBind(binding)
return true
}
})
}
override fun onResume() {
super.onResume()
viewModel.let {
if (it is AsyncLoadViewModel)
it.startLoading()
}
}
protected open fun onPreBind(binding: Binding) {
(binding.root as? ViewGroup)?.startAnimations()
}
fun NavDirections.navigate() {
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt
================================================
package com.topjohnwu.magisk.arch
import android.Manifest.permission.POST_NOTIFICATIONS
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.events.BackPressEvent
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.events.DialogEvent
import com.topjohnwu.magisk.events.NavigationEvent
import com.topjohnwu.magisk.events.PermissionEvent
import com.topjohnwu.magisk.events.SnackbarEvent
abstract class BaseViewModel : ViewModel(), ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
private val _viewEvents = MutableLiveData()
val viewEvents: LiveData get() = _viewEvents
open fun onSaveState(state: Bundle) {}
open fun onRestoreState(state: Bundle) {}
open fun onNetworkChanged(network: Boolean) {}
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
PermissionEvent(permission, callback).publish()
}
inline fun withExternalRW(crossinline callback: () -> Unit) {
withPermission(WRITE_EXTERNAL_STORAGE) {
if (!it) {
SnackbarEvent(R.string.external_rw_permission_denied).publish()
} else {
callback()
}
}
}
@SuppressLint("InlinedApi")
inline fun withInstallPermission(crossinline callback: () -> Unit) {
withPermission(REQUEST_INSTALL_PACKAGES) {
if (!it) {
SnackbarEvent(R.string.install_unknown_denied).publish()
} else {
callback()
}
}
}
@SuppressLint("InlinedApi")
inline fun withPostNotificationPermission(crossinline callback: () -> Unit) {
withPermission(POST_NOTIFICATIONS) {
if (!it) {
SnackbarEvent(R.string.post_notifications_denied).publish()
} else {
callback()
}
}
}
fun back() = BackPressEvent().publish()
fun ViewEvent.publish() {
_viewEvents.postValue(this)
}
fun DialogBuilder.show() {
DialogEvent(this).publish()
}
fun NavDirections.navigate(pop: Boolean = false) {
_viewEvents.postValue(NavigationEvent(this, pop))
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/arch/NavigationActivity.kt
================================================
package com.topjohnwu.magisk.arch
import android.content.ContentResolver
import android.view.KeyEvent
import androidx.databinding.ViewDataBinding
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navOptions
import com.topjohnwu.magisk.utils.AccessibilityUtils
abstract class NavigationActivity : UIActivity() {
abstract val navHostId: Int
private val navHostFragment by lazy {
supportFragmentManager.findFragmentById(navHostId) as NavHostFragment
}
protected val currentFragment get() =
navHostFragment.childFragmentManager.fragments.getOrNull(0) as? BaseFragment<*>
val navigation: NavController get() = navHostFragment.navController
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return if (binded && currentFragment?.onKeyEvent(event) == true) true else super.dispatchKeyEvent(event)
}
override fun onBackPressed() {
if (binded) {
if (currentFragment?.onBackPressed() == false) {
super.onBackPressed()
}
}
}
companion object {
fun navigate(directions: NavDirections, navigation: NavController, cr: ContentResolver) {
if (AccessibilityUtils.isAnimationEnabled(cr)) {
navigation.navigate(directions)
} else {
navigation.navigate(directions, navOptions {})
}
}
}
fun NavDirections.navigate() {
navigate(this, navigation, contentResolver)
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/arch/UIActivity.kt
================================================
package com.topjohnwu.magisk.arch
import android.content.Context
import android.content.res.Resources
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.res.use
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.ActivityExtension
import com.topjohnwu.magisk.core.base.IActivityExtension
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.reflectField
import com.topjohnwu.magisk.core.wrap
import rikka.insets.WindowInsetsHelper
import rikka.layoutinflater.view.LayoutInflaterFactory
abstract class UIActivity
: AppCompatActivity(), ViewModelHolder, IActivityExtension {
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
override val extension = ActivityExtension(this)
protected val binded get() = ::binding.isInitialized
open val snackbarView get() = binding.root
open val snackbarAnchorView: View? get() = null
init {
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap())
}
override fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
extension.onCreate(savedInstanceState)
if (isRunningAsStub) {
// Overwrite private members to avoid nasty "false" stack traces being logged
val delegate = delegate
val clz = delegate.javaClass
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
}
super.onCreate(savedInstanceState)
startObserveLiveData()
// We need to set the window background explicitly since for whatever reason it's not
// propagated upstream
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
.use { it.getDrawable(0) }
.also { window.setBackgroundDrawable(it) }
WindowCompat.setDecorFitsSystemWindows(window, false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window?.decorView?.post {
// If navigation bar is short enough (gesture navigation enabled), make it transparent
if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
window.navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = Color.TRANSPARENT
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
window.isStatusBarContrastEnforced = false
}
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
extension.onSaveInstanceState(outState)
}
fun setContentView() {
binding = DataBindingUtil.setContentView(this, layoutRes).also {
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = this
}
}
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
binding.root.rootView.accessibilityDelegate = delegate
}
fun showSnackbar(
message: CharSequence,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) = Snackbar.make(snackbarView, message, length)
.setAnchorView(snackbarAnchorView).apply(builder).show()
override fun onResume() {
super.onResume()
viewModel.let {
if (it is AsyncLoadViewModel)
it.startLoading()
}
}
override fun onEventDispatched(event: ViewEvent) = when (event) {
is ContextExecutor -> event(this)
is ActivityExecutor -> event(this)
else -> Unit
}
}
fun ViewGroup.startAnimations() {
val transition = AutoTransition()
.setInterpolator(FastOutSlowInInterpolator())
.setDuration(400)
.excludeTarget(R.id.main_toolbar, true)
TransitionManager.beginDelayedTransition(
this,
transition
)
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/arch/ViewEvent.kt
================================================
package com.topjohnwu.magisk.arch
import android.content.Context
/**
* Class for passing events from ViewModels to Activities/Fragments
* (see https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)
*/
abstract class ViewEvent
interface ContextExecutor {
operator fun invoke(context: Context)
}
interface ActivityExecutor {
operator fun invoke(activity: UIActivity<*>)
}
interface FragmentExecutor {
operator fun invoke(fragment: BaseFragment<*>)
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/arch/ViewModelHolder.kt
================================================
package com.topjohnwu.magisk.arch
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.install.InstallViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
interface ViewModelHolder : LifecycleOwner, ViewModelStoreOwner {
val viewModel: BaseViewModel
fun startObserveLiveData() {
viewModel.viewEvents.observe(this, this::onEventDispatched)
Info.isConnected.observe(this, viewModel::onNetworkChanged)
}
/**
* Called for all [ViewEvent]s published by associated viewModel.
*/
fun onEventDispatched(event: ViewEvent) {}
}
object VMFactory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun create(modelClass: Class): T {
return when (modelClass) {
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
InstallViewModel::class.java ->
InstallViewModel(ServiceLocator.networkService, ServiceLocator.markwon)
SuRequestViewModel::class.java ->
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
else -> modelClass.newInstance()
} as T
}
}
inline fun ViewModelHolder.viewModel() =
lazy(LazyThreadSafetyMode.NONE) {
ViewModelProvider(this, VMFactory)[VM::class.java]
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/databinding/DataBindingAdapters.kt
================================================
package com.topjohnwu.magisk.databinding
import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.Spanned
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.Spinner
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.appcompat.widget.Toolbar
import androidx.cardview.widget.CardView
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.updateLayoutParams
import androidx.core.widget.ImageViewCompat
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.databinding.InverseMethod
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.slider.Slider
import com.google.android.material.textfield.TextInputLayout
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.widget.IndeterminateCheckBox
import kotlin.math.roundToInt
@BindingAdapter("gone")
fun setGone(view: View, gone: Boolean) {
view.isGone = gone
}
@BindingAdapter("invisible")
fun setInvisible(view: View, invisible: Boolean) {
view.isInvisible = invisible
}
@BindingAdapter("goneUnless")
fun setGoneUnless(view: View, goneUnless: Boolean) {
setGone(view, goneUnless.not())
}
@BindingAdapter("invisibleUnless")
fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
setInvisible(view, invisibleUnless.not())
}
@BindingAdapter("markdownText")
fun setMarkdownText(tv: TextView, markdown: Spanned) {
ServiceLocator.markwon.setParsedMarkdown(tv, markdown)
}
@BindingAdapter("onNavigationClick")
fun setOnNavigationClickedListener(view: Toolbar, listener: View.OnClickListener) {
view.setNavigationOnClickListener(listener)
}
@BindingAdapter("srcCompat")
fun setImageResource(view: ImageView, @DrawableRes resId: Int) {
view.setImageResource(resId)
}
@BindingAdapter("srcCompat")
fun setImageResource(view: ImageView, drawable: Drawable) {
view.setImageDrawable(drawable)
}
@BindingAdapter("onTouch")
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
view.setOnTouchListener(listener)
}
@BindingAdapter("scrollToLast")
fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
fun scrollToLast() = UiThreadHandler.handler.postDelayed({
view.scrollToPosition(view.adapter?.itemCount?.minus(1) ?: 0)
}, 30)
fun wait(callback: () -> Unit) {
UiThreadHandler.handler.postDelayed(callback, 1000)
}
fun RecyclerView.Adapter<*>.setListener() {
val observer = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
scrollToLast()
}
}
registerAdapterDataObserver(observer)
view.setTag(R.id.recyclerScrollListener, observer)
}
fun RecyclerView.Adapter<*>.removeListener() {
val observer =
view.getTag(R.id.recyclerScrollListener) as? RecyclerView.AdapterDataObserver ?: return
unregisterAdapterDataObserver(observer)
}
fun trySetListener(): Unit = view.adapter?.setListener() ?: wait { trySetListener() }
if (shouldScrollToLast) {
trySetListener()
} else {
view.adapter?.removeListener()
}
}
@BindingAdapter("isEnabled")
fun setEnabled(view: View, isEnabled: Boolean) {
view.isEnabled = isEnabled
}
@BindingAdapter("error")
fun TextInputLayout.setErrorString(error: String) {
val newError = error.let { if (it.isEmpty()) null else it }
if (this.error == null && newError == null) return
this.error = newError
}
// md2
@BindingAdapter(
"android:layout_marginLeft",
"android:layout_marginTop",
"android:layout_marginRight",
"android:layout_marginBottom",
"android:layout_marginStart",
"android:layout_marginEnd",
requireAll = false
)
fun View.setMargins(
marginLeft: Int?,
marginTop: Int?,
marginRight: Int?,
marginBottom: Int?,
marginStart: Int?,
marginEnd: Int?
) = updateLayoutParams {
marginLeft?.let { leftMargin = it }
marginTop?.let { topMargin = it }
marginRight?.let { rightMargin = it }
marginBottom?.let { bottomMargin = it }
marginStart?.let { this.marginStart = it }
marginEnd?.let { this.marginEnd = it }
}
@BindingAdapter("nestedScrollingEnabled")
fun RecyclerView.setNestedScrolling(enabled: Boolean) {
isNestedScrollingEnabled = enabled
}
@BindingAdapter("isSelected")
fun View.isSelected(isSelected: Boolean) {
this.isSelected = isSelected
}
@BindingAdapter("dividerVertical", "dividerHorizontal", requireAll = false)
fun RecyclerView.setDividers(dividerVertical: Drawable?, dividerHorizontal: Drawable?) {
if (dividerHorizontal != null) {
DividerItemDecoration(context, LinearLayoutManager.HORIZONTAL).apply {
setDrawable(dividerHorizontal)
}.let { addItemDecoration(it) }
}
if (dividerVertical != null) {
DividerItemDecoration(context, LinearLayoutManager.VERTICAL).apply {
setDrawable(dividerVertical)
}.let { addItemDecoration(it) }
}
}
@BindingAdapter("icon")
fun Button.setIconRes(res: Int) {
(this as MaterialButton).setIconResource(res)
}
@BindingAdapter("icon")
fun Button.setIcon(drawable: Drawable) {
(this as MaterialButton).icon = drawable
}
@BindingAdapter("strokeWidth")
fun MaterialCardView.setCardStrokeWidthBound(stroke: Float) {
strokeWidth = stroke.roundToInt()
}
@BindingAdapter("onMenuClick")
fun Toolbar.setOnMenuClickListener(listener: Toolbar.OnMenuItemClickListener) {
setOnMenuItemClickListener(listener)
}
@BindingAdapter("onCloseClicked")
fun Chip.setOnCloseClickedListenerBinding(listener: View.OnClickListener) {
setOnCloseIconClickListener(listener)
}
@BindingAdapter("progressAnimated")
fun ProgressBar.setProgressAnimated(newProgress: Int) {
val animator = tag as? ValueAnimator
animator?.cancel()
ValueAnimator.ofInt(progress, newProgress).apply {
interpolator = FastOutSlowInInterpolator()
addUpdateListener { progress = it.animatedValue as Int }
tag = this
}.start()
}
@BindingAdapter("android:text")
fun TextView.setTextSafe(text: Int) {
if (text == 0) this.text = null else setText(text)
}
@BindingAdapter("android:onLongClick")
fun View.setOnLongClickListenerBinding(listener: () -> Unit) {
setOnLongClickListener {
listener()
true
}
}
@BindingAdapter("strikeThrough")
fun TextView.setStrikeThroughEnabled(useStrikeThrough: Boolean) {
paintFlags = if (useStrikeThrough) {
paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
}
@BindingAdapter("spanCount")
fun RecyclerView.setSpanCount(count: Int) {
when (val lama = layoutManager) {
is GridLayoutManager -> lama.spanCount = count
is StaggeredGridLayoutManager -> lama.spanCount = count
}
}
@BindingAdapter("state")
fun setState(view: IndeterminateCheckBox, state: Boolean?) {
if (view.state != state)
view.state = state
}
@InverseBindingAdapter(attribute = "state")
fun getState(view: IndeterminateCheckBox) = view.state
@BindingAdapter("stateAttrChanged")
fun setListeners(
view: IndeterminateCheckBox,
attrChange: InverseBindingListener
) {
view.setOnStateChangedListener { _, _ ->
attrChange.onChange()
}
}
@BindingAdapter("cardBackgroundColorAttr")
fun CardView.setCardBackgroundColorAttr(attr: Int) {
val tv = TypedValue()
context.theme.resolveAttribute(attr, tv, true)
setCardBackgroundColor(tv.data)
}
@BindingAdapter("tint")
fun ImageView.setTint(color: Int) {
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color))
}
@BindingAdapter("tintAttr")
fun ImageView.setTintAttr(attr: Int) {
val tv = TypedValue()
context.theme.resolveAttribute(attr, tv, true)
ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(tv.data))
}
@BindingAdapter("textColorAttr")
fun TextView.setTextColorAttr(attr: Int) {
val tv = TypedValue()
context.theme.resolveAttribute(attr, tv, true)
setTextColor(tv.data)
}
@BindingAdapter("android:text")
fun TextView.setText(text: TextHolder) {
this.text = text.getText(context.resources)
}
@BindingAdapter("items", "layout")
fun Spinner.setAdapter(items: Array, layoutRes: Int) {
adapter = ArrayAdapter(context, layoutRes, items)
}
@BindingAdapter("labelFormatter")
fun Slider.setLabelFormatter(formatter: (Float) -> Int) {
setLabelFormatter { value -> resources.getString(formatter(value)) }
}
@InverseBindingAdapter(attribute = "android:value")
fun Slider.getValueBinding() = value
@BindingAdapter("android:valueAttrChanged")
fun Slider.setListener(attrChange: InverseBindingListener) {
addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
override fun onStartTrackingTouch(slider: Slider) = Unit
override fun onStopTrackingTouch(slider: Slider) = attrChange.onChange()
})
}
@InverseMethod("sliderValueToPolicy")
fun policyToSliderValue(policy: Int): Float {
return when (policy) {
SuPolicy.DENY -> 1f
SuPolicy.RESTRICT -> 2f
SuPolicy.ALLOW -> 3f
else -> 1f
}
}
fun sliderValueToPolicy(value: Float): Int {
return when (value) {
1f -> SuPolicy.DENY
2f -> SuPolicy.RESTRICT
3f -> SuPolicy.ALLOW
else -> SuPolicy.DENY
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/databinding/DiffObservableList.kt
================================================
package com.topjohnwu.magisk.databinding
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.databinding.ListChangeRegistry
import androidx.databinding.ObservableList
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.AbstractList
// Only expose the immutable List types
interface DiffList> : List {
fun calculateDiff(newItems: List): DiffUtil.DiffResult
@MainThread
fun update(newItems: List, diffResult: DiffUtil.DiffResult)
@WorkerThread
suspend fun update(newItems: List)
}
interface FilterList> : List {
fun filter(filter: (T) -> Boolean)
@MainThread
fun set(newItems: List)
}
fun > diffList(): DiffList = DiffObservableList()
fun > filterList(scope: CoroutineScope): FilterList =
FilterableDiffObservableList(scope)
private open class DiffObservableList>
: AbstractList(), ObservableList, DiffList, ListUpdateCallback {
protected var list: List = emptyList()
private val listeners = ListChangeRegistry()
override val size: Int get() = list.size
override fun get(index: Int) = list[index]
override fun calculateDiff(newItems: List): DiffUtil.DiffResult {
return doCalculateDiff(list, newItems)
}
protected fun doCalculateDiff(oldItems: List, newItems: List): DiffUtil.DiffResult {
return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = oldItems.size
override fun getNewListSize() = newItems.size
@Suppress("UNCHECKED_CAST")
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return (oldItem as DiffItem).itemSameAs(newItem)
}
@Suppress("UNCHECKED_CAST")
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
return (oldItem as DiffItem).contentSameAs(newItem)
}
}, true)
}
@MainThread
override fun update(newItems: List, diffResult: DiffUtil.DiffResult) {
list = ArrayList(newItems)
diffResult.dispatchUpdatesTo(this)
}
@WorkerThread
override suspend fun update(newItems: List) {
val diffResult = calculateDiff(newItems)
withContext(Dispatchers.Main) {
update(newItems, diffResult)
}
}
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback>) {
listeners.add(listener)
}
override fun removeOnListChangedCallback(listener: ObservableList.OnListChangedCallback>) {
listeners.remove(listener)
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
listeners.notifyChanged(this, position, count)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
listeners.notifyMoved(this, fromPosition, toPosition, 1)
}
override fun onInserted(position: Int, count: Int) {
modCount += 1
listeners.notifyInserted(this, position, count)
}
override fun onRemoved(position: Int, count: Int) {
modCount += 1
listeners.notifyRemoved(this, position, count)
}
}
private class FilterableDiffObservableList>(
private val scope: CoroutineScope
) : DiffObservableList(), FilterList {
private var sublist: List = emptyList()
private var job: Job? = null
private var lastFilter: ((T) -> Boolean)? = null
// ---
override fun filter(filter: (T) -> Boolean) {
lastFilter = filter
job?.cancel()
job = scope.launch(Dispatchers.Default) {
val oldList = sublist
val newList = list.filter(filter)
val diff = doCalculateDiff(oldList, newList)
withContext(Dispatchers.Main) {
sublist = newList
diff.dispatchUpdatesTo(this@FilterableDiffObservableList)
}
}
}
// ---
override fun get(index: Int): T {
return sublist[index]
}
override val size: Int
get() = sublist.size
@MainThread
override fun set(newItems: List) {
onRemoved(0, sublist.size)
list = newItems
sublist = emptyList()
lastFilter?.let { filter(it) }
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/databinding/MergeObservableList.kt
================================================
package com.topjohnwu.magisk.databinding
import androidx.databinding.ListChangeRegistry
import androidx.databinding.ObservableList
import androidx.databinding.ObservableList.OnListChangedCallback
import java.util.AbstractList
@Suppress("UNCHECKED_CAST")
class MergeObservableList : AbstractList(), ObservableList {
private val lists: MutableList> = mutableListOf()
private val listeners = ListChangeRegistry()
private val callback = Callback()
override fun addOnListChangedCallback(callback: OnListChangedCallback>) {
listeners.add(callback)
}
override fun removeOnListChangedCallback(callback: OnListChangedCallback>) {
listeners.remove(callback)
}
override fun get(index: Int): T {
if (index < 0)
throw IndexOutOfBoundsException()
var idx = index
for (list in lists) {
val size = list.size
if (idx < size) {
return list[idx]
}
idx -= size
}
throw IndexOutOfBoundsException()
}
override val size: Int
get() = lists.fold(0) { i, it -> i + it.size }
fun insertItem(obj: T): MergeObservableList {
val idx = size
lists.add(listOf(obj))
++modCount
listeners.notifyInserted(this, idx, 1)
return this
}
fun insertList(list: List): MergeObservableList {
val idx = size
lists.add(list)
++modCount
(list as? ObservableList)?.addOnListChangedCallback(callback)
if (list.isNotEmpty())
listeners.notifyInserted(this, idx, list.size)
return this
}
fun removeItem(obj: T): Boolean {
var idx = 0
for ((i, list) in lists.withIndex()) {
if (list !is ObservableList<*>) {
if (obj == list[0]) {
lists.removeAt(i)
++modCount
listeners.notifyRemoved(this, idx, 1)
return true
}
}
idx += list.size
}
return false
}
fun removeList(listToRemove: List): Boolean {
var idx = 0
for ((i, list) in lists.withIndex()) {
if (listToRemove === list) {
(list as? ObservableList)?.removeOnListChangedCallback(callback)
lists.removeAt(i)
++modCount
listeners.notifyRemoved(this, idx, list.size)
return true
}
idx += list.size
}
return false
}
override fun clear() {
val sz = size
for (list in lists) {
if (list is ObservableList) {
list.removeOnListChangedCallback(callback)
}
}
++modCount
lists.clear()
if (sz > 0)
listeners.notifyRemoved(this, 0, sz)
}
private fun subIndexToIndex(subList: List<*>, index: Int): Int {
if (index < 0)
throw IndexOutOfBoundsException()
var idx = 0
for (list in lists) {
if (subList === list) {
return idx + index
}
idx += list.size
}
throw IllegalArgumentException()
}
inner class Callback : OnListChangedCallback>() {
override fun onChanged(sender: ObservableList) {
++modCount
listeners.notifyChanged(this@MergeObservableList)
}
override fun onItemRangeChanged(
sender: ObservableList,
positionStart: Int,
itemCount: Int
) {
listeners.notifyChanged(this@MergeObservableList,
subIndexToIndex(sender, positionStart), itemCount)
}
override fun onItemRangeInserted(
sender: ObservableList,
positionStart: Int,
itemCount: Int
) {
++modCount
listeners.notifyInserted(this@MergeObservableList,
subIndexToIndex(sender, positionStart), itemCount)
}
override fun onItemRangeMoved(
sender: ObservableList,
fromPosition: Int,
toPosition: Int,
itemCount: Int
) {
val idx = subIndexToIndex(sender, 0)
listeners.notifyMoved(this@MergeObservableList,
idx + fromPosition, idx + toPosition, itemCount)
}
override fun onItemRangeRemoved(
sender: ObservableList,
positionStart: Int,
itemCount: Int
) {
++modCount
listeners.notifyRemoved(this@MergeObservableList,
subIndexToIndex(sender, positionStart), itemCount)
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/databinding/ObservableHost.kt
================================================
package com.topjohnwu.magisk.databinding
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
/**
* Modified from https://github.com/skoumalcz/teanity/blob/1.2/core/src/main/java/com/skoumal/teanity/observable/Notifyable.kt
*
* Interface that allows user to be observed via DataBinding or manually by assigning listeners.
*
* @see [androidx.databinding.Observable]
* */
interface ObservableHost : Observable {
var callbacks: PropertyChangeRegistry?
/**
* Notifies all observers that something has changed. By default implementation this method is
* synchronous, hence observers will never be notified in undefined order. Observers might
* choose to refresh the view completely, which is beyond the scope of this function.
* */
fun notifyChange() {
synchronized(this) {
callbacks ?: return
}.notifyCallbacks(this, 0, null)
}
/**
* Notifies all observers about field with [fieldId] has been changed. This will happen
* synchronously before or after [notifyChange] has been called. It will never be called during
* the execution of aforementioned method.
* */
fun notifyPropertyChanged(fieldId: Int) {
synchronized(this) {
callbacks ?: return
}.notifyCallbacks(this, fieldId, null)
}
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
synchronized(this) {
callbacks ?: PropertyChangeRegistry().also { callbacks = it }
}.add(callback)
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
synchronized(this) {
callbacks ?: return
}.remove(callback)
}
}
fun ObservableHost.addOnPropertyChangedCallback(
fieldId: Int,
removeAfterChanged: Boolean = false,
callback: () -> Unit
) {
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
if (fieldId == propertyId) {
callback()
if (removeAfterChanged)
removeOnPropertyChangedCallback(this)
}
}
})
}
/**
* Injects boilerplate implementation for {@literal @}[androidx.databinding.Bindable] field setters.
*
* # Examples:
* ```kotlin
* @get:Bindable
* var myField = defaultValue
* set(value) = set(value, field, { field = it }, BR.myField) {
* doSomething(it)
* }
* ```
* */
inline fun ObservableHost.set(
new: T, old: T, setter: (T) -> Unit, fieldId: Int, afterChanged: (T) -> Unit = {}) {
if (old != new) {
setter(new)
notifyPropertyChanged(fieldId)
afterChanged(new)
}
}
inline fun ObservableHost.set(
new: T, old: T, setter: (T) -> Unit, vararg fieldIds: Int, afterChanged: (T) -> Unit = {}) {
if (old != new) {
setter(new)
fieldIds.forEach { notifyPropertyChanged(it) }
afterChanged(new)
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/databinding/RecyclerViewItems.kt
================================================
package com.topjohnwu.magisk.databinding
import androidx.databinding.PropertyChangeRegistry
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
abstract class RvItem {
abstract val layoutRes: Int
}
abstract class ObservableRvItem : RvItem(), ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
}
interface ItemWrapper {
val item: E
}
interface ViewAwareItem {
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
}
interface DiffItem {
fun itemSameAs(other: T): Boolean {
if (this === other) return true
return when (this) {
is ItemWrapper<*> -> item == (other as ItemWrapper<*>).item
is Comparable<*> -> compareValues(this, other as Comparable<*>) == 0
else -> this == other
}
}
fun contentSameAs(other: T) = true
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/databinding/RvItemAdapter.kt
================================================
package com.topjohnwu.magisk.databinding
import android.annotation.SuppressLint
import android.util.SparseArray
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.BindingAdapter
import androidx.databinding.DataBindingUtil
import androidx.databinding.ObservableList
import androidx.databinding.ObservableList.OnListChangedCallback
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.BR
class RvItemAdapter(
val items: List,
val extraBindings: SparseArray<*>?
) : RecyclerView.Adapter() {
private var lifecycleOwner: LifecycleOwner? = null
private var recyclerView: RecyclerView? = null
private val observer by lazy(LazyThreadSafetyMode.NONE) { ListObserver() }
override fun onAttachedToRecyclerView(rv: RecyclerView) {
lifecycleOwner = rv.findViewTreeLifecycleOwner()
recyclerView = rv
if (items is ObservableList)
items.addOnListChangedCallback(observer)
}
override fun onDetachedFromRecyclerView(rv: RecyclerView) {
lifecycleOwner = null
recyclerView = null
if (items is ObservableList)
items.removeOnListChangedCallback(observer)
}
override fun onCreateViewHolder(parent: ViewGroup, layoutRes: Int): ViewHolder {
val inflator = LayoutInflater.from(parent.context)
return ViewHolder(DataBindingUtil.inflate(inflator, layoutRes, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.binding.setVariable(BR.item, item)
extraBindings?.let {
for (i in 0 until it.size()) {
holder.binding.setVariable(it.keyAt(i), it.valueAt(i))
}
}
holder.binding.lifecycleOwner = lifecycleOwner
holder.binding.executePendingBindings()
recyclerView?.let {
if (item is ViewAwareItem)
item.onBind(holder.binding, it)
}
}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int) = items[position].layoutRes
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
inner class ListObserver : OnListChangedCallback>() {
@SuppressLint("NotifyDataSetChanged")
override fun onChanged(sender: ObservableList) {
notifyDataSetChanged()
}
override fun onItemRangeChanged(
sender: ObservableList,
positionStart: Int,
itemCount: Int
) {
notifyItemRangeChanged(positionStart, itemCount)
}
override fun onItemRangeInserted(
sender: ObservableList?,
positionStart: Int,
itemCount: Int
) {
notifyItemRangeInserted(positionStart, itemCount)
}
override fun onItemRangeMoved(
sender: ObservableList?,
fromPosition: Int,
toPosition: Int,
itemCount: Int
) {
for (i in 0 until itemCount) {
notifyItemMoved(fromPosition + i, toPosition + i)
}
}
override fun onItemRangeRemoved(
sender: ObservableList?,
positionStart: Int,
itemCount: Int
) {
notifyItemRangeRemoved(positionStart, itemCount)
}
}
}
inline fun bindExtra(body: (SparseArray) -> Unit) = SparseArray().also(body)
@BindingAdapter("items", "extraBindings", requireAll = false)
fun RecyclerView.setAdapter(items: List?, extraBindings: SparseArray<*>?) {
if (items != null) {
val rva = (adapter as? RvItemAdapter<*>)
if (rva == null || rva.items !== items || rva.extraBindings !== extraBindings) {
adapter = RvItemAdapter(items, extraBindings)
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/dialog/DarkThemeDialog.kt
================================================
package com.topjohnwu.magisk.dialog
import android.app.Activity
import androidx.appcompat.app.AppCompatDelegate
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.core.R as CoreR
class DarkThemeDialog : DialogBuilder {
override fun build(dialog: MagiskDialog) {
val activity = dialog.ownerActivity!!
dialog.apply {
setTitle(CoreR.string.settings_dark_mode_title)
setMessage(CoreR.string.settings_dark_mode_message)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = CoreR.string.settings_dark_mode_light
icon = R.drawable.ic_day
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_NO, activity) }
}
setButton(MagiskDialog.ButtonType.NEUTRAL) {
text = CoreR.string.settings_dark_mode_system
icon = R.drawable.ic_day_night
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, activity) }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = CoreR.string.settings_dark_mode_dark
icon = R.drawable.ic_night
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_YES, activity) }
}
}
}
private fun selectTheme(mode: Int, activity: Activity) {
Config.darkTheme = mode
(activity as UIActivity<*>).delegate.localNightMode = mode
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/dialog/EnvFixDialog.kt
================================================
package com.topjohnwu.magisk.dialog
import android.widget.Toast
import androidx.core.os.postDelayed
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.reboot
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.launch
class EnvFixDialog(private val vm: HomeViewModel, private val code: Int) : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.env_fix_title)
setMessage(R.string.env_fix_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
doNotDismiss = true
onClick {
dialog.apply {
setTitle(R.string.setup_title)
setMessage(R.string.setup_msg)
resetButtons()
setCancelable(false)
}
dialog.activity.lifecycleScope.launch {
MagiskInstaller.FixEnv().exec { success ->
dialog.dismiss()
context.toast(
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
Toast.LENGTH_LONG
)
if (success)
UiThreadHandler.handler.postDelayed(5000) { reboot() }
}
}
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}
if (code == 2 || // No rules block, module policy not loaded
Info.env.versionCode != BuildConfig.APP_VERSION_CODE ||
Info.env.versionString != BuildConfig.APP_VERSION_NAME) {
dialog.setMessage(R.string.env_full_fix_msg)
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
vm.onMagiskPressed()
dialog.dismiss()
}
}
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/dialog/LocalModuleInstallDialog.kt
================================================
package com.topjohnwu.magisk.dialog
import android.net.Uri
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.view.MagiskDialog
class LocalModuleInstallDialog(
private val viewModel: ModuleViewModel,
private val uri: Uri,
private val displayName: String
) : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.confirm_install_title)
setMessage(context.getString(R.string.confirm_install, displayName))
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
viewModel.apply {
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, uri).navigate()
}
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/dialog/ManagerInstallDialog.kt
================================================
package com.topjohnwu.magisk.dialog
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.view.MagiskDialog
import java.io.File
class ManagerInstallDialog : MarkDownDialog() {
override suspend fun getMarkdownText(): String {
val text = Info.update.note
// Cache the changelog
File(AppContext.cacheDir, "${Info.update.versionCode}.md").writeText(text)
return text
}
override fun build(dialog: MagiskDialog) {
super.build(dialog)
dialog.apply {
setCancelable(true)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick { DownloadEngine.startWithActivity(activity, Subject.App()) }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/dialog/MarkDownDialog.kt
================================================
package com.topjohnwu.magisk.dialog
import android.view.LayoutInflater
import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import com.topjohnwu.magisk.core.R as CoreR
abstract class MarkDownDialog : DialogBuilder {
abstract suspend fun getMarkdownText(): String
@CallSuper
override fun build(dialog: MagiskDialog) {
with(dialog) {
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
setView(view)
val tv = view.findViewById(R.id.md_txt)
activity.lifecycleScope.launch {
try {
val text = withContext(Dispatchers.IO) { getMarkdownText() }
ServiceLocator.markwon.setMarkdown(tv, text)
} catch (e: IOException) {
Timber.e(e)
tv.setText(CoreR.string.download_file_error)
}
}
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/dialog/OnlineModuleInstallDialog.kt
================================================
package com.topjohnwu.magisk.dialog
import android.content.Context
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Notifications
import kotlinx.parcelize.Parcelize
class OnlineModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
private val svc get() = ServiceLocator.networkService
override suspend fun getMarkdownText(): String {
val str = svc.fetchString(item.changelog)
return if (str.length > 1000) str.substring(0, 1000) else str
}
@Parcelize
class Module(
override val module: OnlineModule,
override val autoLaunch: Boolean,
override val notifyId: Int = Notifications.nextId()
) : Subject.Module() {
override fun pendingIntent(context: Context) = FlashFragment.installIntent(context, file)
}
override fun build(dialog: MagiskDialog) {
super.build(dialog)
dialog.apply {
fun download(install: Boolean) {
DownloadEngine.startWithActivity(activity, Module(item, install))
}
val title = context.getString(R.string.repo_install_title,
item.name, item.version, item.versionCode)
setTitle(title)
setCancelable(true)
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = R.string.download
onClick { download(false) }
}
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick { download(true) }
}
setButton(MagiskDialog.ButtonType.NEUTRAL) {
text = android.R.string.cancel
}
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/dialog/SecondSlotWarningDialog.kt
================================================
package com.topjohnwu.magisk.dialog
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.view.MagiskDialog
class SecondSlotWarningDialog : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(android.R.string.dialog_alert_title)
setMessage(R.string.install_inactive_slot_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
}
setCancelable(true)
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/dialog/SuperuserRevokeDialog.kt
================================================
package com.topjohnwu.magisk.dialog
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.view.MagiskDialog
class SuperuserRevokeDialog(
private val appName: String,
private val onSuccess: () -> Unit
) : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.su_revoke_title)
setMessage(R.string.su_revoke_msg, appName)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick { onSuccess() }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/dialog/UninstallDialog.kt
================================================
package com.topjohnwu.magisk.dialog
import android.app.ProgressDialog
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.events.DialogBuilder
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.launch
class UninstallDialog : DialogBuilder {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.uninstall_magisk_title)
setMessage(R.string.uninstall_magisk_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.restore_img
onClick { restore(dialog.activity) }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = R.string.complete_uninstall
onClick { completeUninstall(dialog) }
}
}
}
@Suppress("DEPRECATION")
private fun restore(activity: UIActivity<*>) {
val dialog = ProgressDialog(activity).apply {
setMessage(activity.getString(R.string.restore_img_msg))
show()
}
activity.lifecycleScope.launch {
MagiskInstaller.Restore().exec { success ->
dialog.dismiss()
if (success) {
activity.toast(R.string.restore_done, Toast.LENGTH_SHORT)
} else {
activity.toast(R.string.restore_fail, Toast.LENGTH_LONG)
}
}
}
}
private fun completeUninstall(dialog: MagiskDialog) {
(dialog.ownerActivity as NavigationActivity<*>)
.navigation.navigate(FlashFragment.uninstall())
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/events/ViewEvents.kt
================================================
package com.topjohnwu.magisk.events
import android.content.Context
import android.view.View
import androidx.annotation.StringRes
import androidx.navigation.NavDirections
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.base.relaunch
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts
class PermissionEvent(
private val permission: String,
private val callback: (Boolean) -> Unit
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) =
activity.withPermission(permission, callback)
}
class BackPressEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.onBackPressed()
}
}
class DieEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.finish()
}
}
class ShowUIEvent(private val delegate: View.AccessibilityDelegate?)
: ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.setContentView()
activity.setAccessibilityDelegate(delegate)
}
}
class RecreateEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.relaunch()
}
}
class AuthEvent(
private val callback: () -> Unit
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.withAuthentication { if (it) callback() }
}
}
class GetContentEvent(
private val type: String,
private val callback: ContentResultCallback
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
activity.getContent(type, callback)
}
}
class NavigationEvent(
private val directions: NavDirections,
private val pop: Boolean
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
(activity as? NavigationActivity<*>)?.apply {
if (pop) navigation.popBackStack()
directions.navigate()
}
}
}
class AddHomeIconEvent : ViewEvent(), ContextExecutor {
override fun invoke(context: Context) {
Shortcuts.addHomeIcon(context)
}
}
class SnackbarEvent(
private val msg: TextHolder,
private val length: Int = Snackbar.LENGTH_SHORT,
private val builder: Snackbar.() -> Unit = {}
) : ViewEvent(), ActivityExecutor {
constructor(
@StringRes res: Int,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) : this(res.asText(), length, builder)
constructor(
msg: String,
length: Int = Snackbar.LENGTH_SHORT,
builder: Snackbar.() -> Unit = {}
) : this(msg.asText(), length, builder)
override fun invoke(activity: UIActivity<*>) {
activity.showSnackbar(msg.getText(activity.resources), length, builder)
}
}
class DialogEvent(
private val builder: DialogBuilder
) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
MagiskDialog(activity).apply(builder::build).show()
}
}
interface DialogBuilder {
fun build(dialog: MagiskDialog)
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
================================================
package com.topjohnwu.magisk.ui
import android.Manifest
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.forEach
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.arch.startAnimations
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.SplashController
import com.topjohnwu.magisk.core.base.SplashScreenHost
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.launch
import java.io.File
import com.topjohnwu.magisk.core.R as CoreR
class MainViewModel : BaseViewModel()
class MainActivity : NavigationActivity(), SplashScreenHost {
override val layoutRes = R.layout.activity_main_md2
override val viewModel by viewModel()
override val navHostId: Int = R.id.main_nav_host
override val splashController = SplashController(this)
override val snackbarView: View
get() {
val fragmentOverride = currentFragment?.snackbarView
return fragmentOverride ?: super.snackbarView
}
override val snackbarAnchorView: View?
get() {
val fragmentAnchor = currentFragment?.snackbarAnchorView
return when {
fragmentAnchor?.isVisible == true -> fragmentAnchor
binding.mainNavigation.isVisible -> return binding.mainNavigation
else -> null
}
}
private var isRootFragment = true
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Theme.selected.themeRes)
splashController.preOnCreate()
super.onCreate(savedInstanceState)
splashController.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
splashController.onResume()
}
@SuppressLint("InlinedApi")
override fun onCreateUi(savedInstanceState: Bundle?) {
setContentView()
showUnsupportedMessage()
askForHomeShortcut()
// Ask permission to post notifications for background update check
if (Config.checkUpdate) {
withPermission(Manifest.permission.POST_NOTIFICATIONS) {
Config.checkUpdate = it
}
}
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
navigation.addOnDestinationChangedListener { _, destination, _ ->
isRootFragment = when (destination.id) {
R.id.homeFragment,
R.id.modulesFragment,
R.id.superuserFragment,
R.id.logFragment -> true
else -> false
}
setDisplayHomeAsUpEnabled(!isRootFragment)
requestNavigationHidden(!isRootFragment)
binding.mainNavigation.menu.forEach {
if (it.itemId == destination.id) {
it.isChecked = true
}
}
}
setSupportActionBar(binding.mainToolbar)
binding.mainNavigation.setOnItemSelectedListener {
getScreen(it.itemId)?.navigate()
true
}
binding.mainNavigation.setOnItemReselectedListener {
// https://issuetracker.google.com/issues/124538620
}
binding.mainNavigation.menu.apply {
findItem(R.id.superuserFragment)?.isEnabled = Info.showSuperUser
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive && LocalModule.loaded()
}
val section =
if (intent.action == Intent.ACTION_APPLICATION_PREFERENCES)
Const.Nav.SETTINGS
else
intent.getStringExtra(Const.Key.OPEN_SECTION)
getScreen(section)?.navigate()
if (!isRootFragment) {
requestNavigationHidden(requiresAnimation = savedInstanceState == null)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
else -> return super.onOptionsItemSelected(item)
}
return true
}
fun setDisplayHomeAsUpEnabled(isEnabled: Boolean) {
binding.mainToolbar.startAnimations()
when {
isEnabled -> binding.mainToolbar.setNavigationIcon(R.drawable.ic_back_md2)
else -> binding.mainToolbar.navigationIcon = null
}
}
internal fun requestNavigationHidden(hide: Boolean = true, requiresAnimation: Boolean = true) {
val bottomView = binding.mainNavigation
if (requiresAnimation) {
bottomView.isVisible = true
bottomView.isHidden = hide
} else {
bottomView.isGone = hide
}
}
fun invalidateToolbar() {
//binding.mainToolbar.startAnimations()
binding.mainToolbar.invalidate()
}
private fun getScreen(name: String?): NavDirections? {
return when (name) {
Const.Nav.SUPERUSER -> MainDirections.actionSuperuserFragment()
Const.Nav.MODULES -> MainDirections.actionModuleFragment()
Const.Nav.SETTINGS -> HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
else -> null
}
}
private fun getScreen(id: Int): NavDirections? {
return when (id) {
R.id.homeFragment -> MainDirections.actionHomeFragment()
R.id.modulesFragment -> MainDirections.actionModuleFragment()
R.id.superuserFragment -> MainDirections.actionSuperuserFragment()
R.id.logFragment -> MainDirections.actionLogFragment()
else -> null
}
}
@SuppressLint("InlinedApi")
override fun showInvalidStateMessage(): Unit = runOnUiThread {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_nonroot_stub_title)
setMessage(CoreR.string.unsupport_nonroot_stub_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = CoreR.string.install
onClick {
withPermission(REQUEST_INSTALL_PACKAGES) {
if (!it) {
toast(CoreR.string.install_unknown_denied, Toast.LENGTH_SHORT)
showInvalidStateMessage()
} else {
lifecycleScope.launch {
AppMigration.restore(this@MainActivity)
}
}
}
}
}
setCancelable(false)
show()
}
}
private fun showUnsupportedMessage() {
if (Info.env.isUnsupported) {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_magisk_title)
setMessage(CoreR.string.unsupport_magisk_msg, Const.Version.MIN_VERSION)
setButton(MagiskDialog.ButtonType.POSITIVE) { text = android.R.string.ok }
setCancelable(false)
}.show()
}
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
?.split(':')
?.filterNot { File("$it/magisk").exists() }
?.any { File("$it/su").exists() } == true) {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_general_title)
setMessage(CoreR.string.unsupport_other_su_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) { text = android.R.string.ok }
setCancelable(false)
}.show()
}
if (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_general_title)
setMessage(CoreR.string.unsupport_system_app_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) { text = android.R.string.ok }
setCancelable(false)
}.show()
}
if (applicationInfo.flags and ApplicationInfo.FLAG_EXTERNAL_STORAGE != 0) {
MagiskDialog(this).apply {
setTitle(CoreR.string.unsupport_general_title)
setMessage(CoreR.string.unsupport_external_storage_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) { text = android.R.string.ok }
setCancelable(false)
}.show()
}
}
private fun askForHomeShortcut() {
if (isRunningAsStub && !Config.askedHome &&
ShortcutManagerCompat.isRequestPinShortcutSupported(this)) {
// Ask and show dialog
Config.askedHome = true
MagiskDialog(this).apply {
setTitle(CoreR.string.add_shortcut_title)
setMessage(CoreR.string.add_shortcut_msg)
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
Shortcuts.addHomeIcon(this@MainActivity)
}
}
setCancelable(true)
}.show()
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/AppProcessInfo.kt
================================================
package com.topjohnwu.magisk.ui.deny
import android.annotation.SuppressLint
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_ACTIVITIES
import android.content.pm.PackageManager.GET_PROVIDERS
import android.content.pm.PackageManager.GET_RECEIVERS
import android.content.pm.PackageManager.GET_SERVICES
import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import android.content.pm.ServiceInfo
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import androidx.core.os.ProcessCompat
import com.topjohnwu.magisk.core.ktx.getLabel
import java.util.Locale
import java.util.TreeSet
class CmdlineListItem(line: String) {
val packageName: String
val process: String
init {
val split = line.split(Regex("\\|"), 2)
packageName = split[0]
process = split.getOrElse(1) { packageName }
}
}
const val ISOLATED_MAGIC = "isolated"
@SuppressLint("InlinedApi")
class AppProcessInfo(
private val info: ApplicationInfo,
pm: PackageManager,
denyList: List
) : Comparable {
private val denyList = denyList.filter {
it.packageName == info.packageName || it.packageName == ISOLATED_MAGIC
}
val label = info.getLabel(pm)
val iconImage: Drawable = runCatching { info.loadIcon(pm) }.getOrDefault(pm.defaultActivityIcon)
val packageName: String get() = info.packageName
val processes = fetchProcesses(pm)
override fun compareTo(other: AppProcessInfo) = comparator.compare(this, other)
fun isSystemApp() = info.flags and ApplicationInfo.FLAG_SYSTEM != 0
fun isApp() = ProcessCompat.isApplicationUid(info.uid)
private fun createProcess(name: String, pkg: String = info.packageName) =
ProcessInfo(name, pkg, denyList.any { it.process == name && it.packageName == pkg })
private fun ComponentInfo.getProcName(): String = processName
?: applicationInfo.processName
?: applicationInfo.packageName
private val ServiceInfo.isIsolated get() = (flags and ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
private val ServiceInfo.useAppZygote get() = (flags and ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0
private fun Array?.toProcessList() =
orEmpty().map { createProcess(it.getProcName()) }
private fun Array?.toProcessList() = orEmpty().map {
if (it.isIsolated) {
if (it.useAppZygote) {
val proc = info.processName ?: info.packageName
createProcess("${proc}_zygote")
} else {
val proc = if (SDK_INT >= Build.VERSION_CODES.Q)
"${it.getProcName()}:${it.name}" else it.getProcName()
createProcess(proc, ISOLATED_MAGIC)
}
} else {
createProcess(it.getProcName())
}
}
private fun fetchProcesses(pm: PackageManager): Collection {
val flag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES or
GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
val packageInfo = try {
pm.getPackageInfo(info.packageName, flag)
} catch (e: Exception) {
// Exceed binder data transfer limit, parse the package locally
pm.getPackageArchiveInfo(info.sourceDir, flag) ?: return emptyList()
}
val processSet = TreeSet(compareBy({ it.name }, { it.isIsolated }))
processSet += packageInfo.activities.toProcessList()
processSet += packageInfo.services.toProcessList()
processSet += packageInfo.receivers.toProcessList()
processSet += packageInfo.providers.toProcessList()
return processSet
}
companion object {
private val comparator = compareBy(
{ it.label.lowercase(Locale.ROOT) },
{ it.info.packageName }
)
}
}
data class ProcessInfo(
val name: String,
val packageName: String,
var isEnabled: Boolean
) {
val isIsolated = packageName == ISOLATED_MAGIC
val isAppZygote = name.endsWith("_zygote")
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListFragment.kt
================================================
package com.topjohnwu.magisk.ui.deny
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.ktx.hideKeyboard
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class DenyListFragment : BaseFragment(), MenuProvider {
override val layoutRes = R.layout.fragment_deny_md2
override val viewModel by viewModel()
private lateinit var searchView: SearchView
override fun onStart() {
super.onStart()
activity?.setTitle(CoreR.string.denylist)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.appList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState != RecyclerView.SCROLL_STATE_IDLE) activity?.hideKeyboard()
}
})
binding.appList.apply {
addEdgeSpacing(top = R.dimen.l_50, bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
}
}
override fun onPreBind(binding: FragmentDenyMd2Binding) = Unit
override fun onBackPressed(): Boolean {
if (searchView.isIconfiedByDefault && !searchView.isIconified) {
searchView.isIconified = true
return true
}
return super.onBackPressed()
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_deny_md2, menu)
searchView = menu.findItem(R.id.action_search).actionView as SearchView
searchView.queryHint = searchView.context.getString(CoreR.string.hide_filter_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.query = query ?: ""
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.query = newText ?: ""
return true
}
})
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_show_system -> {
val check = !item.isChecked
viewModel.isShowSystem = check
item.isChecked = check
return true
}
R.id.action_show_OS -> {
val check = !item.isChecked
viewModel.isShowOS = check
item.isChecked = check
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onPrepareMenu(menu: Menu) {
val showSystem = menu.findItem(R.id.action_show_system)
val showOS = menu.findItem(R.id.action_show_OS)
showOS.isEnabled = showSystem.isChecked
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListRvItem.kt
================================================
package com.topjohnwu.magisk.ui.deny
import android.view.View
import android.view.ViewGroup
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.startAnimations
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.addOnPropertyChangedCallback
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.superuser.Shell
import kotlin.math.roundToInt
class DenyListRvItem(
val info: AppProcessInfo
) : ObservableRvItem(), DiffItem, Comparable {
override val layoutRes get() = R.layout.item_hide_md2
val processes = info.processes.map { ProcessRvItem(it) }
@get:Bindable
var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded)
var itemsChecked = 0
set(value) = set(value, field, { field = it }, BR.checkedPercent)
val isChecked get() = itemsChecked != 0
@get:Bindable
val checkedPercent get() = (itemsChecked.toFloat() / processes.size * 100).roundToInt()
private var _state: Boolean? = false
set(value) = set(value, field, { field = it }, BR.state)
@get:Bindable
var state: Boolean?
get() = _state
set(value) = set(value, _state, { _state = it }, BR.state) {
if (value == true) {
processes
.filterNot { it.isEnabled }
.filter { isExpanded || it.defaultSelection }
.forEach { it.toggle() }
} else {
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
processes.filter { it.isEnabled }.forEach {
if (it.process.isIsolated) {
it.toggle()
} else {
it.isEnabled = !it.isEnabled
notifyPropertyChanged(BR.enabled)
}
}
}
}
init {
processes.forEach { it.addOnPropertyChangedCallback(BR.enabled) { recalculateChecked() } }
addOnPropertyChangedCallback(BR.expanded) { recalculateChecked() }
recalculateChecked()
}
fun toggleExpand(v: View) {
(v.parent as? ViewGroup)?.startAnimations()
isExpanded = !isExpanded
}
private fun recalculateChecked() {
itemsChecked = processes.count { it.isEnabled }
_state = if (isExpanded) {
when (itemsChecked) {
0 -> false
processes.size -> true
else -> null
}
} else {
val defaultProcesses = processes.filter { it.defaultSelection }
when (defaultProcesses.count { it.isEnabled }) {
0 -> false
defaultProcesses.size -> true
else -> null
}
}
}
override fun compareTo(other: DenyListRvItem) = comparator.compare(this, other)
companion object {
private val comparator = compareBy(
{ it.itemsChecked == 0 },
{ it.info }
)
}
}
class ProcessRvItem(
val process: ProcessInfo
) : ObservableRvItem(), DiffItem {
override val layoutRes get() = R.layout.item_hide_process_md2
val displayName = if (process.isIsolated) "(isolated) ${process.name}" else process.name
@get:Bindable
var isEnabled
get() = process.isEnabled
set(value) = set(value, process.isEnabled, { process.isEnabled = it }, BR.enabled) {
val arg = if (it) "add" else "rm"
val (name, pkg) = process
Shell.cmd("magisk --denylist $arg $pkg \'$name\'").submit()
}
fun toggle() {
isEnabled = !isEnabled
}
val defaultSelection get() =
process.isIsolated || process.isAppZygote || process.name == process.packageName
override fun itemSameAs(other: ProcessRvItem) =
process.name == other.process.name && process.packageName == other.process.packageName
override fun contentSameAs(other: ProcessRvItem) =
process.isEnabled == other.process.isEnabled
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListViewModel.kt
================================================
package com.topjohnwu.magisk.ui.deny
import android.annotation.SuppressLint
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.ktx.concurrentMap
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.filterList
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.withContext
class DenyListViewModel : AsyncLoadViewModel() {
var isShowSystem = false
set(value) {
field = value
doQuery(query)
}
var isShowOS = false
set(value) {
field = value
doQuery(query)
}
var query = ""
set(value) {
field = value
doQuery(value)
}
val items = filterList(viewModelScope)
val extraBindings = bindExtra {
it.put(BR.viewModel, this)
}
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
@SuppressLint("InlinedApi")
override suspend fun doLoadWork() {
loading = true
val apps = withContext(Dispatchers.Default) {
val pm = AppContext.packageManager
val denyList = Shell.cmd("magisk --denylist ls").exec().out
.map { CmdlineListItem(it) }
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES).run {
asFlow()
.filter { AppContext.packageName != it.packageName }
.concurrentMap { AppProcessInfo(it, pm, denyList) }
.filter { it.processes.isNotEmpty() }
.concurrentMap { DenyListRvItem(it) }
.toCollection(ArrayList(size))
}
apps.sort()
apps
}
items.set(apps)
doQuery(query)
}
private fun doQuery(s: String) {
items.filter {
fun filterSystem() = isShowSystem || !it.info.isSystemApp()
fun filterOS() = (isShowSystem && isShowOS) || it.info.isApp()
fun filterQuery(): Boolean {
fun inName() = it.info.label.contains(s, true)
fun inPackage() = it.info.packageName.contains(s, true)
fun inProcesses() = it.processes.any { p -> p.process.name.contains(s, true) }
return inName() || inPackage() || inProcesses()
}
(it.isChecked || (filterSystem() && filterOS())) && filterQuery()
}
loading = false
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/ConsoleItem.kt
================================================
package com.topjohnwu.magisk.ui.flash
import android.view.View
import android.widget.TextView
import androidx.core.view.updateLayoutParams
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.ViewAwareItem
import kotlin.math.max
class ConsoleItem(
override val item: String
) : RvItem(), ViewAwareItem, DiffItem, ItemWrapper {
override val layoutRes = R.layout.item_console_md2
private var parentWidth = -1
override fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView) {
if (parentWidth < 0)
parentWidth = (recyclerView.parent as View).width
val view = binding.root as TextView
view.measure(0, 0)
// We want our recyclerView at least as wide as screen
val desiredWidth = max(view.measuredWidth, parentWidth)
view.updateLayoutParams { width = desiredWidth }
if (recyclerView.width < desiredWidth) {
recyclerView.requestLayout()
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt
================================================
package com.topjohnwu.magisk.ui.flash
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.navigation.NavDeepLinkBuilder
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.core.R as CoreR
class FlashFragment : BaseFragment(), MenuProvider {
override val layoutRes = R.layout.fragment_flash_md2
override val viewModel by viewModel()
override val snackbarView: View get() = binding.snackbarContainer
override val snackbarAnchorView: View?
get() = if (binding.restartBtn.isShown) binding.restartBtn else super.snackbarAnchorView
private var defaultOrientation = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.args = FlashFragmentArgs.fromBundle(requireArguments())
}
override fun onStart() {
super.onStart()
activity?.setTitle(CoreR.string.flash_screen_title)
viewModel.state.observe(this) {
activity?.supportActionBar?.setSubtitle(
when (it) {
FlashViewModel.State.FLASHING -> CoreR.string.flashing
FlashViewModel.State.SUCCESS -> CoreR.string.done
FlashViewModel.State.FAILED -> CoreR.string.failure
}
)
if (it == FlashViewModel.State.SUCCESS && viewModel.showReboot) {
binding.restartBtn.apply {
if (!this.isVisible) this.show()
if (!this.isFocused) this.requestFocus()
}
}
}
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_flash, menu)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return viewModel.onMenuItemClicked(item)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
defaultOrientation = activity?.requestedOrientation ?: -1
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
if (savedInstanceState == null) {
viewModel.startFlashing()
}
}
@SuppressLint("WrongConstant")
override fun onDestroyView() {
if (defaultOrientation != -1) {
activity?.requestedOrientation = defaultOrientation
}
super.onDestroyView()
}
override fun onKeyEvent(event: KeyEvent): Boolean {
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP,
KeyEvent.KEYCODE_VOLUME_DOWN -> true
else -> false
}
}
override fun onBackPressed(): Boolean {
if (viewModel.flashing.value == true)
return true
return super.onBackPressed()
}
override fun onPreBind(binding: FragmentFlashMd2Binding) = Unit
companion object {
private fun createIntent(context: Context, args: FlashFragmentArgs) =
NavDeepLinkBuilder(context)
.setGraph(R.navigation.main)
.setComponentName(MainActivity::class.java.cmp(context.packageName))
.setDestination(R.id.flashFragment)
.setArguments(args.toBundle())
.createPendingIntent()
private fun flashType(isSecondSlot: Boolean) =
if (isSecondSlot) Const.Value.FLASH_INACTIVE_SLOT else Const.Value.FLASH_MAGISK
/* Flashing is understood as installing / flashing magisk itself */
fun flash(isSecondSlot: Boolean) = MainDirections.actionFlashFragment(
action = flashType(isSecondSlot)
)
/* Patching is understood as injecting img files with magisk */
fun patch(uri: Uri) = MainDirections.actionFlashFragment(
action = Const.Value.PATCH_FILE,
additionalData = uri
)
/* Uninstalling is understood as removing magisk entirely */
fun uninstall() = MainDirections.actionFlashFragment(
action = Const.Value.UNINSTALL
)
/* Installing is understood as flashing modules / zips */
fun installIntent(context: Context, file: Uri) = FlashFragmentArgs(
action = Const.Value.FLASH_ZIP,
additionalData = file,
).let { createIntent(context, it) }
fun install(file: Uri) = MainDirections.actionFlashFragment(
action = Const.Value.FLASH_ZIP,
additionalData = file,
)
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt
================================================
package com.topjohnwu.magisk.ui.flash
import android.view.MenuItem
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.reboot
import com.topjohnwu.magisk.core.ktx.synchronized
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.tasks.FlashZip
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.superuser.CallbackList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FlashViewModel : BaseViewModel() {
enum class State {
FLASHING, SUCCESS, FAILED
}
private val _state = MutableLiveData(State.FLASHING)
val state: LiveData get() = _state
val flashing = state.map { it == State.FLASHING }
@get:Bindable
var showReboot = Info.isRooted
set(value) = set(value, field, { field = it }, BR.showReboot)
val items = ObservableArrayList()
lateinit var args: FlashFragmentArgs
private val logItems = mutableListOf().synchronized()
private val outItems = object : CallbackList() {
override fun onAddElement(e: String?) {
e ?: return
items.add(ConsoleItem(e))
logItems.add(e)
}
}
fun startFlashing() {
val (action, uri) = args
viewModelScope.launch {
val result = when (action) {
Const.Value.FLASH_ZIP -> {
uri ?: return@launch
FlashZip(uri, outItems, logItems).exec()
}
Const.Value.UNINSTALL -> {
showReboot = false
MagiskInstaller.Uninstall(outItems, logItems).exec()
}
Const.Value.FLASH_MAGISK -> {
if (Info.isEmulator)
MagiskInstaller.Emulator(outItems, logItems).exec()
else
MagiskInstaller.Direct(outItems, logItems).exec()
}
Const.Value.FLASH_INACTIVE_SLOT -> {
showReboot = false
MagiskInstaller.SecondSlot(outItems, logItems).exec()
}
Const.Value.PATCH_FILE -> {
uri ?: return@launch
showReboot = false
MagiskInstaller.Patch(uri, outItems, logItems).exec()
}
else -> {
back()
return@launch
}
}
onResult(result)
}
}
private fun onResult(success: Boolean) {
_state.value = if (success) State.SUCCESS else State.FAILED
}
fun onMenuItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_save -> savePressed()
}
return true
}
private fun savePressed() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) {
val name = "magisk_install_log_%s.log".format(
System.currentTimeMillis().toTime(timeFormatStandard)
)
val file = MediaStoreUtils.getFile(name)
file.uri.outputStream().bufferedWriter().use { writer ->
synchronized(logItems) {
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
}
SnackbarEvent(file.toString()).publish()
}
}
fun restartPressed() = reboot()
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/home/DeveloperItem.kt
================================================
package com.topjohnwu.magisk.ui.home
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.core.R as CoreR
interface Dev {
val name: String
}
private interface JohnImpl : Dev {
override val name get() = "topjohnwu"
}
private interface VvbImpl : Dev {
override val name get() = "vvb2060"
}
private interface YUImpl : Dev {
override val name get() = "yujincheng08"
}
private interface RikkaImpl : Dev {
override val name get() = "RikkaW"
}
private interface CanyieImpl : Dev {
override val name get() = "canyie"
}
sealed class DeveloperItem : Dev {
abstract val items: List
val handle get() = "@${name}"
object John : DeveloperItem(), JohnImpl {
override val items =
listOf(
object : IconLink.Twitter(), JohnImpl {},
IconLink.Github.Project
)
}
object Vvb : DeveloperItem(), VvbImpl {
override val items =
listOf(
object : IconLink.Twitter(), VvbImpl {},
object : IconLink.Github.User(), VvbImpl {}
)
}
object YU : DeveloperItem(), YUImpl {
override val items =
listOf(
object : IconLink.Twitter() { override val name = "shanasaimoe" },
object : IconLink.Github.User(), YUImpl {},
object : IconLink.Sponsor(), YUImpl {}
)
}
object Rikka : DeveloperItem(), RikkaImpl {
override val items =
listOf(
object : IconLink.Twitter() { override val name = "rikkawww" },
object : IconLink.Github.User(), RikkaImpl {}
)
}
object Canyie : DeveloperItem(), CanyieImpl {
override val items =
listOf(
object : IconLink.Twitter() { override val name = "canyie2977" },
object : IconLink.Github.User(), CanyieImpl {}
)
}
}
sealed class IconLink : RvItem() {
abstract val icon: Int
abstract val title: Int
abstract val link: String
override val layoutRes get() = R.layout.item_icon_link
abstract class PayPal : IconLink(), Dev {
override val icon get() = CoreR.drawable.ic_paypal
override val title get() = CoreR.string.paypal
override val link get() = "https://paypal.me/$name"
object Project : PayPal() {
override val name: String get() = "magiskdonate"
}
}
object Patreon : IconLink() {
override val icon get() = CoreR.drawable.ic_patreon
override val title get() = CoreR.string.patreon
override val link get() = Const.Url.PATREON_URL
}
abstract class Twitter : IconLink(), Dev {
override val icon get() = CoreR.drawable.ic_twitter
override val title get() = CoreR.string.twitter
override val link get() = "https://twitter.com/$name"
}
abstract class Github : IconLink() {
override val icon get() = CoreR.drawable.ic_github
override val title get() = CoreR.string.github
abstract class User : Github(), Dev {
override val link get() = "https://github.com/$name"
}
object Project : Github() {
override val link get() = Const.Url.SOURCE_CODE_URL
}
}
abstract class Sponsor : IconLink(), Dev {
override val icon get() = CoreR.drawable.ic_favorite
override val title get() = CoreR.string.github
override val link get() = "https://github.com/sponsors/$name"
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeFragment.kt
================================================
package com.topjohnwu.magisk.ui.home
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 android.widget.ImageView
import android.widget.TextView
import androidx.core.view.MenuProvider
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import com.topjohnwu.magisk.core.R as CoreR
import androidx.navigation.findNavController
import com.topjohnwu.magisk.arch.NavigationActivity
class HomeFragment : BaseFragment(), MenuProvider {
override val layoutRes = R.layout.fragment_home_md2
override val viewModel by viewModel()
override fun onStart() {
super.onStart()
activity?.setTitle(CoreR.string.section_home)
DownloadEngine.observeProgress(this, viewModel::onProgressUpdate)
}
private fun checkTitle(text: TextView, icon: ImageView) {
text.post {
if (text.layout?.getEllipsisCount(0) != 0) {
with (icon) {
layoutParams.width = 0
layoutParams.height = 0
requestLayout()
}
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
// If titles are squished, hide icons
with(binding.homeMagiskWrapper) {
checkTitle(homeMagiskTitle, homeMagiskIcon)
}
with(binding.homeManagerWrapper) {
checkTitle(homeManagerTitle, homeManagerIcon)
}
return binding.root
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_home_md2, menu)
if (!Info.isRooted)
menu.removeItem(R.id.action_reboot)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings ->
activity?.let {
NavigationActivity.navigate(
HomeFragmentDirections.actionHomeFragmentToSettingsFragment(),
it.findNavController(R.id.main_nav_host),
it.contentResolver,
)
}
R.id.action_reboot -> activity?.let { RebootMenu.inflate(it).show() }
else -> return super.onOptionsItemSelected(item)
}
return true
}
override fun onResume() {
super.onResume()
viewModel.stateManagerProgress = 0
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt
================================================
package com.topjohnwu.magisk.ui.home
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.core.net.toUri
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.download.Subject.App
import com.topjohnwu.magisk.core.ktx.await
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.repository.NetworkService
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.dialog.EnvFixDialog
import com.topjohnwu.magisk.dialog.ManagerInstallDialog
import com.topjohnwu.magisk.dialog.UninstallDialog
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.superuser.Shell
import kotlin.math.roundToInt
import com.topjohnwu.magisk.core.R as CoreR
class HomeViewModel(
private val svc: NetworkService
) : AsyncLoadViewModel() {
enum class State {
LOADING, INVALID, OUTDATED, UP_TO_DATE
}
val magiskTitleBarrierIds =
intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button)
val appTitleBarrierIds =
intArrayOf(R.id.home_manager_icon, R.id.home_manager_title, R.id.home_manager_button)
@get:Bindable
var isNoticeVisible = Config.safetyNotice
set(value) = set(value, field, { field = it }, BR.noticeVisible)
val magiskState
get() = when {
Info.isRooted && Info.env.isUnsupported -> State.OUTDATED
!Info.env.isActive -> State.INVALID
Info.env.versionCode < BuildConfig.APP_VERSION_CODE -> State.OUTDATED
else -> State.UP_TO_DATE
}
@get:Bindable
var appState = State.LOADING
set(value) = set(value, field, { field = it }, BR.appState)
val magiskInstalledVersion
get() = Info.env.run {
if (isActive)
("$versionString ($versionCode)" + if (isDebug) " (D)" else "").asText()
else
CoreR.string.not_available.asText()
}
@get:Bindable
var managerRemoteVersion = CoreR.string.loading.asText()
set(value) = set(value, field, { field = it }, BR.managerRemoteVersion)
val managerInstalledVersion
get() = "${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})" +
if (BuildConfig.DEBUG) " (D)" else ""
@get:Bindable
var stateManagerProgress = 0
set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
val extraBindings = bindExtra {
it.put(BR.viewModel, this)
}
companion object {
private var checkedEnv = false
}
override suspend fun doLoadWork() {
appState = State.LOADING
Info.fetchUpdate(svc)?.apply {
appState = when {
BuildConfig.APP_VERSION_CODE < versionCode -> State.OUTDATED
else -> State.UP_TO_DATE
}
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
managerRemoteVersion =
("$version (${versionCode})" + if (isDebug) " (D)" else "").asText()
} ?: run {
appState = State.INVALID
managerRemoteVersion = CoreR.string.not_available.asText()
}
ensureEnv()
}
override fun onNetworkChanged(network: Boolean) = startLoading()
fun onProgressUpdate(progress: Float, subject: Subject) {
if (subject is App)
stateManagerProgress = progress.times(100f).roundToInt()
}
fun onLinkPressed(link: String) = object : ViewEvent(), ContextExecutor {
override fun invoke(context: Context) {
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
context.toast(CoreR.string.open_link_failed_toast, Toast.LENGTH_SHORT)
}
}
}.publish()
fun onDeletePressed() = UninstallDialog().show()
fun onManagerPressed() = when (appState) {
State.LOADING -> SnackbarEvent(CoreR.string.loading).publish()
State.INVALID -> SnackbarEvent(CoreR.string.no_connection).publish()
else -> withExternalRW {
withInstallPermission {
ManagerInstallDialog().show()
}
}
}
fun onMagiskPressed() = withExternalRW {
HomeFragmentDirections.actionHomeFragmentToInstallFragment().navigate()
}
fun hideNotice() {
Config.safetyNotice = false
isNoticeVisible = false
}
private suspend fun ensureEnv() {
if (magiskState == State.INVALID || checkedEnv) return
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
val code = Shell.cmd(cmd).await().code
if (code != 0) {
EnvFixDialog(this, code).show()
}
checkedEnv = true
}
val showTest = false
fun onTestPressed() = object : ViewEvent(), ActivityExecutor {
override fun invoke(activity: UIActivity<*>) {
/* Entry point to trigger test events within the app */
}
}.publish()
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/home/RebootMenu.kt
================================================
package com.topjohnwu.magisk.ui.home
import android.app.Activity
import android.os.Build
import android.os.PowerManager
import android.view.ContextThemeWrapper
import android.view.MenuItem
import android.widget.PopupMenu
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.ktx.reboot as systemReboot
object RebootMenu {
private fun reboot(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_reboot_normal -> systemReboot()
R.id.action_reboot_userspace -> systemReboot("userspace")
R.id.action_reboot_bootloader -> systemReboot("bootloader")
R.id.action_reboot_download -> systemReboot("download")
R.id.action_reboot_edl -> systemReboot("edl")
R.id.action_reboot_recovery -> systemReboot("recovery")
R.id.action_reboot_safe_mode -> {
val status = !item.isChecked
item.isChecked = status
Config.bootloop = if (status) 2 else 0
}
else -> Unit
}
return true
}
fun inflate(activity: Activity): PopupMenu {
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_PopupMenu)
val menu = PopupMenu(themeWrapper, activity.findViewById(R.id.action_reboot))
activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
menu.setOnMenuItemClickListener(RebootMenu::reboot)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
activity.getSystemService()?.isRebootingUserspaceSupported == true) {
menu.menu.findItem(R.id.action_reboot_userspace).isVisible = true
}
if (Const.Version.atLeast_28_0()) {
menu.menu.findItem(R.id.action_reboot_safe_mode).isChecked = Config.bootloop >= 2
} else {
menu.menu.findItem(R.id.action_reboot_safe_mode).isVisible = false
}
return menu
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt
================================================
package com.topjohnwu.magisk.ui.install
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.core.R as CoreR
class InstallFragment : BaseFragment() {
override val layoutRes = R.layout.fragment_install_md2
override val viewModel by viewModel()
override fun onStart() {
super.onStart()
requireActivity().setTitle(CoreR.string.install)
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt
================================================
package com.topjohnwu.magisk.ui.install
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.text.Spanned
import android.text.SpannedString
import android.widget.Toast
import androidx.databinding.Bindable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.repository.NetworkService
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.dialog.SecondSlotWarningDialog
import com.topjohnwu.magisk.events.GetContentEvent
import com.topjohnwu.magisk.ui.flash.FlashFragment
import io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.io.File
import java.io.IOException
import com.topjohnwu.magisk.core.R as CoreR
class InstallViewModel(svc: NetworkService, markwon: Markwon) : BaseViewModel() {
val isRooted get() = Info.isRooted
val skipOptions = Info.isEmulator || (Info.isSAR && !Info.isFDE && Info.ramdisk)
val noSecondSlot = !isRooted || !Info.isAB || Info.isEmulator
@get:Bindable
var step = if (skipOptions) 1 else 0
set(value) = set(value, field, { field = it }, BR.step)
private var methodId = -1
@get:Bindable
var method
get() = methodId
set(value) = set(value, methodId, { methodId = it }, BR.method) {
when (it) {
R.id.method_patch -> {
GetContentEvent("*/*", UriCallback()).publish()
}
R.id.method_inactive_slot -> {
SecondSlotWarningDialog().show()
}
}
}
val data: LiveData get() = uri
@get:Bindable
var notes: Spanned = SpannedString("")
set(value) = set(value, field, { field = it }, BR.notes)
init {
viewModelScope.launch(Dispatchers.IO) {
try {
val noteFile = File(AppContext.cacheDir, "${APP_VERSION_CODE}.md")
val noteText = when {
noteFile.exists() -> noteFile.readText()
else -> {
val note = svc.fetchUpdate(APP_VERSION_CODE)?.note.orEmpty()
if (note.isEmpty()) return@launch
noteFile.writeText(note)
note
}
}
val spanned = markwon.toMarkdown(noteText)
withContext(Dispatchers.Main) {
notes = spanned
}
} catch (e: IOException) {
Timber.e(e)
}
}
}
fun install() {
when (method) {
R.id.method_patch -> FlashFragment.patch(data.value!!).navigate(true)
R.id.method_direct -> FlashFragment.flash(false).navigate(true)
R.id.method_inactive_slot -> FlashFragment.flash(true).navigate(true)
else -> error("Unknown value")
}
}
override fun onSaveState(state: Bundle) {
state.putParcelable(
INSTALL_STATE_KEY, InstallState(
methodId,
step,
Config.keepVerity,
Config.keepEnc,
Config.recovery
)
)
}
override fun onRestoreState(state: Bundle) {
state.getParcelable(INSTALL_STATE_KEY)?.let {
methodId = it.method
step = it.step
Config.keepVerity = it.keepVerity
Config.keepEnc = it.keepEnc
Config.recovery = it.recovery
}
}
@Parcelize
class UriCallback : ContentResultCallback {
override fun onActivityLaunch() {
AppContext.toast(CoreR.string.patch_file_msg, Toast.LENGTH_LONG)
}
override fun onActivityResult(result: Uri) {
uri.value = result
}
}
@Parcelize
class InstallState(
val method: Int,
val step: Int,
val keepVerity: Boolean,
val keepEnc: Boolean,
val recovery: Boolean,
) : Parcelable
companion object {
private const val INSTALL_STATE_KEY = "install_state"
private val uri = MutableLiveData()
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/log/LogFragment.kt
================================================
package com.topjohnwu.magisk.ui.log
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.HorizontalScrollView
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.utils.AccessibilityUtils
import com.topjohnwu.magisk.utils.MotionRevealHelper
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class LogFragment : BaseFragment(), MenuProvider {
override val layoutRes = R.layout.fragment_log_md2
override val viewModel by viewModel()
override val snackbarView: View?
get() = if (isMagiskLogVisible) binding.logFilterSuperuser.snackbarContainer
else super.snackbarView
override val snackbarAnchorView get() = binding.logFilterToggle
private var actionSave: MenuItem? = null
private var isMagiskLogVisible
get() = binding.logFilter.isVisible
set(value) {
MotionRevealHelper.withViews(binding.logFilter, binding.logFilterToggle, value)
actionSave?.isVisible = !value
with(activity as MainActivity) {
invalidateToolbar()
requestNavigationHidden(value)
setDisplayHomeAsUpEnabled(value)
}
}
override fun onStart() {
super.onStart()
activity?.setTitle(CoreR.string.logs)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.logFilterToggle.setOnClickListener {
isMagiskLogVisible = true
}
binding.logFilterSuperuser.logSuperuser.apply {
addEdgeSpacing(bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
}
if (!AccessibilityUtils.isAnimationEnabled(requireContext().contentResolver)) {
val scrollView = view.findViewById(R.id.log_scroll_magisk)
scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
}
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_log_md2, menu)
actionSave = menu.findItem(R.id.action_save)?.also {
it.isVisible = !isMagiskLogVisible
}
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_save -> viewModel.saveMagiskLog()
R.id.action_clear ->
if (!isMagiskLogVisible) viewModel.clearMagiskLog()
else viewModel.clearLog()
}
return super.onOptionsItemSelected(item)
}
override fun onPreBind(binding: FragmentLogMd2Binding) = Unit
override fun onBackPressed(): Boolean {
if (binding.logFilter.isVisible) {
isMagiskLogVisible = false
return true
}
return super.onBackPressed()
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/log/LogRvItem.kt
================================================
package com.topjohnwu.magisk.ui.log
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textview.MaterialTextView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.ViewAwareItem
class LogRvItem(
override val item: String
) : ObservableRvItem(), DiffItem, ItemWrapper, ViewAwareItem {
override val layoutRes = R.layout.item_log_textview
override fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView) {
val view = binding.root as MaterialTextView
view.measure(0, 0)
val desiredWidth = view.measuredWidth
val layoutParams = view.layoutParams
layoutParams.width = desiredWidth
if (recyclerView.width < desiredWidth) {
recyclerView.requestLayout()
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt
================================================
package com.topjohnwu.magisk.ui.log
import android.system.Os
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.repository.LogRepository
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.diffList
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.view.TextItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileInputStream
class LogViewModel(
private val repo: LogRepository
) : AsyncLoadViewModel() {
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
// --- empty view
val itemEmpty = TextItem(R.string.log_data_none)
val itemMagiskEmpty = TextItem(R.string.log_data_magisk_none)
// --- su log
val items = diffList()
val extraBindings = bindExtra {
it.put(BR.viewModel, this)
}
// --- magisk log
val logs = diffList()
var magiskLogRaw = " "
override suspend fun doLoadWork() {
loading = true
val (suLogs, suDiff) = withContext(Dispatchers.Default) {
magiskLogRaw = repo.fetchMagiskLogs()
val newLogs = magiskLogRaw.split('\n').map { LogRvItem(it) }
logs.update(newLogs)
val suLogs = repo.fetchSuLogs().map { SuLogRvItem(it) }
suLogs to items.calculateDiff(suLogs)
}
items.firstOrNull()?.isTop = false
items.lastOrNull()?.isBottom = false
items.update(suLogs, suDiff)
items.firstOrNull()?.isTop = true
items.lastOrNull()?.isBottom = true
loading = false
}
fun saveMagiskLog() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) {
val filename = "magisk_log_%s.log".format(
System.currentTimeMillis().toTime(timeFormatStandard))
val logFile = MediaStoreUtils.getFile(filename)
logFile.uri.outputStream().bufferedWriter().use { file ->
file.write("---Detected Device Info---\n\n")
file.write("isAB=${Info.isAB}\n")
file.write("isSAR=${Info.isSAR}\n")
file.write("ramdisk=${Info.ramdisk}\n")
val uname = Os.uname()
file.write("kernel=${uname.sysname} ${uname.machine} ${uname.release} ${uname.version}\n")
file.write("\n\n---System Properties---\n\n")
ProcessBuilder("getprop").start()
.inputStream.reader().use { it.copyTo(file) }
file.write("\n\n---Environment Variables---\n\n")
System.getenv().forEach { (key, value) -> file.write("${key}=${value}\n") }
file.write("\n\n---System MountInfo---\n\n")
FileInputStream("/proc/self/mountinfo").reader().use { it.copyTo(file) }
file.write("\n---Magisk Logs---\n")
file.write("${Info.env.versionString} (${Info.env.versionCode})\n\n")
if (Info.env.isActive) file.write(magiskLogRaw)
file.write("\n---Manager Logs---\n")
file.write("${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})\n\n")
ProcessBuilder("logcat", "-d").start()
.inputStream.reader().use { it.copyTo(file) }
}
SnackbarEvent(logFile.toString()).publish()
}
}
fun clearMagiskLog() = repo.clearMagiskLogs {
SnackbarEvent(R.string.logs_cleared).publish()
startLoading()
}
fun clearLog() = viewModelScope.launch {
repo.clearLogs()
SnackbarEvent(R.string.logs_cleared).publish()
startLoading()
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/log/SuLogRvItem.kt
================================================
package com.topjohnwu.magisk.ui.log
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.ktx.timeDateFormat
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.model.su.SuLog
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.core.R as CoreR
class SuLogRvItem(val log: SuLog) : ObservableRvItem(), DiffItem {
override val layoutRes = R.layout.item_log_access_md2
val info = genInfo()
@get:Bindable
var isTop = false
set(value) = set(value, field, { field = it }, BR.top)
@get:Bindable
var isBottom = false
set(value) = set(value, field, { field = it }, BR.bottom)
override fun itemSameAs(other: SuLogRvItem) = log.appName == other.log.appName
private fun genInfo(): String {
val res = AppContext.resources
val sb = StringBuilder()
val date = log.time.toTime(timeDateFormat)
val toUid = res.getString(CoreR.string.target_uid, log.toUid)
val fromPid = res.getString(CoreR.string.pid, log.fromPid)
sb.append("$date\n$toUid $fromPid")
if (log.target != -1) {
val pid = if (log.target == 0) "magiskd" else log.target.toString()
val target = res.getString(CoreR.string.target_pid, pid)
sb.append(" $target")
}
if (log.context.isNotEmpty()) {
val context = res.getString(CoreR.string.selinux_context, log.context)
sb.append("\n$context")
}
if (log.gids.isNotEmpty()) {
val gids = res.getString(CoreR.string.supp_group, log.gids)
sb.append("\n$gids")
}
sb.append("\n${log.command}")
return sb.toString()
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/module/ActionFragment.kt
================================================
package com.topjohnwu.magisk.ui.module
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.databinding.FragmentActionMd2Binding
import com.topjohnwu.magisk.core.R as CoreR
class ActionFragment : BaseFragment(), MenuProvider {
override val layoutRes = R.layout.fragment_action_md2
override val viewModel by viewModel()
override val snackbarView: View get() = binding.snackbarContainer
private var defaultOrientation = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.args = ActionFragmentArgs.fromBundle(requireArguments())
}
override fun onStart() {
super.onStart()
activity?.setTitle(viewModel.args.name)
binding.closeBtn.setOnClickListener {
activity?.onBackPressed()
}
viewModel.state.observe(this) {
if (it != ActionViewModel.State.RUNNING) {
binding.closeBtn.apply {
if (!this.isVisible) this.show()
if (!this.isFocused) this.requestFocus()
}
}
if (it != ActionViewModel.State.SUCCESS) return@observe
view?.viewTreeObserver?.addOnWindowFocusChangeListener(
object : ViewTreeObserver.OnWindowFocusChangeListener {
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) return
view?.viewTreeObserver?.removeOnWindowFocusChangeListener(this)
view?.context?.apply {
toast(
getString(CoreR.string.done_action, viewModel.args.name),
Toast.LENGTH_SHORT
)
}
viewModel.back()
}
}
)
}
}
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_flash, menu)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return viewModel.onMenuItemClicked(item)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
defaultOrientation = activity?.requestedOrientation ?: -1
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
if (savedInstanceState == null) {
viewModel.startRunAction()
}
}
@SuppressLint("WrongConstant")
override fun onDestroyView() {
if (defaultOrientation != -1) {
activity?.requestedOrientation = defaultOrientation
}
super.onDestroyView()
}
override fun onKeyEvent(event: KeyEvent): Boolean {
return when (event.keyCode) {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN -> true
else -> false
}
}
override fun onBackPressed(): Boolean {
if (viewModel.state.value == ActionViewModel.State.RUNNING) return true
return super.onBackPressed()
}
override fun onPreBind(binding: FragmentActionMd2Binding) = Unit
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/module/ActionViewModel.kt
================================================
package com.topjohnwu.magisk.ui.module
import android.view.MenuItem
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.ktx.synchronized
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ui.flash.ConsoleItem
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
class ActionViewModel : BaseViewModel() {
enum class State {
RUNNING, SUCCESS, FAILED
}
private val _state = MutableLiveData(State.RUNNING)
val state: LiveData get() = _state
val items = ObservableArrayList()
lateinit var args: ActionFragmentArgs
private val logItems = mutableListOf().synchronized()
private val outItems = object : CallbackList() {
override fun onAddElement(e: String?) {
e ?: return
items.add(ConsoleItem(e))
logItems.add(e)
}
}
fun startRunAction() = viewModelScope.launch {
onResult(withContext(Dispatchers.IO) {
try {
Shell.cmd("run_action \'${args.id}\'")
.to(outItems, logItems)
.exec().isSuccess
} catch (e: IOException) {
Timber.e(e)
false
}
})
}
private fun onResult(success: Boolean) {
_state.value = if (success) State.SUCCESS else State.FAILED
}
fun onMenuItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_save -> savePressed()
}
return true
}
private fun savePressed() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) {
val name = "%s_action_log_%s.log".format(
args.name,
System.currentTimeMillis().toTime(timeFormatStandard)
)
val file = MediaStoreUtils.getFile(name)
file.uri.outputStream().bufferedWriter().use { writer ->
synchronized(logItems) {
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
}
SnackbarEvent(file.toString()).publish()
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt
================================================
package com.topjohnwu.magisk.ui.module
import android.os.Bundle
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addInvalidateItemDecorationsObserver
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class ModuleFragment : BaseFragment() {
override val layoutRes = R.layout.fragment_module_md2
override val viewModel by viewModel()
override fun onStart() {
super.onStart()
activity?.title = resources.getString(CoreR.string.modules)
viewModel.data.observe(this) {
it ?: return@observe
val displayName = runCatching { it.displayName }.getOrNull() ?: return@observe
viewModel.requestInstallLocalModule(it, displayName)
viewModel.data.value = null
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.moduleList.apply {
addEdgeSpacing(top = R.dimen.l_50, bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
post { addInvalidateItemDecorationsObserver() }
}
}
override fun onPreBind(binding: FragmentModuleMd2Binding) = Unit
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/module/ModuleRvItem.kt
================================================
package com.topjohnwu.magisk.ui.module
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.core.R as CoreR
object InstallModule : RvItem(), DiffItem {
override val layoutRes = R.layout.item_module_download
}
class LocalModuleRvItem(
override val item: LocalModule
) : ObservableRvItem(), DiffItem, ItemWrapper {
override val layoutRes = R.layout.item_module_md2
val showNotice: Boolean
val showAction: Boolean
val noticeText: TextHolder
init {
val isZygisk = item.isZygisk
val isRiru = item.isRiru
val zygiskUnloaded = isZygisk && item.zygiskUnloaded
showNotice = zygiskUnloaded ||
(Info.isZygiskEnabled && isRiru) ||
(!Info.isZygiskEnabled && isZygisk)
showAction = item.hasAction && !showNotice
noticeText =
when {
zygiskUnloaded -> CoreR.string.zygisk_module_unloaded.asText()
isRiru -> CoreR.string.suspend_text_riru.asText(CoreR.string.zygisk.asText())
else -> CoreR.string.suspend_text_zygisk.asText(CoreR.string.zygisk.asText())
}
}
@get:Bindable
var isEnabled = item.enable
set(value) = set(value, field, { field = it }, BR.enabled, BR.updateReady) {
item.enable = value
}
@get:Bindable
var isRemoved = item.remove
set(value) = set(value, field, { field = it }, BR.removed, BR.updateReady) {
item.remove = value
}
@get:Bindable
val showUpdate get() = item.updateInfo != null
@get:Bindable
val updateReady get() = item.outdated && !isRemoved && isEnabled
val isUpdated = item.updated
fun fetchedUpdateInfo() {
notifyPropertyChanged(BR.showUpdate)
notifyPropertyChanged(BR.updateReady)
}
fun delete() {
isRemoved = !isRemoved
}
override fun itemSameAs(other: LocalModuleRvItem): Boolean = item.id == other.item.id
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt
================================================
package com.topjohnwu.magisk.ui.module
import android.net.Uri
import androidx.databinding.Bindable
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.ContentResultCallback
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.databinding.MergeObservableList
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.diffList
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.dialog.LocalModuleInstallDialog
import com.topjohnwu.magisk.dialog.OnlineModuleInstallDialog
import com.topjohnwu.magisk.events.GetContentEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import com.topjohnwu.magisk.core.R as CoreR
class ModuleViewModel : AsyncLoadViewModel() {
val bottomBarBarrierIds = intArrayOf(R.id.module_update, R.id.module_remove)
private val itemsInstalled = diffList()
val items = MergeObservableList()
val extraBindings = bindExtra {
it.put(BR.viewModel, this)
}
val data get() = uri
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
override suspend fun doLoadWork() {
loading = true
val moduleLoaded = Info.env.isActive &&
withContext(Dispatchers.IO) { LocalModule.loaded() }
if (moduleLoaded) {
loadInstalled()
if (items.isEmpty()) {
items.insertItem(InstallModule)
.insertList(itemsInstalled)
}
}
loading = false
loadUpdateInfo()
}
override fun onNetworkChanged(network: Boolean) = startLoading()
private suspend fun loadInstalled() {
withContext(Dispatchers.Default) {
val installed = LocalModule.installed().map { LocalModuleRvItem(it) }
itemsInstalled.update(installed)
}
}
private suspend fun loadUpdateInfo() {
withContext(Dispatchers.IO) {
itemsInstalled.forEach {
if (it.item.fetch())
it.fetchedUpdateInfo()
}
}
}
fun downloadPressed(item: OnlineModule?) =
if (item != null && Info.isConnected.value == true) {
withExternalRW { OnlineModuleInstallDialog(item).show() }
} else {
SnackbarEvent(CoreR.string.no_connection).publish()
}
fun installPressed() = withExternalRW {
GetContentEvent("application/zip", UriCallback()).publish()
}
fun requestInstallLocalModule(uri: Uri, displayName: String) {
LocalModuleInstallDialog(this, uri, displayName).show()
}
@Parcelize
class UriCallback : ContentResultCallback {
override fun onActivityResult(result: Uri) {
uri.value = result
}
}
fun runAction(id: String, name: String) {
MainDirections.actionActionFragment(id, name).navigate()
}
companion object {
private val uri = MutableLiveData()
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/BaseSettingsItem.kt
================================================
package com.topjohnwu.magisk.ui.settings
import android.content.Context
import android.content.res.Resources
import android.view.View
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ktx.activity
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.view.MagiskDialog
sealed class BaseSettingsItem : ObservableRvItem() {
interface Handler {
fun onItemPressed(view: View, item: BaseSettingsItem, andThen: () -> Unit)
fun onItemAction(view: View, item: BaseSettingsItem)
}
override val layoutRes get() = R.layout.item_settings
open val icon: Int get() = 0
open val title: TextHolder get() = TextHolder.EMPTY
@get:Bindable
open val description: TextHolder get() = TextHolder.EMPTY
@get:Bindable
var isEnabled = true
set(value) = set(value, field, { field = it }, BR.enabled, BR.description)
open fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
handler.onItemAction(view, this)
}
}
open fun refresh() {}
// Only for toggle
open val showSwitch get() = false
@get:Bindable
open val isChecked get() = false
fun onToggle(view: View, handler: Handler, checked: Boolean) =
set(checked, isChecked, { onPressed(view, handler) })
abstract class Value : BaseSettingsItem() {
/**
* Represents last agreed-upon value by the validation process and the user for current
* child. Be very aware that this shouldn't be **set** unless both sides agreed that _that_
* is the new value.
* */
abstract var value: T
protected set
}
abstract class Toggle : Value() {
override val showSwitch get() = true
override val isChecked get() = value
override fun onPressed(view: View, handler: Handler) {
// Make sure the checked state is synced
notifyPropertyChanged(BR.checked)
handler.onItemPressed(view, this) {
value = !value
notifyPropertyChanged(BR.checked)
handler.onItemAction(view, this)
}
}
}
abstract class Input : Value() {
@get:Bindable
abstract val inputResult: String?
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.activity).apply {
setTitle(title.getText(view.resources))
setView(getView(view.context))
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
inputResult?.let { result ->
doNotDismiss = false
value = result
handler.onItemAction(view, this@Input)
return@onClick
}
doNotDismiss = true
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}.show()
}
}
abstract fun getView(context: Context): View
}
abstract class Selector : Value() {
open val entryRes get() = -1
open val descriptionRes get() = entryRes
open fun entries(res: Resources) = res.getArrayOrEmpty(entryRes)
open fun descriptions(res: Resources) = res.getArrayOrEmpty(descriptionRes)
override val description = object : TextHolder() {
override fun getText(resources: Resources): CharSequence {
return descriptions(resources).getOrElse(value) { "" }
}
}
private fun Resources.getArrayOrEmpty(id: Int): Array =
runCatching { getStringArray(id) }.getOrDefault(emptyArray())
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.activity).apply {
setTitle(title.getText(view.resources))
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setListItems(entries(view.resources)) {
if (value != it) {
value = it
notifyPropertyChanged(BR.description)
handler.onItemAction(view, this@Selector)
}
}
}.show()
}
}
}
abstract class Blank : BaseSettingsItem()
abstract class Section : BaseSettingsItem() {
override val layoutRes = R.layout.item_settings_section
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt
================================================
package com.topjohnwu.magisk.ui.settings
import android.os.Bundle
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class SettingsFragment : BaseFragment() {
override val layoutRes = R.layout.fragment_settings_md2
override val viewModel by viewModel()
override val snackbarView: View get() = binding.snackbarContainer
override fun onStart() {
super.onStart()
activity?.title = resources.getString(CoreR.string.settings)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.settingsList.apply {
addEdgeSpacing(bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
}
}
override fun onResume() {
super.onResume()
viewModel.items.forEach { it.refresh() }
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt
================================================
package com.topjohnwu.magisk.ui.settings
import android.content.Context
import android.content.res.Resources
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.activity
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
import com.topjohnwu.magisk.core.R as CoreR
// --- Customization
object Customization : BaseSettingsItem.Section() {
override val title = CoreR.string.settings_customization.asText()
}
object Language : BaseSettingsItem.Selector() {
private val names: Array get() = LocaleSetting.available.names
private val tags: Array get() = LocaleSetting.available.tags
override var value
get() = tags.indexOf(Config.locale)
set(value) {
Config.locale = tags[value]
}
override val title = CoreR.string.language.asText()
override fun entries(res: Resources) = names
override fun descriptions(res: Resources) = names
}
object LanguageSystem : BaseSettingsItem.Blank() {
override val title = CoreR.string.language.asText()
override val description: TextHolder
get() {
val locale = LocaleSetting.instance.appLocale
return locale?.getDisplayName(locale)?.asText() ?: CoreR.string.system_default.asText()
}
}
object Theme : BaseSettingsItem.Blank() {
override val icon = R.drawable.ic_paint
override val title = CoreR.string.section_theme.asText()
}
// --- App
object AppSettings : BaseSettingsItem.Section() {
override val title = CoreR.string.home_app_title.asText()
}
object Hide : BaseSettingsItem.Input() {
override val title = CoreR.string.settings_hide_app_title.asText()
override val description = CoreR.string.settings_hide_app_summary.asText()
override var value = ""
override val inputResult
get() = if (isError) null else result
@get:Bindable
var result = "Settings"
set(value) = set(value, field, { field = it }, BR.result, BR.error)
val maxLength
get() = AppMigration.MAX_LABEL_LENGTH
@get:Bindable
val isError
get() = result.length > maxLength || result.isBlank()
override fun getView(context: Context) = DialogSettingsAppNameBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object Restore : BaseSettingsItem.Blank() {
override val title = CoreR.string.settings_restore_app_title.asText()
override val description = CoreR.string.settings_restore_app_summary.asText()
override fun onPressed(view: View, handler: Handler) {
handler.onItemPressed(view, this) {
MagiskDialog(view.activity).apply {
setTitle(CoreR.string.settings_restore_app_title)
setMessage(CoreR.string.restore_app_confirmation)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
handler.onItemAction(view, this@Restore)
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setCancelable(true)
show()
}
}
}
}
object AddShortcut : BaseSettingsItem.Blank() {
override val title = CoreR.string.add_shortcut_title.asText()
override val description = CoreR.string.setting_add_shortcut_summary.asText()
}
object DownloadPath : BaseSettingsItem.Input() {
override var value
get() = Config.downloadDir
set(value) {
Config.downloadDir = value
notifyPropertyChanged(BR.description)
}
override val title = CoreR.string.settings_download_path_title.asText()
override val description get() = MediaStoreUtils.fullPath(value).asText()
override var inputResult: String = value
set(value) = set(value, field, { field = it }, BR.inputResult, BR.path)
@get:Bindable
val path get() = MediaStoreUtils.fullPath(inputResult)
override fun getView(context: Context) = DialogSettingsDownloadPathBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object UpdateChannel : BaseSettingsItem.Selector() {
override var value
get() = Config.updateChannel
set(value) {
Config.updateChannel = value
Info.resetUpdate()
}
override val title = CoreR.string.settings_update_channel_title.asText()
override val entryRes = CoreR.array.update_channel
}
object UpdateChannelUrl : BaseSettingsItem.Input() {
override val title = CoreR.string.settings_update_custom.asText()
override val description get() = value.asText()
override var value
get() = Config.customChannelUrl
set(value) {
Config.customChannelUrl = value
Info.resetUpdate()
notifyPropertyChanged(BR.description)
}
override var inputResult: String = value
set(value) = set(value, field, { field = it }, BR.inputResult)
override fun refresh() {
isEnabled = UpdateChannel.value == Config.Value.CUSTOM_CHANNEL
}
override fun getView(context: Context) = DialogSettingsUpdateChannelBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
}
object UpdateChecker : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_check_update_title.asText()
override val description = CoreR.string.settings_check_update_summary.asText()
override var value by Config::checkUpdate
}
object DoHToggle : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_doh_title.asText()
override val description = CoreR.string.settings_doh_description.asText()
override var value by Config::doh
}
object SystemlessHosts : BaseSettingsItem.Blank() {
override val title = CoreR.string.settings_hosts_title.asText()
override val description = CoreR.string.settings_hosts_summary.asText()
}
object RandNameToggle : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_random_name_title.asText()
override val description = CoreR.string.settings_random_name_description.asText()
override var value by Config::randName
}
// --- Magisk
object Magisk : BaseSettingsItem.Section() {
override val title = CoreR.string.magisk.asText()
}
object Zygisk : BaseSettingsItem.Toggle() {
override val title = CoreR.string.zygisk.asText()
override val description get() =
if (mismatch) CoreR.string.reboot_apply_change.asText()
else CoreR.string.settings_zygisk_summary.asText()
override var value
get() = Config.zygisk
set(value) {
Config.zygisk = value
notifyPropertyChanged(BR.description)
}
val mismatch get() = value != Info.isZygiskEnabled
}
object DenyList : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_denylist_title.asText()
override val description get() = CoreR.string.settings_denylist_summary.asText()
override var value = Config.denyList
set(value) {
field = value
val cmd = if (value) "enable" else "disable"
Shell.cmd("magisk --denylist $cmd").submit { result ->
if (result.isSuccess) {
Config.denyList = value
} else {
field = !value
notifyPropertyChanged(BR.checked)
}
}
}
}
object DenyListConfig : BaseSettingsItem.Blank() {
override val title = CoreR.string.settings_denylist_config_title.asText()
override val description = CoreR.string.settings_denylist_config_summary.asText()
}
// --- Superuser
object Tapjack : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_tapjack_title.asText()
override val description = CoreR.string.settings_su_tapjack_summary.asText()
override var value by Config::suTapjack
}
object Authentication : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_auth_title.asText()
override var description = CoreR.string.settings_su_auth_summary.asText()
override var value by Config::suAuth
override fun refresh() {
isEnabled = Info.isDeviceSecure
if (!isEnabled) {
description = CoreR.string.settings_su_auth_insecure.asText()
}
}
}
object Superuser : BaseSettingsItem.Section() {
override val title = CoreR.string.superuser.asText()
}
object AccessMode : BaseSettingsItem.Selector() {
override val title = CoreR.string.superuser_access.asText()
override val entryRes = CoreR.array.su_access
override var value by Config::rootMode
}
object MultiuserMode : BaseSettingsItem.Selector() {
override val title = CoreR.string.multiuser_mode.asText()
override val entryRes = CoreR.array.multiuser_mode
override val descriptionRes = CoreR.array.multiuser_summary
override var value by Config::suMultiuserMode
override fun refresh() {
isEnabled = Const.USER_ID == 0
}
}
object MountNamespaceMode : BaseSettingsItem.Selector() {
override val title = CoreR.string.mount_namespace_mode.asText()
override val entryRes = CoreR.array.namespace
override val descriptionRes = CoreR.array.namespace_summary
override var value by Config::suMntNamespaceMode
}
object AutomaticResponse : BaseSettingsItem.Selector() {
override val title = CoreR.string.auto_response.asText()
override val entryRes = CoreR.array.auto_response
override var value by Config::suAutoResponse
}
object RequestTimeout : BaseSettingsItem.Selector() {
override val title = CoreR.string.request_timeout.asText()
override val entryRes = CoreR.array.request_timeout
private val entryValues = listOf(10, 15, 20, 30, 45, 60)
override var value = entryValues.indexOfFirst { it == Config.suDefaultTimeout }
set(value) {
field = value
Config.suDefaultTimeout = entryValues[value]
}
}
object SUNotification : BaseSettingsItem.Selector() {
override val title = CoreR.string.superuser_notification.asText()
override val entryRes = CoreR.array.su_notification
override var value by Config::suNotification
}
object Reauthenticate : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_reauth_title.asText()
override val description = CoreR.string.settings_su_reauth_summary.asText()
override var value by Config::suReAuth
override fun refresh() {
isEnabled = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
}
}
object Restrict : BaseSettingsItem.Toggle() {
override val title = CoreR.string.settings_su_restrict_title.asText()
override val description = CoreR.string.settings_su_restrict_summary.asText()
override var value by Config::suRestrict
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsViewModel.kt
================================================
package com.topjohnwu.magisk.ui.settings
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.view.View
import android.widget.Toast
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.activity
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.utils.LocaleSetting
import com.topjohnwu.magisk.core.utils.RootUtils
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.events.AddHomeIconEvent
import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import kotlinx.coroutines.launch
class SettingsViewModel : BaseViewModel(), BaseSettingsItem.Handler {
val items = createItems()
val extraBindings = bindExtra {
it.put(BR.handler, this)
}
private fun createItems(): List {
val context = AppContext
val hidden = context.packageName != BuildConfig.APP_PACKAGE_NAME
// Customization
val list = mutableListOf(
Customization,
Theme, if (LocaleSetting.useLocaleManager) LanguageSystem else Language
)
if (isRunningAsStub && ShortcutManagerCompat.isRequestPinShortcutSupported(context))
list.add(AddShortcut)
// Manager
list.addAll(listOf(
AppSettings,
UpdateChannel, UpdateChannelUrl, DoHToggle, UpdateChecker, DownloadPath, RandNameToggle
))
if (Info.env.isActive && Const.USER_ID == 0) {
if (hidden) list.add(Restore) else list.add(Hide)
}
// Magisk
if (Info.env.isActive) {
list.addAll(listOf(
Magisk,
SystemlessHosts
))
if (Const.Version.atLeast_24_0()) {
list.addAll(listOf(Zygisk, DenyList, DenyListConfig))
}
}
// Superuser
if (Info.showSuperUser) {
list.addAll(listOf(
Superuser,
Tapjack, Authentication, AccessMode, MultiuserMode, MountNamespaceMode,
AutomaticResponse, RequestTimeout, SUNotification
))
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Re-authenticate is not feasible on 8.0+
list.add(Reauthenticate)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Can hide overlay windows on 12.0+
list.remove(Tapjack)
}
if (Const.Version.atLeast_30_1()) {
list.add(Restrict)
}
}
return list
}
override fun onItemPressed(view: View, item: BaseSettingsItem, doAction: () -> Unit) {
when (item) {
DownloadPath -> withExternalRW(doAction)
UpdateChecker -> withPostNotificationPermission(doAction)
Authentication -> AuthEvent(doAction).publish()
AutomaticResponse -> if (Config.suAuth) AuthEvent(doAction).publish() else doAction()
else -> doAction()
}
}
override fun onItemAction(view: View, item: BaseSettingsItem) {
when (item) {
Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate()
LanguageSystem -> view.activity.startActivity(LocaleSetting.localeSettingsIntent)
AddShortcut -> AddHomeIconEvent().publish()
SystemlessHosts -> createHosts()
DenyListConfig -> SettingsFragmentDirections.actionSettingsFragmentToDenyFragment().navigate()
UpdateChannel -> openUrlIfNecessary(view)
is Hide -> viewModelScope.launch { AppMigration.hide(view.activity, item.value) }
Restore -> viewModelScope.launch { AppMigration.restore(view.activity) }
Zygisk -> if (Zygisk.mismatch) SnackbarEvent(R.string.reboot_apply_change).publish()
else -> Unit
}
}
private fun openUrlIfNecessary(view: View) {
UpdateChannelUrl.refresh()
if (UpdateChannelUrl.isEnabled && UpdateChannelUrl.value.isBlank()) {
UpdateChannelUrl.onPressed(view, this)
}
}
private fun createHosts() {
viewModelScope.launch {
RootUtils.addSystemlessHosts()
AppContext.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/superuser/PolicyRvItem.kt
================================================
package com.topjohnwu.magisk.ui.superuser
import android.graphics.drawable.Drawable
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableRvItem
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.core.R as CoreR
class PolicyRvItem(
private val viewModel: SuperuserViewModel,
override val item: SuPolicy,
val packageName: String,
private val isSharedUid: Boolean,
val icon: Drawable,
val appName: String
) : ObservableRvItem(), DiffItem, ItemWrapper {
override val layoutRes = R.layout.item_policy_md2
val title get() = if (isSharedUid) "[SharedUID] $appName" else appName
private inline fun setImpl(new: T, old: T, setter: (T) -> Unit) {
if (old != new) {
setter(new)
}
}
@get:Bindable
var isExpanded = false
set(value) = set(value, field, { field = it }, BR.expanded)
val showSlider = Config.suRestrict || item.policy == SuPolicy.RESTRICT
@get:Bindable
var isEnabled
get() = item.policy >= SuPolicy.ALLOW
set(value) = setImpl(value, isEnabled) {
notifyPropertyChanged(BR.enabled)
viewModel.updatePolicy(this, if (it) SuPolicy.ALLOW else SuPolicy.DENY)
}
@get:Bindable
var sliderValue
get() = item.policy
set(value) = setImpl(value, sliderValue) {
notifyPropertyChanged(BR.sliderValue)
notifyPropertyChanged(BR.enabled)
viewModel.updatePolicy(this, it)
}
val sliderValueToPolicyString: (Float) -> Int = { value ->
when (value.toInt()) {
1 -> CoreR.string.deny
2 -> CoreR.string.restrict
3 -> CoreR.string.grant
else -> CoreR.string.deny
}
}
@get:Bindable
var shouldNotify
get() = item.notification
private set(value) = setImpl(value, shouldNotify) {
item.notification = it
viewModel.updateNotify(this)
}
@get:Bindable
var shouldLog
get() = item.logging
private set(value) = setImpl(value, shouldLog) {
item.logging = it
viewModel.updateLogging(this)
}
fun toggleExpand() {
isExpanded = !isExpanded
}
fun toggleNotify() {
shouldNotify = !shouldNotify
}
fun toggleLog() {
shouldLog = !shouldLog
}
fun revoke() {
viewModel.deletePressed(this)
}
override fun itemSameAs(other: PolicyRvItem) = packageName == other.packageName
override fun contentSameAs(other: PolicyRvItem) = item.policy == other.item.policy
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserFragment.kt
================================================
package com.topjohnwu.magisk.ui.superuser
import android.os.Bundle
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding
import rikka.recyclerview.addEdgeSpacing
import rikka.recyclerview.addItemSpacing
import rikka.recyclerview.fixEdgeEffect
import com.topjohnwu.magisk.core.R as CoreR
class SuperuserFragment : BaseFragment() {
override val layoutRes = R.layout.fragment_superuser_md2
override val viewModel by viewModel()
override fun onStart() {
super.onStart()
activity?.title = resources.getString(CoreR.string.superuser)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.superuserList.apply {
addEdgeSpacing(top = R.dimen.l_50, bottom = R.dimen.l1)
addItemSpacing(R.dimen.l1, R.dimen.l_50, R.dimen.l1)
fixEdgeEffect()
}
}
override fun onPreBind(binding: FragmentSuperuserMd2Binding) {}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/superuser/SuperuserViewModel.kt
================================================
package com.topjohnwu.magisk.ui.superuser
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import android.os.Process
import androidx.databinding.Bindable
import androidx.databinding.ObservableArrayList
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.ktx.getLabel
import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.databinding.MergeObservableList
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.diffList
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.dialog.SuperuserRevokeDialog
import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.utils.asText
import com.topjohnwu.magisk.view.TextItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale
class SuperuserViewModel(
private val db: PolicyDao
) : AsyncLoadViewModel() {
private val itemNoData = TextItem(R.string.superuser_policy_none)
private val itemsHelpers = ObservableArrayList()
private val itemsPolicies = diffList()
val items = MergeObservableList()
.insertList(itemsHelpers)
.insertList(itemsPolicies)
val extraBindings = bindExtra {
it.put(BR.listener, this)
}
@get:Bindable
var loading = true
private set(value) = set(value, field, { field = it }, BR.loading)
@SuppressLint("InlinedApi")
override suspend fun doLoadWork() {
if (!Info.showSuperUser) {
loading = false
return
}
loading = true
withContext(Dispatchers.IO) {
db.deleteOutdated()
db.delete(AppContext.applicationInfo.uid)
val policies = ArrayList()
val pm = AppContext.packageManager
for (policy in db.fetchAll()) {
val pkgs =
if (policy.uid == Process.SYSTEM_UID) arrayOf("android")
else pm.getPackagesForUid(policy.uid)
if (pkgs == null) {
db.delete(policy.uid)
continue
}
val map = pkgs.mapNotNull { pkg ->
try {
val info = pm.getPackageInfo(pkg, MATCH_UNINSTALLED_PACKAGES)
PolicyRvItem(
this@SuperuserViewModel, policy,
info.packageName,
info.sharedUserId != null,
info.applicationInfo?.loadIcon(pm) ?: pm.defaultActivityIcon,
info.applicationInfo?.getLabel(pm) ?: info.packageName
)
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
if (map.isEmpty()) {
db.delete(policy.uid)
continue
}
policies.addAll(map)
}
policies.sortWith(compareBy(
{ it.appName.lowercase(Locale.ROOT) },
{ it.packageName }
))
itemsPolicies.update(policies)
}
if (itemsPolicies.isNotEmpty())
itemsHelpers.clear()
else if (itemsHelpers.isEmpty())
itemsHelpers.add(itemNoData)
loading = false
}
// ---
fun deletePressed(item: PolicyRvItem) {
fun updateState() = viewModelScope.launch {
db.delete(item.item.uid)
val list = ArrayList(itemsPolicies)
list.removeAll { it.item.uid == item.item.uid }
itemsPolicies.update(list)
if (list.isEmpty() && itemsHelpers.isEmpty()) {
itemsHelpers.add(itemNoData)
}
}
if (Config.suAuth) {
AuthEvent { updateState() }.publish()
} else {
SuperuserRevokeDialog(item.title) { updateState() }.show()
}
}
fun updateNotify(item: PolicyRvItem) {
viewModelScope.launch {
db.update(item.item)
val res = when {
item.item.notification -> R.string.su_snack_notif_on
else -> R.string.su_snack_notif_off
}
itemsPolicies.forEach {
if (it.item.uid == item.item.uid) {
it.notifyPropertyChanged(BR.shouldNotify)
}
}
SnackbarEvent(res.asText(item.appName)).publish()
}
}
fun updateLogging(item: PolicyRvItem) {
viewModelScope.launch {
db.update(item.item)
val res = when {
item.item.logging -> R.string.su_snack_log_on
else -> R.string.su_snack_log_off
}
itemsPolicies.forEach {
if (it.item.uid == item.item.uid) {
it.notifyPropertyChanged(BR.shouldLog)
}
}
SnackbarEvent(res.asText(item.appName)).publish()
}
}
fun updatePolicy(item: PolicyRvItem, policy: Int) {
val items = itemsPolicies.filter { it.item.uid == item.item.uid }
fun updateState() {
viewModelScope.launch {
val res = if (policy >= SuPolicy.ALLOW) R.string.su_snack_grant else R.string.su_snack_deny
item.item.policy = policy
db.update(item.item)
items.forEach {
it.notifyPropertyChanged(BR.enabled)
it.notifyPropertyChanged(BR.sliderValue)
}
SnackbarEvent(res.asText(item.appName)).publish()
}
}
if (Config.suAuth) {
AuthEvent { updateState() }.publish()
} else {
updateState()
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestActivity.kt
================================================
package com.topjohnwu.magisk.ui.surequest
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import android.view.Window
import android.view.WindowManager
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.core.base.UntrackedActivity
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
import com.topjohnwu.magisk.ui.theme.Theme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
open class SuRequestActivity : UIActivity(), UntrackedActivity {
override val layoutRes: Int = R.layout.activity_request
override val viewModel: SuRequestViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
window.setHideOverlayWindows(true)
}
setTheme(Theme.selected.themeRes)
super.onCreate(savedInstanceState)
if (intent.action == Intent.ACTION_VIEW) {
val action = intent.getStringExtra("action")
if (action == REQUEST) {
viewModel.handleRequest(intent)
} else {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
SuCallbackHandler.run(this@SuRequestActivity, action, intent.extras)
}
finish()
}
}
} else {
finish()
}
}
override fun getTheme(): Resources.Theme {
val theme = super.getTheme()
theme.applyStyle(R.style.Foundation_Floating, true)
return theme
}
override fun onBackPressed() {
viewModel.denyPressed()
}
override fun finish() {
super.finishAndRemoveTask()
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/surequest/SuRequestViewModel.kt
================================================
package com.topjohnwu.magisk.ui.surequest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.CountDownTimer
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeProvider
import android.widget.Toast
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.ktx.getLabel
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.DENY
import com.topjohnwu.magisk.core.su.SuRequestHandler
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.AuthEvent
import com.topjohnwu.magisk.events.DieEvent
import com.topjohnwu.magisk.events.ShowUIEvent
import com.topjohnwu.magisk.utils.TextHolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit.SECONDS
class SuRequestViewModel(
policyDB: PolicyDao,
private val timeoutPrefs: SharedPreferences
) : BaseViewModel() {
lateinit var icon: Drawable
lateinit var title: String
lateinit var packageName: String
@get:Bindable
val denyText = DenyText()
@get:Bindable
var selectedItemPosition = 0
set(value) = set(value, field, { field = it }, BR.selectedItemPosition)
@get:Bindable
var grantEnabled = false
set(value) = set(value, field, { field = it }, BR.grantEnabled)
@SuppressLint("ClickableViewAccessibility")
val grantTouchListener = View.OnTouchListener { _: View, event: MotionEvent ->
// Filter obscured touches by consuming them.
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
|| event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0) {
if (event.action == MotionEvent.ACTION_UP) {
AppContext.toast(R.string.touch_filtered_warning, Toast.LENGTH_SHORT)
}
return@OnTouchListener Config.suTapjack
}
false
}
private val handler = SuRequestHandler(AppContext.packageManager, policyDB)
private val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
private var timer = SuTimer(millis, 1000)
private var initialized = false
fun grantPressed() {
cancelTimer()
if (Config.suAuth) {
AuthEvent { respond(ALLOW) }.publish()
} else {
respond(ALLOW)
}
}
fun denyPressed() {
respond(DENY)
}
fun spinnerTouched(): Boolean {
cancelTimer()
return false
}
fun handleRequest(intent: Intent) {
viewModelScope.launch(Dispatchers.Default) {
if (handler.start(intent))
showDialog()
else
DieEvent().publish()
}
}
private fun showDialog() {
val pm = handler.pm
val info = handler.pkgInfo
val app = info.applicationInfo
if (app == null) {
// The request is not coming from an app process, and the UID is a
// shared UID. We have no way to know where this request comes from.
icon = pm.defaultActivityIcon
title = "[SharedUID] ${info.sharedUserId}"
packageName = info.sharedUserId.toString()
} else {
val prefix = if (info.sharedUserId == null) "" else "[SharedUID] "
icon = app.loadIcon(pm)
title = "$prefix${app.getLabel(pm)}"
packageName = info.packageName
}
selectedItemPosition = timeoutPrefs.getInt(packageName, 0)
// Set timer
timer.start()
// Actually show the UI
ShowUIEvent(if (Config.suTapjack) EmptyAccessibilityDelegate else null).publish()
initialized = true
}
private fun respond(action: Int) {
if (!initialized) {
// ignore the response until showDialog done
return
}
timer.cancel()
val pos = selectedItemPosition
timeoutPrefs.edit().putInt(packageName, pos).apply()
viewModelScope.launch {
handler.respond(action, Config.Value.TIMEOUT_LIST[pos])
// Kill activity after response
DieEvent().publish()
}
}
private fun cancelTimer() {
timer.cancel()
denyText.seconds = 0
}
private inner class SuTimer(
private val millis: Long,
interval: Long
) : CountDownTimer(millis, interval) {
override fun onTick(remains: Long) {
if (!grantEnabled && remains <= millis - 1000) {
grantEnabled = true
}
denyText.seconds = (remains / 1000).toInt() + 1
}
override fun onFinish() {
denyText.seconds = 0
respond(DENY)
}
}
inner class DenyText : TextHolder() {
var seconds = 0
set(value) = set(value, field, { field = it }, BR.denyText)
override fun getText(resources: Resources): CharSequence {
return if (seconds != 0)
"${resources.getString(R.string.deny)} ($seconds)"
else
resources.getString(R.string.deny)
}
}
// Invisible for accessibility services
object EmptyAccessibilityDelegate : View.AccessibilityDelegate() {
override fun sendAccessibilityEvent(host: View, eventType: Int) {}
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?) = true
override fun sendAccessibilityEventUnchecked(host: View, event: AccessibilityEvent) {}
override fun dispatchPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) = true
override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {}
override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {}
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {}
override fun addExtraDataToAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo, extraDataKey: String, arguments: Bundle?) {}
override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean = false
override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProvider? = null
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/Theme.kt
================================================
package com.topjohnwu.magisk.ui.theme
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
enum class Theme(
val themeName: String,
val themeRes: Int
) {
Piplup(
themeName = "Piplup",
themeRes = R.style.ThemeFoundationMD2_Piplup
),
PiplupAmoled(
themeName = "AMOLED",
themeRes = R.style.ThemeFoundationMD2_Amoled
),
Rayquaza(
themeName = "Rayquaza",
themeRes = R.style.ThemeFoundationMD2_Rayquaza
),
Zapdos(
themeName = "Zapdos",
themeRes = R.style.ThemeFoundationMD2_Zapdos
),
Charmeleon(
themeName = "Charmeleon",
themeRes = R.style.ThemeFoundationMD2_Charmeleon
),
Mew(
themeName = "Mew",
themeRes = R.style.ThemeFoundationMD2_Mew
),
Salamence(
themeName = "Salamence",
themeRes = R.style.ThemeFoundationMD2_Salamence
),
Fraxure(
themeName = "Fraxure (Legacy)",
themeRes = R.style.ThemeFoundationMD2_Fraxure
);
val isSelected get() = Config.themeOrdinal == ordinal
companion object {
val selected get() = values().getOrNull(Config.themeOrdinal) ?: Piplup
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/ThemeFragment.kt
================================================
package com.topjohnwu.magisk.ui.theme
import android.os.Bundle
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseFragment
import com.topjohnwu.magisk.arch.viewModel
import com.topjohnwu.magisk.databinding.FragmentThemeMd2Binding
import com.topjohnwu.magisk.databinding.ItemThemeBindingImpl
import com.topjohnwu.magisk.core.R as CoreR
class ThemeFragment : BaseFragment() {
override val layoutRes = R.layout.fragment_theme_md2
override val viewModel by viewModel()
private fun Array.paired(): List> {
val iterator = iterator()
if (!iterator.hasNext()) return emptyList()
val result = mutableListOf>()
while (iterator.hasNext()) {
val a = iterator.next()
val b = if (iterator.hasNext()) iterator.next() else null
result.add(a to b)
}
return result
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
for ((a, b) in Theme.values().paired()) {
val c = inflater.inflate(R.layout.item_theme_container, null, false)
val left = c.findViewById(R.id.left)
val right = c.findViewById(R.id.right)
for ((theme, view) in listOf(a to left, b to right)) {
theme ?: continue
val themed = ContextThemeWrapper(activity, theme.themeRes)
ItemThemeBindingImpl.inflate(LayoutInflater.from(themed), view, true).also {
it.setVariable(BR.viewModel, viewModel)
it.setVariable(BR.theme, theme)
it.lifecycleOwner = viewLifecycleOwner
}
}
binding.themeContainer.addView(c)
}
return binding.root
}
override fun onStart() {
super.onStart()
activity?.title = getString(CoreR.string.section_theme)
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/ui/theme/ThemeViewModel.kt
================================================
package com.topjohnwu.magisk.ui.theme
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.dialog.DarkThemeDialog
import com.topjohnwu.magisk.events.RecreateEvent
import com.topjohnwu.magisk.view.TappableHeadlineItem
class ThemeViewModel : BaseViewModel(), TappableHeadlineItem.Listener {
val themeHeadline = TappableHeadlineItem.ThemeMode
override fun onItemPressed(item: TappableHeadlineItem) = when (item) {
is TappableHeadlineItem.ThemeMode -> DarkThemeDialog().show()
}
fun saveTheme(theme: Theme) {
if (!theme.isSelected) {
Config.themeOrdinal = theme.ordinal
RecreateEvent().publish()
}
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/utils/AccessibilityUtils.kt
================================================
package com.topjohnwu.magisk.utils
import android.content.ContentResolver
import android.provider.Settings
class AccessibilityUtils {
companion object {
fun isAnimationEnabled(cr: ContentResolver): Boolean {
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/apk/src/main/java/com/topjohnwu/magisk/utils/MotionRevealHelper.kt
================================================
package com.topjohnwu.magisk.utils
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.view.View
import androidx.core.animation.addListener
import androidx.core.text.layoutDirection
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.marginEnd
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.circularreveal.CircularRevealCompat
import com.google.android.material.circularreveal.CircularRevealWidget
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.topjohnwu.magisk.core.utils.LocaleSetting
import kotlin.math.hypot
object MotionRevealHelper {
fun withViews(
revealable: CV,
fab: FloatingActionButton,
expanded: Boolean
) where CV : CircularRevealWidget, CV : View {
revealable.revealInfo = revealable.createRevealInfo(!expanded)
val revealInfo = revealable.createRevealInfo(expanded)
val revealAnim = revealable.createRevealAnim(revealInfo)
val moveAnim = fab.createMoveAnim(revealInfo)
AnimatorSet().also {
if (expanded) {
it.play(revealAnim).after(moveAnim)
} else {
it.play(moveAnim).after(revealAnim)
}
}.start()
}
private fun CV.createRevealAnim(
revealInfo: CircularRevealWidget.RevealInfo
): Animator where CV : CircularRevealWidget, CV : View =
CircularRevealCompat.createCircularReveal(
this,
revealInfo.centerX,
revealInfo.centerY,
revealInfo.radius
).apply {
addListener(onStart = {
isVisible = true
}, onEnd = {
if (revealInfo.radius == 0f) {
isInvisible = true
}
})
}
private fun FloatingActionButton.createMoveAnim(
revealInfo: CircularRevealWidget.RevealInfo
): Animator = AnimatorSet().also {
it.interpolator = FastOutSlowInInterpolator()
it.addListener(onStart = { show() }, onEnd = { if (revealInfo.radius != 0f) hide() })
val rtlMod =
if (LocaleSetting.instance.currentLocale.layoutDirection == View.LAYOUT_DIRECTION_RTL)
1f else -1f
val maxX = revealInfo.centerX - marginEnd - measuredWidth / 2f
val targetX = if (revealInfo.radius == 0f) 0f else maxX * rtlMod
val moveX = ObjectAnimator.ofFloat(this, View.TRANSLATION_X, targetX)
val maxY = revealInfo.centerY - marginBottom - measuredHeight / 2f
val targetY = if (revealInfo.radius == 0f) 0f else -maxY
val moveY = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, targetY)
it.playTogether(moveX, moveY)
}
private fun View.createRevealInfo(expanded: Boolean): CircularRevealWidget.RevealInfo {
val cX = measuredWidth / 2f
val cY = measuredHeight / 2f - paddingBottom
return CircularRevealWidget.RevealInfo(cX, cY, if (expanded) hypot(cX, cY) else 0f)
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/utils/TextHolder.kt
================================================
package com.topjohnwu.magisk.utils
import android.content.res.Resources
abstract class TextHolder {
open val isEmpty: Boolean get() = false
abstract fun getText(resources: Resources): CharSequence
// ---
class String(
private val value: CharSequence
) : TextHolder() {
override val isEmpty get() = value.isEmpty()
override fun getText(resources: Resources) = value
}
open class Resource(
protected val value: Int
) : TextHolder() {
override val isEmpty get() = value == 0
override fun getText(resources: Resources) = resources.getString(value)
}
class ResourceArgs(
value: Int,
private vararg val params: Any
) : Resource(value) {
override fun getText(resources: Resources): kotlin.String {
// Replace TextHolder with strings
val args = params.map { if (it is TextHolder) it.getText(resources) else it }
return resources.getString(value, *args.toTypedArray())
}
}
// ---
companion object {
val EMPTY = String("")
}
}
fun Int.asText(): TextHolder = TextHolder.Resource(this)
fun Int.asText(vararg params: Any): TextHolder = TextHolder.ResourceArgs(this, *params)
fun CharSequence.asText(): TextHolder = TextHolder.String(this)
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/view/MagiskDialog.kt
================================================
package com.topjohnwu.magisk.view
import android.app.Activity
import android.content.DialogInterface
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.databinding.Bindable
import androidx.databinding.PropertyChangeRegistry
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import com.google.android.material.shape.MaterialShapeDrawable
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.databinding.DialogMagiskBaseBinding
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.databinding.bindExtra
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.databinding.setAdapter
import com.topjohnwu.magisk.view.MagiskDialog.DialogClickListener
typealias DialogButtonClickListener = (DialogInterface) -> Unit
class MagiskDialog(
context: Activity, theme: Int = 0
) : AppCompatDialog(context, theme) {
private val binding: DialogMagiskBaseBinding =
DialogMagiskBaseBinding.inflate(LayoutInflater.from(context))
private val data = Data()
val activity: UIActivity<*> get() = ownerActivity as UIActivity<*>
init {
binding.setVariable(BR.data, data)
setCancelable(true)
setOwnerActivity(context)
}
inner class Data : ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
@get:Bindable
var icon: Drawable? = null
set(value) = set(value, field, { field = it }, BR.icon)
@get:Bindable
var title: CharSequence = ""
set(value) = set(value, field, { field = it }, BR.title)
@get:Bindable
var message: CharSequence = ""
set(value) = set(value, field, { field = it }, BR.message)
val buttonPositive = ButtonViewModel()
val buttonNeutral = ButtonViewModel()
val buttonNegative = ButtonViewModel()
}
enum class ButtonType {
POSITIVE, NEUTRAL, NEGATIVE
}
interface Button {
var icon: Int
var text: Any
var isEnabled: Boolean
var doNotDismiss: Boolean
fun onClick(listener: DialogButtonClickListener)
}
inner class ButtonViewModel : Button, ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
@get:Bindable
override var icon = 0
set(value) = set(value, field, { field = it }, BR.icon, BR.gone)
@get:Bindable
var message: String = ""
set(value) = set(value, field, { field = it }, BR.message, BR.gone)
override var text: Any
get() = message
set(value) {
message = when (value) {
is Int -> context.getText(value)
else -> value
}.toString()
}
@get:Bindable
val gone get() = icon == 0 && message.isEmpty()
@get:Bindable
override var isEnabled = true
set(value) = set(value, field, { field = it }, BR.enabled)
override var doNotDismiss = false
private var onClickAction: DialogButtonClickListener = {}
override fun onClick(listener: DialogButtonClickListener) {
onClickAction = listener
}
fun clicked() {
onClickAction(this@MagiskDialog)
if (!doNotDismiss) {
dismiss()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
super.setContentView(binding.root)
val default = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, javaClass.canonicalName)
val surfaceColor = MaterialColors.getColor(context, R.attr.colorSurfaceSurfaceVariant, default)
val materialShapeDrawable = MaterialShapeDrawable(context, null, androidx.appcompat.R.attr.alertDialogStyle, com.google.android.material.R.style.MaterialAlertDialog_MaterialComponents)
materialShapeDrawable.initializeElevationOverlay(context)
materialShapeDrawable.fillColor = ColorStateList.valueOf(surfaceColor)
materialShapeDrawable.elevation = context.resources.getDimension(R.dimen.margin_generic)
materialShapeDrawable.setCornerSize(context.resources.getDimension(R.dimen.l_50))
val inset = context.resources.getDimensionPixelSize(com.google.android.material.R.dimen.appcompat_dialog_background_inset)
window?.apply {
setBackgroundDrawable(InsetDrawable(materialShapeDrawable, inset, inset, inset, inset))
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
override fun setTitle(@StringRes titleId: Int) { data.title = context.getString(titleId) }
override fun setTitle(title: CharSequence?) { data.title = title ?: "" }
fun setMessage(@StringRes msgId: Int, vararg args: Any) {
data.message = context.getString(msgId, *args)
}
fun setMessage(message: CharSequence) { data.message = message }
fun setIcon(@DrawableRes drawableRes: Int) {
data.icon = AppCompatResources.getDrawable(context, drawableRes)
}
fun setIcon(drawable: Drawable) { data.icon = drawable }
fun setButton(buttonType: ButtonType, builder: Button.() -> Unit) {
val button = when (buttonType) {
ButtonType.POSITIVE -> data.buttonPositive
ButtonType.NEUTRAL -> data.buttonNeutral
ButtonType.NEGATIVE -> data.buttonNegative
}
button.apply(builder)
}
class DialogItem(
override val item: CharSequence,
val position: Int
) : RvItem(), DiffItem, ItemWrapper {
override val layoutRes = R.layout.item_list_single_line
}
fun interface DialogClickListener {
fun onClick(position: Int)
}
fun setListItems(
list: Array,
listener: DialogClickListener
) = setView(
RecyclerView(context).also {
it.isNestedScrollingEnabled = false
it.layoutManager = LinearLayoutManager(context)
val items = list.mapIndexed { i, cs -> DialogItem(cs, i) }
val extraBindings = bindExtra { sa ->
sa.put(BR.listener, DialogClickListener { pos ->
listener.onClick(pos)
dismiss()
})
}
it.setAdapter(items, extraBindings)
}
)
fun setView(view: View) {
binding.dialogBaseContainer.removeAllViews()
binding.dialogBaseContainer.addView(
view,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
fun resetButtons() {
ButtonType.values().forEach {
setButton(it) {
text = ""
icon = 0
isEnabled = true
doNotDismiss = false
onClick {}
}
}
}
// Prevent calling setContentView
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
override fun setContentView(layoutResID: Int) {}
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
override fun setContentView(view: View) {}
@Deprecated("Please use setView(view)", level = DeprecationLevel.ERROR)
override fun setContentView(view: View, params: ViewGroup.LayoutParams?) {}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/view/TappableHeadlineItem.kt
================================================
package com.topjohnwu.magisk.view
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.core.R as CoreR
sealed class TappableHeadlineItem : RvItem(), DiffItem {
abstract val title: Int
abstract val icon: Int
override val layoutRes = R.layout.item_tappable_headline
// --- listener
interface Listener {
fun onItemPressed(item: TappableHeadlineItem)
}
// --- objects
object ThemeMode : TappableHeadlineItem() {
override val title = CoreR.string.settings_dark_mode_title
override val icon = R.drawable.ic_day_night
}
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/view/TextItem.kt
================================================
package com.topjohnwu.magisk.view
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.DiffItem
import com.topjohnwu.magisk.databinding.ItemWrapper
import com.topjohnwu.magisk.databinding.RvItem
class TextItem(override val item: Int) : RvItem(), DiffItem, ItemWrapper {
override val layoutRes = R.layout.item_text
}
================================================
FILE: app/apk/src/main/java/com/topjohnwu/magisk/widget/ConcealableBottomNavigationView.java
================================================
package com.topjohnwu.magisk.widget;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.StateListAnimator;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.customview.view.AbsSavedState;
import androidx.interpolator.view.animation.FastOutLinearInInterpolator;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.topjohnwu.magisk.R;
public class ConcealableBottomNavigationView extends BottomNavigationView {
private static final int[] STATE_SET = {
R.attr.state_hidden
};
private boolean isHidden;
public ConcealableBottomNavigationView(@NonNull Context context) {
this(context, null);
}
public ConcealableBottomNavigationView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, com.google.android.material.R.attr.bottomNavigationStyle);
}
public ConcealableBottomNavigationView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, com.google.android.material.R.style.Widget_Design_BottomNavigationView);
}
public ConcealableBottomNavigationView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private void recreateAnimator(int height) {
Animator toHidden = ObjectAnimator.ofFloat(this, "translationY", height);
toHidden.setDuration(175);
toHidden.setInterpolator(new FastOutLinearInInterpolator());
Animator toUnhidden = ObjectAnimator.ofFloat(this, "translationY", 0);
toUnhidden.setDuration(225);
toUnhidden.setInterpolator(new FastOutLinearInInterpolator());
StateListAnimator animator = new StateListAnimator();
animator.addState(STATE_SET, toHidden);
animator.addState(new int[]{}, toUnhidden);
setStateListAnimator(animator);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
recreateAnimator(getMeasuredHeight());
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isHidden()) {
mergeDrawableStates(drawableState, STATE_SET);
}
return drawableState;
}
public boolean isHidden() {
return isHidden;
}
public void setHidden(boolean raised) {
if (isHidden != raised) {
isHidden = raised;
refreshDrawableState();
}
}
@NonNull
@Override
protected Parcelable onSaveInstanceState() {
SavedState state = new SavedState(super.onSaveInstanceState());
state.isHidden = isHidden();
return state;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
final SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
if (ss.isHidden) {
setHidden(isHidden);
}
}
static class SavedState extends AbsSavedState {
public boolean isHidden;
public SavedState(Parcel source) {
super(source, ConcealableBottomNavigationView.class.getClassLoader());
isHidden = source.readByte() != 0;
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeByte(isHidden ? (byte) 1 : (byte) 0);
}
public static final Creator CREATOR = new Creator<>() {
@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
================================================
FILE: app/apk/src/main/res/anim/fragment_enter.xml
================================================
================================================
FILE: app/apk/src/main/res/anim/fragment_enter_pop.xml
================================================
================================================
FILE: app/apk/src/main/res/anim/fragment_exit.xml
================================================
================================================
FILE: app/apk/src/main/res/anim/fragment_exit_pop.xml
================================================
================================================
FILE: app/apk/src/main/res/color/color_card_background_color_selector.xml
================================================
================================================
FILE: app/apk/src/main/res/color/color_error_transient.xml
================================================
================================================
FILE: app/apk/src/main/res/color/color_menu_tint.xml
================================================
================================================
FILE: app/apk/src/main/res/color/color_on_primary_transient.xml
================================================
================================================
FILE: app/apk/src/main/res/color/color_primary_error_transient.xml
================================================
================================================
FILE: app/apk/src/main/res/color/color_primary_transient.xml
================================================
================================================
FILE: app/apk/src/main/res/color/color_state_primary_transient.xml
================================================
================================================
FILE: app/apk/src/main/res/color/color_text_transient.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_bug_from_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_bug_to_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_circle_check_from_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_circle_check_to_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_home_from_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_home_to_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_module_from_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_module_to_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_settings_from_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_settings_to_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_superuser_from_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/avd_superuser_to_filled.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/bg_line_bottom_rounded.xml
================================================
-
================================================
FILE: app/apk/src/main/res/drawable/bg_line_top_rounded.xml
================================================
-
================================================
FILE: app/apk/src/main/res/drawable/bg_selection_circle_green.xml
================================================
-
================================================
FILE: app/apk/src/main/res/drawable/ic_action_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_back_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_bug_filled_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_bug_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_bug_outlined_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_check_circle_checked_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_check_circle_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_check_circle_unchecked_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_check_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_close_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_day.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_day_night.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_delete_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_download_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_folder_list.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_forth_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_home_filled_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_home_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_home_outlined_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_install.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_manager.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_module_filled_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_module_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_module_outlined_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_module_storage_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_night.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_notifications_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_paint.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_restart.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_save_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_search_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_settings_filled_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_settings_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_settings_outlined_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_superuser_filled_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_superuser_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_superuser_outlined_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/drawable/ic_update_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/activity_main_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/activity_request.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/dialog_magisk_base.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/dialog_settings_app_name.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/dialog_settings_download_path.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/dialog_settings_update_channel.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_action_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_deny_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_flash_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_home_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_install_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_log_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_module_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_settings_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_superuser_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/fragment_theme_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/include_home_magisk.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/include_home_manager.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/include_log_magisk.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/include_log_superuser.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_console_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_developer.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_hide_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_hide_process_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_icon_link.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_list_single_line.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_log_access_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_log_textview.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_log_track_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_module_download.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_module_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_policy_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_settings.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_settings_section.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_spinner.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_tappable_headline.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_text.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_theme.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/item_theme_container.xml
================================================
================================================
FILE: app/apk/src/main/res/layout/markdown_window_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/menu/menu_bottom_nav.xml
================================================
================================================
FILE: app/apk/src/main/res/menu/menu_deny_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/menu/menu_flash.xml
================================================
================================================
FILE: app/apk/src/main/res/menu/menu_home_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/menu/menu_log_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/menu/menu_reboot.xml
================================================
================================================
FILE: app/apk/src/main/res/navigation/main.xml
================================================
================================================
FILE: app/apk/src/main/res/values/attrs.xml
================================================
================================================
FILE: app/apk/src/main/res/values/dimens.xml
================================================
16dp
2dp
4dp
8dp
12dp
16dp
32dp
48dp
8dp
56dp
================================================
FILE: app/apk/src/main/res/values/ids.xml
================================================
================================================
FILE: app/apk/src/main/res/values/styles_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/values/styles_md2_appearance.xml
================================================
================================================
FILE: app/apk/src/main/res/values/styles_md2_impl.xml
================================================
================================================
FILE: app/apk/src/main/res/values/styles_view_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/values/theme_overlay.xml
================================================
================================================
FILE: app/apk/src/main/res/values/themes.xml
================================================
================================================
FILE: app/apk/src/main/res/values/themes_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/values/themes_override.xml
================================================
================================================
FILE: app/apk/src/main/res/values-night/styles_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/values-night/themes_md2.xml
================================================
================================================
FILE: app/apk/src/main/res/values-v27/themes.xml
================================================
================================================
FILE: app/apk-ng/build.gradle.kts
================================================
plugins {
id("com.android.application")
kotlin("plugin.parcelize")
kotlin("plugin.compose")
kotlin("plugin.serialization")
}
setupMainApk()
android {
buildFeatures {
compose = true
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
packaging {
jniLibs {
excludes += "lib/*/libandroidx.graphics.path.so"
}
}
defaultConfig {
proguardFile("proguard-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
}
}
}
dependencies {
implementation(project(":core"))
coreLibraryDesugaring(libs.jdk.libs)
implementation(libs.appcompat)
implementation(libs.material)
// Compose
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling)
implementation(libs.activity.compose)
implementation(libs.lifecycle.runtime.compose)
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.miuix)
implementation(libs.miuix.icons)
implementation(libs.miuix.navigation3.ui)
// Navigation3
implementation(libs.navigation3.runtime)
implementation(libs.navigationevent.compose)
implementation(libs.lifecycle.viewmodel.navigation3)
}
================================================
FILE: app/apk-ng/proguard-rules.pro
================================================
# Excessive obfuscation
-flattenpackagehierarchy
-allowaccessmodification
================================================
FILE: app/apk-ng/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/arch/AsyncLoadViewModel.kt
================================================
package com.topjohnwu.magisk.arch
import androidx.annotation.MainThread
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
abstract class AsyncLoadViewModel : BaseViewModel() {
private var loadingJob: Job? = null
@MainThread
fun startLoading() {
if (loadingJob?.isActive == true) {
return
}
loadingJob = viewModelScope.launch { doLoadWork() }
}
@MainThread
fun reload() {
loadingJob?.cancel()
loadingJob = viewModelScope.launch { doLoadWork() }
}
protected abstract suspend fun doLoadWork()
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/arch/BaseViewModel.kt
================================================
package com.topjohnwu.magisk.arch
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.ui.navigation.Route
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
abstract class BaseViewModel : ViewModel() {
private val _navEvents = MutableSharedFlow(extraBufferCapacity = 1)
val navEvents: SharedFlow = _navEvents
open fun onSaveState(state: Bundle) {}
open fun onRestoreState(state: Bundle) {}
fun showSnackbar(@StringRes resId: Int) {
AppContext.toast(resId, Toast.LENGTH_SHORT)
}
fun showSnackbar(msg: String) {
AppContext.toast(msg, Toast.LENGTH_SHORT)
}
fun navigateTo(route: Route) {
_navEvents.tryEmit(route)
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/arch/ViewModelFactory.kt
================================================
package com.topjohnwu.magisk.arch
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.install.InstallViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
object VMFactory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun create(modelClass: Class): T {
return when (modelClass) {
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
InstallViewModel::class.java ->
InstallViewModel(ServiceLocator.networkService)
SuRequestViewModel::class.java ->
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
else -> modelClass.newInstance()
} as T
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/terminal/TerminalBuffer.kt
================================================
package com.topjohnwu.magisk.terminal
import java.util.Arrays
/**
* A circular buffer of [TerminalRow]s which keeps notes about what is visible on a logical screen and the scroll
* history.
*
* See [externalToInternalRow] for how to map from logical screen rows to array indices.
*/
class TerminalBuffer(columns: Int, totalRows: Int, screenRows: Int) {
var lines: Array
/** The length of [lines]. */
var totalRows: Int = totalRows
private set
/** The number of rows and columns visible on the screen. */
var screenRows: Int = screenRows
var columns: Int = columns
/** The number of rows kept in history. */
var activeTranscriptRows: Int = 0
private set
/** The index in the circular buffer where the visible screen starts. */
private var screenFirstRow = 0
init {
lines = arrayOfNulls(totalRows)
blockSet(0, 0, columns, screenRows, ' '.code, TextStyle.NORMAL)
}
val transcriptText: String
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows).trim()
val transcriptTextWithoutJoinedLines: String
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows, false).trim()
val transcriptTextWithFullLinesJoined: String
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows, joinBackLines = true, joinFullLines = true).trim()
fun getSelectedText(selX1: Int, selY1: Int, selX2: Int, selY2: Int, joinBackLines: Boolean = true, joinFullLines: Boolean = false): String {
val builder = StringBuilder()
var y1 = selY1
var y2 = selY2
if (y1 < -activeTranscriptRows) y1 = -activeTranscriptRows
if (y2 >= screenRows) y2 = screenRows - 1
for (row in y1..y2) {
val x1 = if (row == y1) selX1 else 0
var x2: Int
if (row == y2) {
x2 = selX2 + 1
if (x2 > columns) x2 = columns
} else {
x2 = columns
}
val lineObject = lines[externalToInternalRow(row)]!!
val x1Index = lineObject.findStartOfColumn(x1)
var x2Index = if (x2 < columns) lineObject.findStartOfColumn(x2) else lineObject.spaceUsed
if (x2Index == x1Index) {
x2Index = lineObject.findStartOfColumn(x2 + 1)
}
val line = lineObject.text
var lastPrintingCharIndex = -1
val rowLineWrap = getLineWrap(row)
if (rowLineWrap && x2 == columns) {
lastPrintingCharIndex = x2Index - 1
} else {
for (i in x1Index until x2Index) {
val c = line[i]
if (c != ' ') lastPrintingCharIndex = i
}
}
val len = lastPrintingCharIndex - x1Index + 1
if (lastPrintingCharIndex != -1 && len > 0)
builder.append(line, x1Index, len)
val lineFillsWidth = lastPrintingCharIndex == x2Index - 1
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
&& row < y2 && row < screenRows - 1) builder.append('\n')
}
return builder.toString()
}
fun getWordAtLocation(x: Int, y: Int): String {
var y1 = y
var y2 = y
while (y1 > 0 && !getSelectedText(0, y1 - 1, columns, y, joinBackLines = true, joinFullLines = true).contains("\n")) {
y1--
}
while (y2 < screenRows && !getSelectedText(0, y, columns, y2 + 1, joinBackLines = true, joinFullLines = true).contains("\n")) {
y2++
}
val text = getSelectedText(0, y1, columns, y2, joinBackLines = true, joinFullLines = true)
val textOffset = (y - y1) * columns + x
if (textOffset >= text.length) {
return ""
}
val x1 = text.lastIndexOf(' ', textOffset)
var x2 = text.indexOf(' ', textOffset)
if (x2 == -1) {
x2 = text.length
}
if (x1 == x2) {
return ""
}
return text.substring(x1 + 1, x2)
}
val activeRows: Int get() = activeTranscriptRows + screenRows
/**
* Convert a row value from the public external coordinate system to our internal private coordinate system.
*
* ```
* - External coordinate system: -activeTranscriptRows to screenRows-1, with the screen being 0..screenRows-1.
* - Internal coordinate system: the screenRows lines starting at screenFirstRow comprise the screen, while the
* activeTranscriptRows lines ending at screenFirstRow-1 form the transcript (as a circular buffer).
*
* External <-> Internal:
*
* [ ... ] [ ... ]
* [ -activeTranscriptRows ] [ screenFirstRow - activeTranscriptRows ]
* [ ... ] [ ... ]
* [ 0 (visible screen starts here) ] <-> [ screenFirstRow ]
* [ ... ] [ ... ]
* [ screenRows-1 ] [ screenFirstRow + screenRows-1 ]
* ```
*
* @param externalRow a row in the external coordinate system.
* @return The row corresponding to the input argument in the private coordinate system.
*/
fun externalToInternalRow(externalRow: Int): Int {
if (externalRow < -activeTranscriptRows || externalRow > screenRows)
throw IllegalArgumentException("extRow=$externalRow, screenRows=$screenRows, activeTranscriptRows=$activeTranscriptRows")
val internalRow = screenFirstRow + externalRow
return if (internalRow < 0) (totalRows + internalRow) else (internalRow % totalRows)
}
fun setLineWrap(row: Int) {
lines[externalToInternalRow(row)]!!.lineWrap = true
}
fun getLineWrap(row: Int): Boolean {
return lines[externalToInternalRow(row)]!!.lineWrap
}
fun clearLineWrap(row: Int) {
lines[externalToInternalRow(row)]!!.lineWrap = false
}
/**
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
* change or the rows expand (that is, it only works when shrinking the number of rows).
*
* @param newColumns The number of columns the screen should have.
* @param newRows The number of rows the screen should have.
* @param cursor An int[2] containing the (column, row) cursor location.
*/
fun resize(newColumns: Int, newRows: Int, newTotalRows: Int, cursor: IntArray, currentStyle: Long, altScreen: Boolean) {
// newRows > totalRows should not normally happen since totalRows is TRANSCRIPT_ROWS (10000):
if (newColumns == columns && newRows <= totalRows) {
// Fast resize where just the rows changed.
var shiftDownOfTopRow = screenRows - newRows
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < screenRows) {
// Shrinking. Check if we can skip blank rows at bottom below cursor.
for (i in screenRows - 1 downTo 1) {
if (cursor[1] >= i) break
val r = externalToInternalRow(i)
if (lines[r] == null || lines[r]!!.isBlank()) {
if (--shiftDownOfTopRow == 0) break
}
}
} else if (shiftDownOfTopRow < 0) {
// Negative shift down = expanding. Only move screen up if there is transcript to show:
val actualShift = maxOf(shiftDownOfTopRow, -activeTranscriptRows)
if (shiftDownOfTopRow != actualShift) {
for (i in 0 until actualShift - shiftDownOfTopRow)
allocateFullLineIfNecessary((screenFirstRow + screenRows + i) % totalRows).clear(currentStyle)
shiftDownOfTopRow = actualShift
}
}
screenFirstRow += shiftDownOfTopRow
screenFirstRow = if (screenFirstRow < 0) (screenFirstRow + totalRows) else (screenFirstRow % totalRows)
totalRows = newTotalRows
activeTranscriptRows = if (altScreen) 0 else maxOf(0, activeTranscriptRows + shiftDownOfTopRow)
cursor[1] -= shiftDownOfTopRow
screenRows = newRows
} else {
// Copy away old state and update new:
val oldLines = lines
lines = arrayOfNulls(newTotalRows)
for (i in 0 until newTotalRows)
lines[i] = TerminalRow(newColumns, currentStyle)
val oldActiveTranscriptRows = activeTranscriptRows
val oldScreenFirstRow = screenFirstRow
val oldScreenRows = screenRows
val oldTotalRows = totalRows
totalRows = newTotalRows
screenRows = newRows
activeTranscriptRows = 0
screenFirstRow = 0
columns = newColumns
var newCursorRow = -1
var newCursorColumn = -1
val oldCursorRow = cursor[1]
val oldCursorColumn = cursor[0]
var newCursorPlaced = false
var currentOutputExternalRow = 0
var currentOutputExternalColumn = 0
var skippedBlankLines = 0
for (externalOldRow in -oldActiveTranscriptRows until oldScreenRows) {
var internalOldRow = oldScreenFirstRow + externalOldRow
internalOldRow = if (internalOldRow < 0) (oldTotalRows + internalOldRow) else (internalOldRow % oldTotalRows)
val oldLine = oldLines[internalOldRow]
val cursorAtThisRow = externalOldRow == oldCursorRow
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
skippedBlankLines++
continue
} else if (skippedBlankLines > 0) {
for (i in 0 until skippedBlankLines) {
if (currentOutputExternalRow == screenRows - 1) {
scrollDownOneLine(0, screenRows, currentStyle)
} else {
currentOutputExternalRow++
}
currentOutputExternalColumn = 0
}
skippedBlankLines = 0
}
var lastNonSpaceIndex = 0
var justToCursor = false
if (cursorAtThisRow || oldLine.lineWrap) {
lastNonSpaceIndex = oldLine.spaceUsed
if (cursorAtThisRow) justToCursor = true
} else {
for (i in 0 until oldLine.spaceUsed)
// NEWLY INTRODUCED BUG! Should not index oldLine.styles with char indices
if (oldLine.text[i] != ' '/* || oldLine.styles[i] != currentStyle */)
lastNonSpaceIndex = i + 1
}
var currentOldCol = 0
var styleAtCol = 0L
var i = 0
while (i < lastNonSpaceIndex) {
val c = oldLine.text[i]
val codePoint: Int
if (Character.isHighSurrogate(c)) {
i++
codePoint = Character.toCodePoint(c, oldLine.text[i])
} else {
codePoint = c.code
}
val displayWidth = WcWidth.width(codePoint)
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol)
if (currentOutputExternalColumn + displayWidth > columns) {
setLineWrap(currentOutputExternalRow)
if (currentOutputExternalRow == screenRows - 1) {
if (newCursorPlaced) newCursorRow--
scrollDownOneLine(0, screenRows, currentStyle)
} else {
currentOutputExternalRow++
}
currentOutputExternalColumn = 0
}
val offsetDueToCombiningChar = if (displayWidth <= 0 && currentOutputExternalColumn > 0) 1 else 0
val outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol)
if (displayWidth > 0) {
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
newCursorColumn = currentOutputExternalColumn
newCursorRow = currentOutputExternalRow
newCursorPlaced = true
}
currentOldCol += displayWidth
currentOutputExternalColumn += displayWidth
if (justToCursor && newCursorPlaced) break
}
i++
}
if (externalOldRow != (oldScreenRows - 1) && !oldLine.lineWrap) {
if (currentOutputExternalRow == screenRows - 1) {
if (newCursorPlaced) newCursorRow--
scrollDownOneLine(0, screenRows, currentStyle)
} else {
currentOutputExternalRow++
}
currentOutputExternalColumn = 0
}
}
cursor[0] = newCursorColumn
cursor[1] = newCursorRow
}
// Handle cursor scrolling off screen:
if (cursor[0] < 0 || cursor[1] < 0) {
cursor[0] = 0
cursor[1] = 0
}
}
/**
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
* into account.
*
* @param srcInternal The first line to be copied.
* @param len The number of lines to be copied.
*/
private fun blockCopyLinesDown(srcInternal: Int, len: Int) {
if (len == 0) return
val start = len - 1
val lineToBeOverWritten = lines[(srcInternal + start + 1) % totalRows]
for (i in start downTo 0)
lines[(srcInternal + i + 1) % totalRows] = lines[(srcInternal + i) % totalRows]
lines[srcInternal % totalRows] = lineToBeOverWritten
}
/**
* Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
*
* @param topMargin First line that is scrolled.
* @param bottomMargin One line after the last line that is scrolled.
* @param style the style for the newly exposed line.
*/
fun scrollDownOneLine(topMargin: Int, bottomMargin: Int, style: Long) {
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > screenRows)
throw IllegalArgumentException("topMargin=$topMargin, bottomMargin=$bottomMargin, screenRows=$screenRows")
blockCopyLinesDown(screenFirstRow, topMargin)
blockCopyLinesDown(externalToInternalRow(bottomMargin), screenRows - bottomMargin)
screenFirstRow = (screenFirstRow + 1) % totalRows
if (activeTranscriptRows < totalRows - screenRows) activeTranscriptRows++
val blankRow = externalToInternalRow(bottomMargin - 1)
if (lines[blankRow] == null) {
lines[blankRow] = TerminalRow(columns, style)
} else {
lines[blankRow]!!.clear(style)
}
}
/**
* Block copy characters from one position in the screen to another. The two positions can overlap. All characters
* of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
* be thrown.
*
* @param sx source X coordinate
* @param sy source Y coordinate
* @param w width
* @param h height
* @param dx destination X coordinate
* @param dy destination Y coordinate
*/
fun blockCopy(sx: Int, sy: Int, w: Int, h: Int, dx: Int, dy: Int) {
if (w == 0) return
if (sx < 0 || sx + w > columns || sy < 0 || sy + h > screenRows || dx < 0 || dx + w > columns || dy < 0 || dy + h > screenRows)
throw IllegalArgumentException()
val copyingUp = sy > dy
for (y in 0 until h) {
val y2 = if (copyingUp) y else (h - (y + 1))
val sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2))
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx)
}
}
/**
* Block set characters. All characters must be within the bounds of the screen, or else an
* InvalidParameterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
* of characters.
*/
fun blockSet(sx: Int, sy: Int, w: Int, h: Int, `val`: Int, style: Long) {
if (sx < 0 || sx + w > columns || sy < 0 || sy + h > screenRows) {
throw IllegalArgumentException(
"Illegal arguments! blockSet($sx, $sy, $w, $h, $`val`, $columns, $screenRows)")
}
for (y in 0 until h)
for (x in 0 until w)
setChar(sx + x, sy + y, `val`, style)
}
fun allocateFullLineIfNecessary(row: Int): TerminalRow {
return lines[row] ?: TerminalRow(columns, 0).also { lines[row] = it }
}
fun setChar(column: Int, row: Int, codePoint: Int, style: Long) {
if (row < 0 || row >= screenRows || column < 0 || column >= columns)
throw IllegalArgumentException("TerminalBuffer.setChar(): row=$row, column=$column, screenRows=$screenRows, columns=$columns")
val internalRow = externalToInternalRow(row)
allocateFullLineIfNecessary(internalRow).setChar(column, codePoint, style)
}
fun getStyleAt(externalRow: Int, column: Int): Long {
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column)
}
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
fun setOrClearEffect(bits: Int, setOrClear: Boolean, reverse: Boolean, rectangular: Boolean, leftMargin: Int, rightMargin: Int, top: Int, left: Int,
bottom: Int, right: Int) {
for (y in top until bottom) {
val line = lines[externalToInternalRow(y)]!!
val startOfLine = if (rectangular || y == top) left else leftMargin
val endOfLine = if (rectangular || y + 1 == bottom) right else rightMargin
for (x in startOfLine until endOfLine) {
val currentStyle = line.getStyle(x)
val foreColor = TextStyle.decodeForeColor(currentStyle)
val backColor = TextStyle.decodeBackColor(currentStyle)
var effect = TextStyle.decodeEffect(currentStyle)
if (reverse) {
effect = (effect and bits.inv()) or (bits and effect.inv())
} else if (setOrClear) {
effect = effect or bits
} else {
effect = effect and bits.inv()
}
line.styles[x] = TextStyle.encode(foreColor, backColor, effect)
}
}
}
fun clearTranscript() {
if (screenFirstRow < activeTranscriptRows) {
Arrays.fill(lines, totalRows + screenFirstRow - activeTranscriptRows, totalRows, null)
Arrays.fill(lines, 0, screenFirstRow, null)
} else {
Arrays.fill(lines, screenFirstRow - activeTranscriptRows, screenFirstRow, null)
}
activeTranscriptRows = 0
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/terminal/TerminalEmulator.kt
================================================
package com.topjohnwu.magisk.terminal
import android.util.Base64
import timber.log.Timber
import java.util.Stack
/**
* Renders text into a screen. Contains all the terminal-specific knowledge and state. Emulates a subset of the X Window
* System xterm terminal, which in turn is an emulator for a subset of the Digital Equipment Corporation vt100 terminal.
*
* References:
* - http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
* - http://en.wikipedia.org/wiki/ANSI_escape_code
* - http://man.he.net/man4/console_codes
* - http://bazaar.launchpad.net/~leonerd/libvterm/trunk/view/head:/src/state.c
* - http://www.columbia.edu/~kermit/k95manual/iso2022.html
* - http://www.vt100.net/docs/vt510-rm/chapter4
* - http://en.wikipedia.org/wiki/ISO/IEC_2022 - for 7-bit and 8-bit GL GR explanation
* - http://bjh21.me.uk/all-escapes/all-escapes.txt - extensive!
* - http://woldlab.caltech.edu/~diane/kde4.10/workingdir/kubuntu/konsole/doc/developer/old-documents/VT100/techref.html
*/
class TerminalEmulator(
columns: Int,
rows: Int,
cellWidthPixels: Int,
cellHeightPixels: Int,
transcriptRows: Int?,
) {
companion object {
private const val LOG_ESCAPE_SEQUENCES = false
/** Used for invalid data - http://en.wikipedia.org/wiki/Replacement_character#Replacement_character */
const val UNICODE_REPLACEMENT_CHAR = 0xFFFD
private const val ESC_NONE = 0
private const val ESC = 1
private const val ESC_POUND = 2
private const val ESC_SELECT_LEFT_PAREN = 3
private const val ESC_SELECT_RIGHT_PAREN = 4
private const val ESC_CSI = 6
private const val ESC_CSI_QUESTIONMARK = 7
private const val ESC_CSI_DOLLAR = 8
private const val ESC_PERCENT = 9
private const val ESC_OSC = 10
private const val ESC_OSC_ESC = 11
private const val ESC_CSI_BIGGERTHAN = 12
private const val ESC_P = 13
private const val ESC_CSI_QUESTIONMARK_ARG_DOLLAR = 14
private const val ESC_CSI_ARGS_SPACE = 15
private const val ESC_CSI_ARGS_ASTERIX = 16
private const val ESC_CSI_DOUBLE_QUOTE = 17
private const val ESC_CSI_SINGLE_QUOTE = 18
private const val ESC_CSI_EXCLAMATION = 19
private const val ESC_APC = 20
private const val ESC_APC_ESCAPE = 21
private const val ESC_CSI_UNSUPPORTED_PARAMETER_BYTE = 22
private const val ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE = 23
private const val MAX_ESCAPE_PARAMETERS = 32
private const val MAX_OSC_STRING_LENGTH = 8192
private const val DECSET_BIT_APPLICATION_CURSOR_KEYS = 1
private const val DECSET_BIT_REVERSE_VIDEO = 1 shl 1
private const val DECSET_BIT_ORIGIN_MODE = 1 shl 2
private const val DECSET_BIT_AUTOWRAP = 1 shl 3
private const val DECSET_BIT_CURSOR_ENABLED = 1 shl 4
private const val DECSET_BIT_APPLICATION_KEYPAD = 1 shl 5
private const val DECSET_BIT_MOUSE_TRACKING_PRESS_RELEASE = 1 shl 6
private const val DECSET_BIT_MOUSE_TRACKING_BUTTON_EVENT = 1 shl 7
private const val DECSET_BIT_SEND_FOCUS_EVENTS = 1 shl 8
private const val DECSET_BIT_MOUSE_PROTOCOL_SGR = 1 shl 9
private const val DECSET_BIT_BRACKETED_PASTE_MODE = 1 shl 10
private const val DECSET_BIT_LEFTRIGHT_MARGIN_MODE = 1 shl 11
private const val DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE = 1 shl 12
const val TERMINAL_TRANSCRIPT_ROWS_MIN = 100
const val TERMINAL_TRANSCRIPT_ROWS_MAX = 50000
const val DEFAULT_TERMINAL_TRANSCRIPT_ROWS = 2000
const val TERMINAL_CURSOR_STYLE_BLOCK = 0
const val TERMINAL_CURSOR_STYLE_UNDERLINE = 1
const val TERMINAL_CURSOR_STYLE_BAR = 2
const val DEFAULT_TERMINAL_CURSOR_STYLE = TERMINAL_CURSOR_STYLE_BLOCK
val TERMINAL_CURSOR_STYLES_LIST = intArrayOf(
TERMINAL_CURSOR_STYLE_BLOCK,
TERMINAL_CURSOR_STYLE_UNDERLINE,
TERMINAL_CURSOR_STYLE_BAR
)
private const val LOG_TAG = "TerminalEmulator"
fun mapDecSetBitToInternalBit(decsetBit: Int): Int = when (decsetBit) {
1 -> DECSET_BIT_APPLICATION_CURSOR_KEYS
5 -> DECSET_BIT_REVERSE_VIDEO
6 -> DECSET_BIT_ORIGIN_MODE
7 -> DECSET_BIT_AUTOWRAP
25 -> DECSET_BIT_CURSOR_ENABLED
66 -> DECSET_BIT_APPLICATION_KEYPAD
69 -> DECSET_BIT_LEFTRIGHT_MARGIN_MODE
1000 -> DECSET_BIT_MOUSE_TRACKING_PRESS_RELEASE
1002 -> DECSET_BIT_MOUSE_TRACKING_BUTTON_EVENT
1004 -> DECSET_BIT_SEND_FOCUS_EVENTS
1006 -> DECSET_BIT_MOUSE_PROTOCOL_SGR
2004 -> DECSET_BIT_BRACKETED_PASTE_MODE
else -> -1
}
}
var title: String? = null
private set
private val titleStack = Stack()
var cursorRow = 0
private set
var cursorCol = 0
private set
var mRows: Int = rows
var mColumns: Int = columns
private var mCellWidthPixels: Int = cellWidthPixels
private var mCellHeightPixels: Int = cellHeightPixels
var cursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE
private set
private val mMainBuffer: TerminalBuffer
internal val mAltBuffer: TerminalBuffer
var screen: TerminalBuffer
private set
var onCopyToClipboard: ((String) -> Unit)? = null
var onScreenUpdate: (() -> Unit)? = null
private var mArgIndex = 0
private val mArgs = IntArray(MAX_ESCAPE_PARAMETERS)
private var mArgsSubParamsBitSet = 0
private val mOSCOrDeviceControlArgs = StringBuilder()
private var mContinueSequence = false
private var mEscapeState = ESC_NONE
private val mSavedStateMain = SavedScreenState()
private val mSavedStateAlt = SavedScreenState()
private var mUseLineDrawingG0 = false
private var mUseLineDrawingG1 = false
private var mUseLineDrawingUsesG0 = true
private var mCurrentDecSetFlags = 0
private var mSavedDecSetFlags = 0
private var mInsertMode = false
private var mTabStop: BooleanArray
private var mTopMargin = 0
private var mBottomMargin = 0
private var mLeftMargin = 0
private var mRightMargin = 0
private var mAboutToAutoWrap = false
private var mCursorBlinkingEnabled = false
private var mCursorBlinkState = false
private var mForeColor = 0
private var mBackColor = 0
private var mUnderlineColor = 0
private var mEffect = 0
var scrollCounter = 0
private set
var isAutoScrollDisabled = false
private set
private var mUtf8ToFollow = 0
private var mUtf8Index = 0
private val mUtf8InputBuffer = ByteArray(4)
private var mLastEmittedCodePoint = -1
val mColors = TerminalColors()
init {
mMainBuffer = TerminalBuffer(columns, getTerminalTranscriptRows(transcriptRows), rows)
mAltBuffer = TerminalBuffer(columns, rows, rows)
screen = mMainBuffer
mTabStop = BooleanArray(mColumns)
reset()
}
val isAlternateBufferActive: Boolean get() = screen === mAltBuffer
private fun getTerminalTranscriptRows(transcriptRows: Int?): Int {
return if (transcriptRows == null || transcriptRows < TERMINAL_TRANSCRIPT_ROWS_MIN || transcriptRows > TERMINAL_TRANSCRIPT_ROWS_MAX)
DEFAULT_TERMINAL_TRANSCRIPT_ROWS
else
transcriptRows
}
fun resize(columns: Int, rows: Int, cellWidthPixels: Int, cellHeightPixels: Int) {
this.mCellWidthPixels = cellWidthPixels
this.mCellHeightPixels = cellHeightPixels
if (mRows == rows && mColumns == columns) {
return
} else if (columns < 2 || rows < 2) {
throw IllegalArgumentException("rows=$rows, columns=$columns")
}
if (mRows != rows) {
mRows = rows
mTopMargin = 0
mBottomMargin = mRows
}
if (mColumns != columns) {
val oldColumns = mColumns
mColumns = columns
val oldTabStop = mTabStop
mTabStop = BooleanArray(mColumns)
setDefaultTabStops()
val toTransfer = minOf(oldColumns, columns)
System.arraycopy(oldTabStop, 0, mTabStop, 0, toTransfer)
mLeftMargin = 0
mRightMargin = mColumns
}
resizeScreen()
}
private fun resizeScreen() {
val cursor = intArrayOf(cursorCol, cursorRow)
val newTotalRows = if (screen === mAltBuffer) mRows else mMainBuffer.totalRows
screen.resize(mColumns, mRows, newTotalRows, cursor, style, isAlternateBufferActive)
cursorCol = cursor[0]
cursorRow = cursor[1]
}
fun setCursorStyle() {
cursorStyle = DEFAULT_TERMINAL_CURSOR_STYLE
}
val isReverseVideo: Boolean get() = isDecsetInternalBitSet(DECSET_BIT_REVERSE_VIDEO)
val isCursorEnabled: Boolean get() = isDecsetInternalBitSet(DECSET_BIT_CURSOR_ENABLED)
fun shouldCursorBeVisible(): Boolean {
if (!isCursorEnabled) return false
return if (mCursorBlinkingEnabled) mCursorBlinkState else true
}
fun setCursorBlinkingEnabled(cursorBlinkingEnabled: Boolean) {
this.mCursorBlinkingEnabled = cursorBlinkingEnabled
}
fun setCursorBlinkState(cursorBlinkState: Boolean) {
this.mCursorBlinkState = cursorBlinkState
}
fun isKeypadApplicationMode(): Boolean = isDecsetInternalBitSet(DECSET_BIT_APPLICATION_KEYPAD)
private fun isDecsetInternalBitSet(bit: Int): Boolean = (mCurrentDecSetFlags and bit) != 0
private fun setDecsetinternalBit(internalBit: Int, set: Boolean) {
if (set) {
if (internalBit == DECSET_BIT_MOUSE_TRACKING_PRESS_RELEASE) {
setDecsetinternalBit(DECSET_BIT_MOUSE_TRACKING_BUTTON_EVENT, false)
} else if (internalBit == DECSET_BIT_MOUSE_TRACKING_BUTTON_EVENT) {
setDecsetinternalBit(DECSET_BIT_MOUSE_TRACKING_PRESS_RELEASE, false)
}
}
if (set) {
mCurrentDecSetFlags = mCurrentDecSetFlags or internalBit
} else {
mCurrentDecSetFlags = mCurrentDecSetFlags and internalBit.inv()
}
}
private fun setDefaultTabStops() {
for (i in 0 until mColumns)
mTabStop[i] = (i and 7) == 0 && i != 0
}
fun append(buffer: ByteArray, length: Int) {
for (i in 0 until length)
processByte(buffer[i])
}
private fun processByte(byteToProcess: Byte) {
if (mUtf8ToFollow > 0) {
if ((byteToProcess.toInt() and 0b11000000) == 0b10000000) {
mUtf8InputBuffer[mUtf8Index] = byteToProcess
mUtf8Index++
if (--mUtf8ToFollow == 0) {
val firstByteMask = when (mUtf8Index) {
2 -> 0b00011111
3 -> 0b00001111
else -> 0b00000111
}
var codePoint = mUtf8InputBuffer[0].toInt() and firstByteMask
for (i in 1 until mUtf8Index)
codePoint = (codePoint shl 6) or (mUtf8InputBuffer[i].toInt() and 0b00111111)
if (((codePoint <= 0b1111111) && mUtf8Index > 1) || (codePoint < 0b11111111111 && mUtf8Index > 2)
|| (codePoint < 0b1111111111111111 && mUtf8Index > 3)
) {
codePoint = UNICODE_REPLACEMENT_CHAR
}
mUtf8Index = 0
mUtf8ToFollow = 0
if (codePoint in 0x80..0x9F) {
// C1 control character, ignore
} else {
when (Character.getType(codePoint).toByte()) {
Character.UNASSIGNED, Character.SURROGATE ->
codePoint = UNICODE_REPLACEMENT_CHAR
}
processCodePoint(codePoint)
}
}
} else {
mUtf8Index = 0
mUtf8ToFollow = 0
emitCodePoint(UNICODE_REPLACEMENT_CHAR)
processByte(byteToProcess)
}
} else {
if ((byteToProcess.toInt() and 0b10000000) == 0) {
processCodePoint(byteToProcess.toInt())
return
} else if ((byteToProcess.toInt() and 0b11100000) == 0b11000000) {
mUtf8ToFollow = 1
} else if ((byteToProcess.toInt() and 0b11110000) == 0b11100000) {
mUtf8ToFollow = 2
} else if ((byteToProcess.toInt() and 0b11111000) == 0b11110000) {
mUtf8ToFollow = 3
} else {
processCodePoint(UNICODE_REPLACEMENT_CHAR)
return
}
mUtf8InputBuffer[mUtf8Index] = byteToProcess
mUtf8Index++
}
}
fun processCodePoint(b: Int) {
if (mEscapeState == ESC_APC) {
doApc(b)
return
} else if (mEscapeState == ESC_APC_ESCAPE) {
doApcEscape(b)
return
}
when (b) {
0 -> { /* NUL, do nothing */ }
7 -> {
if (mEscapeState == ESC_OSC) doOsc(b)
}
8 -> {
if (mLeftMargin == cursorCol) {
val previousRow = cursorRow - 1
if (previousRow >= 0 && screen.getLineWrap(previousRow)) {
screen.clearLineWrap(previousRow)
setCursorRowCol(previousRow, mRightMargin - 1)
}
} else {
setCursorCol(cursorCol - 1)
}
}
9 -> cursorCol = nextTabStop(1)
10, 11, 12 -> doLinefeed()
13 -> setCursorCol(mLeftMargin)
14 -> mUseLineDrawingUsesG0 = false
15 -> mUseLineDrawingUsesG0 = true
24, 26 -> {
if (mEscapeState != ESC_NONE) {
mEscapeState = ESC_NONE
emitCodePoint(127)
}
}
27 -> {
if (mEscapeState == ESC_P) {
return
} else if (mEscapeState != ESC_OSC) {
startEscapeSequence()
} else {
doOsc(b)
}
}
else -> {
mContinueSequence = false
when (mEscapeState) {
ESC_NONE -> if (b >= 32) emitCodePoint(b)
ESC -> doEsc(b)
ESC_POUND -> doEscPound(b)
ESC_SELECT_LEFT_PAREN -> mUseLineDrawingG0 = (b == '0'.code)
ESC_SELECT_RIGHT_PAREN -> mUseLineDrawingG1 = (b == '0'.code)
ESC_CSI -> doCsi(b)
ESC_CSI_UNSUPPORTED_PARAMETER_BYTE, ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE ->
doCsiUnsupportedParameterOrIntermediateByte(b)
ESC_CSI_EXCLAMATION -> {
if (b == 'p'.code) reset() else unknownSequence(b)
}
ESC_CSI_QUESTIONMARK -> doCsiQuestionMark(b)
ESC_CSI_BIGGERTHAN -> doCsiBiggerThan(b)
ESC_CSI_DOLLAR -> {
val originMode = isDecsetInternalBitSet(DECSET_BIT_ORIGIN_MODE)
val effectiveTopMargin = if (originMode) mTopMargin else 0
val effectiveBottomMargin = if (originMode) mBottomMargin else mRows
val effectiveLeftMargin = if (originMode) mLeftMargin else 0
val effectiveRightMargin = if (originMode) mRightMargin else mColumns
when (b) {
'v'.code -> {
val topSource = minOf(getArg(0, 1, true) - 1 + effectiveTopMargin, mRows)
val leftSource = minOf(getArg(1, 1, true) - 1 + effectiveLeftMargin, mColumns)
val bottomSource = minOf(maxOf(getArg(2, mRows, true) + effectiveTopMargin, topSource), mRows)
val rightSource = minOf(maxOf(getArg(3, mColumns, true) + effectiveLeftMargin, leftSource), mColumns)
val destionationTop = minOf(getArg(5, 1, true) - 1 + effectiveTopMargin, mRows)
val destinationLeft = minOf(getArg(6, 1, true) - 1 + effectiveLeftMargin, mColumns)
val heightToCopy = minOf(mRows - destionationTop, bottomSource - topSource)
val widthToCopy = minOf(mColumns - destinationLeft, rightSource - leftSource)
screen.blockCopy(leftSource, topSource, widthToCopy, heightToCopy, destinationLeft, destionationTop)
}
'{'.code, 'x'.code, 'z'.code -> {
val erase = b != 'x'.code
val selective = b == '{'.code
val keepVisualAttributes = erase && selective
var argIndex = 0
val fillChar = if (erase) ' '.code else getArg(argIndex++, -1, true)
if ((fillChar in 32..126) || (fillChar in 160..255)) {
val top = minOf(getArg(argIndex++, 1, true) + effectiveTopMargin, effectiveBottomMargin + 1)
val left = minOf(getArg(argIndex++, 1, true) + effectiveLeftMargin, effectiveRightMargin + 1)
val bottom = minOf(getArg(argIndex++, mRows, true) + effectiveTopMargin, effectiveBottomMargin)
val right = minOf(getArg(argIndex, mColumns, true) + effectiveLeftMargin, effectiveRightMargin)
val style = style
for (row in top - 1 until bottom)
for (col in left - 1 until right)
if (!selective || (TextStyle.decodeEffect(screen.getStyleAt(row, col)) and TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0)
screen.setChar(col, row, fillChar, if (keepVisualAttributes) screen.getStyleAt(row, col) else style)
}
}
'r'.code, 't'.code -> {
val reverse = b == 't'.code
val top = minOf(getArg(0, 1, true) - 1, effectiveBottomMargin) + effectiveTopMargin
val left = minOf(getArg(1, 1, true) - 1, effectiveRightMargin) + effectiveLeftMargin
val bottom = minOf(getArg(2, mRows, true) + 1, effectiveBottomMargin - 1) + effectiveTopMargin
val right = minOf(getArg(3, mColumns, true) + 1, effectiveRightMargin - 1) + effectiveLeftMargin
if (mArgIndex >= 4) {
if (mArgIndex >= mArgs.size) mArgIndex = mArgs.size - 1
for (i in 4..mArgIndex) {
var bits = 0
var setOrClear = true
when (getArg(i, 0, false)) {
0 -> {
bits = (TextStyle.CHARACTER_ATTRIBUTE_BOLD or TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE or TextStyle.CHARACTER_ATTRIBUTE_BLINK
or TextStyle.CHARACTER_ATTRIBUTE_INVERSE)
if (!reverse) setOrClear = false
}
1 -> bits = TextStyle.CHARACTER_ATTRIBUTE_BOLD
4 -> bits = TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE
5 -> bits = TextStyle.CHARACTER_ATTRIBUTE_BLINK
7 -> bits = TextStyle.CHARACTER_ATTRIBUTE_INVERSE
22 -> {
bits = TextStyle.CHARACTER_ATTRIBUTE_BOLD
setOrClear = false
}
24 -> {
bits = TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE
setOrClear = false
}
25 -> {
bits = TextStyle.CHARACTER_ATTRIBUTE_BLINK
setOrClear = false
}
27 -> {
bits = TextStyle.CHARACTER_ATTRIBUTE_INVERSE
setOrClear = false
}
}
if (reverse && !setOrClear) {
// Reverse attributes in rectangular area ignores non-(1,4,5,7) bits.
} else {
screen.setOrClearEffect(
bits, setOrClear, reverse, isDecsetInternalBitSet(DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE),
effectiveLeftMargin, effectiveRightMargin, top, left, bottom, right
)
}
}
}
}
else -> unknownSequence(b)
}
}
ESC_CSI_DOUBLE_QUOTE -> {
if (b == 'q'.code) {
val arg = getArg0(0)
if (arg == 0 || arg == 2) {
mEffect = mEffect and TextStyle.CHARACTER_ATTRIBUTE_PROTECTED.inv()
} else if (arg == 1) {
mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_PROTECTED
} else {
unknownSequence(b)
}
} else {
unknownSequence(b)
}
}
ESC_CSI_SINGLE_QUOTE -> {
when (b) {
'}'.code -> {
val columnsAfterCursor = mRightMargin - cursorCol
val columnsToInsert = minOf(getArg0(1), columnsAfterCursor)
val columnsToMove = columnsAfterCursor - columnsToInsert
screen.blockCopy(cursorCol, 0, columnsToMove, mRows, cursorCol + columnsToInsert, 0)
blockClear(cursorCol, 0, columnsToInsert, mRows)
}
'~'.code -> {
val columnsAfterCursor = mRightMargin - cursorCol
val columnsToDelete = minOf(getArg0(1), columnsAfterCursor)
val columnsToMove = columnsAfterCursor - columnsToDelete
screen.blockCopy(cursorCol + columnsToDelete, 0, columnsToMove, mRows, cursorCol, 0)
}
else -> unknownSequence(b)
}
}
ESC_PERCENT -> { /* ignore */ }
ESC_OSC -> doOsc(b)
ESC_OSC_ESC -> doOscEsc(b)
ESC_P -> doDeviceControl(b)
ESC_CSI_QUESTIONMARK_ARG_DOLLAR -> {
if (b != 'p'.code) unknownSequence(b)
}
ESC_CSI_ARGS_SPACE -> {
val arg = getArg0(0)
when (b) {
'q'.code -> when (arg) {
0, 1, 2 -> cursorStyle = TERMINAL_CURSOR_STYLE_BLOCK
3, 4 -> cursorStyle = TERMINAL_CURSOR_STYLE_UNDERLINE
5, 6 -> cursorStyle = TERMINAL_CURSOR_STYLE_BAR
}
't'.code, 'u'.code -> { /* Set margin-bell volume - ignore */ }
else -> unknownSequence(b)
}
}
ESC_CSI_ARGS_ASTERIX -> {
val attributeChangeExtent = getArg0(0)
if (b == 'x'.code && attributeChangeExtent in 0..2) {
setDecsetinternalBit(DECSET_BIT_RECTANGULAR_CHANGEATTRIBUTE, attributeChangeExtent == 2)
} else {
unknownSequence(b)
}
}
else -> unknownSequence(b)
}
if (!mContinueSequence) mEscapeState = ESC_NONE
}
}
}
private fun doDeviceControl(b: Int) {
when (b) {
'\\'.code -> {
val dcs = mOSCOrDeviceControlArgs.toString()
if (!dcs.startsWith("\$q") && !dcs.startsWith("+q")) {
if (LOG_ESCAPE_SEQUENCES)
Timber.tag(LOG_TAG).e("Unrecognized device control string: $dcs")
}
finishSequence()
}
else -> {
if (mOSCOrDeviceControlArgs.length > MAX_OSC_STRING_LENGTH) {
mOSCOrDeviceControlArgs.clear()
finishSequence()
} else {
mOSCOrDeviceControlArgs.appendCodePoint(b)
continueSequence(mEscapeState)
}
}
}
}
private fun doApc(b: Int) {
if (b == 27) {
continueSequence(ESC_APC_ESCAPE)
}
}
private fun doApcEscape(b: Int) {
if (b == '\\'.code) {
finishSequence()
} else {
continueSequence(ESC_APC)
}
}
private fun nextTabStop(numTabs: Int): Int {
var remaining = numTabs
for (i in cursorCol + 1 until mColumns)
if (mTabStop[i] && --remaining == 0) return minOf(i, mRightMargin)
return mRightMargin - 1
}
private fun doCsiUnsupportedParameterOrIntermediateByte(b: Int) {
if (mEscapeState == ESC_CSI_UNSUPPORTED_PARAMETER_BYTE && b in 0x30..0x3F) {
continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE)
} else if (b in 0x20..0x2F) {
continueSequence(ESC_CSI_UNSUPPORTED_INTERMEDIATE_BYTE)
} else if (b in 0x40..0x7E) {
finishSequence()
} else {
unknownSequence(b)
}
}
private fun doCsiQuestionMark(b: Int) {
when (b) {
'J'.code, 'K'.code -> {
mAboutToAutoWrap = false
val fillChar = ' '.code
var startCol = -1
var startRow = -1
var endCol = -1
var endRow = -1
val justRow = (b == 'K'.code)
when (getArg0(0)) {
0 -> {
startCol = cursorCol; startRow = cursorRow
endCol = mColumns; endRow = if (justRow) cursorRow + 1 else mRows
}
1 -> {
startCol = 0; startRow = if (justRow) cursorRow else 0
endCol = cursorCol + 1; endRow = cursorRow + 1
}
2 -> {
startCol = 0; startRow = if (justRow) cursorRow else 0
endCol = mColumns; endRow = if (justRow) cursorRow + 1 else mRows
}
else -> unknownSequence(b)
}
val style = style
for (row in startRow until endRow) {
for (col in startCol until endCol) {
if ((TextStyle.decodeEffect(screen.getStyleAt(row, col)) and TextStyle.CHARACTER_ATTRIBUTE_PROTECTED) == 0)
screen.setChar(col, row, fillChar, style)
}
}
}
'h'.code, 'l'.code -> {
if (mArgIndex >= mArgs.size) mArgIndex = mArgs.size - 1
for (i in 0..mArgIndex)
doDecSetOrReset(b == 'h'.code, mArgs[i])
}
'n'.code -> {
finishSequence()
return
}
'r'.code, 's'.code -> {
if (mArgIndex >= mArgs.size) mArgIndex = mArgs.size - 1
for (i in 0..mArgIndex) {
val externalBit = mArgs[i]
val internalBit = mapDecSetBitToInternalBit(externalBit)
if (internalBit == -1) {
Timber.tag(LOG_TAG).w("Ignoring request to save/recall decset bit=$externalBit")
} else {
if (b == 's'.code) {
mSavedDecSetFlags = mSavedDecSetFlags or internalBit
} else {
doDecSetOrReset((mSavedDecSetFlags and internalBit) != 0, externalBit)
}
}
}
}
'$'.code -> {
continueSequence(ESC_CSI_QUESTIONMARK_ARG_DOLLAR)
return
}
else -> parseArg(b)
}
}
fun doDecSetOrReset(setting: Boolean, externalBit: Int) {
val internalBit = mapDecSetBitToInternalBit(externalBit)
if (internalBit != -1) {
setDecsetinternalBit(internalBit, setting)
}
when (externalBit) {
1 -> { /* Application Cursor Keys (DECCKM) */ }
3 -> {
mLeftMargin = 0
mTopMargin = 0
mBottomMargin = mRows
mRightMargin = mColumns
setDecsetinternalBit(DECSET_BIT_LEFTRIGHT_MARGIN_MODE, false)
blockClear(0, 0, mColumns, mRows)
setCursorRowCol(0, 0)
}
4 -> { /* DECSCLM-Scrolling Mode. Ignore */ }
5 -> { /* Reverse video. No action */ }
6 -> if (setting) setCursorPosition(0, 0)
7, 8, 9, 12, 25 -> { /* Cursor state change - ignored for read-only */ }
40, 45, 66 -> { /* Ignore */ }
69 -> {
if (!setting) {
mLeftMargin = 0
mRightMargin = mColumns
}
}
1000, 1001, 1002, 1003, 1004, 1005, 1006, 1015, 1034 -> { /* Ignore */ }
1048 -> if (setting) saveCursor() else restoreCursor()
47, 1047, 1049 -> {
val newScreen = if (setting) mAltBuffer else mMainBuffer
if (newScreen !== screen) {
val resized = !(newScreen.columns == mColumns && newScreen.screenRows == mRows)
if (setting) saveCursor()
screen = newScreen
if (!setting) {
val col = mSavedStateMain.mSavedCursorCol
val row = mSavedStateMain.mSavedCursorRow
restoreCursor()
if (resized) {
cursorCol = col
cursorRow = row
}
}
if (resized) resizeScreen()
if (newScreen === mAltBuffer)
newScreen.blockSet(0, 0, mColumns, mRows, ' '.code, style)
}
}
2004 -> { /* Bracketed paste mode - setting bit is enough */ }
else -> unknownParameter(externalBit)
}
}
private fun doCsiBiggerThan(b: Int) {
when (b) {
'c'.code -> { /* Secondary device attributes - ignored for read-only */ }
'm'.code -> Timber.tag(LOG_TAG).e("(ignored) CSI > MODIFY RESOURCE: ${getArg0(-1)} to ${getArg1(-1)}")
else -> parseArg(b)
}
}
private fun startEscapeSequence() {
mEscapeState = ESC
mArgIndex = 0
mArgs.fill(-1)
mArgsSubParamsBitSet = 0
}
private fun doLinefeed() {
val belowScrollingRegion = cursorRow >= mBottomMargin
var newCursorRow = cursorRow + 1
if (belowScrollingRegion) {
if (cursorRow != mRows - 1) {
setCursorRow(newCursorRow)
}
} else {
if (newCursorRow == mBottomMargin) {
scrollDownOneLine()
newCursorRow = mBottomMargin - 1
}
setCursorRow(newCursorRow)
}
}
private fun continueSequence(state: Int) {
mEscapeState = state
mContinueSequence = true
}
private fun doEscPound(b: Int) {
when (b) {
'8'.code -> screen.blockSet(0, 0, mColumns, mRows, 'E'.code, style)
else -> unknownSequence(b)
}
}
private fun doEsc(b: Int) {
when (b) {
'#'.code -> continueSequence(ESC_POUND)
'('.code -> continueSequence(ESC_SELECT_LEFT_PAREN)
')'.code -> continueSequence(ESC_SELECT_RIGHT_PAREN)
'6'.code -> {
if (cursorCol > mLeftMargin) {
cursorCol--
} else {
val rows = mBottomMargin - mTopMargin
screen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin - 1, rows, mLeftMargin + 1, mTopMargin)
screen.blockSet(mLeftMargin, mTopMargin, 1, rows, ' '.code, TextStyle.encode(mForeColor, mBackColor, 0))
}
}
'7'.code -> saveCursor()
'8'.code -> restoreCursor()
'9'.code -> {
if (cursorCol < mRightMargin - 1) {
cursorCol++
} else {
val rows = mBottomMargin - mTopMargin
screen.blockCopy(mLeftMargin + 1, mTopMargin, mRightMargin - mLeftMargin - 1, rows, mLeftMargin, mTopMargin)
screen.blockSet(mRightMargin - 1, mTopMargin, 1, rows, ' '.code, TextStyle.encode(mForeColor, mBackColor, 0))
}
}
'c'.code -> {
reset()
mMainBuffer.clearTranscript()
blockClear(0, 0, mColumns, mRows)
setCursorPosition(0, 0)
}
'D'.code -> doLinefeed()
'E'.code -> {
setCursorCol(if (isDecsetInternalBitSet(DECSET_BIT_ORIGIN_MODE)) mLeftMargin else 0)
doLinefeed()
}
'F'.code -> setCursorRowCol(0, mBottomMargin - 1)
'H'.code -> mTabStop[cursorCol] = true
'M'.code -> {
if (cursorRow <= mTopMargin) {
screen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, mBottomMargin - (mTopMargin + 1), mLeftMargin, mTopMargin + 1)
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin)
} else {
cursorRow--
}
}
'N'.code, '0'.code -> { /* SS2/SS3, ignore */ }
'P'.code -> {
mOSCOrDeviceControlArgs.clear()
continueSequence(ESC_P)
}
'['.code -> continueSequence(ESC_CSI)
'='.code -> setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, true)
']'.code -> {
mOSCOrDeviceControlArgs.clear()
continueSequence(ESC_OSC)
}
'>'.code -> setDecsetinternalBit(DECSET_BIT_APPLICATION_KEYPAD, false)
'_'.code -> continueSequence(ESC_APC)
'%'.code -> continueSequence(ESC_PERCENT)
else -> unknownSequence(b)
}
}
private fun saveCursor() {
val state = if (screen === mMainBuffer) mSavedStateMain else mSavedStateAlt
state.mSavedCursorRow = cursorRow
state.mSavedCursorCol = cursorCol
state.mSavedEffect = mEffect
state.mSavedForeColor = mForeColor
state.mSavedBackColor = mBackColor
state.mSavedDecFlags = mCurrentDecSetFlags
state.mUseLineDrawingG0 = mUseLineDrawingG0
state.mUseLineDrawingG1 = mUseLineDrawingG1
state.mUseLineDrawingUsesG0 = mUseLineDrawingUsesG0
}
private fun restoreCursor() {
val state = if (screen === mMainBuffer) mSavedStateMain else mSavedStateAlt
setCursorRowCol(state.mSavedCursorRow, state.mSavedCursorCol)
mEffect = state.mSavedEffect
mForeColor = state.mSavedForeColor
mBackColor = state.mSavedBackColor
val mask = DECSET_BIT_AUTOWRAP or DECSET_BIT_ORIGIN_MODE
mCurrentDecSetFlags = (mCurrentDecSetFlags and mask.inv()) or (state.mSavedDecFlags and mask)
mUseLineDrawingG0 = state.mUseLineDrawingG0
mUseLineDrawingG1 = state.mUseLineDrawingG1
mUseLineDrawingUsesG0 = state.mUseLineDrawingUsesG0
}
private fun doCsi(b: Int) {
when (b) {
'!'.code -> continueSequence(ESC_CSI_EXCLAMATION)
'"'.code -> continueSequence(ESC_CSI_DOUBLE_QUOTE)
'\''.code -> continueSequence(ESC_CSI_SINGLE_QUOTE)
'$'.code -> continueSequence(ESC_CSI_DOLLAR)
'*'.code -> continueSequence(ESC_CSI_ARGS_ASTERIX)
'@'.code -> {
mAboutToAutoWrap = false
val columnsAfterCursor = mColumns - cursorCol
val spacesToInsert = minOf(getArg0(1), columnsAfterCursor)
val charsToMove = columnsAfterCursor - spacesToInsert
screen.blockCopy(cursorCol, cursorRow, charsToMove, 1, cursorCol + spacesToInsert, cursorRow)
blockClear(cursorCol, cursorRow, spacesToInsert)
}
'A'.code -> setCursorRow(maxOf(0, cursorRow - getArg0(1)))
'B'.code -> setCursorRow(minOf(mRows - 1, cursorRow + getArg0(1)))
'C'.code, 'a'.code -> setCursorCol(minOf(mRightMargin - 1, cursorCol + getArg0(1)))
'D'.code -> setCursorCol(maxOf(mLeftMargin, cursorCol - getArg0(1)))
'E'.code -> setCursorPosition(0, cursorRow + getArg0(1))
'F'.code -> setCursorPosition(0, cursorRow - getArg0(1))
'G'.code -> setCursorCol(minOf(maxOf(1, getArg0(1)), mColumns) - 1)
'H'.code, 'f'.code -> setCursorPosition(getArg1(1) - 1, getArg0(1) - 1)
'I'.code -> setCursorCol(nextTabStop(getArg0(1)))
'J'.code -> {
when (getArg0(0)) {
0 -> {
blockClear(cursorCol, cursorRow, mColumns - cursorCol)
blockClear(0, cursorRow + 1, mColumns, mRows - (cursorRow + 1))
}
1 -> {
blockClear(0, 0, mColumns, cursorRow)
blockClear(0, cursorRow, cursorCol + 1)
}
2 -> blockClear(0, 0, mColumns, mRows)
3 -> mMainBuffer.clearTranscript()
else -> {
unknownSequence(b)
return
}
}
mAboutToAutoWrap = false
}
'K'.code -> {
when (getArg0(0)) {
0 -> blockClear(cursorCol, cursorRow, mColumns - cursorCol)
1 -> blockClear(0, cursorRow, cursorCol + 1)
2 -> blockClear(0, cursorRow, mColumns)
else -> {
unknownSequence(b)
return
}
}
mAboutToAutoWrap = false
}
'L'.code -> {
val linesAfterCursor = mBottomMargin - cursorRow
val linesToInsert = minOf(getArg0(1), linesAfterCursor)
val linesToMove = linesAfterCursor - linesToInsert
screen.blockCopy(0, cursorRow, mColumns, linesToMove, 0, cursorRow + linesToInsert)
blockClear(0, cursorRow, mColumns, linesToInsert)
}
'M'.code -> {
mAboutToAutoWrap = false
val linesAfterCursor = mBottomMargin - cursorRow
val linesToDelete = minOf(getArg0(1), linesAfterCursor)
val linesToMove = linesAfterCursor - linesToDelete
screen.blockCopy(0, cursorRow + linesToDelete, mColumns, linesToMove, 0, cursorRow)
blockClear(0, cursorRow + linesToMove, mColumns, linesToDelete)
}
'P'.code -> {
mAboutToAutoWrap = false
val cellsAfterCursor = mColumns - cursorCol
val cellsToDelete = minOf(getArg0(1), cellsAfterCursor)
val cellsToMove = cellsAfterCursor - cellsToDelete
screen.blockCopy(cursorCol + cellsToDelete, cursorRow, cellsToMove, 1, cursorCol, cursorRow)
blockClear(cursorCol + cellsToMove, cursorRow, cellsToDelete)
}
'S'.code -> {
val linesToScroll = getArg0(1)
for (i in 0 until linesToScroll)
scrollDownOneLine()
}
'T'.code -> {
if (mArgIndex == 0) {
val linesToScrollArg = getArg0(1)
val linesBetweenTopAndBottomMargins = mBottomMargin - mTopMargin
val linesToScroll = minOf(linesBetweenTopAndBottomMargins, linesToScrollArg)
screen.blockCopy(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesBetweenTopAndBottomMargins - linesToScroll, mLeftMargin, mTopMargin + linesToScroll)
blockClear(mLeftMargin, mTopMargin, mRightMargin - mLeftMargin, linesToScroll)
} else {
unimplementedSequence(b)
}
}
'X'.code -> {
mAboutToAutoWrap = false
screen.blockSet(cursorCol, cursorRow, minOf(getArg0(1), mColumns - cursorCol), 1, ' '.code, style)
}
'Z'.code -> {
var numberOfTabs = getArg0(1)
var newCol = mLeftMargin
for (i in cursorCol - 1 downTo 0)
if (mTabStop[i]) {
if (--numberOfTabs == 0) {
newCol = maxOf(i, mLeftMargin)
break
}
}
cursorCol = newCol
}
'?'.code -> continueSequence(ESC_CSI_QUESTIONMARK)
'>'.code -> continueSequence(ESC_CSI_BIGGERTHAN)
'<'.code, '='.code -> continueSequence(ESC_CSI_UNSUPPORTED_PARAMETER_BYTE)
'`'.code -> setCursorColRespectingOriginMode(getArg0(1) - 1)
'b'.code -> {
if (mLastEmittedCodePoint == -1) return
val numRepeat = getArg0(1)
for (i in 0 until numRepeat) emitCodePoint(mLastEmittedCodePoint)
}
'c'.code -> { /* Primary device attributes - ignored for read-only */ }
'd'.code -> setCursorRow(minOf(maxOf(1, getArg0(1)), mRows) - 1)
'e'.code -> setCursorPosition(cursorCol, cursorRow + getArg0(1))
'g'.code -> {
when (getArg0(0)) {
0 -> mTabStop[cursorCol] = false
3 -> for (i in 0 until mColumns) mTabStop[i] = false
}
}
'h'.code -> doSetMode(true)
'l'.code -> doSetMode(false)
'm'.code -> selectGraphicRendition()
'n'.code -> { /* Device Status Report - ignored for read-only */ }
'r'.code -> {
mTopMargin = maxOf(0, minOf(getArg0(1) - 1, mRows - 2))
mBottomMargin = maxOf(mTopMargin + 2, minOf(getArg1(mRows), mRows))
setCursorPosition(0, 0)
}
's'.code -> {
if (isDecsetInternalBitSet(DECSET_BIT_LEFTRIGHT_MARGIN_MODE)) {
mLeftMargin = minOf(getArg0(1) - 1, mColumns - 2)
mRightMargin = maxOf(mLeftMargin + 1, minOf(getArg1(mColumns), mColumns))
setCursorPosition(0, 0)
} else {
saveCursor()
}
}
't'.code -> {
when (getArg0(0)) {
22 -> {
titleStack.push(title)
if (titleStack.size > 20) {
titleStack.removeAt(0)
}
}
23 -> if (titleStack.isNotEmpty()) setTitle(titleStack.pop())
}
}
'u'.code -> restoreCursor()
' '.code -> continueSequence(ESC_CSI_ARGS_SPACE)
else -> parseArg(b)
}
}
private fun selectGraphicRendition() {
if (mArgIndex >= mArgs.size) mArgIndex = mArgs.size - 1
var i = 0
while (i <= mArgIndex) {
if ((mArgsSubParamsBitSet and (1 shl i)) != 0) {
i++
continue
}
var code = getArg(i, 0, false)
if (code < 0) {
if (mArgIndex > 0) {
i++
continue
} else {
code = 0
}
}
when {
code == 0 -> {
mForeColor = TextStyle.COLOR_INDEX_FOREGROUND
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND
mEffect = 0
}
code == 1 -> mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_BOLD
code == 2 -> mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_DIM
code == 3 -> mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_ITALIC
code == 4 -> {
if (i + 1 <= mArgIndex && ((mArgsSubParamsBitSet and (1 shl (i + 1))) != 0)) {
i++
if (mArgs[i] == 0) {
mEffect = mEffect and TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE.inv()
} else {
mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE
}
} else {
mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE
}
}
code == 5 -> mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_BLINK
code == 7 -> mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_INVERSE
code == 8 -> mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE
code == 9 -> mEffect = mEffect or TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH
code == 10 -> { /* Exit alt charset (TERM=linux) - ignore */ }
code == 11 -> { /* Enter alt charset (TERM=linux) - ignore */ }
code == 22 -> mEffect = mEffect and (TextStyle.CHARACTER_ATTRIBUTE_BOLD or TextStyle.CHARACTER_ATTRIBUTE_DIM).inv()
code == 23 -> mEffect = mEffect and TextStyle.CHARACTER_ATTRIBUTE_ITALIC.inv()
code == 24 -> mEffect = mEffect and TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE.inv()
code == 25 -> mEffect = mEffect and TextStyle.CHARACTER_ATTRIBUTE_BLINK.inv()
code == 27 -> mEffect = mEffect and TextStyle.CHARACTER_ATTRIBUTE_INVERSE.inv()
code == 28 -> mEffect = mEffect and TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE.inv()
code == 29 -> mEffect = mEffect and TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH.inv()
code in 30..37 -> mForeColor = code - 30
code == 38 || code == 48 || code == 58 -> {
if (i + 2 > mArgIndex) { i++; continue }
val firstArg = mArgs[i + 1]
if (firstArg == 2) {
if (i + 4 > mArgIndex) {
Timber.tag(LOG_TAG).w("Too few CSI${code};2 RGB arguments")
} else {
val red = getArg(i + 2, 0, false)
val green = getArg(i + 3, 0, false)
val blue = getArg(i + 4, 0, false)
if (red < 0 || green < 0 || blue < 0 || red > 255 || green > 255 || blue > 255) {
finishSequenceAndLogError("Invalid RGB: $red,$green,$blue")
} else {
val argbColor = 0xff000000.toInt() or (red shl 16) or (green shl 8) or blue
when (code) {
38 -> mForeColor = argbColor
48 -> mBackColor = argbColor
58 -> mUnderlineColor = argbColor
}
}
i += 4
}
} else if (firstArg == 5) {
val color = getArg(i + 2, 0, false)
i += 2
if (color in 0 until TextStyle.NUM_INDEXED_COLORS) {
when (code) {
38 -> mForeColor = color
48 -> mBackColor = color
58 -> mUnderlineColor = color
}
} else {
if (LOG_ESCAPE_SEQUENCES) Timber.tag(LOG_TAG).w("Invalid color index: $color")
}
} else {
finishSequenceAndLogError("Invalid ISO-8613-3 SGR first argument: $firstArg")
}
}
code == 39 -> mForeColor = TextStyle.COLOR_INDEX_FOREGROUND
code in 40..47 -> mBackColor = code - 40
code == 49 -> mBackColor = TextStyle.COLOR_INDEX_BACKGROUND
code == 59 -> mUnderlineColor = TextStyle.COLOR_INDEX_FOREGROUND
code in 90..97 -> mForeColor = code - 90 + 8
code in 100..107 -> mBackColor = code - 100 + 8
else -> {
if (LOG_ESCAPE_SEQUENCES)
Timber.tag(LOG_TAG).w(String.format("SGR unknown code %d", code))
}
}
i++
}
}
private fun doOsc(b: Int) {
when (b) {
7 -> doOscSetTextParameters("\u0007")
27 -> continueSequence(ESC_OSC_ESC)
else -> collectOSCArgs(b)
}
}
private fun doOscEsc(b: Int) {
when (b) {
'\\'.code -> doOscSetTextParameters("\u001b\\")
else -> {
collectOSCArgs(27)
collectOSCArgs(b)
continueSequence(ESC_OSC)
}
}
}
private fun doOscSetTextParameters(bellOrStringTerminator: String) {
var value = -1
var textParameter = ""
for (idx in 0 until mOSCOrDeviceControlArgs.length) {
val b = mOSCOrDeviceControlArgs[idx]
if (b == ';') {
textParameter = mOSCOrDeviceControlArgs.substring(idx + 1)
break
} else if (b in '0'..'9') {
value = (if (value < 0) 0 else value * 10) + (b - '0')
} else {
unknownSequence(b.code)
return
}
}
when (value) {
0, 1, 2 -> setTitle(textParameter)
4 -> {
var colorIndex = -1
var parsingPairStart = -1
var i = 0
while (true) {
val endOfInput = i == textParameter.length
val ch = if (endOfInput) ';' else textParameter[i]
if (ch == ';') {
if (parsingPairStart < 0) {
parsingPairStart = i + 1
} else {
if (colorIndex < 0 || colorIndex > 255) {
unknownSequence(ch.code)
return
} else {
mColors.tryParseColor(colorIndex, textParameter.substring(parsingPairStart, i))
colorIndex = -1
parsingPairStart = -1
}
}
} else if (parsingPairStart >= 0) {
// Passing through color spec
} else if (parsingPairStart < 0 && ch in '0'..'9') {
colorIndex = (if (colorIndex < 0) 0 else colorIndex * 10) + (ch - '0')
} else {
unknownSequence(ch.code)
return
}
if (endOfInput) break
i++
}
}
10, 11, 12 -> {
var specialIndex = TextStyle.COLOR_INDEX_FOREGROUND + (value - 10)
var lastSemiIndex = 0
var charIndex = 0
while (true) {
val endOfInput = charIndex == textParameter.length
if (endOfInput || textParameter[charIndex] == ';') {
try {
val colorSpec = textParameter.substring(lastSemiIndex, charIndex)
if ("?" != colorSpec) {
mColors.tryParseColor(specialIndex, colorSpec)
}
specialIndex++
charIndex++
if (endOfInput || specialIndex > TextStyle.COLOR_INDEX_CURSOR || charIndex >= textParameter.length)
break
lastSemiIndex = charIndex
} catch (_: NumberFormatException) {
// Ignore
}
}
charIndex++
}
}
52 -> {
val startIndex = textParameter.indexOf(";") + 1
try {
val clipboardText = String(Base64.decode(textParameter.substring(startIndex), 0), Charsets.UTF_8)
onCopyToClipboard?.invoke(clipboardText)
} catch (_: Exception) {
Timber.tag(LOG_TAG).e("OSC Manipulate selection, invalid string '$textParameter'")
}
}
104 -> {
if (textParameter.isEmpty()) {
mColors.reset()
} else {
var lastIndex = 0
var charIndex = 0
while (true) {
val endOfInput = charIndex == textParameter.length
if (endOfInput || textParameter[charIndex] == ';') {
try {
val colorToReset = textParameter.substring(lastIndex, charIndex).toInt()
mColors.reset(colorToReset)
if (endOfInput) break
charIndex++
lastIndex = charIndex
} catch (_: NumberFormatException) {
// Ignore
}
}
charIndex++
}
}
}
110, 111, 112 -> {
mColors.reset(TextStyle.COLOR_INDEX_FOREGROUND + (value - 110))
}
119 -> { /* Reset highlight color - ignore */ }
else -> unknownParameter(value)
}
finishSequence()
}
private fun blockClear(sx: Int, sy: Int, w: Int) {
blockClear(sx, sy, w, 1)
}
private fun blockClear(sx: Int, sy: Int, w: Int, h: Int) {
screen.blockSet(sx, sy, w, h, ' '.code, style)
}
private val style: Long get() = TextStyle.encode(mForeColor, mBackColor, mEffect)
private fun doSetMode(newValue: Boolean) {
val modeBit = getArg0(0)
when (modeBit) {
4 -> mInsertMode = newValue
20 -> unknownParameter(modeBit)
34 -> { /* Normal cursor visibility - ignore */ }
else -> unknownParameter(modeBit)
}
}
private fun setCursorPosition(x: Int, y: Int) {
val originMode = isDecsetInternalBitSet(DECSET_BIT_ORIGIN_MODE)
val effectiveTopMargin = if (originMode) mTopMargin else 0
val effectiveBottomMargin = if (originMode) mBottomMargin else mRows
val effectiveLeftMargin = if (originMode) mLeftMargin else 0
val effectiveRightMargin = if (originMode) mRightMargin else mColumns
val newRow = maxOf(effectiveTopMargin, minOf(effectiveTopMargin + y, effectiveBottomMargin - 1))
val newCol = maxOf(effectiveLeftMargin, minOf(effectiveLeftMargin + x, effectiveRightMargin - 1))
setCursorRowCol(newRow, newCol)
}
private fun scrollDownOneLine() {
scrollCounter++
val currentStyle = style
if (mLeftMargin != 0 || mRightMargin != mColumns) {
screen.blockCopy(mLeftMargin, mTopMargin + 1, mRightMargin - mLeftMargin, mBottomMargin - mTopMargin - 1, mLeftMargin, mTopMargin)
screen.blockSet(mLeftMargin, mBottomMargin - 1, mRightMargin - mLeftMargin, 1, ' '.code, currentStyle)
} else {
screen.scrollDownOneLine(mTopMargin, mBottomMargin, currentStyle)
}
}
private fun parseArg(b: Int) {
if (b >= '0'.code && b <= '9'.code) {
if (mArgIndex < mArgs.size) {
val oldValue = mArgs[mArgIndex]
val thisDigit = b - '0'.code
val value: Int = if (oldValue >= 0) {
oldValue * 10 + thisDigit
} else {
thisDigit
}
mArgs[mArgIndex] = if (value > 9999) 9999 else value
}
continueSequence(mEscapeState)
} else if (b == ';'.code || b == ':'.code) {
if (mArgIndex + 1 < mArgs.size) {
mArgIndex++
if (b == ':'.code) {
mArgsSubParamsBitSet = mArgsSubParamsBitSet or (1 shl mArgIndex)
}
} else {
logError("Too many parameters when in state: $mEscapeState")
}
continueSequence(mEscapeState)
} else {
unknownSequence(b)
}
}
private fun getArg0(defaultValue: Int): Int = getArg(0, defaultValue, true)
private fun getArg1(defaultValue: Int): Int = getArg(1, defaultValue, true)
private fun getArg(index: Int, defaultValue: Int, treatZeroAsDefault: Boolean): Int {
var result = mArgs[index]
if (result < 0 || (result == 0 && treatZeroAsDefault)) {
result = defaultValue
}
return result
}
private fun collectOSCArgs(b: Int) {
if (mOSCOrDeviceControlArgs.length < MAX_OSC_STRING_LENGTH) {
mOSCOrDeviceControlArgs.appendCodePoint(b)
continueSequence(mEscapeState)
} else {
unknownSequence(b)
}
}
private fun unimplementedSequence(b: Int) {
logError("Unimplemented sequence char '${b.toChar()}' (U+${String.format("%04x", b)})")
finishSequence()
}
private fun unknownSequence(b: Int) {
logError("Unknown sequence char '${b.toChar()}' (numeric value=$b)")
finishSequence()
}
private fun unknownParameter(parameter: Int) {
logError("Unknown parameter: $parameter")
finishSequence()
}
private fun logError(errorType: String) {
if (LOG_ESCAPE_SEQUENCES) {
val buf = StringBuilder()
buf.append(errorType)
buf.append(", escapeState=")
buf.append(mEscapeState)
var firstArg = true
if (mArgIndex >= mArgs.size) mArgIndex = mArgs.size - 1
for (i in 0..mArgIndex) {
val value = mArgs[i]
if (value >= 0) {
if (firstArg) {
firstArg = false
buf.append(", args={")
} else {
buf.append(',')
}
buf.append(value)
}
}
if (!firstArg) buf.append('}')
finishSequenceAndLogError(buf.toString())
}
}
private fun finishSequenceAndLogError(error: String) {
if (LOG_ESCAPE_SEQUENCES) Timber.tag(LOG_TAG).w(error)
finishSequence()
}
private fun finishSequence() {
mEscapeState = ESC_NONE
}
private fun emitCodePoint(codePoint: Int) {
var cp = codePoint
mLastEmittedCodePoint = cp
if (if (mUseLineDrawingUsesG0) mUseLineDrawingG0 else mUseLineDrawingG1) {
when (cp) {
'_'.code -> cp = ' '.code
'`'.code -> cp = '\u25C6'.code // Diamond
'0'.code -> cp = '\u2588'.code // Solid block
'a'.code -> cp = '\u2592'.code // Checker board
'b'.code -> cp = '\u2409'.code // Horizontal tab
'c'.code -> cp = '\u240C'.code // Form feed
'd'.code -> cp = '\r'.code // Carriage return
'e'.code -> cp = '\u240A'.code // Linefeed
'f'.code -> cp = '\u00B0'.code // Degree
'g'.code -> cp = '\u00B1'.code // Plus-minus
'h'.code -> cp = '\n'.code // Newline
'i'.code -> cp = '\u240B'.code // Vertical tab
'j'.code -> cp = '\u2518'.code // Lower right corner
'k'.code -> cp = '\u2510'.code // Upper right corner
'l'.code -> cp = '\u250C'.code // Upper left corner
'm'.code -> cp = '\u2514'.code // Lower left corner
'n'.code -> cp = '\u253C'.code // Crossing lines
'o'.code -> cp = '\u23BA'.code // Horizontal line - scan 1
'p'.code -> cp = '\u23BB'.code // Horizontal line - scan 3
'q'.code -> cp = '\u2500'.code // Horizontal line - scan 5
'r'.code -> cp = '\u23BC'.code // Horizontal line - scan 7
's'.code -> cp = '\u23BD'.code // Horizontal line - scan 9
't'.code -> cp = '\u251C'.code // T facing rightwards
'u'.code -> cp = '\u2524'.code // T facing leftwards
'v'.code -> cp = '\u2534'.code // T facing upwards
'w'.code -> cp = '\u252C'.code // T facing downwards
'x'.code -> cp = '\u2502'.code // Vertical line
'y'.code -> cp = '\u2264'.code // Less than or equal to
'z'.code -> cp = '\u2265'.code // Greater than or equal to
'{'.code -> cp = '\u03C0'.code // Pi
'|'.code -> cp = '\u2260'.code // Not equal to
'}'.code -> cp = '\u00A3'.code // UK pound
'~'.code -> cp = '\u00B7'.code // Centered dot
}
}
val autoWrap = isDecsetInternalBitSet(DECSET_BIT_AUTOWRAP)
val displayWidth = WcWidth.width(cp)
val cursorInLastColumn = cursorCol == mRightMargin - 1
if (autoWrap) {
if (cursorInLastColumn && ((mAboutToAutoWrap && displayWidth == 1) || displayWidth == 2)) {
screen.setLineWrap(cursorRow)
cursorCol = mLeftMargin
if (cursorRow + 1 < mBottomMargin) {
cursorRow++
} else {
scrollDownOneLine()
}
}
} else if (cursorInLastColumn && displayWidth == 2) {
return
}
if (mInsertMode && displayWidth > 0) {
val destCol = cursorCol + displayWidth
if (destCol < mRightMargin)
screen.blockCopy(cursorCol, cursorRow, mRightMargin - destCol, 1, destCol, cursorRow)
}
val offsetDueToCombiningChar = if (displayWidth <= 0 && cursorCol > 0 && !mAboutToAutoWrap) 1 else 0
var column = cursorCol - offsetDueToCombiningChar
if (column < 0) column = 0
screen.setChar(column, cursorRow, cp, style)
if (autoWrap && displayWidth > 0)
mAboutToAutoWrap = (cursorCol == mRightMargin - displayWidth)
cursorCol = minOf(cursorCol + displayWidth, mRightMargin - 1)
}
private fun setCursorRow(row: Int) {
cursorRow = row
mAboutToAutoWrap = false
}
private fun setCursorCol(col: Int) {
cursorCol = col
mAboutToAutoWrap = false
}
private fun setCursorColRespectingOriginMode(col: Int) {
setCursorPosition(col, cursorRow)
}
private fun setCursorRowCol(row: Int, col: Int) {
cursorRow = maxOf(0, minOf(row, mRows - 1))
cursorCol = maxOf(0, minOf(col, mColumns - 1))
mAboutToAutoWrap = false
}
fun clearScrollCounter() {
scrollCounter = 0
}
fun toggleAutoScrollDisabled() {
isAutoScrollDisabled = !isAutoScrollDisabled
}
fun reset() {
setCursorStyle()
mArgIndex = 0
mContinueSequence = false
mEscapeState = ESC_NONE
mInsertMode = false
mTopMargin = 0
mLeftMargin = 0
mBottomMargin = mRows
mRightMargin = mColumns
mAboutToAutoWrap = false
mForeColor = TextStyle.COLOR_INDEX_FOREGROUND
mSavedStateMain.mSavedForeColor = TextStyle.COLOR_INDEX_FOREGROUND
mSavedStateAlt.mSavedForeColor = TextStyle.COLOR_INDEX_FOREGROUND
mBackColor = TextStyle.COLOR_INDEX_BACKGROUND
mSavedStateMain.mSavedBackColor = TextStyle.COLOR_INDEX_BACKGROUND
mSavedStateAlt.mSavedBackColor = TextStyle.COLOR_INDEX_BACKGROUND
setDefaultTabStops()
mUseLineDrawingG0 = false
mUseLineDrawingG1 = false
mUseLineDrawingUsesG0 = true
mSavedStateMain.mSavedCursorRow = 0
mSavedStateMain.mSavedCursorCol = 0
mSavedStateMain.mSavedEffect = 0
mSavedStateMain.mSavedDecFlags = 0
mSavedStateAlt.mSavedCursorRow = 0
mSavedStateAlt.mSavedCursorCol = 0
mSavedStateAlt.mSavedEffect = 0
mSavedStateAlt.mSavedDecFlags = 0
mCurrentDecSetFlags = 0
setDecsetinternalBit(DECSET_BIT_AUTOWRAP, true)
setDecsetinternalBit(DECSET_BIT_CURSOR_ENABLED, true)
mSavedDecSetFlags = mCurrentDecSetFlags
mSavedStateMain.mSavedDecFlags = mCurrentDecSetFlags
mSavedStateAlt.mSavedDecFlags = mCurrentDecSetFlags
mUtf8Index = 0
mUtf8ToFollow = 0
mColors.reset()
}
fun getSelectedText(x1: Int, y1: Int, x2: Int, y2: Int): String =
screen.getSelectedText(x1, y1, x2, y2)
private fun setTitle(newTitle: String?) {
title = newTitle
}
internal class SavedScreenState {
var mSavedCursorRow = 0
var mSavedCursorCol = 0
var mSavedEffect = 0
var mSavedForeColor = 0
var mSavedBackColor = 0
var mSavedDecFlags = 0
var mUseLineDrawingG0 = false
var mUseLineDrawingG1 = false
var mUseLineDrawingUsesG0 = true
}
override fun toString(): String =
"TerminalEmulator[size=${screen.columns}x${screen.screenRows}, margins={$mTopMargin,$mRightMargin,$mBottomMargin,$mLeftMargin}]"
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/terminal/TerminalProcess.kt
================================================
package com.topjohnwu.magisk.terminal
import android.os.Handler
import android.os.Looper
import com.topjohnwu.superuser.Shell
import timber.log.Timber
private val busyboxPath: String by lazy {
Shell.cmd("readlink /proc/self/exe").exec().out.firstOrNull()
?: "/data/adb/magisk/busybox"
}
private val mainHandler = Handler(Looper.getMainLooper())
fun TerminalEmulator.appendOnMain(bytes: ByteArray, len: Int) {
mainHandler.post {
append(bytes, len)
onScreenUpdate?.invoke()
}
}
fun TerminalEmulator.appendLineOnMain(line: String) {
val bytes = "$line\r\n".toByteArray(Charsets.UTF_8)
appendOnMain(bytes, bytes.size)
}
/**
* Run a command as root inside a PTY (via busybox script).
* Reads raw bytes from the process and feeds them to the terminal emulator.
* Must be called from a background thread.
* Returns true if the process exits with code 0.
*/
fun runSuCommand(emulator: TerminalEmulator, command: String): Boolean {
return try {
val cols = emulator.mColumns
val rows = emulator.mRows
val wrappedCmd = "export TERM=xterm-256color; stty cols $cols rows $rows 2>/dev/null; $command"
val escapedCmd = wrappedCmd.replace("'", "'\\''")
val process = ProcessBuilder(
"su", "-c",
"$busyboxPath script -q -c '$escapedCmd' /dev/null"
).redirectErrorStream(true).start()
process.outputStream.close()
val buffer = ByteArray(4096)
process.inputStream.use { input ->
while (true) {
val n = input.read(buffer)
if (n == -1) break
emulator.appendOnMain(buffer.copyOf(n), n)
}
}
process.waitFor() == 0
} catch (e: Exception) {
Timber.e(e, "runSuCommand failed")
emulator.appendLineOnMain("! Error: ${e.message}")
false
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/terminal/TerminalRow.kt
================================================
package com.topjohnwu.magisk.terminal
import java.util.Arrays
/**
* A row in a terminal, composed of a fixed number of cells.
*
* The text in the row is stored in a char[] array, [text], for quick access during rendering.
*/
class TerminalRow(private val columns: Int, style: Long) {
/**
* Max combining characters that can exist in a column, that are separate from the base character
* itself. Any additional combining characters will be ignored and not added to the column.
*
* There does not seem to be limit in unicode standard for max number of combination characters
* that can be combined but such characters are primarily under 10.
*
* "Section 3.6 Combination" of unicode standard contains combining characters info.
* - https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf
* - https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
* - https://stackoverflow.com/questions/71237212/what-is-the-maximum-number-of-unicode-combined-characters-that-may-be-needed-to
*
* UAX15-D3 Stream-Safe Text Format limits to max 30 combining characters.
* > The value of 30 is chosen to be significantly beyond what is required for any linguistic or technical usage.
* > While it would have been feasible to chose a smaller number, this value provides a very wide margin,
* > yet is well within the buffer size limits of practical implementations.
* - https://unicode.org/reports/tr15/#Stream_Safe_Text_Format
* - https://stackoverflow.com/a/11983435/14686958
*
* We choose the value 15 because it should be enough for terminal based applications and keep
* the memory usage low for a terminal row, won't affect performance or cause terminal to
* lag or hang, and will keep malicious applications from causing harm. The value can be
* increased if ever needed for legitimate applications.
*/
companion object {
private const val SPARE_CAPACITY_FACTOR = 1.5f
private const val MAX_COMBINING_CHARACTERS_PER_COLUMN = 15
}
/** The text filling this terminal row. */
var text: CharArray = CharArray((SPARE_CAPACITY_FACTOR * columns).toInt())
/** The number of java chars used in [text]. */
private var _spaceUsed: Short = 0
/** If this row has been line wrapped due to text output at the end of line. */
var lineWrap: Boolean = false
/** The style bits of each cell in the row. See [TextStyle]. */
val styles: LongArray = LongArray(columns)
/** If this row might contain chars with width != 1, used for deactivating fast path */
var hasNonOneWidthOrSurrogateChars: Boolean = false
init {
clear(style)
}
/** NOTE: The sourceX2 is exclusive. */
fun copyInterval(line: TerminalRow, sourceX1: Int, sourceX2: Int, destinationX: Int) {
hasNonOneWidthOrSurrogateChars = hasNonOneWidthOrSurrogateChars or line.hasNonOneWidthOrSurrogateChars
val x1 = line.findStartOfColumn(sourceX1)
val x2 = line.findStartOfColumn(sourceX2)
var startingFromSecondHalfOfWideChar = sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1)
val sourceChars = if (this === line) line.text.copyOf() else line.text
var latestNonCombiningWidth = 0
var destX = destinationX
var srcX1 = sourceX1
var i = x1
while (i < x2) {
val sourceChar = sourceChars[i]
var codePoint: Int
if (Character.isHighSurrogate(sourceChar)) {
i++
codePoint = Character.toCodePoint(sourceChar, sourceChars[i])
} else {
codePoint = sourceChar.code
}
if (startingFromSecondHalfOfWideChar) {
codePoint = ' '.code
startingFromSecondHalfOfWideChar = false
}
val w = WcWidth.width(codePoint)
if (w > 0) {
destX += latestNonCombiningWidth
srcX1 += latestNonCombiningWidth
latestNonCombiningWidth = w
}
setChar(destX, codePoint, line.getStyle(srcX1))
i++
}
}
val spaceUsed: Int get() = _spaceUsed.toInt()
/** Note that the column may end of second half of wide character. */
fun findStartOfColumn(column: Int): Int {
if (column == columns) return spaceUsed
var currentColumn = 0
var currentCharIndex = 0
while (true) {
var newCharIndex = currentCharIndex
val c = text[newCharIndex++]
val isHigh = Character.isHighSurrogate(c)
val codePoint = if (isHigh) Character.toCodePoint(c, text[newCharIndex++]) else c.code
val wcwidth = WcWidth.width(codePoint)
if (wcwidth > 0) {
currentColumn += wcwidth
if (currentColumn == column) {
while (newCharIndex < _spaceUsed) {
if (Character.isHighSurrogate(text[newCharIndex])) {
if (WcWidth.width(Character.toCodePoint(text[newCharIndex], text[newCharIndex + 1])) <= 0) {
newCharIndex += 2
} else {
break
}
} else if (WcWidth.width(text[newCharIndex].code) <= 0) {
newCharIndex++
} else {
break
}
}
return newCharIndex
} else if (currentColumn > column) {
return currentCharIndex
}
}
currentCharIndex = newCharIndex
}
}
private fun wideDisplayCharacterStartingAt(column: Int): Boolean {
var currentCharIndex = 0
var currentColumn = 0
while (currentCharIndex < _spaceUsed) {
val c = text[currentCharIndex++]
val codePoint = if (Character.isHighSurrogate(c)) Character.toCodePoint(c, text[currentCharIndex++]) else c.code
val wcwidth = WcWidth.width(codePoint)
if (wcwidth > 0) {
if (currentColumn == column && wcwidth == 2) return true
currentColumn += wcwidth
if (currentColumn > column) return false
}
}
return false
}
fun clear(style: Long) {
Arrays.fill(text, ' ')
Arrays.fill(styles, style)
_spaceUsed = columns.toShort()
hasNonOneWidthOrSurrogateChars = false
}
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
fun setChar(columnToSet: Int, codePoint: Int, style: Long) {
if (columnToSet < 0 || columnToSet >= styles.size)
throw IllegalArgumentException("TerminalRow.setChar(): columnToSet=$columnToSet, codePoint=$codePoint, style=$style")
styles[columnToSet] = style
val newCodePointDisplayWidth = WcWidth.width(codePoint)
// Fast path when we don't have any chars with width != 1
if (!hasNonOneWidthOrSurrogateChars) {
if (codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT || newCodePointDisplayWidth != 1) {
hasNonOneWidthOrSurrogateChars = true
} else {
text[columnToSet] = codePoint.toChar()
return
}
}
val newIsCombining = newCodePointDisplayWidth <= 0
val wasExtraColForWideChar = columnToSet > 0 && wideDisplayCharacterStartingAt(columnToSet - 1)
var col = columnToSet
if (newIsCombining) {
if (wasExtraColForWideChar) col--
} else {
if (wasExtraColForWideChar) setChar(col - 1, ' '.code, style)
val overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(col + 1)
if (overwritingWideCharInNextColumn) setChar(col + 1, ' '.code, style)
}
var textArray = text
val oldStartOfColumnIndex = findStartOfColumn(col)
val oldCodePointDisplayWidth = WcWidth.width(textArray, oldStartOfColumnIndex)
val oldCharactersUsedForColumn: Int
if (col + oldCodePointDisplayWidth < columns) {
val oldEndOfColumnIndex = findStartOfColumn(col + oldCodePointDisplayWidth)
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex
} else {
oldCharactersUsedForColumn = _spaceUsed - oldStartOfColumnIndex
}
if (newIsCombining) {
val combiningCharsCount = WcWidth.zeroWidthCharsCount(textArray, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn)
if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
return
}
var newCharactersUsedForColumn = Character.charCount(codePoint)
if (newIsCombining) {
newCharactersUsedForColumn += oldCharactersUsedForColumn
}
val oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn
val newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn
val javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn
if (javaCharDifference > 0) {
val oldCharactersAfterColumn = _spaceUsed - oldNextColumnIndex
if (_spaceUsed + javaCharDifference > textArray.size) {
val newText = CharArray(textArray.size + columns)
System.arraycopy(textArray, 0, newText, 0, oldNextColumnIndex)
System.arraycopy(textArray, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn)
text = newText
textArray = newText
} else {
System.arraycopy(textArray, oldNextColumnIndex, textArray, newNextColumnIndex, oldCharactersAfterColumn)
}
} else if (javaCharDifference < 0) {
System.arraycopy(textArray, oldNextColumnIndex, textArray, newNextColumnIndex, _spaceUsed - oldNextColumnIndex)
}
_spaceUsed = (_spaceUsed + javaCharDifference).toShort()
Character.toChars(codePoint, textArray, oldStartOfColumnIndex + if (newIsCombining) oldCharactersUsedForColumn else 0)
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
if (_spaceUsed + 1 > textArray.size) {
val newText = CharArray(textArray.size + columns)
System.arraycopy(textArray, 0, newText, 0, newNextColumnIndex)
System.arraycopy(textArray, newNextColumnIndex, newText, newNextColumnIndex + 1, _spaceUsed - newNextColumnIndex)
text = newText
textArray = newText
} else {
System.arraycopy(textArray, newNextColumnIndex, textArray, newNextColumnIndex + 1, _spaceUsed - newNextColumnIndex)
}
textArray[newNextColumnIndex] = ' '
++_spaceUsed
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
if (col == columns - 1) {
throw IllegalArgumentException("Cannot put wide character in last column")
} else if (col == columns - 2) {
_spaceUsed = newNextColumnIndex.toShort()
} else {
val newNextNextColumnIndex = newNextColumnIndex + if (Character.isHighSurrogate(textArray[newNextColumnIndex])) 2 else 1
val nextLen = newNextNextColumnIndex - newNextColumnIndex
System.arraycopy(textArray, newNextNextColumnIndex, textArray, newNextColumnIndex, _spaceUsed - newNextNextColumnIndex)
_spaceUsed = (_spaceUsed - nextLen).toShort()
}
}
}
internal fun isBlank(): Boolean {
for (charIndex in 0 until spaceUsed)
if (text[charIndex] != ' ') return false
return true
}
fun getStyle(column: Int): Long = styles[column]
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/terminal/TerminalStyle.kt
================================================
package com.topjohnwu.magisk.terminal
import android.graphics.Color
import java.util.Properties
import kotlin.math.floor
import kotlin.math.pow
import kotlin.math.sqrt
object TextStyle {
const val CHARACTER_ATTRIBUTE_BOLD = 1
const val CHARACTER_ATTRIBUTE_ITALIC = 1 shl 1
const val CHARACTER_ATTRIBUTE_UNDERLINE = 1 shl 2
const val CHARACTER_ATTRIBUTE_BLINK = 1 shl 3
const val CHARACTER_ATTRIBUTE_INVERSE = 1 shl 4
const val CHARACTER_ATTRIBUTE_INVISIBLE = 1 shl 5
const val CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 shl 6
const val CHARACTER_ATTRIBUTE_PROTECTED = 1 shl 7
const val CHARACTER_ATTRIBUTE_DIM = 1 shl 8
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 shl 9
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND = 1 shl 10
const val COLOR_INDEX_FOREGROUND = 256
const val COLOR_INDEX_BACKGROUND = 257
const val COLOR_INDEX_CURSOR = 258
const val NUM_INDEXED_COLORS = 259
val NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0)
fun encode(foreColor: Int, backColor: Int, effect: Int): Long {
var result = (effect and 0b111111111).toLong()
if (foreColor and 0xff000000.toInt() == 0xff000000.toInt()) {
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong() or ((foreColor.toLong() and 0x00ffffffL) shl 40)
} else {
result = result or ((foreColor.toLong() and 0b111111111L) shl 40)
}
if (backColor and 0xff000000.toInt() == 0xff000000.toInt()) {
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong() or ((backColor.toLong() and 0x00ffffffL) shl 16)
} else {
result = result or ((backColor.toLong() and 0b111111111L) shl 16)
}
return result
}
fun decodeForeColor(style: Long): Int {
return if (style and CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong() == 0L) {
((style ushr 40) and 0b111111111L).toInt()
} else {
0xff000000.toInt() or ((style ushr 40) and 0x00ffffffL).toInt()
}
}
fun decodeBackColor(style: Long): Int {
return if (style and CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong() == 0L) {
((style ushr 16) and 0b111111111L).toInt()
} else {
0xff000000.toInt() or ((style ushr 16) and 0x00ffffffL).toInt()
}
}
fun decodeEffect(style: Long): Int {
return (style and 0b11111111111L).toInt()
}
}
/**
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
* Operating System Control (OSC) sequences.
*/
class TerminalColorScheme {
val defaultColors: IntArray = IntArray(TextStyle.NUM_INDEXED_COLORS)
init {
reset()
}
fun updateWith(props: Properties) {
reset()
var cursorPropExists = false
for ((keyObj, valueObj) in props) {
val key = keyObj as String
val value = valueObj as String
val colorIndex: Int = when {
key == "foreground" -> TextStyle.COLOR_INDEX_FOREGROUND
key == "background" -> TextStyle.COLOR_INDEX_BACKGROUND
key == "cursor" -> {
cursorPropExists = true
TextStyle.COLOR_INDEX_CURSOR
}
key.startsWith("color") -> {
try {
key.substring(5).toInt()
} catch (_: NumberFormatException) {
throw IllegalArgumentException("Invalid property: '$key'")
}
}
else -> throw IllegalArgumentException("Invalid property: '$key'")
}
val colorValue = TerminalColors.parse(value)
if (colorValue == 0) {
throw IllegalArgumentException("Property '$key' has invalid color: '$value'")
}
defaultColors[colorIndex] = colorValue
}
if (!cursorPropExists) {
setCursorColorForBackground()
}
}
fun setCursorColorForBackground() {
val backgroundColor = defaultColors[TextStyle.COLOR_INDEX_BACKGROUND]
val brightness = TerminalColors.perceivedBrightness(backgroundColor)
if (brightness > 0) {
defaultColors[TextStyle.COLOR_INDEX_CURSOR] = if (brightness < 130) {
0xffffffff.toInt()
} else {
0xff000000.toInt()
}
}
}
private fun reset() {
System.arraycopy(DEFAULT_COLORSCHEME, 0, defaultColors, 0, TextStyle.NUM_INDEXED_COLORS)
}
companion object {
private val DEFAULT_COLORSCHEME = longArrayOf(
// 16 original colors. First 8 are dim.
0xff000000, // black
0xffcd0000, // dim red
0xff00cd00, // dim green
0xffcdcd00, // dim yellow
0xff6495ed, // dim blue
0xffcd00cd, // dim magenta
0xff00cdcd, // dim cyan
0xffe5e5e5, // dim white
// Second 8 are bright:
0xff7f7f7f, // medium grey
0xffff0000, // bright red
0xff00ff00, // bright green
0xffffff00, // bright yellow
0xff5c5cff, // light blue
0xffff00ff, // bright magenta
0xff00ffff, // bright cyan
0xffffffffL, // bright white
// 216 color cube, six shades of each color:
0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffffL,
// 24 grey scale ramp:
0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
0xffffffffL, 0xff000000L, 0xffffffffL
).map { it.toInt() }.toIntArray()
}
}
/** Current terminal colors (if different from default). */
class TerminalColors {
val currentColors: IntArray = IntArray(TextStyle.NUM_INDEXED_COLORS)
init {
reset()
}
fun reset(index: Int) {
currentColors[index] = COLOR_SCHEME.defaultColors[index]
}
fun reset() {
System.arraycopy(COLOR_SCHEME.defaultColors, 0, currentColors, 0, TextStyle.NUM_INDEXED_COLORS)
}
fun tryParseColor(intoIndex: Int, textParameter: String) {
val c = parse(textParameter)
if (c != 0) currentColors[intoIndex] = c
}
companion object {
val COLOR_SCHEME = TerminalColorScheme()
internal fun parse(c: String): Int {
return try {
val (skipInitial, skipBetween) = when {
c[0] == '#' -> 1 to 0
c.startsWith("rgb:") -> 4 to 1
else -> return 0
}
val charsForColors = c.length - skipInitial - 2 * skipBetween
if (charsForColors % 3 != 0) return 0
val componentLength = charsForColors / 3
val mult = 255.0 / (2.0.pow(componentLength * 4) - 1)
var currentPosition = skipInitial
val rString = c.substring(currentPosition, currentPosition + componentLength)
currentPosition += componentLength + skipBetween
val gString = c.substring(currentPosition, currentPosition + componentLength)
currentPosition += componentLength + skipBetween
val bString = c.substring(currentPosition, currentPosition + componentLength)
val r = (rString.toInt(16) * mult).toInt()
val g = (gString.toInt(16) * mult).toInt()
val b = (bString.toInt(16) * mult).toInt()
(0xFF shl 24) or (r shl 16) or (g shl 8) or b
} catch (_: NumberFormatException) {
0
} catch (_: IndexOutOfBoundsException) {
0
}
}
fun perceivedBrightness(color: Int): Int {
return floor(
sqrt(
Color.red(color).toDouble().pow(2) * 0.241 +
Color.green(color).toDouble().pow(2) * 0.691 +
Color.blue(color).toDouble().pow(2) * 0.068
)
).toInt()
}
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/terminal/WcWidth.kt
================================================
package com.topjohnwu.magisk.terminal
/**
* Implementation of wcwidth(3) for Unicode 15.
*
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
*
* IMPORTANT:
* Must be kept in sync with the following:
* https://github.com/termux/wcwidth
* https://github.com/termux/libandroid-support
* https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
*/
object WcWidth {
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
// from https://github.com/jquast/wcwidth/pull/64
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
private val ZERO_WIDTH = arrayOf(
intArrayOf(0x00300, 0x0036f), // Combining Grave Accent ..Combining Latin Small Le
intArrayOf(0x00483, 0x00489), // Combining Cyrillic Titlo..Combining Cyrillic Milli
intArrayOf(0x00591, 0x005bd), // Hebrew Accent Etnahta ..Hebrew Point Meteg
intArrayOf(0x005bf, 0x005bf), // Hebrew Point Rafe ..Hebrew Point Rafe
intArrayOf(0x005c1, 0x005c2), // Hebrew Point Shin Dot ..Hebrew Point Sin Dot
intArrayOf(0x005c4, 0x005c5), // Hebrew Mark Upper Dot ..Hebrew Mark Lower Dot
intArrayOf(0x005c7, 0x005c7), // Hebrew Point Qamats Qata..Hebrew Point Qamats Qata
intArrayOf(0x00610, 0x0061a), // Arabic Sign Sallallahou ..Arabic Small Kasra
intArrayOf(0x0064b, 0x0065f), // Arabic Fathatan ..Arabic Wavy Hamza Below
intArrayOf(0x00670, 0x00670), // Arabic Letter Superscrip..Arabic Letter Superscrip
intArrayOf(0x006d6, 0x006dc), // Arabic Small High Ligatu..Arabic Small High Seen
intArrayOf(0x006df, 0x006e4), // Arabic Small High Rounde..Arabic Small High Madda
intArrayOf(0x006e7, 0x006e8), // Arabic Small High Yeh ..Arabic Small High Noon
intArrayOf(0x006ea, 0x006ed), // Arabic Empty Centre Low ..Arabic Small Low Meem
intArrayOf(0x00711, 0x00711), // Syriac Letter Superscrip..Syriac Letter Superscrip
intArrayOf(0x00730, 0x0074a), // Syriac Pthaha Above ..Syriac Barrekh
intArrayOf(0x007a6, 0x007b0), // Thaana Abafili ..Thaana Sukun
intArrayOf(0x007eb, 0x007f3), // Nko Combining Short High..Nko Combining Double Dot
intArrayOf(0x007fd, 0x007fd), // Nko Dantayalan ..Nko Dantayalan
intArrayOf(0x00816, 0x00819), // Samaritan Mark In ..Samaritan Mark Dagesh
intArrayOf(0x0081b, 0x00823), // Samaritan Mark Epentheti..Samaritan Vowel Sign A
intArrayOf(0x00825, 0x00827), // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
intArrayOf(0x00829, 0x0082d), // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
intArrayOf(0x00859, 0x0085b), // Mandaic Affrication Mark..Mandaic Gemination Mark
intArrayOf(0x00898, 0x0089f), // Arabic Small High Word A..Arabic Half Madda Over M
intArrayOf(0x008ca, 0x008e1), // Arabic Small High Farsi ..Arabic Small High Sign S
intArrayOf(0x008e3, 0x00902), // Arabic Turned Damma Belo..Devanagari Sign Anusvara
intArrayOf(0x0093a, 0x0093a), // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
intArrayOf(0x0093c, 0x0093c), // Devanagari Sign Nukta ..Devanagari Sign Nukta
intArrayOf(0x00941, 0x00948), // Devanagari Vowel Sign U ..Devanagari Vowel Sign Ai
intArrayOf(0x0094d, 0x0094d), // Devanagari Sign Virama ..Devanagari Sign Virama
intArrayOf(0x00951, 0x00957), // Devanagari Stress Sign U..Devanagari Vowel Sign Uu
intArrayOf(0x00962, 0x00963), // Devanagari Vowel Sign Vo..Devanagari Vowel Sign Vo
intArrayOf(0x00981, 0x00981), // Bengali Sign Candrabindu..Bengali Sign Candrabindu
intArrayOf(0x009bc, 0x009bc), // Bengali Sign Nukta ..Bengali Sign Nukta
intArrayOf(0x009c1, 0x009c4), // Bengali Vowel Sign U ..Bengali Vowel Sign Vocal
intArrayOf(0x009cd, 0x009cd), // Bengali Sign Virama ..Bengali Sign Virama
intArrayOf(0x009e2, 0x009e3), // Bengali Vowel Sign Vocal..Bengali Vowel Sign Vocal
intArrayOf(0x009fe, 0x009fe), // Bengali Sandhi Mark ..Bengali Sandhi Mark
intArrayOf(0x00a01, 0x00a02), // Gurmukhi Sign Adak Bindi..Gurmukhi Sign Bindi
intArrayOf(0x00a3c, 0x00a3c), // Gurmukhi Sign Nukta ..Gurmukhi Sign Nukta
intArrayOf(0x00a41, 0x00a42), // Gurmukhi Vowel Sign U ..Gurmukhi Vowel Sign Uu
intArrayOf(0x00a47, 0x00a48), // Gurmukhi Vowel Sign Ee ..Gurmukhi Vowel Sign Ai
intArrayOf(0x00a4b, 0x00a4d), // Gurmukhi Vowel Sign Oo ..Gurmukhi Sign Virama
intArrayOf(0x00a51, 0x00a51), // Gurmukhi Sign Udaat ..Gurmukhi Sign Udaat
intArrayOf(0x00a70, 0x00a71), // Gurmukhi Tippi ..Gurmukhi Addak
intArrayOf(0x00a75, 0x00a75), // Gurmukhi Sign Yakash ..Gurmukhi Sign Yakash
intArrayOf(0x00a81, 0x00a82), // Gujarati Sign Candrabind..Gujarati Sign Anusvara
intArrayOf(0x00abc, 0x00abc), // Gujarati Sign Nukta ..Gujarati Sign Nukta
intArrayOf(0x00ac1, 0x00ac5), // Gujarati Vowel Sign U ..Gujarati Vowel Sign Cand
intArrayOf(0x00ac7, 0x00ac8), // Gujarati Vowel Sign E ..Gujarati Vowel Sign Ai
intArrayOf(0x00acd, 0x00acd), // Gujarati Sign Virama ..Gujarati Sign Virama
intArrayOf(0x00ae2, 0x00ae3), // Gujarati Vowel Sign Voca..Gujarati Vowel Sign Voca
intArrayOf(0x00afa, 0x00aff), // Gujarati Sign Sukun ..Gujarati Sign Two-circle
intArrayOf(0x00b01, 0x00b01), // Oriya Sign Candrabindu ..Oriya Sign Candrabindu
intArrayOf(0x00b3c, 0x00b3c), // Oriya Sign Nukta ..Oriya Sign Nukta
intArrayOf(0x00b3f, 0x00b3f), // Oriya Vowel Sign I ..Oriya Vowel Sign I
intArrayOf(0x00b41, 0x00b44), // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
intArrayOf(0x00b4d, 0x00b4d), // Oriya Sign Virama ..Oriya Sign Virama
intArrayOf(0x00b55, 0x00b56), // Oriya Sign Overline ..Oriya Ai Length Mark
intArrayOf(0x00b62, 0x00b63), // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
intArrayOf(0x00b82, 0x00b82), // Tamil Sign Anusvara ..Tamil Sign Anusvara
intArrayOf(0x00bc0, 0x00bc0), // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
intArrayOf(0x00bcd, 0x00bcd), // Tamil Sign Virama ..Tamil Sign Virama
intArrayOf(0x00c00, 0x00c00), // Telugu Sign Combining Ca..Telugu Sign Combining Ca
intArrayOf(0x00c04, 0x00c04), // Telugu Sign Combining An..Telugu Sign Combining An
intArrayOf(0x00c3c, 0x00c3c), // Telugu Sign Nukta ..Telugu Sign Nukta
intArrayOf(0x00c3e, 0x00c40), // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
intArrayOf(0x00c46, 0x00c48), // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
intArrayOf(0x00c4a, 0x00c4d), // Telugu Vowel Sign O ..Telugu Sign Virama
intArrayOf(0x00c55, 0x00c56), // Telugu Length Mark ..Telugu Ai Length Mark
intArrayOf(0x00c62, 0x00c63), // Telugu Vowel Sign Vocali..Telugu Vowel Sign Vocali
intArrayOf(0x00c81, 0x00c81), // Kannada Sign Candrabindu..Kannada Sign Candrabindu
intArrayOf(0x00cbc, 0x00cbc), // Kannada Sign Nukta ..Kannada Sign Nukta
intArrayOf(0x00cbf, 0x00cbf), // Kannada Vowel Sign I ..Kannada Vowel Sign I
intArrayOf(0x00cc6, 0x00cc6), // Kannada Vowel Sign E ..Kannada Vowel Sign E
intArrayOf(0x00ccc, 0x00ccd), // Kannada Vowel Sign Au ..Kannada Sign Virama
intArrayOf(0x00ce2, 0x00ce3), // Kannada Vowel Sign Vocal..Kannada Vowel Sign Vocal
intArrayOf(0x00d00, 0x00d01), // Malayalam Sign Combining..Malayalam Sign Candrabin
intArrayOf(0x00d3b, 0x00d3c), // Malayalam Sign Vertical ..Malayalam Sign Circular
intArrayOf(0x00d41, 0x00d44), // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
intArrayOf(0x00d4d, 0x00d4d), // Malayalam Sign Virama ..Malayalam Sign Virama
intArrayOf(0x00d62, 0x00d63), // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
intArrayOf(0x00d81, 0x00d81), // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
intArrayOf(0x00dca, 0x00dca), // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
intArrayOf(0x00dd2, 0x00dd4), // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
intArrayOf(0x00dd6, 0x00dd6), // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
intArrayOf(0x00e31, 0x00e31), // Thai Character Mai Han-a..Thai Character Mai Han-a
intArrayOf(0x00e34, 0x00e3a), // Thai Character Sara I ..Thai Character Phinthu
intArrayOf(0x00e47, 0x00e4e), // Thai Character Maitaikhu..Thai Character Yamakkan
intArrayOf(0x00eb1, 0x00eb1), // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
intArrayOf(0x00eb4, 0x00ebc), // Lao Vowel Sign I ..Lao Semivowel Sign Lo
intArrayOf(0x00ec8, 0x00ece), // Lao Tone Mai Ek ..(nil)
intArrayOf(0x00f18, 0x00f19), // Tibetan Astrological Sig..Tibetan Astrological Sig
intArrayOf(0x00f35, 0x00f35), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
intArrayOf(0x00f37, 0x00f37), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
intArrayOf(0x00f39, 0x00f39), // Tibetan Mark Tsa -phru ..Tibetan Mark Tsa -phru
intArrayOf(0x00f71, 0x00f7e), // Tibetan Vowel Sign Aa ..Tibetan Sign Rjes Su Nga
intArrayOf(0x00f80, 0x00f84), // Tibetan Vowel Sign Rever..Tibetan Mark Halanta
intArrayOf(0x00f86, 0x00f87), // Tibetan Sign Lci Rtags ..Tibetan Sign Yang Rtags
intArrayOf(0x00f8d, 0x00f97), // Tibetan Subjoined Sign L..Tibetan Subjoined Letter
intArrayOf(0x00f99, 0x00fbc), // Tibetan Subjoined Letter..Tibetan Subjoined Letter
intArrayOf(0x00fc6, 0x00fc6), // Tibetan Symbol Padma Gda..Tibetan Symbol Padma Gda
intArrayOf(0x0102d, 0x01030), // Myanmar Vowel Sign I ..Myanmar Vowel Sign Uu
intArrayOf(0x01032, 0x01037), // Myanmar Vowel Sign Ai ..Myanmar Sign Dot Below
intArrayOf(0x01039, 0x0103a), // Myanmar Sign Virama ..Myanmar Sign Asat
intArrayOf(0x0103d, 0x0103e), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
intArrayOf(0x01058, 0x01059), // Myanmar Vowel Sign Vocal..Myanmar Vowel Sign Vocal
intArrayOf(0x0105e, 0x01060), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
intArrayOf(0x01071, 0x01074), // Myanmar Vowel Sign Geba ..Myanmar Vowel Sign Kayah
intArrayOf(0x01082, 0x01082), // Myanmar Consonant Sign S..Myanmar Consonant Sign S
intArrayOf(0x01085, 0x01086), // Myanmar Vowel Sign Shan ..Myanmar Vowel Sign Shan
intArrayOf(0x0108d, 0x0108d), // Myanmar Sign Shan Counci..Myanmar Sign Shan Counci
intArrayOf(0x0109d, 0x0109d), // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
intArrayOf(0x0135d, 0x0135f), // Ethiopic Combining Gemin..Ethiopic Combining Gemin
intArrayOf(0x01712, 0x01714), // Tagalog Vowel Sign I ..Tagalog Sign Virama
intArrayOf(0x01732, 0x01733), // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
intArrayOf(0x01752, 0x01753), // Buhid Vowel Sign I ..Buhid Vowel Sign U
intArrayOf(0x01772, 0x01773), // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
intArrayOf(0x017b4, 0x017b5), // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
intArrayOf(0x017b7, 0x017bd), // Khmer Vowel Sign I ..Khmer Vowel Sign Ua
intArrayOf(0x017c6, 0x017c6), // Khmer Sign Nikahit ..Khmer Sign Nikahit
intArrayOf(0x017c9, 0x017d3), // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
intArrayOf(0x017dd, 0x017dd), // Khmer Sign Atthacan ..Khmer Sign Atthacan
intArrayOf(0x0180b, 0x0180d), // Mongolian Free Variation..Mongolian Free Variation
intArrayOf(0x0180f, 0x0180f), // Mongolian Free Variation..Mongolian Free Variation
intArrayOf(0x01885, 0x01886), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
intArrayOf(0x018a9, 0x018a9), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
intArrayOf(0x01920, 0x01922), // Limbu Vowel Sign A ..Limbu Vowel Sign U
intArrayOf(0x01927, 0x01928), // Limbu Vowel Sign E ..Limbu Vowel Sign O
intArrayOf(0x01932, 0x01932), // Limbu Small Letter Anusv..Limbu Small Letter Anusv
intArrayOf(0x01939, 0x0193b), // Limbu Sign Mukphreng ..Limbu Sign Sa-i
intArrayOf(0x01a17, 0x01a18), // Buginese Vowel Sign I ..Buginese Vowel Sign U
intArrayOf(0x01a1b, 0x01a1b), // Buginese Vowel Sign Ae ..Buginese Vowel Sign Ae
intArrayOf(0x01a56, 0x01a56), // Tai Tham Consonant Sign ..Tai Tham Consonant Sign
intArrayOf(0x01a58, 0x01a5e), // Tai Tham Sign Mai Kang L..Tai Tham Consonant Sign
intArrayOf(0x01a60, 0x01a60), // Tai Tham Sign Sakot ..Tai Tham Sign Sakot
intArrayOf(0x01a62, 0x01a62), // Tai Tham Vowel Sign Mai ..Tai Tham Vowel Sign Mai
intArrayOf(0x01a65, 0x01a6c), // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
intArrayOf(0x01a73, 0x01a7c), // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
intArrayOf(0x01a7f, 0x01a7f), // Tai Tham Combining Crypt..Tai Tham Combining Crypt
intArrayOf(0x01ab0, 0x01ace), // Combining Doubled Circum..Combining Latin Small Le
intArrayOf(0x01b00, 0x01b03), // Balinese Sign Ulu Ricem ..Balinese Sign Surang
intArrayOf(0x01b34, 0x01b34), // Balinese Sign Rerekan ..Balinese Sign Rerekan
intArrayOf(0x01b36, 0x01b3a), // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
intArrayOf(0x01b3c, 0x01b3c), // Balinese Vowel Sign La L..Balinese Vowel Sign La L
intArrayOf(0x01b42, 0x01b42), // Balinese Vowel Sign Pepe..Balinese Vowel Sign Pepe
intArrayOf(0x01b6b, 0x01b73), // Balinese Musical Symbol ..Balinese Musical Symbol
intArrayOf(0x01b80, 0x01b81), // Sundanese Sign Panyecek ..Sundanese Sign Panglayar
intArrayOf(0x01ba2, 0x01ba5), // Sundanese Consonant Sign..Sundanese Vowel Sign Pan
intArrayOf(0x01ba8, 0x01ba9), // Sundanese Vowel Sign Pam..Sundanese Vowel Sign Pan
intArrayOf(0x01bab, 0x01bad), // Sundanese Sign Virama ..Sundanese Consonant Sign
intArrayOf(0x01be6, 0x01be6), // Batak Sign Tompi ..Batak Sign Tompi
intArrayOf(0x01be8, 0x01be9), // Batak Vowel Sign Pakpak ..Batak Vowel Sign Ee
intArrayOf(0x01bed, 0x01bed), // Batak Vowel Sign Karo O ..Batak Vowel Sign Karo O
intArrayOf(0x01bef, 0x01bf1), // Batak Vowel Sign U For S..Batak Consonant Sign H
intArrayOf(0x01c2c, 0x01c33), // Lepcha Vowel Sign E ..Lepcha Consonant Sign T
intArrayOf(0x01c36, 0x01c37), // Lepcha Sign Ran ..Lepcha Sign Nukta
intArrayOf(0x01cd0, 0x01cd2), // Vedic Tone Karshana ..Vedic Tone Prenkha
intArrayOf(0x01cd4, 0x01ce0), // Vedic Sign Yajurvedic Mi..Vedic Tone Rigvedic Kash
intArrayOf(0x01ce2, 0x01ce8), // Vedic Sign Visarga Svari..Vedic Sign Visarga Anuda
intArrayOf(0x01ced, 0x01ced), // Vedic Sign Tiryak ..Vedic Sign Tiryak
intArrayOf(0x01cf4, 0x01cf4), // Vedic Tone Candra Above ..Vedic Tone Candra Above
intArrayOf(0x01cf8, 0x01cf9), // Vedic Tone Ring Above ..Vedic Tone Double Ring A
intArrayOf(0x01dc0, 0x01dff), // Combining Dotted Grave A..Combining Right Arrowhea
intArrayOf(0x020d0, 0x020f0), // Combining Left Harpoon A..Combining Asterisk Above
intArrayOf(0x02cef, 0x02cf1), // Coptic Combining Ni Abov..Coptic Combining Spiritu
intArrayOf(0x02d7f, 0x02d7f), // Tifinagh Consonant Joine..Tifinagh Consonant Joine
intArrayOf(0x02de0, 0x02dff), // Combining Cyrillic Lette..Combining Cyrillic Lette
intArrayOf(0x0302a, 0x0302d), // Ideographic Level Tone M..Ideographic Entering Ton
intArrayOf(0x03099, 0x0309a), // Combining Katakana-hirag..Combining Katakana-hirag
intArrayOf(0x0a66f, 0x0a672), // Combining Cyrillic Vzmet..Combining Cyrillic Thous
intArrayOf(0x0a674, 0x0a67d), // Combining Cyrillic Lette..Combining Cyrillic Payer
intArrayOf(0x0a69e, 0x0a69f), // Combining Cyrillic Lette..Combining Cyrillic Lette
intArrayOf(0x0a6f0, 0x0a6f1), // Bamum Combining Mark Koq..Bamum Combining Mark Tuk
intArrayOf(0x0a802, 0x0a802), // Syloti Nagri Sign Dvisva..Syloti Nagri Sign Dvisva
intArrayOf(0x0a806, 0x0a806), // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
intArrayOf(0x0a80b, 0x0a80b), // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
intArrayOf(0x0a825, 0x0a826), // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
intArrayOf(0x0a82c, 0x0a82c), // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
intArrayOf(0x0a8c4, 0x0a8c5), // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
intArrayOf(0x0a8e0, 0x0a8f1), // Combining Devanagari Dig..Combining Devanagari Sig
intArrayOf(0x0a8ff, 0x0a8ff), // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
intArrayOf(0x0a926, 0x0a92d), // Kayah Li Vowel Ue ..Kayah Li Tone Calya Plop
intArrayOf(0x0a947, 0x0a951), // Rejang Vowel Sign I ..Rejang Consonant Sign R
intArrayOf(0x0a980, 0x0a982), // Javanese Sign Panyangga ..Javanese Sign Layar
intArrayOf(0x0a9b3, 0x0a9b3), // Javanese Sign Cecak Telu..Javanese Sign Cecak Telu
intArrayOf(0x0a9b6, 0x0a9b9), // Javanese Vowel Sign Wulu..Javanese Vowel Sign Suku
intArrayOf(0x0a9bc, 0x0a9bd), // Javanese Vowel Sign Pepe..Javanese Consonant Sign
intArrayOf(0x0a9e5, 0x0a9e5), // Myanmar Sign Shan Saw ..Myanmar Sign Shan Saw
intArrayOf(0x0aa29, 0x0aa2e), // Cham Vowel Sign Aa ..Cham Vowel Sign Oe
intArrayOf(0x0aa31, 0x0aa32), // Cham Vowel Sign Au ..Cham Vowel Sign Ue
intArrayOf(0x0aa35, 0x0aa36), // Cham Consonant Sign La ..Cham Consonant Sign Wa
intArrayOf(0x0aa43, 0x0aa43), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
intArrayOf(0x0aa4c, 0x0aa4c), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
intArrayOf(0x0aa7c, 0x0aa7c), // Myanmar Sign Tai Laing T..Myanmar Sign Tai Laing T
intArrayOf(0x0aab0, 0x0aab0), // Tai Viet Mai Kang ..Tai Viet Mai Kang
intArrayOf(0x0aab2, 0x0aab4), // Tai Viet Vowel I ..Tai Viet Vowel U
intArrayOf(0x0aab7, 0x0aab8), // Tai Viet Mai Khit ..Tai Viet Vowel Ia
intArrayOf(0x0aabe, 0x0aabf), // Tai Viet Vowel Am ..Tai Viet Tone Mai Ek
intArrayOf(0x0aac1, 0x0aac1), // Tai Viet Tone Mai Tho ..Tai Viet Tone Mai Tho
intArrayOf(0x0aaec, 0x0aaed), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
intArrayOf(0x0aaf6, 0x0aaf6), // Meetei Mayek Virama ..Meetei Mayek Virama
intArrayOf(0x0abe5, 0x0abe5), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
intArrayOf(0x0abe8, 0x0abe8), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
intArrayOf(0x0abed, 0x0abed), // Meetei Mayek Apun Iyek ..Meetei Mayek Apun Iyek
intArrayOf(0x0fb1e, 0x0fb1e), // Hebrew Point Judeo-spani..Hebrew Point Judeo-spani
intArrayOf(0x0fe00, 0x0fe0f), // Variation Selector-1 ..Variation Selector-16
intArrayOf(0x0fe20, 0x0fe2f), // Combining Ligature Left ..Combining Cyrillic Titlo
intArrayOf(0x101fd, 0x101fd), // Phaistos Disc Sign Combi..Phaistos Disc Sign Combi
intArrayOf(0x102e0, 0x102e0), // Coptic Epact Thousands M..Coptic Epact Thousands M
intArrayOf(0x10376, 0x1037a), // Combining Old Permic Let..Combining Old Permic Let
intArrayOf(0x10a01, 0x10a03), // Kharoshthi Vowel Sign I ..Kharoshthi Vowel Sign Vo
intArrayOf(0x10a05, 0x10a06), // Kharoshthi Vowel Sign E ..Kharoshthi Vowel Sign O
intArrayOf(0x10a0c, 0x10a0f), // Kharoshthi Vowel Length ..Kharoshthi Sign Visarga
intArrayOf(0x10a38, 0x10a3a), // Kharoshthi Sign Bar Abov..Kharoshthi Sign Dot Belo
intArrayOf(0x10a3f, 0x10a3f), // Kharoshthi Virama ..Kharoshthi Virama
intArrayOf(0x10ae5, 0x10ae6), // Manichaean Abbreviation ..Manichaean Abbreviation
intArrayOf(0x10d24, 0x10d27), // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
intArrayOf(0x10eab, 0x10eac), // Yezidi Combining Hamza M..Yezidi Combining Madda M
intArrayOf(0x10efd, 0x10eff), // (nil) ..(nil)
intArrayOf(0x10f46, 0x10f50), // Sogdian Combining Dot Be..Sogdian Combining Stroke
intArrayOf(0x10f82, 0x10f85), // Old Uyghur Combining Dot..Old Uyghur Combining Two
intArrayOf(0x11001, 0x11001), // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
intArrayOf(0x11038, 0x11046), // Brahmi Vowel Sign Aa ..Brahmi Virama
intArrayOf(0x11070, 0x11070), // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
intArrayOf(0x11073, 0x11074), // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
intArrayOf(0x1107f, 0x11081), // Brahmi Number Joiner ..Kaithi Sign Anusvara
intArrayOf(0x110b3, 0x110b6), // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
intArrayOf(0x110b9, 0x110ba), // Kaithi Sign Virama ..Kaithi Sign Nukta
intArrayOf(0x110c2, 0x110c2), // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
intArrayOf(0x11100, 0x11102), // Chakma Sign Candrabindu ..Chakma Sign Visarga
intArrayOf(0x11127, 0x1112b), // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
intArrayOf(0x1112d, 0x11134), // Chakma Vowel Sign Ai ..Chakma Maayyaa
intArrayOf(0x11173, 0x11173), // Mahajani Sign Nukta ..Mahajani Sign Nukta
intArrayOf(0x11180, 0x11181), // Sharada Sign Candrabindu..Sharada Sign Anusvara
intArrayOf(0x111b6, 0x111be), // Sharada Vowel Sign U ..Sharada Vowel Sign O
intArrayOf(0x111c9, 0x111cc), // Sharada Sandhi Mark ..Sharada Extra Short Vowe
intArrayOf(0x111cf, 0x111cf), // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
intArrayOf(0x1122f, 0x11231), // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
intArrayOf(0x11234, 0x11234), // Khojki Sign Anusvara ..Khojki Sign Anusvara
intArrayOf(0x11236, 0x11237), // Khojki Sign Nukta ..Khojki Sign Shadda
intArrayOf(0x1123e, 0x1123e), // Khojki Sign Sukun ..Khojki Sign Sukun
intArrayOf(0x11241, 0x11241), // (nil) ..(nil)
intArrayOf(0x112df, 0x112df), // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
intArrayOf(0x112e3, 0x112ea), // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
intArrayOf(0x11300, 0x11301), // Grantha Sign Combining A..Grantha Sign Candrabindu
intArrayOf(0x1133b, 0x1133c), // Combining Bindu Below ..Grantha Sign Nukta
intArrayOf(0x11340, 0x11340), // Grantha Vowel Sign Ii ..Grantha Vowel Sign Ii
intArrayOf(0x11366, 0x1136c), // Combining Grantha Digit ..Combining Grantha Digit
intArrayOf(0x11370, 0x11374), // Combining Grantha Letter..Combining Grantha Letter
intArrayOf(0x11438, 0x1143f), // Newa Vowel Sign U ..Newa Vowel Sign Ai
intArrayOf(0x11442, 0x11444), // Newa Sign Virama ..Newa Sign Anusvara
intArrayOf(0x11446, 0x11446), // Newa Sign Nukta ..Newa Sign Nukta
intArrayOf(0x1145e, 0x1145e), // Newa Sandhi Mark ..Newa Sandhi Mark
intArrayOf(0x114b3, 0x114b8), // Tirhuta Vowel Sign U ..Tirhuta Vowel Sign Vocal
intArrayOf(0x114ba, 0x114ba), // Tirhuta Vowel Sign Short..Tirhuta Vowel Sign Short
intArrayOf(0x114bf, 0x114c0), // Tirhuta Sign Candrabindu..Tirhuta Sign Anusvara
intArrayOf(0x114c2, 0x114c3), // Tirhuta Sign Virama ..Tirhuta Sign Nukta
intArrayOf(0x115b2, 0x115b5), // Siddham Vowel Sign U ..Siddham Vowel Sign Vocal
intArrayOf(0x115bc, 0x115bd), // Siddham Sign Candrabindu..Siddham Sign Anusvara
intArrayOf(0x115bf, 0x115c0), // Siddham Sign Virama ..Siddham Sign Nukta
intArrayOf(0x115dc, 0x115dd), // Siddham Vowel Sign Alter..Siddham Vowel Sign Alter
intArrayOf(0x11633, 0x1163a), // Modi Vowel Sign U ..Modi Vowel Sign Ai
intArrayOf(0x1163d, 0x1163d), // Modi Sign Anusvara ..Modi Sign Anusvara
intArrayOf(0x1163f, 0x11640), // Modi Sign Virama ..Modi Sign Ardhacandra
intArrayOf(0x116ab, 0x116ab), // Takri Sign Anusvara ..Takri Sign Anusvara
intArrayOf(0x116ad, 0x116ad), // Takri Vowel Sign Aa ..Takri Vowel Sign Aa
intArrayOf(0x116b0, 0x116b5), // Takri Vowel Sign U ..Takri Vowel Sign Au
intArrayOf(0x116b7, 0x116b7), // Takri Sign Nukta ..Takri Sign Nukta
intArrayOf(0x1171d, 0x1171f), // Ahom Consonant Sign Medi..Ahom Consonant Sign Medi
intArrayOf(0x11722, 0x11725), // Ahom Vowel Sign I ..Ahom Vowel Sign Uu
intArrayOf(0x11727, 0x1172b), // Ahom Vowel Sign Aw ..Ahom Sign Killer
intArrayOf(0x1182f, 0x11837), // Dogra Vowel Sign U ..Dogra Sign Anusvara
intArrayOf(0x11839, 0x1183a), // Dogra Sign Virama ..Dogra Sign Nukta
intArrayOf(0x1193b, 0x1193c), // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
intArrayOf(0x1193e, 0x1193e), // Dives Akuru Virama ..Dives Akuru Virama
intArrayOf(0x11943, 0x11943), // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
intArrayOf(0x119d4, 0x119d7), // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
intArrayOf(0x119da, 0x119db), // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
intArrayOf(0x119e0, 0x119e0), // Nandinagari Sign Virama ..Nandinagari Sign Virama
intArrayOf(0x11a01, 0x11a0a), // Zanabazar Square Vowel S..Zanabazar Square Vowel L
intArrayOf(0x11a33, 0x11a38), // Zanabazar Square Final C..Zanabazar Square Sign An
intArrayOf(0x11a3b, 0x11a3e), // Zanabazar Square Cluster..Zanabazar Square Cluster
intArrayOf(0x11a47, 0x11a47), // Zanabazar Square Subjoin..Zanabazar Square Subjoin
intArrayOf(0x11a51, 0x11a56), // Soyombo Vowel Sign I ..Soyombo Vowel Sign Oe
intArrayOf(0x11a59, 0x11a5b), // Soyombo Vowel Sign Vocal..Soyombo Vowel Length Mar
intArrayOf(0x11a8a, 0x11a96), // Soyombo Final Consonant ..Soyombo Sign Anusvara
intArrayOf(0x11a98, 0x11a99), // Soyombo Gemination Mark ..Soyombo Subjoiner
intArrayOf(0x11c30, 0x11c36), // Bhaiksuki Vowel Sign I ..Bhaiksuki Vowel Sign Voc
intArrayOf(0x11c38, 0x11c3d), // Bhaiksuki Vowel Sign E ..Bhaiksuki Sign Anusvara
intArrayOf(0x11c3f, 0x11c3f), // Bhaiksuki Sign Virama ..Bhaiksuki Sign Virama
intArrayOf(0x11c92, 0x11ca7), // Marchen Subjoined Letter..Marchen Subjoined Letter
intArrayOf(0x11caa, 0x11cb0), // Marchen Subjoined Letter..Marchen Vowel Sign Aa
intArrayOf(0x11cb2, 0x11cb3), // Marchen Vowel Sign U ..Marchen Vowel Sign E
intArrayOf(0x11cb5, 0x11cb6), // Marchen Sign Anusvara ..Marchen Sign Candrabindu
intArrayOf(0x11d31, 0x11d36), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
intArrayOf(0x11d3a, 0x11d3a), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
intArrayOf(0x11d3c, 0x11d3d), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
intArrayOf(0x11d3f, 0x11d45), // Masaram Gondi Vowel Sign..Masaram Gondi Virama
intArrayOf(0x11d47, 0x11d47), // Masaram Gondi Ra-kara ..Masaram Gondi Ra-kara
intArrayOf(0x11d90, 0x11d91), // Gunjala Gondi Vowel Sign..Gunjala Gondi Vowel Sign
intArrayOf(0x11d95, 0x11d95), // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
intArrayOf(0x11d97, 0x11d97), // Gunjala Gondi Virama ..Gunjala Gondi Virama
intArrayOf(0x11ef3, 0x11ef4), // Makasar Vowel Sign I ..Makasar Vowel Sign U
intArrayOf(0x11f00, 0x11f01), // (nil) ..(nil)
intArrayOf(0x11f36, 0x11f3a), // (nil) ..(nil)
intArrayOf(0x11f40, 0x11f40), // (nil) ..(nil)
intArrayOf(0x11f42, 0x11f42), // (nil) ..(nil)
intArrayOf(0x13440, 0x13440), // (nil) ..(nil)
intArrayOf(0x13447, 0x13455), // (nil) ..(nil)
intArrayOf(0x16af0, 0x16af4), // Bassa Vah Combining High..Bassa Vah Combining High
intArrayOf(0x16b30, 0x16b36), // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
intArrayOf(0x16f4f, 0x16f4f), // Miao Sign Consonant Modi..Miao Sign Consonant Modi
intArrayOf(0x16f8f, 0x16f92), // Miao Tone Right ..Miao Tone Below
intArrayOf(0x16fe4, 0x16fe4), // Khitan Small Script Fill..Khitan Small Script Fill
intArrayOf(0x1bc9d, 0x1bc9e), // Duployan Thick Letter Se..Duployan Double Mark
intArrayOf(0x1cf00, 0x1cf2d), // Znamenny Combining Mark ..Znamenny Combining Mark
intArrayOf(0x1cf30, 0x1cf46), // Znamenny Combining Tonal..Znamenny Priznak Modifie
intArrayOf(0x1d167, 0x1d169), // Musical Symbol Combining..Musical Symbol Combining
intArrayOf(0x1d17b, 0x1d182), // Musical Symbol Combining..Musical Symbol Combining
intArrayOf(0x1d185, 0x1d18b), // Musical Symbol Combining..Musical Symbol Combining
intArrayOf(0x1d1aa, 0x1d1ad), // Musical Symbol Combining..Musical Symbol Combining
intArrayOf(0x1d242, 0x1d244), // Combining Greek Musical ..Combining Greek Musical
intArrayOf(0x1da00, 0x1da36), // Signwriting Head Rim ..Signwriting Air Sucking
intArrayOf(0x1da3b, 0x1da6c), // Signwriting Mouth Closed..Signwriting Excitement
intArrayOf(0x1da75, 0x1da75), // Signwriting Upper Body T..Signwriting Upper Body T
intArrayOf(0x1da84, 0x1da84), // Signwriting Location Hea..Signwriting Location Hea
intArrayOf(0x1da9b, 0x1da9f), // Signwriting Fill Modifie..Signwriting Fill Modifie
intArrayOf(0x1daa1, 0x1daaf), // Signwriting Rotation Mod..Signwriting Rotation Mod
intArrayOf(0x1e000, 0x1e006), // Combining Glagolitic Let..Combining Glagolitic Let
intArrayOf(0x1e008, 0x1e018), // Combining Glagolitic Let..Combining Glagolitic Let
intArrayOf(0x1e01b, 0x1e021), // Combining Glagolitic Let..Combining Glagolitic Let
intArrayOf(0x1e023, 0x1e024), // Combining Glagolitic Let..Combining Glagolitic Let
intArrayOf(0x1e026, 0x1e02a), // Combining Glagolitic Let..Combining Glagolitic Let
intArrayOf(0x1e08f, 0x1e08f), // (nil) ..(nil)
intArrayOf(0x1e130, 0x1e136), // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
intArrayOf(0x1e2ae, 0x1e2ae), // Toto Sign Rising Tone ..Toto Sign Rising Tone
intArrayOf(0x1e2ec, 0x1e2ef), // Wancho Tone Tup ..Wancho Tone Koini
intArrayOf(0x1e4ec, 0x1e4ef), // (nil) ..(nil)
intArrayOf(0x1e8d0, 0x1e8d6), // Mende Kikakui Combining ..Mende Kikakui Combining
intArrayOf(0x1e944, 0x1e94a), // Adlam Alif Lengthener ..Adlam Nukta
intArrayOf(0xe0100, 0xe01ef), // Variation Selector-17 ..Variation Selector-256
)
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
// from https://github.com/jquast/wcwidth/pull/64
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
private val WIDE_EASTASIAN = arrayOf(
intArrayOf(0x01100, 0x0115f), // Hangul Choseong Kiyeok ..Hangul Choseong Filler
intArrayOf(0x0231a, 0x0231b), // Watch ..Hourglass
intArrayOf(0x02329, 0x0232a), // Left-pointing Angle Brac..Right-pointing Angle Bra
intArrayOf(0x023e9, 0x023ec), // Black Right-pointing Dou..Black Down-pointing Doub
intArrayOf(0x023f0, 0x023f0), // Alarm Clock ..Alarm Clock
intArrayOf(0x023f3, 0x023f3), // Hourglass With Flowing S..Hourglass With Flowing S
intArrayOf(0x025fd, 0x025fe), // White Medium Small Squar..Black Medium Small Squar
intArrayOf(0x02614, 0x02615), // Umbrella With Rain Drops..Hot Beverage
intArrayOf(0x02648, 0x02653), // Aries ..Pisces
intArrayOf(0x0267f, 0x0267f), // Wheelchair Symbol ..Wheelchair Symbol
intArrayOf(0x02693, 0x02693), // Anchor ..Anchor
intArrayOf(0x026a1, 0x026a1), // High Voltage Sign ..High Voltage Sign
intArrayOf(0x026aa, 0x026ab), // Medium White Circle ..Medium Black Circle
intArrayOf(0x026bd, 0x026be), // Soccer Ball ..Baseball
intArrayOf(0x026c4, 0x026c5), // Snowman Without Snow ..Sun Behind Cloud
intArrayOf(0x026ce, 0x026ce), // Ophiuchus ..Ophiuchus
intArrayOf(0x026d4, 0x026d4), // No Entry ..No Entry
intArrayOf(0x026ea, 0x026ea), // Church ..Church
intArrayOf(0x026f2, 0x026f3), // Fountain ..Flag In Hole
intArrayOf(0x026f5, 0x026f5), // Sailboat ..Sailboat
intArrayOf(0x026fa, 0x026fa), // Tent ..Tent
intArrayOf(0x026fd, 0x026fd), // Fuel Pump ..Fuel Pump
intArrayOf(0x02705, 0x02705), // White Heavy Check Mark ..White Heavy Check Mark
intArrayOf(0x0270a, 0x0270b), // Raised Fist ..Raised Hand
intArrayOf(0x02728, 0x02728), // Sparkles ..Sparkles
intArrayOf(0x0274c, 0x0274c), // Cross Mark ..Cross Mark
intArrayOf(0x0274e, 0x0274e), // Negative Squared Cross M..Negative Squared Cross M
intArrayOf(0x02753, 0x02755), // Black Question Mark Orna..White Exclamation Mark O
intArrayOf(0x02757, 0x02757), // Heavy Exclamation Mark S..Heavy Exclamation Mark S
intArrayOf(0x02795, 0x02797), // Heavy Plus Sign ..Heavy Division Sign
intArrayOf(0x027b0, 0x027b0), // Curly Loop ..Curly Loop
intArrayOf(0x027bf, 0x027bf), // Double Curly Loop ..Double Curly Loop
intArrayOf(0x02b1b, 0x02b1c), // Black Large Square ..White Large Square
intArrayOf(0x02b50, 0x02b50), // White Medium Star ..White Medium Star
intArrayOf(0x02b55, 0x02b55), // Heavy Large Circle ..Heavy Large Circle
intArrayOf(0x02e80, 0x02e99), // Cjk Radical Repeat ..Cjk Radical Rap
intArrayOf(0x02e9b, 0x02ef3), // Cjk Radical Choke ..Cjk Radical C-simplified
intArrayOf(0x02f00, 0x02fd5), // Kangxi Radical One ..Kangxi Radical Flute
intArrayOf(0x02ff0, 0x02ffb), // Ideographic Description ..Ideographic Description
intArrayOf(0x03000, 0x0303e), // Ideographic Space ..Ideographic Variation In
intArrayOf(0x03041, 0x03096), // Hiragana Letter Small A ..Hiragana Letter Small Ke
intArrayOf(0x03099, 0x030ff), // Combining Katakana-hirag..Katakana Digraph Koto
intArrayOf(0x03105, 0x0312f), // Bopomofo Letter B ..Bopomofo Letter Nn
intArrayOf(0x03131, 0x0318e), // Hangul Letter Kiyeok ..Hangul Letter Araeae
intArrayOf(0x03190, 0x031e3), // Ideographic Annotation L..Cjk Stroke Q
intArrayOf(0x031f0, 0x0321e), // Katakana Letter Small Ku..Parenthesized Korean Cha
intArrayOf(0x03220, 0x03247), // Parenthesized Ideograph ..Circled Ideograph Koto
intArrayOf(0x03250, 0x04dbf), // Partnership Sign ..Cjk Unified Ideograph-4d
intArrayOf(0x04e00, 0x0a48c), // Cjk Unified Ideograph-4e..Yi Syllable Yyr
intArrayOf(0x0a490, 0x0a4c6), // Yi Radical Qot ..Yi Radical Ke
intArrayOf(0x0a960, 0x0a97c), // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
intArrayOf(0x0ac00, 0x0d7a3), // Hangul Syllable Ga ..Hangul Syllable Hih
intArrayOf(0x0f900, 0x0faff), // Cjk Compatibility Ideogr..(nil)
intArrayOf(0x0fe10, 0x0fe19), // Presentation Form For Ve..Presentation Form For Ve
intArrayOf(0x0fe30, 0x0fe52), // Presentation Form For Ve..Small Full Stop
intArrayOf(0x0fe54, 0x0fe66), // Small Semicolon ..Small Equals Sign
intArrayOf(0x0fe68, 0x0fe6b), // Small Reverse Solidus ..Small Commercial At
intArrayOf(0x0ff01, 0x0ff60), // Fullwidth Exclamation Ma..Fullwidth Right White Pa
intArrayOf(0x0ffe0, 0x0ffe6), // Fullwidth Cent Sign ..Fullwidth Won Sign
intArrayOf(0x16fe0, 0x16fe4), // Tangut Iteration Mark ..Khitan Small Script Fill
intArrayOf(0x16ff0, 0x16ff1), // Vietnamese Alternate Rea..Vietnamese Alternate Rea
intArrayOf(0x17000, 0x187f7), // (nil) ..(nil)
intArrayOf(0x18800, 0x18cd5), // Tangut Component-001 ..Khitan Small Script Char
intArrayOf(0x18d00, 0x18d08), // (nil) ..(nil)
intArrayOf(0x1aff0, 0x1aff3), // Katakana Letter Minnan T..Katakana Letter Minnan T
intArrayOf(0x1aff5, 0x1affb), // Katakana Letter Minnan T..Katakana Letter Minnan N
intArrayOf(0x1affd, 0x1affe), // Katakana Letter Minnan N..Katakana Letter Minnan N
intArrayOf(0x1b000, 0x1b122), // Katakana Letter Archaic ..Katakana Letter Archaic
intArrayOf(0x1b132, 0x1b132), // (nil) ..(nil)
intArrayOf(0x1b150, 0x1b152), // Hiragana Letter Small Wi..Hiragana Letter Small Wo
intArrayOf(0x1b155, 0x1b155), // (nil) ..(nil)
intArrayOf(0x1b164, 0x1b167), // Katakana Letter Small Wi..Katakana Letter Small N
intArrayOf(0x1b170, 0x1b2fb), // Nushu Character-1b170 ..Nushu Character-1b2fb
intArrayOf(0x1f004, 0x1f004), // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
intArrayOf(0x1f0cf, 0x1f0cf), // Playing Card Black Joker..Playing Card Black Joker
intArrayOf(0x1f18e, 0x1f18e), // Negative Squared Ab ..Negative Squared Ab
intArrayOf(0x1f191, 0x1f19a), // Squared Cl ..Squared Vs
intArrayOf(0x1f200, 0x1f202), // Square Hiragana Hoka ..Squared Katakana Sa
intArrayOf(0x1f210, 0x1f23b), // Squared Cjk Unified Ideo..Squared Cjk Unified Ideo
intArrayOf(0x1f240, 0x1f248), // Tortoise Shell Bracketed..Tortoise Shell Bracketed
intArrayOf(0x1f250, 0x1f251), // Circled Ideograph Advant..Circled Ideograph Accept
intArrayOf(0x1f260, 0x1f265), // Rounded Symbol For Fu ..Rounded Symbol For Cai
intArrayOf(0x1f300, 0x1f320), // Cyclone ..Shooting Star
intArrayOf(0x1f32d, 0x1f335), // Hot Dog ..Cactus
intArrayOf(0x1f337, 0x1f37c), // Tulip ..Baby Bottle
intArrayOf(0x1f37e, 0x1f393), // Bottle With Popping Cork..Graduation Cap
intArrayOf(0x1f3a0, 0x1f3ca), // Carousel Horse ..Swimmer
intArrayOf(0x1f3cf, 0x1f3d3), // Cricket Bat And Ball ..Table Tennis Paddle And
intArrayOf(0x1f3e0, 0x1f3f0), // House Building ..European Castle
intArrayOf(0x1f3f4, 0x1f3f4), // Waving Black Flag ..Waving Black Flag
intArrayOf(0x1f3f8, 0x1f43e), // Badminton Racquet And Sh..Paw Prints
intArrayOf(0x1f440, 0x1f440), // Eyes ..Eyes
intArrayOf(0x1f442, 0x1f4fc), // Ear ..Videocassette
intArrayOf(0x1f4ff, 0x1f53d), // Prayer Beads ..Down-pointing Small Red
intArrayOf(0x1f54b, 0x1f54e), // Kaaba ..Menorah With Nine Branch
intArrayOf(0x1f550, 0x1f567), // Clock Face One Oclock ..Clock Face Twelve-thirty
intArrayOf(0x1f57a, 0x1f57a), // Man Dancing ..Man Dancing
intArrayOf(0x1f595, 0x1f596), // Reversed Hand With Middl..Raised Hand With Part Be
intArrayOf(0x1f5a4, 0x1f5a4), // Black Heart ..Black Heart
intArrayOf(0x1f5fb, 0x1f64f), // Mount Fuji ..Person With Folded Hands
intArrayOf(0x1f680, 0x1f6c5), // Rocket ..Left Luggage
intArrayOf(0x1f6cc, 0x1f6cc), // Sleeping Accommodation ..Sleeping Accommodation
intArrayOf(0x1f6d0, 0x1f6d2), // Place Of Worship ..Shopping Trolley
intArrayOf(0x1f6d5, 0x1f6d7), // Hindu Temple ..Elevator
intArrayOf(0x1f6dc, 0x1f6df), // (nil) ..Ring Buoy
intArrayOf(0x1f6eb, 0x1f6ec), // Airplane Departure ..Airplane Arriving
intArrayOf(0x1f6f4, 0x1f6fc), // Scooter ..Roller Skate
intArrayOf(0x1f7e0, 0x1f7eb), // Large Orange Circle ..Large Brown Square
intArrayOf(0x1f7f0, 0x1f7f0), // Heavy Equals Sign ..Heavy Equals Sign
intArrayOf(0x1f90c, 0x1f93a), // Pinched Fingers ..Fencer
intArrayOf(0x1f93c, 0x1f945), // Wrestlers ..Goal Net
intArrayOf(0x1f947, 0x1f9ff), // First Place Medal ..Nazar Amulet
intArrayOf(0x1fa70, 0x1fa7c), // Ballet Shoes ..Crutch
intArrayOf(0x1fa80, 0x1fa88), // Yo-yo ..(nil)
intArrayOf(0x1fa90, 0x1fabd), // Ringed Planet ..(nil)
intArrayOf(0x1fabf, 0x1fac5), // (nil) ..Person With Crown
intArrayOf(0x1face, 0x1fadb), // (nil) ..(nil)
intArrayOf(0x1fae0, 0x1fae8), // Melting Face ..(nil)
intArrayOf(0x1faf0, 0x1faf8), // Hand With Index Finger A..(nil)
intArrayOf(0x20000, 0x2fffd), // Cjk Unified Ideograph-20..(nil)
intArrayOf(0x30000, 0x3fffd), // Cjk Unified Ideograph-30..(nil)
)
private fun intable(table: Array, c: Int): Boolean {
if (c < table[0][0]) return false
var bot = 0
var top = table.size - 1
while (top >= bot) {
val mid = (bot + top) / 2
if (table[mid][1] < c) {
bot = mid + 1
} else if (table[mid][0] > c) {
top = mid - 1
} else {
return true
}
}
return false
}
/** Return the terminal display width of a code point: 0, 1 or 2. */
fun width(ucs: Int): Int {
if (ucs == 0 ||
ucs == 0x034F ||
(ucs in 0x200B..0x200F) ||
ucs == 0x2028 ||
ucs == 0x2029 ||
(ucs in 0x202A..0x202E) ||
(ucs in 0x2060..0x2063)) {
return 0
}
// C0/C1 control characters
// Termux change: Return 0 instead of -1.
if (ucs < 32 || (ucs in 0x07F until 0x0A0)) return 0
if (intable(ZERO_WIDTH, ucs)) return 0
return if (intable(WIDE_EASTASIAN, ucs)) 2 else 1
}
/** The width at an index position in a java char array. */
fun width(chars: CharArray, index: Int): Int {
val c = chars[index]
return if (Character.isHighSurrogate(c)) width(Character.toCodePoint(c, chars[index + 1])) else width(c.code)
}
/**
* The zero width characters count like combining characters in the `chars` array from start
* index to end index (exclusive).
*/
fun zeroWidthCharsCount(chars: CharArray, start: Int, end: Int): Int {
if (start < 0 || start >= chars.size) return 0
var count = 0
var i = start
while (i < end && i < chars.size) {
if (Character.isHighSurrogate(chars[i])) {
if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
count++
}
i += 2
} else {
if (width(chars[i].code) <= 0) {
count++
}
i++
}
}
return count
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
================================================
package com.topjohnwu.magisk.ui
import android.Manifest
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.res.Resources
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.content.res.use
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.ActivityExtension
import com.topjohnwu.magisk.core.base.SplashController
import com.topjohnwu.magisk.core.base.SplashScreenHost
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.ktx.reflectField
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.ui.deny.DenyListScreen
import com.topjohnwu.magisk.ui.deny.DenyListViewModel
import com.topjohnwu.magisk.ui.flash.FlashScreen
import com.topjohnwu.magisk.ui.flash.FlashUtils
import com.topjohnwu.magisk.ui.flash.FlashViewModel
import com.topjohnwu.magisk.ui.module.ActionScreen
import com.topjohnwu.magisk.ui.module.ActionViewModel
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
import com.topjohnwu.magisk.ui.navigation.Navigator
import com.topjohnwu.magisk.ui.navigation.Route
import com.topjohnwu.magisk.ui.navigation.rememberNavigator
import com.topjohnwu.magisk.ui.superuser.SuperuserDetailScreen
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.theme.MagiskTheme
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.MiuixPopupHost
import com.topjohnwu.magisk.core.R as CoreR
class MainActivity : AppCompatActivity(), SplashScreenHost {
override val extension = ActivityExtension(this)
override val splashController = SplashController(this)
private val intentState = MutableStateFlow(0)
internal val showInvalidState = MutableStateFlow(false)
internal val showUnsupported = MutableStateFlow>>(emptyList())
internal val showShortcutPrompt = MutableStateFlow(false)
init {
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap())
}
override fun onCreate(savedInstanceState: Bundle?) {
extension.onCreate(savedInstanceState)
if (isRunningAsStub) {
val delegate = delegate
val clz = delegate.javaClass
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
}
setTheme(Theme.selected.themeRes)
splashController.preOnCreate()
super.onCreate(savedInstanceState)
splashController.onCreate(savedInstanceState)
setupWindow()
}
override fun onResume() {
super.onResume()
splashController.onResume()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
extension.onSaveInstanceState(outState)
}
private fun setupWindow() {
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
.use { it.getDrawable(0) }
.also { window.setBackgroundDrawable(it) }
WindowCompat.setDecorFitsSystemWindows(window, false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window?.decorView?.post {
if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
window.navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = Color.TRANSPARENT
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
window.isStatusBarContrastEnforced = false
}
}
}
}
}
@SuppressLint("InlinedApi")
override fun onCreateUi(savedInstanceState: Bundle?) {
showUnsupportedMessage()
askForHomeShortcut()
if (Config.checkUpdate) {
extension.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
Config.checkUpdate = it
}
}
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
val initialTab = getInitialTab(intent)
setContent {
MagiskTheme {
Box(modifier = Modifier.fillMaxSize()) {
val navigator = rememberNavigator(Route.Main)
CompositionLocalProvider(LocalNavigator provides navigator) {
HandleFlashIntent(navigator)
NavDisplay(
backStack = navigator.backStack,
onBack = { navigator.pop() },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry {
MainScreen(initialTab = initialTab)
}
entry { _ ->
val vm: DenyListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
LaunchedEffect(Unit) { vm.startLoading() }
DenyListScreen(vm, onBack = { navigator.pop() })
}
entry { key ->
val vm: FlashViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
LaunchedEffect(key) {
if (vm.flashAction.isEmpty()) {
vm.flashAction = key.action
vm.flashUri = key.additionalData?.let { Uri.parse(it) }
vm.startFlashing()
}
}
FlashScreen(vm, action = key.action, onBack = { navigator.pop() })
}
entry { key ->
val vm: SuperuserViewModel = androidx.lifecycle.viewmodel.compose.viewModel(
viewModelStoreOwner = this@MainActivity, factory = VMFactory
)
LaunchedEffect(Unit) {
vm.authenticate = { onSuccess ->
extension.withAuthentication { if (it) onSuccess() }
}
}
SuperuserDetailScreen(uid = key.uid, viewModel = vm, onBack = { navigator.pop() })
}
entry { key ->
val vm: ActionViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
LaunchedEffect(key) {
if (vm.actionId.isEmpty()) {
vm.actionId = key.id
vm.actionName = key.name
vm.startRunAction()
}
}
ActionScreen(vm, actionName = key.name, onBack = { navigator.pop() })
}
}
)
}
MainActivityDialogs(activity = this@MainActivity)
MiuixPopupHost()
}
}
}
}
@Composable
private fun HandleFlashIntent(navigator: Navigator) {
val intentVersion by intentState.collectAsState()
LaunchedEffect(intentVersion) {
val currentIntent = intent ?: return@LaunchedEffect
if (currentIntent.action == FlashUtils.INTENT_FLASH) {
val action = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_ACTION)
?: return@LaunchedEffect
val uri = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_URI)
navigator.push(Route.Flash(action, uri))
currentIntent.action = null
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
intentState.value += 1
}
private fun getInitialTab(intent: Intent?): Int {
val section = if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) {
Const.Nav.SETTINGS
} else {
intent?.getStringExtra(Const.Key.OPEN_SECTION)
}
return when (section) {
Const.Nav.SUPERUSER -> Tab.SUPERUSER.ordinal
Const.Nav.MODULES -> Tab.MODULES.ordinal
Const.Nav.SETTINGS -> Tab.SETTINGS.ordinal
else -> Tab.HOME.ordinal
}
}
@SuppressLint("InlinedApi")
override fun showInvalidStateMessage() {
showInvalidState.value = true
}
internal fun handleInvalidStateInstall() {
extension.withPermission(REQUEST_INSTALL_PACKAGES) {
if (!it) {
toast(CoreR.string.install_unknown_denied, Toast.LENGTH_SHORT)
showInvalidState.value = true
} else {
lifecycleScope.launch {
if (!AppMigration.restoreApp(this@MainActivity)) {
toast(CoreR.string.failure, Toast.LENGTH_LONG)
}
}
}
}
}
private fun showUnsupportedMessage() {
val messages = mutableListOf>()
if (Info.env.isUnsupported) {
messages.add(CoreR.string.unsupport_magisk_title to CoreR.string.unsupport_magisk_msg)
}
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
?.split(':')
?.filterNot { java.io.File("$it/magisk").exists() }
?.any { java.io.File("$it/su").exists() } == true) {
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_other_su_msg)
}
if (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_system_app_msg)
}
if (applicationInfo.flags and ApplicationInfo.FLAG_EXTERNAL_STORAGE != 0) {
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_external_storage_msg)
}
if (messages.isNotEmpty()) {
showUnsupported.value = messages
}
}
private fun askForHomeShortcut() {
if (isRunningAsStub && !Config.askedHome &&
ShortcutManagerCompat.isRequestPinShortcutSupported(this)) {
Config.askedHome = true
showShortcutPrompt.value = true
}
}
}
@Composable
private fun MainActivityDialogs(activity: MainActivity) {
val showInvalid by activity.showInvalidState.collectAsState()
val unsupportedMessages by activity.showUnsupported.collectAsState()
val showShortcut by activity.showShortcutPrompt.collectAsState()
val invalidDialog = com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
onConfirm = {
activity.showInvalidState.value = false
activity.handleInvalidStateInstall()
},
onDismiss = {}
)
LaunchedEffect(showInvalid) {
if (showInvalid) {
invalidDialog.showConfirm(
title = activity.getString(CoreR.string.unsupport_nonroot_stub_title),
content = activity.getString(CoreR.string.unsupport_nonroot_stub_msg),
confirm = activity.getString(CoreR.string.install),
)
}
}
for ((index, pair) in unsupportedMessages.withIndex()) {
val (titleRes, msgRes) = pair
val show = rememberSaveable { androidx.compose.runtime.mutableStateOf(true) }
com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
onConfirm = { show.value = false },
).also { dialog ->
LaunchedEffect(Unit) {
dialog.showConfirm(
title = activity.getString(titleRes),
content = activity.getString(msgRes),
)
}
}
}
val shortcutDialog = com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
onConfirm = {
activity.showShortcutPrompt.value = false
Shortcuts.addHomeIcon(activity)
},
onDismiss = { activity.showShortcutPrompt.value = false }
)
LaunchedEffect(showShortcut) {
if (showShortcut) {
shortcutDialog.showConfirm(
title = activity.getString(CoreR.string.add_shortcut_title),
content = activity.getString(CoreR.string.add_shortcut_msg),
)
}
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt
================================================
package com.topjohnwu.magisk.ui
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.VMFactory
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.ui.home.HomeScreen
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.install.InstallViewModel
import com.topjohnwu.magisk.ui.log.LogScreen
import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.module.ModuleScreen
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.navigation.CollectNavEvents
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
import com.topjohnwu.magisk.ui.settings.SettingsScreen
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserScreen
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
enum class Tab(val titleRes: Int, val iconRes: Int) {
MODULES(CoreR.string.modules, R.drawable.ic_module_outlined_md2),
SUPERUSER(CoreR.string.superuser, R.drawable.ic_superuser_outlined_md2),
HOME(CoreR.string.section_home, R.drawable.ic_home_outlined_md2),
LOG(CoreR.string.logs, R.drawable.ic_bug_outlined_md2),
SETTINGS(CoreR.string.settings, R.drawable.ic_settings_outlined_md2);
}
@Composable
fun MainScreen(initialTab: Int = Tab.HOME.ordinal) {
val navigator = LocalNavigator.current
val visibleTabs = remember {
Tab.entries.filter { tab ->
when (tab) {
Tab.SUPERUSER -> Info.showSuperUser
Tab.MODULES -> Info.env.isActive && LocalModule.loaded()
else -> true
}
}
}
val initialPage = visibleTabs.indexOf(Tab.entries[initialTab]).coerceAtLeast(0)
val pagerState = rememberPagerState(initialPage = initialPage, pageCount = { visibleTabs.size })
Box(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
beyondViewportPageCount = visibleTabs.size - 1,
userScrollEnabled = true,
) { page ->
when (visibleTabs[page]) {
Tab.HOME -> {
val vm: HomeViewModel = viewModel(factory = VMFactory)
val installVm: InstallViewModel = viewModel(factory = VMFactory)
LaunchedEffect(Unit) { vm.startLoading() }
CollectNavEvents(vm, navigator)
CollectNavEvents(installVm, navigator)
HomeScreen(vm, installVm)
}
Tab.SUPERUSER -> {
val activity = LocalContext.current as MainActivity
val vm: SuperuserViewModel = viewModel(viewModelStoreOwner = activity, factory = VMFactory)
LaunchedEffect(Unit) {
vm.authenticate = { onSuccess ->
activity.extension.withAuthentication { if (it) onSuccess() }
}
vm.startLoading()
}
SuperuserScreen(vm)
}
Tab.LOG -> {
val vm: LogViewModel = viewModel(factory = VMFactory)
LaunchedEffect(Unit) { vm.startLoading() }
LogScreen(vm)
}
Tab.MODULES -> {
val vm: ModuleViewModel = viewModel(factory = VMFactory)
LaunchedEffect(Unit) { vm.startLoading() }
CollectNavEvents(vm, navigator)
ModuleScreen(vm)
}
Tab.SETTINGS -> {
val activity = LocalContext.current as MainActivity
val vm: SettingsViewModel = viewModel(factory = VMFactory)
LaunchedEffect(Unit) {
vm.authenticate = { onSuccess ->
activity.extension.withAuthentication { if (it) onSuccess() }
}
}
CollectNavEvents(vm, navigator)
SettingsScreen(vm)
}
}
}
FloatingNavigationBar(
pagerState = pagerState,
visibleTabs = visibleTabs,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
@Composable
private fun FloatingNavigationBar(
pagerState: PagerState,
visibleTabs: List,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
val shape = RoundedCornerShape(28.dp)
val navBarInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
Row(
modifier = modifier
.padding(bottom = navBarInset + 12.dp, start = 24.dp, end = 24.dp)
.shadow(elevation = 6.dp, shape = shape)
.clip(shape)
.background(MiuixTheme.colorScheme.surfaceContainer)
.fillMaxWidth()
.height(64.dp)
.padding(horizontal = 4.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
visibleTabs.forEachIndexed { index, tab ->
FloatingNavItem(
icon = ImageVector.vectorResource(tab.iconRes),
label = stringResource(tab.titleRes),
selected = pagerState.currentPage == index,
enabled = true,
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
modifier = Modifier.weight(1f)
)
}
}
}
@Composable
private fun FloatingNavItem(
icon: ImageVector,
label: String,
selected: Boolean,
enabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val contentColor by animateColorAsState(
targetValue = when {
!enabled -> MiuixTheme.colorScheme.disabledOnSecondaryVariant
selected -> MiuixTheme.colorScheme.primary
else -> MiuixTheme.colorScheme.onSurfaceVariantActions
},
animationSpec = tween(200),
label = "navItemColor"
)
Column(
modifier = modifier
.clickable(
enabled = enabled,
indication = null,
interactionSource = remember { MutableInteractionSource() },
role = Role.Tab,
onClick = onClick,
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Icon(
imageVector = icon,
contentDescription = label,
modifier = Modifier.size(24.dp),
tint = contentColor,
)
Spacer(Modifier.height(2.dp))
Text(
text = label,
fontSize = 11.sp,
color = contentColor,
)
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/component/Dialog.kt
================================================
package com.topjohnwu.magisk.ui.component
import android.widget.TextView
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.topjohnwu.magisk.core.di.ServiceLocator
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import timber.log.Timber
import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.extra.SuperDialog
import top.yukonga.miuix.kmp.theme.MiuixTheme
import java.io.IOException
import kotlin.coroutines.resume
sealed interface ConfirmResult {
data object Confirmed : ConfirmResult
data object Canceled : ConfirmResult
}
data class DialogVisuals(
val title: String = "",
val content: String? = null,
val markdown: Boolean = false,
val confirm: String? = null,
val dismiss: String? = null,
)
interface LoadingDialogHandle {
suspend fun withLoading(block: suspend () -> R): R
}
interface ConfirmDialogHandle {
fun showConfirm(
title: String,
content: String? = null,
markdown: Boolean = false,
confirm: String? = null,
dismiss: String? = null
)
suspend fun awaitConfirm(
title: String,
content: String? = null,
markdown: Boolean = false,
confirm: String? = null,
dismiss: String? = null
): ConfirmResult
}
private class LoadingDialogHandleImpl(
private val visible: MutableState,
private val coroutineScope: CoroutineScope
) : LoadingDialogHandle {
override suspend fun withLoading(block: suspend () -> R): R {
return coroutineScope.async {
try {
visible.value = true
block()
} finally {
visible.value = false
}
}.await()
}
}
private class ConfirmDialogHandleImpl(
private val visible: MutableState,
private val coroutineScope: CoroutineScope,
private val callback: ConfirmCallback,
private val resultChannel: Channel
) : ConfirmDialogHandle {
var visuals by mutableStateOf(DialogVisuals())
private set
private var awaitContinuation: CancellableContinuation? = null
init {
coroutineScope.launch {
resultChannel
.consumeAsFlow()
.onEach { result ->
awaitContinuation?.let {
awaitContinuation = null
if (it.isActive) it.resume(result)
}
}
.onEach { visible.value = false }
.collect { result ->
when (result) {
ConfirmResult.Confirmed -> callback.onConfirm?.invoke()
ConfirmResult.Canceled -> callback.onDismiss?.invoke()
}
}
}
}
override fun showConfirm(
title: String,
content: String?,
markdown: Boolean,
confirm: String?,
dismiss: String?
) {
coroutineScope.launch {
visuals = DialogVisuals(title, content, markdown, confirm, dismiss)
visible.value = true
}
}
override suspend fun awaitConfirm(
title: String,
content: String?,
markdown: Boolean,
confirm: String?,
dismiss: String?
): ConfirmResult {
coroutineScope.launch {
visuals = DialogVisuals(title, content, markdown, confirm, dismiss)
visible.value = true
}
return suspendCancellableCoroutine { cont ->
awaitContinuation = cont.apply {
invokeOnCancellation { visible.value = false }
}
}
}
}
interface ConfirmCallback {
val onConfirm: (() -> Unit)?
val onDismiss: (() -> Unit)?
}
@Composable
fun rememberConfirmCallback(
onConfirm: (() -> Unit)? = null,
onDismiss: (() -> Unit)? = null
): ConfirmCallback {
val currentOnConfirm by rememberUpdatedState(onConfirm)
val currentOnDismiss by rememberUpdatedState(onDismiss)
return remember {
object : ConfirmCallback {
override val onConfirm get() = currentOnConfirm
override val onDismiss get() = currentOnDismiss
}
}
}
@Composable
fun rememberLoadingDialog(): LoadingDialogHandle {
val visible = remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
LoadingDialog(visible)
return remember { LoadingDialogHandleImpl(visible, scope) }
}
@Composable
fun rememberConfirmDialog(
onConfirm: (() -> Unit)? = null,
onDismiss: (() -> Unit)? = null
): ConfirmDialogHandle {
return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss))
}
@Composable
fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle {
val visible = rememberSaveable { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val resultChannel = remember { Channel() }
val handle = remember {
ConfirmDialogHandleImpl(visible, scope, callback, resultChannel)
}
if (visible.value) {
ConfirmDialogContent(
visuals = handle.visuals,
confirm = { scope.launch { resultChannel.send(ConfirmResult.Confirmed) } },
dismiss = { scope.launch { resultChannel.send(ConfirmResult.Canceled) } },
showDialog = visible
)
}
return handle
}
@Composable
private fun LoadingDialog(showDialog: MutableState) {
SuperDialog(
show = showDialog,
onDismissRequest = {},
content = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.CenterStart
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
InfiniteProgressIndicator(
color = MiuixTheme.colorScheme.onBackground
)
Text(
modifier = Modifier.padding(start = 12.dp),
text = stringResource(com.topjohnwu.magisk.core.R.string.loading),
)
}
}
}
)
}
@Composable
private fun ConfirmDialogContent(
visuals: DialogVisuals,
confirm: () -> Unit,
dismiss: () -> Unit,
showDialog: MutableState
) {
SuperDialog(
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
show = showDialog,
title = visuals.title,
onDismissRequest = {
dismiss()
showDialog.value = false
},
content = {
Layout(
content = {
visuals.content?.let { content ->
if (visuals.markdown) {
MarkdownText(content)
} else {
Text(
text = content,
color = MiuixTheme.colorScheme.onSurface,
)
}
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.padding(top = 12.dp)
) {
TextButton(
text = visuals.dismiss
?: stringResource(android.R.string.cancel),
onClick = {
dismiss()
showDialog.value = false
},
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(20.dp))
TextButton(
text = visuals.confirm
?: stringResource(android.R.string.ok),
onClick = {
confirm()
showDialog.value = false
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary()
)
}
}
) { measurables, constraints ->
if (measurables.size != 2) {
val button = measurables[0].measure(constraints)
layout(constraints.maxWidth, button.height) {
button.place(0, 0)
}
} else {
val button = measurables[1].measure(constraints)
val content = measurables[0].measure(
constraints.copy(maxHeight = constraints.maxHeight - button.height)
)
layout(constraints.maxWidth, content.height + button.height) {
content.place(0, 0)
button.place(0, content.height)
}
}
}
}
)
}
@Composable
fun MarkdownText(text: String) {
val contentColor = MiuixTheme.colorScheme.onBackground.toArgb()
AndroidView(
factory = { context ->
TextView(context).apply {
setTextColor(contentColor)
ServiceLocator.markwon.setMarkdown(this, text)
}
},
update = { textView ->
textView.setTextColor(contentColor)
ServiceLocator.markwon.setMarkdown(textView, text)
},
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 300.dp)
)
}
@Composable
fun MarkdownTextAsync(getMarkdownText: suspend () -> String) {
var mdText by remember { mutableStateOf(null) }
var error by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
try {
mdText = withContext(Dispatchers.IO) { getMarkdownText() }
} catch (e: IOException) {
Timber.e(e)
error = true
}
}
when {
error -> Text(stringResource(com.topjohnwu.magisk.core.R.string.download_file_error))
mdText != null -> MarkdownText(mdText!!)
else -> Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
InfiniteProgressIndicator(color = MiuixTheme.colorScheme.onBackground)
}
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/component/MenuPositionProvider.kt
================================================
package com.topjohnwu.magisk.ui.component
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import top.yukonga.miuix.kmp.basic.PopupPositionProvider
object ListPopupDefaults {
val MenuPositionProvider = object : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowBounds: IntRect,
layoutDirection: LayoutDirection,
popupContentSize: IntSize,
popupMargin: IntRect,
alignment: PopupPositionProvider.Align,
): IntOffset {
val resolved = alignment.resolve(layoutDirection)
val offsetX: Int
val offsetY: Int
when (resolved) {
PopupPositionProvider.Align.TopStart -> {
offsetX = anchorBounds.left + popupMargin.left
offsetY = anchorBounds.bottom + popupMargin.top
}
PopupPositionProvider.Align.TopEnd -> {
offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right
offsetY = anchorBounds.bottom + popupMargin.top
}
PopupPositionProvider.Align.BottomStart -> {
offsetX = anchorBounds.left + popupMargin.left
offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom
}
PopupPositionProvider.Align.BottomEnd -> {
offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right
offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom
}
else -> {
offsetX = if (resolved == PopupPositionProvider.Align.End) {
anchorBounds.right - popupContentSize.width - popupMargin.right
} else {
anchorBounds.left + popupMargin.left
}
offsetY = if (windowBounds.bottom - anchorBounds.bottom > popupContentSize.height) {
anchorBounds.bottom + popupMargin.bottom
} else if (anchorBounds.top - windowBounds.top > popupContentSize.height) {
anchorBounds.top - popupContentSize.height - popupMargin.top
} else {
anchorBounds.top + anchorBounds.height / 2 - popupContentSize.height / 2
}
}
}
return IntOffset(
x = offsetX.coerceIn(
windowBounds.left,
(windowBounds.right - popupContentSize.width - popupMargin.right)
.coerceAtLeast(windowBounds.left),
),
y = offsetY.coerceIn(
(windowBounds.top + popupMargin.top)
.coerceAtMost(windowBounds.bottom - popupContentSize.height - popupMargin.bottom),
windowBounds.bottom - popupContentSize.height - popupMargin.bottom,
),
)
}
override fun getMargins(): PaddingValues = PaddingValues(start = 20.dp)
}
}
private fun PopupPositionProvider.Align.resolve(layoutDirection: LayoutDirection): PopupPositionProvider.Align {
if (layoutDirection == LayoutDirection.Ltr) return this
return when (this) {
PopupPositionProvider.Align.Start -> PopupPositionProvider.Align.End
PopupPositionProvider.Align.End -> PopupPositionProvider.Align.Start
PopupPositionProvider.Align.TopStart -> PopupPositionProvider.Align.TopEnd
PopupPositionProvider.Align.TopEnd -> PopupPositionProvider.Align.TopStart
PopupPositionProvider.Align.BottomStart -> PopupPositionProvider.Align.BottomEnd
PopupPositionProvider.Align.BottomEnd -> PopupPositionProvider.Align.BottomStart
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/deny/AppProcessInfo.kt
================================================
package com.topjohnwu.magisk.ui.deny
import android.annotation.SuppressLint
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_ACTIVITIES
import android.content.pm.PackageManager.GET_PROVIDERS
import android.content.pm.PackageManager.GET_RECEIVERS
import android.content.pm.PackageManager.GET_SERVICES
import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import android.content.pm.ServiceInfo
import android.graphics.drawable.Drawable
import androidx.core.os.ProcessCompat
import com.topjohnwu.magisk.core.ktx.getLabel
import java.util.Locale
import java.util.TreeSet
class CmdlineListItem(line: String) {
val packageName: String
val process: String
init {
val split = line.split(Regex("\\|"), 2)
packageName = split[0]
process = split.getOrElse(1) { packageName }
}
}
const val ISOLATED_MAGIC = "isolated"
@SuppressLint("InlinedApi")
class AppProcessInfo(
private val info: ApplicationInfo,
pm: PackageManager,
denyList: List
) : Comparable {
private val denyList = denyList.filter {
it.packageName == info.packageName || it.packageName == ISOLATED_MAGIC
}
val label = info.getLabel(pm)
val iconImage: Drawable = runCatching { info.loadIcon(pm) }.getOrDefault(pm.defaultActivityIcon)
val packageName: String get() = info.packageName
var firstInstallTime: Long = 0L
private set
var lastUpdateTime: Long = 0L
private set
val processes = fetchProcesses(pm)
override fun compareTo(other: AppProcessInfo) = comparator.compare(this, other)
fun isSystemApp() = info.flags and ApplicationInfo.FLAG_SYSTEM != 0
fun isApp() = ProcessCompat.isApplicationUid(info.uid)
private fun createProcess(name: String, pkg: String = info.packageName) =
ProcessInfo(name, pkg, denyList.any { it.process == name && it.packageName == pkg })
private fun ComponentInfo.getProcName(): String = processName
?: applicationInfo.processName
?: applicationInfo.packageName
private val ServiceInfo.isIsolated get() = (flags and ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
private val ServiceInfo.useAppZygote get() = (flags and ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0
private fun Array?.toProcessList() =
orEmpty().map { createProcess(it.getProcName()) }
private fun Array?.toProcessList(): List {
if (this == null) return emptyList()
val result = mutableListOf()
var hasIsolated = false
for (si in this) {
if (si.isIsolated) {
if (si.useAppZygote) {
val proc = info.processName ?: info.packageName
result.add(createProcess("${proc}_zygote"))
} else {
hasIsolated = true
}
} else {
result.add(createProcess(si.getProcName()))
}
}
if (hasIsolated) {
val prefix = "${info.processName ?: info.packageName}:"
val isEnabled = denyList.any {
it.packageName == ISOLATED_MAGIC && it.process.startsWith(prefix)
}
result.add(ProcessInfo(prefix, ISOLATED_MAGIC, isEnabled))
}
return result
}
private fun fetchProcesses(pm: PackageManager): Collection {
val flag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES or
GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
val packageInfo = try {
pm.getPackageInfo(info.packageName, flag)
} catch (e: Exception) {
// Exceed binder data transfer limit, parse the package locally
pm.getPackageArchiveInfo(info.sourceDir, flag) ?: return emptyList()
}
firstInstallTime = packageInfo.firstInstallTime
lastUpdateTime = packageInfo.lastUpdateTime
val processSet = TreeSet(compareBy({ it.name }, { it.isIsolated }))
processSet += packageInfo.activities.toProcessList()
processSet += packageInfo.services.toProcessList()
processSet += packageInfo.receivers.toProcessList()
processSet += packageInfo.providers.toProcessList()
return processSet
}
companion object {
private val comparator = compareBy(
{ it.label.lowercase(Locale.ROOT) },
{ it.info.packageName }
)
}
}
data class ProcessInfo(
val name: String,
val packageName: String,
var isEnabled: Boolean
) {
val isIsolated = packageName == ISOLATED_MAGIC
val isAppZygote = name.endsWith("_zygote")
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListScreen.kt
================================================
package com.topjohnwu.magisk.ui.deny
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.dp
import com.topjohnwu.magisk.ui.component.ListPopupDefaults.MenuPositionProvider
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Checkbox
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
import top.yukonga.miuix.kmp.basic.DropdownImpl
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator
import top.yukonga.miuix.kmp.basic.ListPopupColumn
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.PopupPositionProvider
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.extra.SuperListPopup
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Back
import top.yukonga.miuix.kmp.icon.extended.Sort
import top.yukonga.miuix.kmp.icon.extended.Tune
import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun DenyListScreen(viewModel: DenyListViewModel, onBack: () -> Unit) {
val loading by viewModel.loading.collectAsState()
val apps by viewModel.filteredApps.collectAsState()
val query by viewModel.query.collectAsState()
val showSystem by viewModel.showSystem.collectAsState()
val showOS by viewModel.showOS.collectAsState()
val sortBy by viewModel.sortBy.collectAsState()
val sortReverse by viewModel.sortReverse.collectAsState()
val showSortMenu = remember { mutableStateOf(false) }
val showFilterMenu = remember { mutableStateOf(false) }
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.denylist),
navigationIcon = {
IconButton(
modifier = Modifier.padding(start = 16.dp),
onClick = onBack
) {
Icon(
imageVector = MiuixIcons.Back,
contentDescription = null,
tint = MiuixTheme.colorScheme.onBackground
)
}
},
actions = {
Box {
IconButton(
onClick = { showSortMenu.value = true },
holdDownState = showSortMenu.value,
) {
Icon(
imageVector = MiuixIcons.Sort,
contentDescription = stringResource(CoreR.string.menu_sort),
)
}
SuperListPopup(
show = showSortMenu,
popupPositionProvider = MenuPositionProvider,
alignment = PopupPositionProvider.Align.End,
onDismissRequest = { showSortMenu.value = false }
) {
ListPopupColumn {
val sortOptions = listOf(
CoreR.string.sort_by_name to SortBy.NAME,
CoreR.string.sort_by_package_name to SortBy.PACKAGE_NAME,
CoreR.string.sort_by_install_time to SortBy.INSTALL_TIME,
CoreR.string.sort_by_update_time to SortBy.UPDATE_TIME,
)
val totalSize = sortOptions.size + 1
sortOptions.forEachIndexed { index, (resId, sort) ->
DropdownImpl(
text = stringResource(resId),
optionSize = totalSize,
isSelected = sortBy == sort,
index = index,
onSelectedIndexChange = {
viewModel.setSortBy(sort)
showSortMenu.value = false
}
)
}
DropdownImpl(
text = stringResource(CoreR.string.sort_reverse),
optionSize = totalSize,
isSelected = sortReverse,
index = sortOptions.size,
onSelectedIndexChange = {
viewModel.toggleSortReverse()
showSortMenu.value = false
}
)
}
}
}
Box {
IconButton(
modifier = Modifier.padding(end = 16.dp),
onClick = { showFilterMenu.value = true },
holdDownState = showFilterMenu.value,
) {
Icon(
imageVector = MiuixIcons.Tune,
contentDescription = stringResource(CoreR.string.hide_filter_hint),
)
}
SuperListPopup(
show = showFilterMenu,
popupPositionProvider = MenuPositionProvider,
alignment = PopupPositionProvider.Align.End,
onDismissRequest = { showFilterMenu.value = false }
) {
ListPopupColumn {
DropdownImpl(
text = stringResource(CoreR.string.show_system_app),
optionSize = 2,
isSelected = showSystem,
index = 0,
onSelectedIndexChange = {
viewModel.setShowSystem(!showSystem)
showFilterMenu.value = false
}
)
DropdownImpl(
text = stringResource(CoreR.string.show_os_app),
optionSize = 2,
isSelected = showOS,
index = 1,
onSelectedIndexChange = {
if (!showOS && !showSystem) {
viewModel.setShowSystem(true)
}
viewModel.setShowOS(!showOS)
showFilterMenu.value = false
}
)
}
}
}
},
scrollBehavior = scrollBehavior
)
},
popupHost = { }
) { padding ->
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
SearchInput(
query = query,
onQueryChange = viewModel::setQuery,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
)
if (loading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(CoreR.string.loading),
style = MiuixTheme.textStyles.headline2
)
Spacer(Modifier.height(16.dp))
CircularProgressIndicator()
}
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.padding(horizontal = 12.dp),
contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = apps,
key = { it.info.packageName }
) { app ->
DenyAppCard(app)
}
}
}
}
}
}
@Composable
private fun SearchInput(query: String, onQueryChange: (String) -> Unit, modifier: Modifier = Modifier) {
top.yukonga.miuix.kmp.basic.TextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier,
label = stringResource(CoreR.string.hide_filter_hint)
)
}
@Composable
private fun DenyAppCard(app: DenyAppState) {
Card(modifier = Modifier.fillMaxWidth()) {
Column {
if (app.checkedPercent > 0f) {
LinearProgressIndicator(
progress = app.checkedPercent,
modifier = Modifier.fillMaxWidth()
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { app.isExpanded = !app.isExpanded }
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberDrawablePainter(app.info.iconImage),
contentDescription = app.info.label,
modifier = Modifier.size(40.dp)
)
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = app.info.label,
style = MiuixTheme.textStyles.body1,
)
Text(
text = app.info.packageName,
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
)
}
Spacer(Modifier.width(8.dp))
Checkbox(
state = when {
app.itemsChecked == 0 -> ToggleableState.Off
app.checkedPercent < 1f -> ToggleableState.Indeterminate
else -> ToggleableState.On
},
onClick = { app.toggleAll() }
)
}
AnimatedVisibility(visible = app.isExpanded) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 52.dp)
) {
app.processes.forEach { proc ->
ProcessRow(proc)
}
}
}
}
}
}
@Composable
private fun ProcessRow(proc: DenyProcessState) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { proc.toggle() }
.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = proc.displayName,
style = MiuixTheme.textStyles.body2,
color = if (proc.isEnabled) MiuixTheme.colorScheme.onSurface
else MiuixTheme.colorScheme.onSurfaceVariantSummary,
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(8.dp))
Checkbox(
state = ToggleableState(proc.isEnabled),
onClick = { proc.toggle() }
)
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/deny/DenyListViewModel.kt
================================================
package com.topjohnwu.magisk.ui.deny
import android.annotation.SuppressLint
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.ktx.concurrentMap
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.toCollection
import kotlinx.coroutines.withContext
enum class SortBy { NAME, PACKAGE_NAME, INSTALL_TIME, UPDATE_TIME }
class DenyListViewModel : AsyncLoadViewModel() {
private val _loading = MutableStateFlow(true)
val loading: StateFlow = _loading.asStateFlow()
private val _allApps = MutableStateFlow>(emptyList())
private val _query = MutableStateFlow("")
val query: StateFlow = _query.asStateFlow()
private val _showSystem = MutableStateFlow(false)
val showSystem: StateFlow = _showSystem.asStateFlow()
private val _showOS = MutableStateFlow(false)
val showOS: StateFlow = _showOS.asStateFlow()
private val _sortBy = MutableStateFlow(SortBy.NAME)
val sortBy: StateFlow = _sortBy.asStateFlow()
private val _sortReverse = MutableStateFlow(false)
val sortReverse: StateFlow = _sortReverse.asStateFlow()
val filteredApps: StateFlow> = combine(
_allApps, _query, _showSystem, _showOS, _sortBy, _sortReverse
) { args ->
@Suppress("UNCHECKED_CAST")
val apps = args[0] as List
val q = args[1] as String
val showSys = args[2] as Boolean
val showOS = args[3] as Boolean
val sort = args[4] as SortBy
val reverse = args[5] as Boolean
val filtered = apps.filter { app ->
val passFilter = app.isChecked ||
((showSys || !app.info.isSystemApp()) &&
((showSys && showOS) || app.info.isApp()))
val passQuery = q.isBlank() ||
app.info.label.contains(q, true) ||
app.info.packageName.contains(q, true) ||
app.processes.any { it.process.name.contains(q, true) }
passFilter && passQuery
}
val secondary: Comparator = when (sort) {
SortBy.NAME -> compareBy(String.CASE_INSENSITIVE_ORDER) { it.info.label }
SortBy.PACKAGE_NAME -> compareBy(String.CASE_INSENSITIVE_ORDER) { it.info.packageName }
SortBy.INSTALL_TIME -> compareByDescending { it.info.firstInstallTime }
SortBy.UPDATE_TIME -> compareByDescending { it.info.lastUpdateTime }
}
val comparator = compareBy { it.itemsChecked == 0 }
.then(if (reverse) secondary.reversed() else secondary)
filtered.sortedWith(comparator)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
fun setQuery(q: String) { _query.value = q }
fun setShowSystem(v: Boolean) {
_showSystem.value = v
if (!v) _showOS.value = false
}
fun setShowOS(v: Boolean) { _showOS.value = v }
fun setSortBy(s: SortBy) { _sortBy.value = s }
fun toggleSortReverse() { _sortReverse.value = !_sortReverse.value }
@SuppressLint("InlinedApi")
override suspend fun doLoadWork() {
_loading.value = true
val apps = withContext(Dispatchers.Default) {
val pm = AppContext.packageManager
val denyList = Shell.cmd("magisk --denylist ls").exec().out
.map { CmdlineListItem(it) }
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES).run {
asFlow()
.filter { AppContext.packageName != it.packageName }
.concurrentMap { AppProcessInfo(it, pm, denyList) }
.filter { it.processes.isNotEmpty() }
.concurrentMap { DenyAppState(it) }
.toCollection(ArrayList(size))
}
apps.sortWith(compareBy(
{ it.processes.count { p -> p.isEnabled } == 0 },
{ it.info }
))
apps
}
_allApps.value = apps
_loading.value = false
}
}
class DenyAppState(val info: AppProcessInfo) : Comparable {
val processes = info.processes.map { DenyProcessState(it) }
var isExpanded by mutableStateOf(false)
val itemsChecked: Int get() = processes.count { it.isEnabled }
val isChecked: Boolean get() = itemsChecked > 0
val checkedPercent: Float get() = if (processes.isEmpty()) 0f else itemsChecked.toFloat() / processes.size
fun toggleAll() {
if (isChecked) {
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
processes.filter { it.isEnabled }.forEach { proc ->
if (proc.process.isIsolated) {
proc.toggle()
} else {
proc.isEnabled = false
}
}
} else {
processes.filterNot { it.isEnabled }.forEach { it.toggle() }
}
}
override fun compareTo(other: DenyAppState) = comparator.compare(this, other)
companion object {
private val comparator = compareBy(
{ it.itemsChecked == 0 },
{ it.info }
)
}
}
class DenyProcessState(val process: ProcessInfo) {
var isEnabled by mutableStateOf(process.isEnabled)
val displayName: String =
if (process.isIsolated) "(isolated) ${process.name}*" else process.name
fun toggle() {
isEnabled = !isEnabled
val arg = if (isEnabled) "add" else "rm"
val (name, pkg) = process
Shell.cmd("magisk --denylist $arg $pkg \'$name\'").submit()
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/flash/FlashScreen.kt
================================================
package com.topjohnwu.magisk.ui.flash
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.ui.terminal.TerminalScreen
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Back
import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun FlashScreen(viewModel: FlashViewModel, action: String, onBack: () -> Unit) {
val flashState by viewModel.flashState.collectAsState()
val showReboot by viewModel.showReboot.collectAsState()
val finished = flashState != FlashViewModel.State.FLASHING
val useTerminal = action == Const.Value.FLASH_ZIP
val statusText = when (flashState) {
FlashViewModel.State.FLASHING -> stringResource(CoreR.string.flashing)
FlashViewModel.State.SUCCESS -> stringResource(CoreR.string.done)
FlashViewModel.State.FAILED -> stringResource(CoreR.string.failure)
}
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
SmallTopAppBar(
title = "${stringResource(CoreR.string.flash_screen_title)} - $statusText",
navigationIcon = {
IconButton(
modifier = Modifier.padding(start = 16.dp),
onClick = onBack
) {
Icon(
imageVector = MiuixIcons.Back,
contentDescription = null,
tint = MiuixTheme.colorScheme.onBackground
)
}
},
actions = {
if (finished) {
IconButton(
modifier = Modifier.padding(end = 4.dp),
onClick = { viewModel.saveLog() }
) {
Icon(
painter = painterResource(R.drawable.ic_save_md2),
contentDescription = stringResource(CoreR.string.menuSaveLog),
tint = MiuixTheme.colorScheme.onBackground
)
}
}
if (flashState == FlashViewModel.State.SUCCESS && showReboot) {
IconButton(
modifier = Modifier.padding(end = 16.dp),
onClick = { viewModel.restartPressed() }
) {
Icon(
painter = painterResource(R.drawable.ic_restart),
contentDescription = stringResource(CoreR.string.reboot),
tint = MiuixTheme.colorScheme.onBackground
)
}
}
},
scrollBehavior = scrollBehavior
)
},
popupHost = { }
) { padding ->
if (useTerminal) {
TerminalScreen(
modifier = Modifier
.fillMaxSize()
.padding(padding),
onEmulatorCreated = { viewModel.onEmulatorCreated(it) },
)
} else {
val items = viewModel.consoleItems
val listState = rememberLazyListState()
LaunchedEffect(items.size) {
if (items.isNotEmpty()) {
listState.animateScrollToItem(items.size - 1)
}
}
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.padding(padding)
.horizontalScroll(rememberScrollState())
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
itemsIndexed(items) { _, line ->
Text(
text = line,
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
lineHeight = 16.sp,
color = MiuixTheme.colorScheme.onSurface,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/flash/FlashUtils.kt
================================================
package com.topjohnwu.magisk.ui.flash
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.ui.MainActivity
object FlashUtils {
const val INTENT_FLASH = "com.topjohnwu.magisk.intent.FLASH"
const val EXTRA_FLASH_ACTION = "flash_action"
const val EXTRA_FLASH_URI = "flash_uri"
fun installIntent(context: Context, file: Uri): PendingIntent {
val intent = Intent(context, MainActivity::class.java).apply {
component = MainActivity::class.java.cmp(context.packageName)
action = INTENT_FLASH
putExtra(EXTRA_FLASH_ACTION, Const.Value.FLASH_ZIP)
putExtra(EXTRA_FLASH_URI, file.toString())
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
return PendingIntent.getActivity(
context, file.hashCode(), intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt
================================================
package com.topjohnwu.magisk.ui.flash
import android.net.Uri
import androidx.compose.runtime.mutableStateListOf
import androidx.core.net.toFile
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.reboot
import com.topjohnwu.magisk.core.ktx.synchronized
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.ktx.writeTo
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.terminal.TerminalEmulator
import com.topjohnwu.magisk.terminal.appendLineOnMain
import com.topjohnwu.magisk.terminal.runSuCommand
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
class FlashViewModel : BaseViewModel() {
enum class State {
FLASHING, SUCCESS, FAILED
}
private val _flashState = MutableStateFlow(State.FLASHING)
val flashState: StateFlow = _flashState.asStateFlow()
private val _showReboot = MutableStateFlow(Info.isRooted)
val showReboot: StateFlow = _showReboot.asStateFlow()
var flashAction: String = ""
var flashUri: Uri? = null
// --- TerminalScreen mode (FLASH_ZIP) ---
private var emulator: TerminalEmulator? = null
private val emulatorReady = CompletableDeferred()
fun onEmulatorCreated(emu: TerminalEmulator) {
emulator = emu
emulatorReady.complete(emu)
}
// --- LazyColumn mode (MagiskInstaller) ---
val consoleItems = mutableStateListOf()
private val logItems = mutableListOf().synchronized()
private val outItems = object : CallbackList() {
override fun onAddElement(e: String?) {
e ?: return
consoleItems.add(e)
logItems.add(e)
}
}
// --- Shared ---
fun startFlashing() {
val action = flashAction
val uri = flashUri
viewModelScope.launch {
when (action) {
Const.Value.FLASH_ZIP -> {
uri ?: return@launch
flashZip(uri)
}
Const.Value.UNINSTALL -> {
_showReboot.value = false
onResult(withContext(Dispatchers.IO) {
MagiskInstaller.Uninstall(outItems, logItems).exec()
})
}
Const.Value.FLASH_MAGISK -> {
onResult(withContext(Dispatchers.IO) {
if (Info.isEmulator)
MagiskInstaller.Emulator(outItems, logItems).exec()
else
MagiskInstaller.Direct(outItems, logItems).exec()
})
}
Const.Value.FLASH_INACTIVE_SLOT -> {
_showReboot.value = false
onResult(withContext(Dispatchers.IO) {
MagiskInstaller.SecondSlot(outItems, logItems).exec()
})
}
Const.Value.PATCH_FILE -> {
uri ?: return@launch
_showReboot.value = false
onResult(withContext(Dispatchers.IO) {
MagiskInstaller.Patch(uri, outItems, logItems).exec()
})
}
}
}
}
private fun onResult(success: Boolean) {
_flashState.value = if (success) State.SUCCESS else State.FAILED
}
private suspend fun flashZip(uri: Uri) {
val emu = emulatorReady.await()
val installDir = File(AppContext.cacheDir, "flash")
val result = withContext(Dispatchers.IO) {
try {
installDir.deleteRecursively()
installDir.mkdirs()
val zipFile = if (uri.scheme == "file") {
uri.toFile()
} else {
File(installDir, "install.zip").also {
try {
uri.inputStream().writeTo(it)
} catch (e: IOException) {
val msg = if (e is FileNotFoundException) "Invalid Uri" else "Cannot copy to cache"
return@withContext msg to null
}
}
}
val binary = File(installDir, "update-binary")
AppContext.assets.open("module_installer.sh").use { it.writeTo(binary) }
val name = uri.displayName
null to Triple(installDir, zipFile, name)
} catch (e: IOException) {
Timber.e(e)
"Unable to extract files" to null
}
}
val (error, prepResult) = result
if (prepResult == null) {
emu.appendLineOnMain("! ${error ?: "Installation failed"}")
_flashState.value = State.FAILED
return
}
val (dir, zipFile, displayName) = prepResult
val success = withContext(Dispatchers.IO) {
runSuCommand(
emu,
"echo '- Installing $displayName'; " +
"sh $dir/update-binary dummy 1 '${zipFile.absolutePath}'; " +
"EXIT=\$?; " +
"if [ \$EXIT -ne 0 ]; then echo '! Installation failed'; fi; " +
"exit \$EXIT"
)
}
Shell.cmd("cd /", "rm -rf $dir ${Const.TMPDIR}").submit()
_flashState.value = if (success) State.SUCCESS else State.FAILED
}
fun saveLog() {
viewModelScope.launch(Dispatchers.IO) {
val name = "magisk_install_log_%s.log".format(
System.currentTimeMillis().toTime(timeFormatStandard)
)
val file = MediaStoreUtils.getFile(name)
file.uri.outputStream().bufferedWriter().use { writer ->
val transcript = emulator?.screen?.transcriptText
if (transcript != null) {
writer.write(transcript)
} else {
synchronized(logItems) {
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
}
}
showSnackbar(file.toString())
}
}
fun restartPressed() = reboot()
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt
================================================
package com.topjohnwu.magisk.ui.home
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.ktx.reboot
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.tasks.AppMigration
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.component.ConfirmResult
import com.topjohnwu.magisk.ui.component.ListPopupDefaults.MenuPositionProvider
import com.topjohnwu.magisk.ui.component.LoadingDialogHandle
import com.topjohnwu.magisk.ui.component.MarkdownText
import com.topjohnwu.magisk.ui.component.MarkdownTextAsync
import com.topjohnwu.magisk.ui.component.rememberConfirmDialog
import com.topjohnwu.magisk.ui.component.rememberLoadingDialog
import com.topjohnwu.magisk.ui.flash.FlashUtils
import com.topjohnwu.magisk.ui.install.InstallViewModel
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Checkbox
import top.yukonga.miuix.kmp.basic.DropdownImpl
import top.yukonga.miuix.kmp.basic.HorizontalDivider
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator
import top.yukonga.miuix.kmp.basic.ListPopupColumn
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.PopupPositionProvider
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTitle
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.basic.VerticalDivider
import top.yukonga.miuix.kmp.extra.SuperArrow
import top.yukonga.miuix.kmp.extra.SuperBottomSheet
import top.yukonga.miuix.kmp.extra.SuperListPopup
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Close
import top.yukonga.miuix.kmp.icon.extended.Delete
import top.yukonga.miuix.kmp.icon.extended.Hide
import top.yukonga.miuix.kmp.icon.extended.Show
import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun HomeScreen(viewModel: HomeViewModel, installVm: InstallViewModel) {
val uiState by viewModel.uiState.collectAsState()
val installUiState by installVm.uiState.collectAsState()
val context = LocalContext.current
val activity = context as MainActivity
val scrollBehavior = MiuixScrollBehavior()
val scope = rememberCoroutineScope()
val loadingDialog = rememberLoadingDialog()
val navigator = com.topjohnwu.magisk.ui.navigation.LocalNavigator.current
val showUninstallDialog = rememberSaveable { mutableStateOf(false) }
val showManagerDialog = rememberSaveable { mutableStateOf(false) }
val showEnvFixDialog = rememberSaveable { mutableStateOf(false) }
var showHideDialog by rememberSaveable { mutableStateOf(false) }
var showRestoreDialog by rememberSaveable { mutableStateOf(false) }
val showInstallSheet = rememberSaveable { mutableStateOf(false) }
var envFixCode by remember { mutableIntStateOf(0) }
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { installVm.onPatchFileSelected(it) }
}
val secondSlotDialog = rememberConfirmDialog()
val secondSlotTitle = stringResource(android.R.string.dialog_alert_title)
val secondSlotMsg = stringResource(CoreR.string.install_inactive_slot_msg)
LaunchedEffect(installUiState.requestFilePicker) {
if (installUiState.requestFilePicker) {
filePicker.launch("*/*")
installVm.onFilePickerConsumed()
}
}
LaunchedEffect(installUiState.showSecondSlotWarning) {
if (installUiState.showSecondSlotWarning) {
val result = secondSlotDialog.awaitConfirm(title = secondSlotTitle, content = secondSlotMsg)
installVm.onSecondSlotWarningConsumed()
if (result == ConfirmResult.Confirmed) {
installVm.install()
}
}
}
LaunchedEffect(uiState.showUninstall) {
if (uiState.showUninstall) {
showUninstallDialog.value = true
viewModel.onUninstallConsumed()
}
}
LaunchedEffect(uiState.showManagerInstall) {
if (uiState.showManagerInstall) {
showManagerDialog.value = true
viewModel.onManagerInstallConsumed()
}
}
LaunchedEffect(uiState.envFixCode) {
if (uiState.envFixCode != 0) {
envFixCode = uiState.envFixCode
showEnvFixDialog.value = true
viewModel.onEnvFixConsumed()
}
}
LaunchedEffect(uiState.showHideRestore) {
if (uiState.showHideRestore) {
val hidden = context.packageName != BuildConfig.APP_PACKAGE_NAME
if (hidden) showRestoreDialog = true else showHideDialog = true
viewModel.onHideRestoreConsumed()
}
}
if (showUninstallDialog.value) {
UninstallComposableDialog(
showDialog = showUninstallDialog,
activity = activity,
loadingDialog = loadingDialog,
)
}
if (showManagerDialog.value) {
ManagerInstallComposableDialog(
showDialog = showManagerDialog,
activity = activity,
)
}
if (showEnvFixDialog.value) {
EnvFixComposableDialog(
showDialog = showEnvFixDialog,
code = envFixCode,
activity = activity,
loadingDialog = loadingDialog,
onNavigateInstall = { showInstallSheet.value = true },
)
}
if (showHideDialog) {
HideAppDialog(
onDismiss = { showHideDialog = false },
onConfirm = { name ->
showHideDialog = false
scope.launch {
loadingDialog.withLoading {
AppMigration.patchAndHide(context, name)
}
}
}
)
}
if (showRestoreDialog) {
RestoreAppDialog(
onDismiss = { showRestoreDialog = false },
onConfirm = {
showRestoreDialog = false
scope.launch {
loadingDialog.withLoading {
AppMigration.restoreApp(context)
}
}
}
)
}
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.section_home),
scrollBehavior = scrollBehavior,
actions = {
if (Info.isRooted) {
RebootButton()
}
}
)
},
popupHost = { }
) { padding ->
Column(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(top = 12.dp, bottom = 88.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (uiState.isNoticeVisible) {
NoticeCard(onHide = viewModel::hideNotice)
}
Row(
modifier = Modifier.height(IntrinsicSize.Max),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
CoreCard(
modifier = Modifier.weight(1f).fillMaxHeight(),
state = viewModel.magiskState,
version = viewModel.magiskInstalledVersion,
remoteVersion = if (viewModel.magiskState == HomeViewModel.State.OUTDATED)
"${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})" else null,
onInstallClicked = { showInstallSheet.value = true },
onUninstallClicked = { viewModel.onDeletePressed() },
)
AppCard(
modifier = Modifier.weight(1f).fillMaxHeight(),
state = uiState.appState,
version = viewModel.managerInstalledVersion,
remoteVersion = if (uiState.appState == HomeViewModel.State.OUTDATED)
uiState.managerRemoteVersion else null,
progress = uiState.managerProgress,
isHidden = context.packageName != BuildConfig.APP_PACKAGE_NAME,
onManagerPressed = { viewModel.onManagerPressed() },
onHideRestorePressed = viewModel::onHideRestorePressed,
)
}
SmallTitle(text = stringResource(CoreR.string.home_status_title))
StatusCard()
val showDonateSheet = rememberSaveable { mutableStateOf(false) }
SmallTitle(text = stringResource(CoreR.string.home_support_title))
Card(modifier = Modifier.fillMaxWidth()) {
SuperArrow(
title = stringResource(CoreR.string.documents),
onClick = { openLink(context, "https://topjohnwu.github.io/Magisk/") }
)
SuperArrow(
title = stringResource(CoreR.string.report_bugs),
onClick = { openLink(context, "${Const.Url.SOURCE_CODE_URL}/issues") }
)
SuperArrow(
title = stringResource(CoreR.string.donate),
onClick = { showDonateSheet.value = true }
)
}
SupportBottomSheet(
show = showDonateSheet,
onLinkClicked = { viewModel.onLinkPressed(it) }
)
SmallTitle(text = stringResource(CoreR.string.home_follow_title))
DevelopersCard(onLinkClicked = { openLink(context, it) })
}
}
InstallBottomSheet(
show = showInstallSheet,
installVm = installVm,
installUiState = installUiState,
)
}
@Composable
private fun RebootButton() {
val showMenu = remember { mutableStateOf(false) }
val context = LocalContext.current
var safeModeEnabled by remember { mutableIntStateOf(Config.bootloop) }
val showUserspace = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
context.getSystemService()?.isRebootingUserspaceSupported == true
val showSafeMode = Const.Version.atLeast_28_0()
val items = buildList {
add(RebootOption(CoreR.string.reboot) { reboot() })
if (showUserspace) {
add(RebootOption(CoreR.string.reboot_userspace) { reboot("userspace") })
}
add(RebootOption(CoreR.string.reboot_recovery) { reboot("recovery") })
add(RebootOption(CoreR.string.reboot_bootloader) { reboot("bootloader") })
add(RebootOption(CoreR.string.reboot_download) { reboot("download") })
add(RebootOption(CoreR.string.reboot_edl) { reboot("edl") })
if (showSafeMode) {
add(RebootOption(CoreR.string.reboot_safe_mode) {
val newVal = if (safeModeEnabled >= 2) 0 else 2
Config.bootloop = newVal
safeModeEnabled = newVal
})
}
}
Box {
IconButton(
modifier = Modifier.padding(end = 16.dp),
onClick = { showMenu.value = true },
holdDownState = showMenu.value,
) {
Icon(
painter = painterResource(R.drawable.ic_restart),
contentDescription = stringResource(CoreR.string.reboot),
)
}
SuperListPopup(
show = showMenu,
popupPositionProvider = MenuPositionProvider,
alignment = PopupPositionProvider.Align.End,
onDismissRequest = { showMenu.value = false }
) {
ListPopupColumn {
items.forEachIndexed { index, item ->
val isSafeMode = item.labelRes == CoreR.string.reboot_safe_mode
DropdownImpl(
text = stringResource(item.labelRes),
optionSize = items.size,
isSelected = isSafeMode && safeModeEnabled >= 2,
index = index,
onSelectedIndexChange = {
item.action()
if (!isSafeMode) showMenu.value = false
}
)
}
}
}
}
}
private class RebootOption(val labelRes: Int, val action: () -> Unit)
@Composable
private fun NoticeCard(onHide: () -> Unit) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
MiuixTheme.colorScheme.tertiaryContainer,
RoundedCornerShape(16.dp)
)
.padding(start = 16.dp, top = 4.dp, bottom = 4.dp, end = 4.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(CoreR.string.home_notice_content),
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onTertiaryContainer,
modifier = Modifier.weight(1f).padding(vertical = 8.dp)
)
IconButton(onClick = onHide) {
Icon(
imageVector = MiuixIcons.Close,
contentDescription = stringResource(CoreR.string.hide),
modifier = Modifier.size(15.dp),
tint = MiuixTheme.colorScheme.onTertiaryContainer,
)
}
}
}
}
@Composable
private fun CoreCard(
modifier: Modifier = Modifier,
state: HomeViewModel.State,
version: String,
remoteVersion: String? = null,
onInstallClicked: () -> Unit,
onUninstallClicked: () -> Unit,
) {
val actionLabel = when (state) {
HomeViewModel.State.OUTDATED -> stringResource(CoreR.string.update)
HomeViewModel.State.INVALID -> stringResource(CoreR.string.install)
HomeViewModel.State.UP_TO_DATE -> stringResource(CoreR.string.reinstall)
HomeViewModel.State.LOADING -> null
}
val actionColor = when (state) {
HomeViewModel.State.OUTDATED, HomeViewModel.State.INVALID -> MiuixTheme.colorScheme.primary
else -> MiuixTheme.colorScheme.onSurfaceVariantActions
}
val uninstallEnabled = Info.env.isActive
Card(modifier = modifier) {
Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Icon(
painter = painterResource(CoreR.drawable.ic_magisk_outline),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MiuixTheme.colorScheme.primary
)
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(CoreR.string.home_core_title),
style = MiuixTheme.textStyles.headline2,
)
Text(
text = version.ifEmpty { stringResource(CoreR.string.not_available) },
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
)
}
Column(
modifier = Modifier.align(Alignment.TopEnd).padding(4.dp),
verticalArrangement = Arrangement.spacedBy(0.dp),
) {
IconButton(
onClick = onUninstallClicked,
enabled = uninstallEnabled,
) {
Icon(
imageVector = MiuixIcons.Delete,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = if (uninstallEnabled) MiuixTheme.colorScheme.error
else MiuixTheme.colorScheme.onSurfaceVariantActions,
)
}
if (remoteVersion != null) {
UpdateBadge(
version = remoteVersion,
modifier = Modifier.align(Alignment.End).padding(end = 4.dp)
)
}
}
}
if (actionLabel != null) {
HorizontalDivider(thickness = 0.75.dp)
Text(
text = actionLabel,
style = MiuixTheme.textStyles.body2,
color = actionColor,
textAlign = TextAlign.Center,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.clickable(onClick = onInstallClicked)
.padding(horizontal = 12.dp, vertical = 12.dp)
)
}
}
}
}
@Composable
private fun AppCard(
modifier: Modifier = Modifier,
state: HomeViewModel.State,
version: String,
remoteVersion: String? = null,
progress: Int,
isHidden: Boolean,
onManagerPressed: () -> Unit,
onHideRestorePressed: () -> Unit,
) {
val actionLabel = when (state) {
HomeViewModel.State.OUTDATED -> stringResource(CoreR.string.update)
HomeViewModel.State.UP_TO_DATE -> stringResource(CoreR.string.reinstall)
else -> null
}
val actionColor = when (state) {
HomeViewModel.State.OUTDATED -> MiuixTheme.colorScheme.primary
else -> MiuixTheme.colorScheme.onSurfaceVariantActions
}
val hideRestoreIcon = if (isHidden) MiuixIcons.Show else MiuixIcons.Hide
Card(modifier = modifier) {
Column(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Icon(
painter = painterResource(R.drawable.ic_manager),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MiuixTheme.colorScheme.primary
)
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(CoreR.string.home_app_title),
style = MiuixTheme.textStyles.headline2,
)
Text(
text = version,
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
)
if (progress in 1..99) {
Spacer(Modifier.height(8.dp))
LinearProgressIndicator(
progress = progress / 100f,
modifier = Modifier.fillMaxWidth()
)
}
}
Column(
modifier = Modifier.align(Alignment.TopEnd).padding(4.dp),
verticalArrangement = Arrangement.spacedBy(0.dp),
) {
if (Info.env.isActive) {
IconButton(onClick = onHideRestorePressed) {
Icon(
imageVector = hideRestoreIcon,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = MiuixTheme.colorScheme.primary,
)
}
}
if (remoteVersion != null) {
UpdateBadge(
version = remoteVersion,
modifier = Modifier.align(Alignment.End).padding(end = 4.dp)
)
}
}
}
if (actionLabel != null) {
HorizontalDivider(thickness = 0.75.dp)
Text(
text = actionLabel,
style = MiuixTheme.textStyles.body2,
color = actionColor,
textAlign = TextAlign.Center,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.clickable(onClick = onManagerPressed)
.padding(horizontal = 12.dp, vertical = 12.dp)
)
}
}
}
}
@Composable
private fun UpdateBadge(version: String, modifier: Modifier = Modifier) {
Text(
text = version,
color = MiuixTheme.colorScheme.onPrimary,
fontSize = 10.sp,
maxLines = 1,
modifier = modifier
.background(MiuixTheme.colorScheme.primary, RoundedCornerShape(6.dp))
.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
@Composable
private fun StatusCard() {
Card(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
) {
Column(
modifier = Modifier.weight(1f).padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(CoreR.string.ramdisk),
style = MiuixTheme.textStyles.headline2,
)
Text(
text = stringResource(if (Info.ramdisk) CoreR.string.yes else CoreR.string.no),
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
)
}
VerticalDivider(thickness = 0.75.dp)
Column(
modifier = Modifier.weight(1f).padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(CoreR.string.zygisk),
style = MiuixTheme.textStyles.headline2,
)
Text(
text = stringResource(if (Info.isZygiskEnabled) CoreR.string.yes else CoreR.string.no),
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
)
}
VerticalDivider(thickness = 0.75.dp)
Column(
modifier = Modifier.weight(1f).padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(CoreR.string.denylist),
style = MiuixTheme.textStyles.headline2,
)
Text(
text = stringResource(if (Config.denyList) CoreR.string.enabled else CoreR.string.disabled),
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
)
}
}
}
}
@Composable
private fun SupportBottomSheet(
show: MutableState,
onLinkClicked: (String) -> Unit,
) {
SuperBottomSheet(
show = show,
onDismissRequest = { show.value = false },
title = stringResource(CoreR.string.home_support_title),
) {
Column(modifier = Modifier.padding(bottom = 16.dp)) {
Text(
text = stringResource(CoreR.string.home_support_content),
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
SuperArrow(
title = stringResource(CoreR.string.patreon),
onClick = {
show.value = false
onLinkClicked(Const.Url.PATREON_URL)
},
startAction = {
Icon(
painter = painterResource(CoreR.drawable.ic_patreon),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MiuixTheme.colorScheme.onSurfaceVariantActions
)
}
)
SuperArrow(
title = stringResource(CoreR.string.paypal),
onClick = {
show.value = false
onLinkClicked("https://paypal.me/magiskdonate")
},
startAction = {
Icon(
painter = painterResource(CoreR.drawable.ic_paypal),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MiuixTheme.colorScheme.onSurfaceVariantActions
)
}
)
}
}
}
private data class LinkInfo(val label: String, val icon: Int, val url: String)
private data class DeveloperInfo(val name: String, val links: List)
private val developers = listOf(
DeveloperInfo("topjohnwu", listOf(
LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/topjohnwu"),
LinkInfo("GitHub", CoreR.drawable.ic_github, Const.Url.SOURCE_CODE_URL),
)),
DeveloperInfo("vvb2060", listOf(
LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/vvb2060"),
LinkInfo("GitHub", CoreR.drawable.ic_github, "https://github.com/vvb2060"),
)),
DeveloperInfo("yujincheng08", listOf(
LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/shanasaimoe"),
LinkInfo("GitHub", CoreR.drawable.ic_github, "https://github.com/yujincheng08"),
LinkInfo("Sponsor", CoreR.drawable.ic_favorite, "https://github.com/sponsors/yujincheng08"),
)),
DeveloperInfo("rikkawww", listOf(
LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/rikkawww"),
LinkInfo("GitHub", CoreR.drawable.ic_github, "https://github.com/rikkawww"),
)),
DeveloperInfo("canyie", listOf(
LinkInfo("Twitter", CoreR.drawable.ic_twitter, "https://twitter.com/canyie2977"),
LinkInfo("GitHub", CoreR.drawable.ic_github, "https://github.com/canyie"),
)),
)
@Composable
private fun DevelopersCard(onLinkClicked: (String) -> Unit) {
var selectedDev by remember { mutableStateOf(null) }
val showSheet = rememberSaveable { mutableStateOf(false) }
Card(modifier = Modifier.fillMaxWidth()) {
developers.forEach { dev ->
SuperArrow(
title = "@${dev.name}",
onClick = {
selectedDev = dev
showSheet.value = true
}
)
}
}
val currentDev = selectedDev
if (currentDev != null) {
SuperBottomSheet(
show = showSheet,
onDismissRequest = {
showSheet.value = false
selectedDev = null
},
title = "@${currentDev.name}",
) {
Column(modifier = Modifier.padding(bottom = 16.dp)) {
currentDev.links.forEach { link ->
SuperArrow(
title = link.label,
onClick = {
showSheet.value = false
onLinkClicked(link.url)
selectedDev = null
},
startAction = {
Icon(
painter = painterResource(link.icon),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MiuixTheme.colorScheme.onSurfaceVariantActions
)
}
)
}
}
}
}
}
private fun openLink(context: Context, url: String) {
try {
context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
} catch (_: ActivityNotFoundException) { }
}
@Composable
private fun InstallBottomSheet(
show: MutableState,
installVm: InstallViewModel,
installUiState: InstallViewModel.UiState,
) {
SuperBottomSheet(
show = show,
onDismissRequest = { show.value = false },
title = stringResource(CoreR.string.install),
) {
Column(modifier = Modifier.padding(bottom = 16.dp)) {
if (installUiState.notes.isNotEmpty()) {
Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
MarkdownText(installUiState.notes)
}
HorizontalDivider(thickness = 0.75.dp)
}
if (!installVm.skipOptions) {
InstallOptionsSection(installUiState, installVm)
}
SuperArrow(
title = stringResource(CoreR.string.select_patch_file),
summary = stringResource(CoreR.string.select_patch_file_summary),
onClick = {
show.value = false
installVm.selectMethod(InstallViewModel.Method.PATCH)
},
enabled = installUiState.step >= 1 || installVm.skipOptions
)
if (installVm.isRooted) {
SuperArrow(
title = stringResource(CoreR.string.direct_install),
summary = stringResource(CoreR.string.direct_install_summary),
onClick = {
show.value = false
installVm.selectMethod(InstallViewModel.Method.DIRECT)
installVm.install()
},
enabled = installUiState.step >= 1 || installVm.skipOptions
)
}
if (!installVm.noSecondSlot) {
SuperArrow(
title = stringResource(CoreR.string.install_inactive_slot),
summary = stringResource(CoreR.string.install_inactive_slot_summary),
onClick = {
show.value = false
installVm.selectMethod(InstallViewModel.Method.INACTIVE_SLOT)
},
enabled = installUiState.step >= 1 || installVm.skipOptions
)
}
}
}
}
@Composable
private fun InstallOptionsSection(
uiState: InstallViewModel.UiState,
viewModel: InstallViewModel
) {
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(CoreR.string.install_options_title),
style = MiuixTheme.textStyles.headline2,
)
if (uiState.step == 0) {
TextButton(
text = stringResource(CoreR.string.install_next),
onClick = { viewModel.nextStep() }
)
}
}
if (uiState.step == 0) {
Spacer(Modifier.height(8.dp))
if (!Info.isSAR) {
CheckboxRow(
label = stringResource(CoreR.string.keep_dm_verity),
checked = Config.keepVerity,
onCheckedChange = { Config.keepVerity = it }
)
}
if (Info.isFDE) {
CheckboxRow(
label = stringResource(CoreR.string.keep_force_encryption),
checked = Config.keepEnc,
onCheckedChange = { Config.keepEnc = it }
)
}
if (!Info.ramdisk) {
CheckboxRow(
label = stringResource(CoreR.string.recovery_mode),
checked = Config.recovery,
onCheckedChange = { Config.recovery = it }
)
}
}
}
}
@Composable
private fun CheckboxRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Checkbox(
checked = checked,
onCheckedChange = { onCheckedChange(it) }
)
Text(
text = label,
style = MiuixTheme.textStyles.body1,
)
}
}
@Composable
private fun UninstallComposableDialog(
showDialog: MutableState,
activity: MainActivity,
loadingDialog: LoadingDialogHandle,
) {
val scope = rememberCoroutineScope()
top.yukonga.miuix.kmp.extra.SuperDialog(
show = showDialog,
title = stringResource(CoreR.string.uninstall_magisk_title),
onDismissRequest = { showDialog.value = false },
) {
Text(
text = stringResource(CoreR.string.uninstall_magisk_msg),
style = MiuixTheme.textStyles.body1,
color = MiuixTheme.colorScheme.onSurface,
)
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
TextButton(
text = stringResource(CoreR.string.restore_img),
onClick = {
showDialog.value = false
scope.launch {
val success = loadingDialog.withLoading {
MagiskInstaller.Restore().exec()
}
activity.toast(
if (success) CoreR.string.restore_done else CoreR.string.restore_fail,
Toast.LENGTH_SHORT
)
}
},
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(20.dp))
TextButton(
text = stringResource(CoreR.string.complete_uninstall),
onClick = {
showDialog.value = false
val intent = Intent(activity, activity.javaClass).apply {
action = FlashUtils.INTENT_FLASH
putExtra(FlashUtils.EXTRA_FLASH_ACTION, Const.Value.UNINSTALL)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
activity.startActivity(intent)
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary()
)
}
}
}
@Composable
private fun ManagerInstallComposableDialog(
showDialog: MutableState,
activity: MainActivity,
) {
top.yukonga.miuix.kmp.extra.SuperDialog(
show = showDialog,
title = stringResource(CoreR.string.install),
onDismissRequest = { showDialog.value = false },
) {
MarkdownTextAsync {
val text = Info.update.note
java.io.File(activity.cacheDir, "${Info.update.versionCode}.md").writeText(text)
text
}
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
TextButton(
text = stringResource(android.R.string.cancel),
onClick = { showDialog.value = false },
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(20.dp))
TextButton(
text = stringResource(CoreR.string.install),
onClick = {
showDialog.value = false
DownloadEngine.startWithActivity(activity, Subject.App())
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary()
)
}
}
}
@Composable
private fun EnvFixComposableDialog(
showDialog: MutableState,
code: Int,
activity: MainActivity,
loadingDialog: LoadingDialogHandle,
onNavigateInstall: () -> Unit,
) {
val scope = rememberCoroutineScope()
val needsFullFix = code == 2 ||
Info.env.versionCode != com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE ||
Info.env.versionString != com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_NAME
top.yukonga.miuix.kmp.extra.SuperDialog(
show = showDialog,
title = stringResource(CoreR.string.env_fix_title),
onDismissRequest = { showDialog.value = false },
) {
Text(
text = stringResource(
if (needsFullFix) CoreR.string.env_full_fix_msg else CoreR.string.env_fix_msg
),
style = MiuixTheme.textStyles.body1,
color = MiuixTheme.colorScheme.onSurface,
)
Spacer(Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) {
TextButton(
text = stringResource(android.R.string.cancel),
onClick = { showDialog.value = false },
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(20.dp))
TextButton(
text = stringResource(android.R.string.ok),
onClick = {
showDialog.value = false
if (needsFullFix) {
onNavigateInstall()
} else {
scope.launch {
val success = loadingDialog.withLoading {
MagiskInstaller.FixEnv().exec()
}
activity.toast(
if (success) CoreR.string.reboot_delay_toast else CoreR.string.setup_fail,
Toast.LENGTH_LONG
)
if (success) {
@Suppress("DEPRECATION")
android.os.Handler(android.os.Looper.getMainLooper())
.postDelayed({ reboot() }, 5000)
}
}
}
},
modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary()
)
}
}
}
@Composable
private fun HideAppDialog(onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
val showState = rememberSaveable { mutableStateOf(true) }
var appName by rememberSaveable { mutableStateOf("Settings") }
val isError = appName.length > AppMigration.MAX_LABEL_LENGTH || appName.isBlank()
top.yukonga.miuix.kmp.extra.SuperDialog(
show = showState,
title = stringResource(CoreR.string.settings_hide_app_title),
onDismissRequest = onDismiss,
insideMargin = DpSize(24.dp, 24.dp)
) {
Column(modifier = Modifier.padding(top = 8.dp)) {
top.yukonga.miuix.kmp.basic.TextField(
value = appName,
onValueChange = { appName = it },
modifier = Modifier.fillMaxWidth(),
label = stringResource(CoreR.string.settings_app_name_hint),
)
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceBetween) {
TextButton(
text = stringResource(android.R.string.cancel),
onClick = onDismiss,
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(20.dp))
TextButton(
text = stringResource(android.R.string.ok),
onClick = { if (!isError) onConfirm(appName) },
modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary()
)
}
}
}
}
@Composable
private fun RestoreAppDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) {
val showState = rememberSaveable { mutableStateOf(true) }
top.yukonga.miuix.kmp.extra.SuperDialog(
show = showState,
title = stringResource(CoreR.string.settings_restore_app_title),
onDismissRequest = onDismiss,
insideMargin = DpSize(24.dp, 24.dp)
) {
Column(modifier = Modifier.padding(top = 8.dp)) {
Text(
text = stringResource(CoreR.string.restore_app_confirmation),
style = MiuixTheme.textStyles.body1,
color = MiuixTheme.colorScheme.onSurface,
)
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.SpaceBetween) {
TextButton(
text = stringResource(android.R.string.cancel),
onClick = onDismiss,
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(20.dp))
TextButton(
text = stringResource(android.R.string.ok),
onClick = onConfirm,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.textButtonColorsPrimary()
)
}
}
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt
================================================
package com.topjohnwu.magisk.ui.home
import android.content.ActivityNotFoundException
import android.content.Intent
import android.widget.Toast
import androidx.core.net.toUri
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.download.Subject.App
import com.topjohnwu.magisk.core.ktx.await
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.repository.NetworkService
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlin.math.roundToInt
import com.topjohnwu.magisk.core.R as CoreR
class HomeViewModel(
private val svc: NetworkService
) : AsyncLoadViewModel() {
enum class State {
LOADING, INVALID, OUTDATED, UP_TO_DATE
}
data class UiState(
val isNoticeVisible: Boolean = Config.safetyNotice,
val appState: State = State.LOADING,
val managerRemoteVersion: String = "",
val managerProgress: Int = 0,
val showUninstall: Boolean = false,
val showManagerInstall: Boolean = false,
val showHideRestore: Boolean = false,
val envFixCode: Int = 0,
)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow = _uiState.asStateFlow()
val magiskState
get() = when {
Info.isRooted && Info.env.isUnsupported -> State.OUTDATED
!Info.env.isActive -> State.INVALID
Info.env.versionCode < BuildConfig.APP_VERSION_CODE -> State.OUTDATED
else -> State.UP_TO_DATE
}
val magiskInstalledVersion: String
get() = Info.env.run {
if (isActive)
"$versionString ($versionCode)" + if (isDebug) " (D)" else ""
else
""
}
val managerInstalledVersion: String
get() = "${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})" +
if (BuildConfig.DEBUG) " (D)" else ""
companion object {
private var checkedEnv = false
}
override suspend fun doLoadWork() {
_uiState.update { it.copy(appState = State.LOADING) }
Info.fetchUpdate(svc)?.apply {
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
_uiState.update {
it.copy(
appState = if (BuildConfig.APP_VERSION_CODE < versionCode) State.OUTDATED else State.UP_TO_DATE,
managerRemoteVersion = "$version ($versionCode)" + if (isDebug) " (D)" else ""
)
}
} ?: run {
_uiState.update { it.copy(appState = State.INVALID, managerRemoteVersion = "") }
}
ensureEnv()
}
private val networkObserver: (Boolean) -> Unit = { startLoading() }
init {
Info.isConnected.observeForever(networkObserver)
}
override fun onCleared() {
super.onCleared()
Info.isConnected.removeObserver(networkObserver)
}
fun onProgressUpdate(progress: Float, subject: Subject) {
if (subject is App)
_uiState.update { it.copy(managerProgress = progress.times(100f).roundToInt()) }
}
fun resetProgress() {
_uiState.update { it.copy(managerProgress = 0) }
}
fun onLinkPressed(link: String) {
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
AppContext.startActivity(intent)
} catch (e: ActivityNotFoundException) {
AppContext.toast(CoreR.string.open_link_failed_toast, Toast.LENGTH_SHORT)
}
}
fun onDeletePressed() {
_uiState.update { it.copy(showUninstall = true) }
}
fun onUninstallConsumed() {
_uiState.update { it.copy(showUninstall = false) }
}
fun onManagerPressed() {
when (_uiState.value.appState) {
State.LOADING -> showSnackbar(CoreR.string.loading)
State.INVALID -> showSnackbar(CoreR.string.no_connection)
else -> _uiState.update { it.copy(showManagerInstall = true) }
}
}
fun onManagerInstallConsumed() {
_uiState.update { it.copy(showManagerInstall = false) }
}
fun onHideRestorePressed() {
_uiState.update { it.copy(showHideRestore = true) }
}
fun onHideRestoreConsumed() {
_uiState.update { it.copy(showHideRestore = false) }
}
fun onEnvFixConsumed() {
_uiState.update { it.copy(envFixCode = 0) }
}
fun hideNotice() {
Config.safetyNotice = false
_uiState.update { it.copy(isNoticeVisible = false) }
}
private suspend fun ensureEnv() {
if (magiskState == State.INVALID || checkedEnv) return
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
val code = Shell.cmd(cmd).await().code
if (code != 0) {
_uiState.update { it.copy(envFixCode = code) }
}
checkedEnv = true
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt
================================================
package com.topjohnwu.magisk.ui.install
import android.net.Uri
import android.widget.Toast
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.AppContext
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.ktx.toast
import com.topjohnwu.magisk.core.repository.NetworkService
import com.topjohnwu.magisk.ui.navigation.Route
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.IOException
import com.topjohnwu.magisk.core.R as CoreR
class InstallViewModel(svc: NetworkService) : BaseViewModel() {
enum class Method { NONE, PATCH, DIRECT, INACTIVE_SLOT }
data class UiState(
val step: Int = 0,
val method: Method = Method.NONE,
val notes: String = "",
val patchUri: Uri? = null,
val requestFilePicker: Boolean = false,
val showSecondSlotWarning: Boolean = false,
)
val isRooted get() = Info.isRooted
val skipOptions = Info.isEmulator || (Info.isSAR && !Info.isFDE && Info.ramdisk)
val noSecondSlot = !isRooted || !Info.isAB || Info.isEmulator
private val _uiState = MutableStateFlow(UiState(step = if (skipOptions) 1 else 0))
val uiState: StateFlow = _uiState.asStateFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
try {
val noteFile = File(AppContext.cacheDir, "${APP_VERSION_CODE}.md")
val noteText = when {
noteFile.exists() -> noteFile.readText()
else -> {
val note = svc.fetchUpdate(APP_VERSION_CODE)?.note.orEmpty()
if (note.isEmpty()) return@launch
noteFile.writeText(note)
note
}
}
withContext(Dispatchers.Main) {
_uiState.update { it.copy(notes = noteText) }
}
} catch (e: IOException) {
Timber.e(e)
}
}
}
fun nextStep() {
_uiState.update { it.copy(step = 1) }
}
fun selectMethod(method: Method) {
_uiState.update { it.copy(method = method) }
when (method) {
Method.PATCH -> {
AppContext.toast(CoreR.string.patch_file_msg, Toast.LENGTH_LONG)
_uiState.update { it.copy(requestFilePicker = true) }
}
Method.INACTIVE_SLOT -> {
_uiState.update { it.copy(showSecondSlotWarning = true) }
}
else -> {}
}
}
fun onFilePickerConsumed() {
_uiState.update { it.copy(requestFilePicker = false) }
}
fun onSecondSlotWarningConsumed() {
_uiState.update { it.copy(showSecondSlotWarning = false) }
}
fun onPatchFileSelected(uri: Uri) {
_uiState.update { it.copy(patchUri = uri) }
if (_uiState.value.method == Method.PATCH) {
install()
}
}
fun install() {
when (_uiState.value.method) {
Method.PATCH -> navigateTo(Route.Flash(
action = Const.Value.PATCH_FILE,
additionalData = _uiState.value.patchUri!!.toString()
))
Method.DIRECT -> navigateTo(Route.Flash(
action = Const.Value.FLASH_MAGISK
))
Method.INACTIVE_SLOT -> navigateTo(Route.Flash(
action = Const.Value.FLASH_INACTIVE_SLOT
))
else -> error("Unknown method")
}
}
val canInstall: Boolean
get() {
val state = _uiState.value
return when (state.method) {
Method.PATCH -> state.patchUri != null
Method.DIRECT, Method.INACTIVE_SLOT -> true
Method.NONE -> false
}
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/log/LogScreen.kt
================================================
package com.topjohnwu.magisk.ui.log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.topjohnwu.magisk.core.ktx.timeDateFormat
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.model.su.SuLog
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.TabRow
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Delete
import top.yukonga.miuix.kmp.icon.extended.Download
import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun LogScreen(viewModel: LogViewModel) {
val uiState by viewModel.uiState.collectAsState()
var selectedTab by rememberSaveable { mutableIntStateOf(0) }
val tabTitles = listOf(
stringResource(CoreR.string.superuser),
stringResource(CoreR.string.magisk)
)
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = stringResource(CoreR.string.logs),
actions = {
if (selectedTab == 1) {
IconButton(onClick = { viewModel.saveMagiskLog() }) {
Icon(
imageVector = MiuixIcons.Download,
contentDescription = stringResource(CoreR.string.save_log),
)
}
}
IconButton(
modifier = Modifier.padding(end = 16.dp),
onClick = {
if (selectedTab == 0) viewModel.clearLog()
else viewModel.clearMagiskLog()
}
) {
Icon(
imageVector = MiuixIcons.Delete,
contentDescription = stringResource(CoreR.string.clear_log),
)
}
},
scrollBehavior = scrollBehavior
)
},
popupHost = { }
) { padding ->
Column(modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
TabRow(
tabs = tabTitles,
selectedTabIndex = selectedTab,
onTabSelected = { selectedTab = it },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp)
)
if (uiState.loading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
when (selectedTab) {
0 -> SuLogTab(
logs = uiState.suLogs,
nestedScrollConnection = scrollBehavior.nestedScrollConnection
)
1 -> MagiskLogTab(
entries = uiState.magiskLogEntries,
nestedScrollConnection = scrollBehavior.nestedScrollConnection
)
}
}
}
}
}
@Composable
private fun SuLogTab(logs: List, nestedScrollConnection: NestedScrollConnection) {
Column(modifier = Modifier.fillMaxSize()) {
if (logs.isEmpty()) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(CoreR.string.log_data_none),
style = MiuixTheme.textStyles.body1,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
textAlign = TextAlign.Center,
)
}
} else {
LazyColumn(
modifier = Modifier
.weight(1f)
.nestedScroll(nestedScrollConnection)
.padding(horizontal = 12.dp),
contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(logs, key = { it.id }) { log ->
SuLogCard(log)
}
}
}
}
}
@Composable
private fun SuLogCard(log: SuLog) {
val res = LocalContext.current.resources
val pm = LocalContext.current.packageManager
val icon = remember(log.packageName) {
runCatching {
pm.getApplicationInfo(log.packageName, 0).loadIcon(pm)
}.getOrDefault(pm.defaultActivityIcon)
}
val allowed = log.action >= 2
val uidPidText = buildString {
append("UID: ${log.toUid} PID: ${log.fromPid}")
if (log.target != -1) {
val target = if (log.target == 0) "magiskd" else log.target.toString()
append(" → $target")
}
}
val details = buildString {
if (log.context.isNotEmpty()) {
append(res.getString(CoreR.string.selinux_context, log.context))
}
if (log.gids.isNotEmpty()) {
if (isNotEmpty()) append("\n")
append(res.getString(CoreR.string.supp_group, log.gids))
}
if (log.command.isNotEmpty()) {
if (isNotEmpty()) append("\n")
append(log.command)
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(12.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top
) {
Image(
painter = rememberDrawablePainter(icon),
contentDescription = log.appName,
modifier = Modifier.size(36.dp)
)
Spacer(Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = log.appName,
style = MiuixTheme.textStyles.body1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = uidPidText,
style = MiuixTheme.textStyles.body2,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Spacer(Modifier.width(8.dp))
Column(horizontalAlignment = Alignment.End) {
Text(
text = log.time.toTime(timeDateFormat),
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
maxLines = 1,
)
Spacer(Modifier.height(4.dp))
SuActionBadge(allowed)
}
}
if (details.isNotEmpty()) {
Spacer(Modifier.height(6.dp))
Text(
text = details,
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
lineHeight = 16.sp,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
)
}
}
}
}
@Composable
private fun SuActionBadge(allowed: Boolean) {
val bg = if (allowed) MiuixTheme.colorScheme.primary else MiuixTheme.colorScheme.error
val fg = if (allowed) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onError
val text = if (allowed) "Approved" else "Rejected"
Text(
text = text,
color = fg,
fontSize = 10.sp,
maxLines = 1,
modifier = Modifier
.background(bg, RoundedCornerShape(6.dp))
.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
@Composable
private fun MagiskLogTab(
entries: List,
nestedScrollConnection: NestedScrollConnection
) {
Column(modifier = Modifier.fillMaxSize()) {
if (entries.isEmpty()) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(CoreR.string.log_data_magisk_none),
style = MiuixTheme.textStyles.body1,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
textAlign = TextAlign.Center,
)
}
} else {
val listState = rememberLazyListState(initialFirstVisibleItemIndex = entries.size - 1)
LazyColumn(
state = listState,
modifier = Modifier
.weight(1f)
.nestedScroll(nestedScrollConnection)
.padding(horizontal = 12.dp),
contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(entries.size, key = { it }) { index ->
MagiskLogCard(entries[index])
}
}
}
}
}
@Composable
private fun MagiskLogCard(entry: MagiskLogEntry) {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = !expanded }
) {
Column(modifier = Modifier.padding(12.dp)) {
if (entry.isParsed) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.weight(1f)
) {
LogLevelBadge(entry.level)
Text(
text = entry.tag,
style = MiuixTheme.textStyles.body1,
fontWeight = FontWeight.Normal,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Spacer(Modifier.width(8.dp))
Text(
text = entry.timestamp,
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
maxLines = 1,
)
}
Spacer(Modifier.height(4.dp))
}
Text(
text = entry.message,
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
lineHeight = 16.sp,
color = MiuixTheme.colorScheme.onSurface,
maxLines = if (expanded) Int.MAX_VALUE else 3,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun LogLevelBadge(level: Char) {
val (bg, fg) = when (level) {
'V' -> Color(0xFF9E9E9E) to Color.White
'D' -> Color(0xFF2196F3) to Color.White
'I' -> Color(0xFF4CAF50) to Color.White
'W' -> Color(0xFFFFC107) to Color.Black
'E' -> Color(0xFFF44336) to Color.White
'F' -> Color(0xFF9C27B0) to Color.White
else -> Color(0xFF757575) to Color.White
}
Box(
modifier = Modifier
.clip(RoundedCornerShape(4.dp))
.background(bg)
.padding(horizontal = 5.dp, vertical = 1.dp),
contentAlignment = Alignment.Center
) {
Text(
text = level.toString(),
fontSize = 10.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace,
color = fg,
)
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt
================================================
package com.topjohnwu.magisk.ui.log
import android.system.Os
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
import com.topjohnwu.magisk.core.BuildConfig
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.R
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.model.su.SuLog
import com.topjohnwu.magisk.core.repository.LogRepository
import com.topjohnwu.magisk.core.su.SuEvents
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileInputStream
class LogViewModel(
private val repo: LogRepository
) : AsyncLoadViewModel() {
init {
@OptIn(kotlinx.coroutines.FlowPreview::class)
viewModelScope.launch {
SuEvents.logUpdated.debounce(500).collect { reload() }
}
}
data class UiState(
val loading: Boolean = true,
val magiskLog: String = "",
val magiskLogEntries: List = emptyList(),
val suLogs: List = emptyList(),
)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow = _uiState.asStateFlow()
private var magiskLogRaw = ""
override suspend fun doLoadWork() {
_uiState.update { it.copy(loading = true) }
withContext(Dispatchers.Default) {
magiskLogRaw = repo.fetchMagiskLogs()
val suLogs = repo.fetchSuLogs()
val entries = MagiskLogParser.parse(magiskLogRaw)
_uiState.update { it.copy(
loading = false,
magiskLog = magiskLogRaw,
magiskLogEntries = entries,
suLogs = suLogs,
) }
}
}
fun saveMagiskLog() {
viewModelScope.launch(Dispatchers.IO) {
val filename = "magisk_log_%s.log".format(
System.currentTimeMillis().toTime(timeFormatStandard))
val logFile = MediaStoreUtils.getFile(filename)
logFile.uri.outputStream().bufferedWriter().use { file ->
file.write("---Detected Device Info---\n\n")
file.write("isAB=${Info.isAB}\n")
file.write("isSAR=${Info.isSAR}\n")
file.write("ramdisk=${Info.ramdisk}\n")
val uname = Os.uname()
file.write("kernel=${uname.sysname} ${uname.machine} ${uname.release} ${uname.version}\n")
file.write("\n\n---System Properties---\n\n")
ProcessBuilder("getprop").start()
.inputStream.reader().use { it.copyTo(file) }
file.write("\n\n---Environment Variables---\n\n")
System.getenv().forEach { (key, value) -> file.write("${key}=${value}\n") }
file.write("\n\n---System MountInfo---\n\n")
FileInputStream("/proc/self/mountinfo").reader().use { it.copyTo(file) }
file.write("\n---Magisk Logs---\n")
file.write("${Info.env.versionString} (${Info.env.versionCode})\n\n")
if (Info.env.isActive) file.write(magiskLogRaw)
file.write("\n---Manager Logs---\n")
file.write("${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})\n\n")
ProcessBuilder("logcat", "-d").start()
.inputStream.reader().use { it.copyTo(file) }
}
showSnackbar(logFile.toString())
}
}
fun clearMagiskLog() = repo.clearMagiskLogs {
showSnackbar(R.string.logs_cleared)
startLoading()
}
fun clearLog() = viewModelScope.launch {
repo.clearLogs()
showSnackbar(R.string.logs_cleared)
startLoading()
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/log/MagiskLogParser.kt
================================================
package com.topjohnwu.magisk.ui.log
data class MagiskLogEntry(
val timestamp: String = "",
val pid: Int = 0,
val tid: Int = 0,
val level: Char = 'I',
val tag: String = "",
val message: String = "",
val isParsed: Boolean = false,
)
object MagiskLogParser {
// Logcat format: "MM-DD HH:MM:SS.mmm PID TID LEVEL TAG : message"
private val logcatRegex = Regex(
"""(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([VDIWEF])\s+(.+?)\s*:\s+(.*)"""
)
fun parse(raw: String): List {
if (raw.isBlank()) return emptyList()
val lines = raw.lines()
val result = mutableListOf()
for (line in lines) {
if (line.isBlank()) continue
val match = logcatRegex.find(line)
if (match != null) {
result.add(
MagiskLogEntry(
timestamp = match.groupValues[1],
pid = match.groupValues[2].toIntOrNull() ?: 0,
tid = match.groupValues[3].toIntOrNull() ?: 0,
level = match.groupValues[4].firstOrNull() ?: 'I',
tag = match.groupValues[5].trim(),
message = match.groupValues[6],
isParsed = true,
)
)
} else if (result.isNotEmpty() && result.last().isParsed) {
// Continuation line — append to previous entry
val prev = result.last()
result[result.lastIndex] = prev.copy(
message = prev.message + "\n" + line.trimEnd()
)
} else {
result.add(
MagiskLogEntry(message = line.trimEnd())
)
}
}
return result
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/module/ActionScreen.kt
================================================
package com.topjohnwu.magisk.ui.module
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.ui.terminal.TerminalScreen
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Back
import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> Unit) {
val actionState by viewModel.actionState.collectAsState()
val finished = actionState != ActionViewModel.State.RUNNING
val scrollBehavior = MiuixScrollBehavior()
Scaffold(
topBar = {
SmallTopAppBar(
title = actionName,
navigationIcon = {
IconButton(
modifier = Modifier.padding(start = 16.dp),
onClick = onBack
) {
Icon(
imageVector = MiuixIcons.Back,
contentDescription = null,
tint = MiuixTheme.colorScheme.onBackground
)
}
},
actions = {
if (finished) {
IconButton(
modifier = Modifier.padding(end = 16.dp),
onClick = { viewModel.saveLog() }
) {
Icon(
painter = painterResource(R.drawable.ic_save_md2),
contentDescription = stringResource(CoreR.string.menuSaveLog),
tint = MiuixTheme.colorScheme.onBackground
)
}
}
},
scrollBehavior = scrollBehavior
)
},
popupHost = { }
) { padding ->
TerminalScreen(
modifier = Modifier
.fillMaxSize()
.padding(padding),
onEmulatorCreated = { viewModel.onEmulatorCreated(it) },
)
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/module/ActionViewModel.kt
================================================
package com.topjohnwu.magisk.ui.module
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
import com.topjohnwu.magisk.core.ktx.toTime
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.terminal.TerminalEmulator
import com.topjohnwu.magisk.terminal.runSuCommand
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ActionViewModel : BaseViewModel() {
enum class State {
RUNNING, SUCCESS, FAILED
}
private val _actionState = MutableStateFlow(State.RUNNING)
val actionState: StateFlow = _actionState.asStateFlow()
var actionId: String = ""
var actionName: String = ""
private var emulator: TerminalEmulator? = null
private val emulatorReady = CompletableDeferred()
fun onEmulatorCreated(emu: TerminalEmulator) {
emulator = emu
emulatorReady.complete(emu)
}
fun startRunAction() {
viewModelScope.launch {
val emu = emulatorReady.await()
val success = withContext(Dispatchers.IO) {
runSuCommand(
emu,
"cd /data/adb/modules/$actionId && sh ./action.sh"
)
}
_actionState.value = if (success) State.SUCCESS else State.FAILED
}
}
fun saveLog() {
viewModelScope.launch(Dispatchers.IO) {
val name = "%s_action_log_%s.log".format(
actionName,
System.currentTimeMillis().toTime(timeFormatStandard)
)
val file = MediaStoreUtils.getFile(name)
file.uri.outputStream().bufferedWriter().use { writer ->
val transcript = emulator?.screen?.transcriptText
if (transcript != null) {
writer.write(transcript)
}
}
showSnackbar(file.toString())
}
}
}
================================================
FILE: app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/module/ModuleScreen.kt
================================================
package com.topjohnwu.magisk.ui.module
import android.net.Uri
import android.provider.OpenableColumns
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.di.ServiceLocator
import com.topjohnwu.magisk.core.download.DownloadEngine
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.component.ConfirmResult
import com.topjohnwu.magisk.ui.component.MarkdownTextAsync
import com.topjohnwu.magisk.ui.component.rememberConfirmDialog
import kotlinx.coroutines.launch
import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
import top.yukonga.miuix.kmp.basic.FloatingActionButton
import top.yukonga.miuix.kmp.basic.HorizontalDivider
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.Switch
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.extra.SuperDialog
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Add
import top.yukonga.miuix.kmp.icon.extended.Delete
import top.yukonga.miuix.kmp.icon.extended.Play
import top.yukonga.miuix.kmp.icon.extended.Undo
import top.yukonga.miuix.kmp.icon.extended.UploadCloud
import top.yukonga.miuix.kmp.theme.MiuixTheme
import com.topjohnwu.magisk.core.R as CoreR
@Composable
fun ModuleScreen(viewModel: ModuleViewModel) {
val uiState by viewModel.uiState.collectAsState()
val scrollBehavior = MiuixScrollBehavior()
val colorScheme = MiuixTheme.colorScheme
val context = LocalContext.current
val scope = rememberCoroutineScope()
val activity = context as MainActivity
var pendingZipUri by remember { mutableStateOf