From aa480474b39a6bd4515cebcf72d3ce07d854f23c Mon Sep 17 00:00:00 2001 From: Fredrik Fornwall Date: Sun, 8 Oct 2023 22:13:48 +0200 Subject: [PATCH] WIP: Android 10+ as target and Google Play distribution support --- .github/workflows/debug_build.yml | 17 +- .github/workflows/run_tests.yml | 5 + LICENSE.md | 1 - README.md | 14 - app/build.gradle | 75 +- app/proguard-rules.pro | 5 - app/src/main/AndroidManifest.xml | 131 +- .../com/termux/app/RunCommandService.java | 287 --- .../java/com/termux/app/TermuxActivity.java | 714 ++---- .../java/com/termux/app/TermuxAppShell.java | 135 ++ .../com/termux/app/TermuxApplication.java | 85 - .../java/com/termux/app/TermuxConstants.java | 14 + .../TermuxDocumentsProvider.java | 7 +- ...y.java => TermuxFileReceiverActivity.java} | 128 +- ...pActivity.java => TermuxHelpActivity.java} | 13 +- .../java/com/termux/app/TermuxInstaller.java | 217 +- .../termux/app/TermuxMessageDialogUtils.java | 52 +- .../com/termux/app/TermuxOpenReceiver.java | 52 +- .../com/termux/app/TermuxPermissionUtils.java | 171 ++ .../com/termux/app/TermuxPreferences.java | 87 + .../java/com/termux/app/TermuxProperties.java | 55 + .../java/com/termux/app/TermuxService.java | 805 ++----- .../java/com/termux/app/TermuxSession.java | 105 + .../TermuxSessionsListViewController.java | 23 +- .../com/termux/app/TermuxShellManager.java | 37 + .../com/termux/app}/TermuxShellUtils.java | 111 +- .../TermuxTerminalSessionActivityClient.java | 288 ++- .../termux/app/TermuxTerminalViewClient.java | 413 ++++ .../java/com/termux/app/TermuxUrlUtils.java | 235 ++ .../app/activities/SettingsActivity.java | 169 -- .../termux/app/event/SystemEventReceiver.java | 91 - .../termux/app}/extrakeys/ExtraKeyButton.java | 2 +- .../app}/extrakeys/ExtraKeysConstants.java | 6 +- .../termux/app}/extrakeys/ExtraKeysInfo.java | 43 +- .../termux/app}/extrakeys/ExtraKeysView.java | 305 ++- .../io => extrakeys}/KeyboardShortcut.java | 2 +- .../termux/app}/extrakeys/SpecialButton.java | 2 +- .../app}/extrakeys/SpecialButtonState.java | 2 +- .../TerminalToolbarViewPager.java | 19 +- .../extrakeys/TermuxTerminalExtraKeys.java | 179 ++ .../TermuxAPIPreferencesFragment.java | 49 - .../TermuxFloatPreferencesFragment.java | 49 - .../settings/TermuxPreferencesFragment.java | 49 - .../TermuxTaskerPreferencesFragment.java | 49 - .../TermuxWidgetPreferencesFragment.java | 49 - .../termux/DebuggingPreferencesFragment.java | 155 -- .../termux/TerminalIOPreferencesFragment.java | 82 - .../TerminalViewPreferencesFragment.java | 77 - .../DebuggingPreferencesFragment.java | 101 - .../DebuggingPreferencesFragment.java | 126 - .../DebuggingPreferencesFragment.java | 101 - .../DebuggingPreferencesFragment.java | 101 - .../com/termux/app/models/UserAction.java | 18 - .../app/terminal/TermuxActivityRootView.java | 284 --- .../TermuxTerminalSessionServiceClient.java | 31 - .../terminal/TermuxTerminalViewClient.java | 802 ------- .../app/terminal/io/FullScreenWorkAround.java | 68 - .../terminal/io/TermuxTerminalExtraKeys.java | 108 - app/src/main/res/layout/activity_settings.xml | 16 - app/src/main/res/layout/activity_termux.xml | 31 +- .../res/layout/preference_markdown_text.xml | 20 - .../view_terminal_toolbar_extra_keys.xml | 2 +- .../src/main/res/raw/bell.ogg | Bin app/src/main/res/values-night/themes.xml | 6 +- app/src/main/res/values/attrs.xml | 5 + app/src/main/res/values/colors.xml | 20 + app/src/main/res/values/strings.xml | 174 +- app/src/main/res/values/styles.xml | 4 +- app/src/main/res/values/themes.xml | 14 +- app/src/main/res/xml/root_preferences.xml | 49 - .../xml/termux_api_debugging_preferences.xml | 15 - .../main/res/xml/termux_api_preferences.xml | 8 - .../res/xml/termux_debugging_preferences.xml | 33 - .../termux_float_debugging_preferences.xml | 21 - .../main/res/xml/termux_float_preferences.xml | 8 - app/src/main/res/xml/termux_preferences.xml | 18 - .../termux_tasker_debugging_preferences.xml | 15 - .../res/xml/termux_tasker_preferences.xml | 8 - .../xml/termux_terminal_io_preferences.xml | 21 - .../xml/termux_terminal_view_preferences.xml | 15 - .../termux_widget_debugging_preferences.xml | 15 - .../res/xml/termux_widget_preferences.xml | 8 - .../com/termux/app/TermuxActivityTest.java | 2 - ...va => TermuxFileReceiverActivityTest.java} | 10 +- build.gradle | 6 +- gradle.properties | 11 +- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 294 ++- gradlew.bat | 15 +- settings.gradle | 2 +- terminal-emulator/build.gradle | 8 +- .../src/main/AndroidManifest.xml | 2 +- .../main/java/com/termux/terminal/Logger.java | 37 +- .../com/termux/terminal/TerminalEmulator.java | 324 ++- .../com/termux/terminal/TerminalSession.java | 15 +- .../terminal/TerminalSessionClient.java | 22 - terminal-view/build.gradle | 8 +- terminal-view/src/main/AndroidManifest.xml | 2 +- .../view/GestureAndScaleRecognizer.java | 4 +- .../com/termux/view/TerminalRenderer.java | 8 +- .../java/com/termux/view/TerminalView.java | 280 +-- .../com/termux/view/TerminalViewClient.java | 30 - .../support/PopupWindowCompatGingerbread.java | 75 - .../TextSelectionCursorController.java | 6 - .../TextSelectionHandleView.java | 11 +- termux-shared/.gitignore | 1 - termux-shared/LICENSE.md | 22 - termux-shared/build.gradle | 90 - termux-shared/proguard-rules.pro | 10 - .../shared/ExampleInstrumentedTest.java | 26 - termux-shared/src/main/AndroidManifest.xml | 5 - termux-shared/src/main/cpp/Android.mk | 6 - termux-shared/src/main/cpp/Application.mk | 1 - termux-shared/src/main/cpp/local-socket.cpp | 603 ----- .../shared/activities/ReportActivity.java | 476 ---- .../shared/activities/TextIOActivity.java | 278 --- .../termux/shared/activity/ActivityErrno.java | 20 - .../termux/shared/activity/ActivityUtils.java | 137 -- .../media/AppCompatActivityUtils.java | 120 - .../termux/shared/android/AndroidUtils.java | 270 --- .../shared/android/FeatureFlagUtils.java | 169 -- .../termux/shared/android/PackageUtils.java | 830 ------- .../shared/android/PermissionUtils.java | 573 ----- .../shared/android/PhantomProcessUtils.java | 115 - .../termux/shared/android/ProcessUtils.java | 58 - .../termux/shared/android/SELinuxUtils.java | 96 - .../shared/android/SettingsProviderUtils.java | 99 - .../com/termux/shared/android/UserUtils.java | 143 -- .../android/resource/ResourceUtils.java | 136 -- .../com/termux/shared/crash/CrashHandler.java | 158 -- .../com/termux/shared/data/DataUtils.java | 258 --- .../com/termux/shared/data/IntentUtils.java | 166 -- .../java/com/termux/shared/errors/Errno.java | 118 - .../java/com/termux/shared/errors/Error.java | 298 --- .../termux/shared/errors/FunctionErrno.java | 22 - .../com/termux/shared/file/FileUtils.java | 2044 ----------------- .../termux/shared/file/FileUtilsErrno.java | 111 - .../file/filesystem/FileAttributes.java | 418 ---- .../shared/file/filesystem/FileKey.java | 68 - .../file/filesystem/FilePermission.java | 88 - .../file/filesystem/FilePermissions.java | 145 -- .../shared/file/filesystem/FileTime.java | 156 -- .../shared/file/filesystem/FileType.java | 32 - .../shared/file/filesystem/FileTypes.java | 119 - .../file/filesystem/NativeDispatcher.java | 58 - .../shared/file/filesystem/UnixConstants.java | 158 -- .../shared/file/tests/FileUtilsTests.java | 396 ---- .../shared/interact/MessageDialogUtils.java | 99 - .../termux/shared/interact/ShareUtils.java | 225 -- .../termux/shared/jni/models/JniResult.java | 109 - .../java/com/termux/shared/logger/Logger.java | 502 ---- .../termux/shared/markdown/MarkdownUtils.java | 207 -- .../com/termux/shared/models/ReportInfo.java | 98 - .../com/termux/shared/models/TextIOInfo.java | 234 -- .../net/socket/local/ILocalSocketManager.java | 72 - .../net/socket/local/LocalClientSocket.java | 483 ---- .../net/socket/local/LocalServerSocket.java | 303 --- .../net/socket/local/LocalSocketErrno.java | 43 - .../net/socket/local/LocalSocketManager.java | 450 ---- .../local/LocalSocketManagerClientBase.java | 47 - .../socket/local/LocalSocketRunConfig.java | 265 --- .../shared/net/socket/local/PeerCred.java | 142 -- .../com/termux/shared/net/uri/UriScheme.java | 28 - .../com/termux/shared/net/uri/UriUtils.java | 102 - .../com/termux/shared/net/url/UrlUtils.java | 113 - .../notification/NotificationUtils.java | 146 -- .../shared/reflection/ReflectionUtils.java | 282 --- .../preferences/AppSharedPreferences.java | 49 - .../preferences/SharedPreferenceUtils.java | 432 ---- .../settings/properties/SharedProperties.java | 645 ------ .../properties/SharedPropertiesParser.java | 37 - .../shared/shell/ArgumentTokenizer.java | 229 -- .../com/termux/shared/shell/ShellUtils.java | 76 - .../termux/shared/shell/StreamGobbler.java | 325 --- .../shared/shell/am/AmSocketServer.java | 258 --- .../shared/shell/am/AmSocketServerErrno.java | 18 - .../shell/am/AmSocketServerRunConfig.java | 108 - .../shell/command/ExecutionCommand.java | 691 ------ .../shell/command/ShellCommandConstants.java | 75 - .../environment/AndroidShellEnvironment.java | 100 - .../environment/IShellEnvironment.java | 52 - .../ShellCommandShellEnvironment.java | 62 - .../environment/ShellEnvironmentUtils.java | 180 -- .../environment/ShellEnvironmentVariable.java | 28 - .../environment/UnixShellEnvironment.java | 83 - .../shell/command/result/ResultConfig.java | 170 -- .../shell/command/result/ResultData.java | 258 --- .../shell/command/result/ResultSender.java | 349 --- .../command/result/ResultSenderErrno.java | 22 - .../shell/command/runner/app/AppShell.java | 349 --- .../termux/shared/termux/TermuxBootstrap.java | 219 -- .../termux/shared/termux/TermuxConstants.java | 1296 ----------- .../com/termux/shared/termux/TermuxUtils.java | 730 ------ .../shared/termux/crash/TermuxCrashUtils.java | 411 ---- .../shared/termux/data/TermuxUrlUtils.java | 104 - .../shared/termux/file/TermuxFileUtils.java | 414 ---- .../shared/termux/models/UserAction.java | 18 - .../notification/TermuxNotificationUtils.java | 109 - .../termux/plugins/TermuxPluginUtils.java | 469 ---- .../TermuxAPIAppSharedPreferences.java | 84 - .../TermuxAppSharedPreferences.java | 261 --- .../TermuxBootAppSharedPreferences.java | 75 - .../TermuxFloatAppSharedPreferences.java | 161 -- .../TermuxPreferenceConstants.java | 319 --- .../TermuxStylingAppSharedPreferences.java | 75 - .../TermuxTaskerAppSharedPreferences.java | 85 - .../TermuxWidgetAppSharedPreferences.java | 94 - .../properties/TermuxAppSharedProperties.java | 42 - .../properties/TermuxPropertyConstants.java | 481 ---- .../properties/TermuxSharedProperties.java | 721 ------ .../termux/shell/TermuxShellManager.java | 123 - .../termux/shell/am/TermuxAmSocketServer.java | 232 -- .../TermuxAPIShellEnvironment.java | 43 - .../TermuxAppShellEnvironment.java | 172 -- .../TermuxShellCommandShellEnvironment.java | 48 - .../environment/TermuxShellEnvironment.java | 117 - .../runner/terminal/TermuxSession.java | 296 --- .../TermuxTerminalSessionClientBase.java | 94 - .../TermuxTerminalViewClientBase.java | 127 - .../termux/terminal/io/BellHandler.java | 79 - .../termux/terminal/io/TerminalExtraKeys.java | 85 - .../shared/termux/theme/TermuxThemeUtils.java | 25 - .../com/termux/shared/theme/NightMode.java | 91 - .../com/termux/shared/theme/ThemeUtils.java | 86 - .../com/termux/shared/view/KeyboardUtils.java | 198 -- .../com/termux/shared/view/ViewUtils.java | 247 -- .../src/main/res/drawable/ic_copy.xml | 5 - .../res/drawable/ic_error_notification.xml | 37 - .../src/main/res/drawable/ic_info.xml | 5 - .../src/main/res/drawable/ic_settings.xml | 5 - .../src/main/res/drawable/ic_share.xml | 5 - .../src/main/res/layout/activity_report.xml | 21 - .../src/main/res/layout/activity_text_io.xml | 93 - .../main/res/layout/dialog_show_message.xml | 50 - .../markdown_adapter_node_code_block.xml | 28 - .../layout/markdown_adapter_node_default.xml | 16 - .../res/layout/partial_primary_toolbar.xml | 22 - .../src/main/res/menu/menu_report.xml | 20 - .../src/main/res/menu/menu_text_io.xml | 20 - .../src/main/res/raw/apt_info_script.sh | 59 - termux-shared/src/main/res/raw/keep.xml | 3 - .../src/main/res/values-night/themes.xml | 65 - termux-shared/src/main/res/values/attrs.xml | 7 - termux-shared/src/main/res/values/colors.xml | 23 - termux-shared/src/main/res/values/dimens.xml | 11 - termux-shared/src/main/res/values/strings.xml | 138 -- termux-shared/src/main/res/values/styles.xml | 67 - termux-shared/src/main/res/values/themes.xml | 102 - 249 files changed, 2949 insertions(+), 32830 deletions(-) delete mode 100644 app/src/main/java/com/termux/app/RunCommandService.java create mode 100644 app/src/main/java/com/termux/app/TermuxAppShell.java delete mode 100644 app/src/main/java/com/termux/app/TermuxApplication.java create mode 100644 app/src/main/java/com/termux/app/TermuxConstants.java rename app/src/main/java/com/termux/{filepicker => app}/TermuxDocumentsProvider.java (98%) rename app/src/main/java/com/termux/app/{api/file/FileReceiverActivity.java => TermuxFileReceiverActivity.java} (55%) rename app/src/main/java/com/termux/app/{activities/HelpActivity.java => TermuxHelpActivity.java} (86%) rename termux-shared/src/main/java/com/termux/shared/termux/interact/TextInputDialogUtils.java => app/src/main/java/com/termux/app/TermuxMessageDialogUtils.java (58%) create mode 100644 app/src/main/java/com/termux/app/TermuxPermissionUtils.java create mode 100644 app/src/main/java/com/termux/app/TermuxPreferences.java create mode 100644 app/src/main/java/com/termux/app/TermuxProperties.java create mode 100644 app/src/main/java/com/termux/app/TermuxSession.java rename app/src/main/java/com/termux/app/{terminal => }/TermuxSessionsListViewController.java (81%) create mode 100644 app/src/main/java/com/termux/app/TermuxShellManager.java rename {termux-shared/src/main/java/com/termux/shared/termux/shell => app/src/main/java/com/termux/app}/TermuxShellUtils.java (50%) rename app/src/main/java/com/termux/app/{terminal => }/TermuxTerminalSessionActivityClient.java (66%) create mode 100644 app/src/main/java/com/termux/app/TermuxTerminalViewClient.java create mode 100644 app/src/main/java/com/termux/app/TermuxUrlUtils.java delete mode 100644 app/src/main/java/com/termux/app/activities/SettingsActivity.java delete mode 100644 app/src/main/java/com/termux/app/event/SystemEventReceiver.java rename {termux-shared/src/main/java/com/termux/shared/termux => app/src/main/java/com/termux/app}/extrakeys/ExtraKeyButton.java (99%) rename {termux-shared/src/main/java/com/termux/shared/termux => app/src/main/java/com/termux/app}/extrakeys/ExtraKeysConstants.java (99%) rename {termux-shared/src/main/java/com/termux/shared/termux => app/src/main/java/com/termux/app}/extrakeys/ExtraKeysInfo.java (75%) rename {termux-shared/src/main/java/com/termux/shared/termux => app/src/main/java/com/termux/app}/extrakeys/ExtraKeysView.java (70%) rename app/src/main/java/com/termux/app/{terminal/io => extrakeys}/KeyboardShortcut.java (87%) rename {termux-shared/src/main/java/com/termux/shared/termux => app/src/main/java/com/termux/app}/extrakeys/SpecialButton.java (97%) rename {termux-shared/src/main/java/com/termux/shared/termux => app/src/main/java/com/termux/app}/extrakeys/SpecialButtonState.java (97%) rename app/src/main/java/com/termux/app/{terminal/io => extrakeys}/TerminalToolbarViewPager.java (88%) create mode 100644 app/src/main/java/com/termux/app/extrakeys/TermuxTerminalExtraKeys.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/TermuxAPIPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/TermuxFloatPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/TermuxPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/TermuxTaskerPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/TermuxWidgetPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/termux/DebuggingPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/termux/TerminalIOPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/termux/TerminalViewPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/termux_api/DebuggingPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/termux_float/DebuggingPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/termux_tasker/DebuggingPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/fragments/settings/termux_widget/DebuggingPreferencesFragment.java delete mode 100644 app/src/main/java/com/termux/app/models/UserAction.java delete mode 100644 app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java delete mode 100644 app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionServiceClient.java delete mode 100644 app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java delete mode 100644 app/src/main/java/com/termux/app/terminal/io/FullScreenWorkAround.java delete mode 100644 app/src/main/java/com/termux/app/terminal/io/TermuxTerminalExtraKeys.java delete mode 100644 app/src/main/res/layout/activity_settings.xml delete mode 100644 app/src/main/res/layout/preference_markdown_text.xml rename {termux-shared => app}/src/main/res/raw/bell.ogg (100%) delete mode 100644 app/src/main/res/xml/root_preferences.xml delete mode 100644 app/src/main/res/xml/termux_api_debugging_preferences.xml delete mode 100644 app/src/main/res/xml/termux_api_preferences.xml delete mode 100644 app/src/main/res/xml/termux_debugging_preferences.xml delete mode 100644 app/src/main/res/xml/termux_float_debugging_preferences.xml delete mode 100644 app/src/main/res/xml/termux_float_preferences.xml delete mode 100644 app/src/main/res/xml/termux_preferences.xml delete mode 100644 app/src/main/res/xml/termux_tasker_debugging_preferences.xml delete mode 100644 app/src/main/res/xml/termux_tasker_preferences.xml delete mode 100644 app/src/main/res/xml/termux_terminal_io_preferences.xml delete mode 100644 app/src/main/res/xml/termux_terminal_view_preferences.xml delete mode 100644 app/src/main/res/xml/termux_widget_debugging_preferences.xml delete mode 100644 app/src/main/res/xml/termux_widget_preferences.xml rename app/src/test/java/com/termux/app/{api/file/FileReceiverActivityTest.java => TermuxFileReceiverActivityTest.java} (77%) delete mode 100644 terminal-view/src/main/java/com/termux/view/support/PopupWindowCompatGingerbread.java delete mode 100644 termux-shared/.gitignore delete mode 100644 termux-shared/LICENSE.md delete mode 100644 termux-shared/build.gradle delete mode 100644 termux-shared/proguard-rules.pro delete mode 100644 termux-shared/src/androidTest/java/com/termux/shared/ExampleInstrumentedTest.java delete mode 100644 termux-shared/src/main/AndroidManifest.xml delete mode 100644 termux-shared/src/main/cpp/Android.mk delete mode 100644 termux-shared/src/main/cpp/Application.mk delete mode 100644 termux-shared/src/main/cpp/local-socket.cpp delete mode 100644 termux-shared/src/main/java/com/termux/shared/activities/ReportActivity.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/activities/TextIOActivity.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/activity/ActivityErrno.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/activity/ActivityUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/activity/media/AppCompatActivityUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/AndroidUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/FeatureFlagUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/PackageUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/PermissionUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/PhantomProcessUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/ProcessUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/SELinuxUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/SettingsProviderUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/UserUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/android/resource/ResourceUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/crash/CrashHandler.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/data/DataUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/data/IntentUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/errors/Errno.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/errors/Error.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/errors/FunctionErrno.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/FileUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/FileUtilsErrno.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/filesystem/FileAttributes.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/filesystem/FileKey.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/filesystem/FilePermission.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/filesystem/FilePermissions.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTime.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/filesystem/FileType.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/filesystem/FileTypes.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/filesystem/UnixConstants.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/file/tests/FileUtilsTests.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/interact/MessageDialogUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/interact/ShareUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/jni/models/JniResult.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/logger/Logger.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/markdown/MarkdownUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/models/ReportInfo.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/models/TextIOInfo.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/socket/local/ILocalSocketManager.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalClientSocket.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalServerSocket.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketErrno.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManager.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketManagerClientBase.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/socket/local/LocalSocketRunConfig.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/socket/local/PeerCred.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/uri/UriScheme.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/uri/UriUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/net/url/UrlUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/notification/NotificationUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/reflection/ReflectionUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/settings/preferences/AppSharedPreferences.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/settings/properties/SharedProperties.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/ArgumentTokenizer.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/ShellUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/StreamGobbler.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServer.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServerErrno.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/am/AmSocketServerRunConfig.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/ExecutionCommand.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/ShellCommandConstants.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/AndroidShellEnvironment.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/IShellEnvironment.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellCommandShellEnvironment.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/ShellEnvironmentVariable.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/environment/UnixShellEnvironment.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultConfig.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultData.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultSender.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/result/ResultSenderErrno.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/shell/command/runner/app/AppShell.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/TermuxBootstrap.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/crash/TermuxCrashUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/data/TermuxUrlUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/file/TermuxFileUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/models/UserAction.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/notification/TermuxNotificationUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/plugins/TermuxPluginUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAPIAppSharedPreferences.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxAppSharedPreferences.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxBootAppSharedPreferences.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxFloatAppSharedPreferences.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxPreferenceConstants.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxStylingAppSharedPreferences.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxTaskerAppSharedPreferences.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/preferences/TermuxWidgetAppSharedPreferences.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxAppSharedProperties.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxPropertyConstants.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/settings/properties/TermuxSharedProperties.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellManager.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/shell/am/TermuxAmSocketServer.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxAPIShellEnvironment.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxAppShellEnvironment.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellCommandShellEnvironment.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/shell/command/environment/TermuxShellEnvironment.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/shell/command/runner/terminal/TermuxSession.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/terminal/TermuxTerminalSessionClientBase.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/terminal/TermuxTerminalViewClientBase.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/terminal/io/BellHandler.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/terminal/io/TerminalExtraKeys.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/termux/theme/TermuxThemeUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/theme/NightMode.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/theme/ThemeUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/view/KeyboardUtils.java delete mode 100644 termux-shared/src/main/java/com/termux/shared/view/ViewUtils.java delete mode 100644 termux-shared/src/main/res/drawable/ic_copy.xml delete mode 100644 termux-shared/src/main/res/drawable/ic_error_notification.xml delete mode 100644 termux-shared/src/main/res/drawable/ic_info.xml delete mode 100644 termux-shared/src/main/res/drawable/ic_settings.xml delete mode 100644 termux-shared/src/main/res/drawable/ic_share.xml delete mode 100644 termux-shared/src/main/res/layout/activity_report.xml delete mode 100644 termux-shared/src/main/res/layout/activity_text_io.xml delete mode 100644 termux-shared/src/main/res/layout/dialog_show_message.xml delete mode 100644 termux-shared/src/main/res/layout/markdown_adapter_node_code_block.xml delete mode 100644 termux-shared/src/main/res/layout/markdown_adapter_node_default.xml delete mode 100644 termux-shared/src/main/res/layout/partial_primary_toolbar.xml delete mode 100644 termux-shared/src/main/res/menu/menu_report.xml delete mode 100644 termux-shared/src/main/res/menu/menu_text_io.xml delete mode 100644 termux-shared/src/main/res/raw/apt_info_script.sh delete mode 100644 termux-shared/src/main/res/raw/keep.xml delete mode 100755 termux-shared/src/main/res/values-night/themes.xml delete mode 100644 termux-shared/src/main/res/values/attrs.xml delete mode 100644 termux-shared/src/main/res/values/colors.xml delete mode 100644 termux-shared/src/main/res/values/dimens.xml delete mode 100644 termux-shared/src/main/res/values/strings.xml delete mode 100644 termux-shared/src/main/res/values/styles.xml delete mode 100644 termux-shared/src/main/res/values/themes.xml diff --git a/.github/workflows/debug_build.yml b/.github/workflows/debug_build.yml index f4cde739..76d6e265 100644 --- a/.github/workflows/debug_build.yml +++ b/.github/workflows/debug_build.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - google-play pull_request: branches: - master @@ -11,19 +12,18 @@ on: jobs: build: runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - package_variant: [ apt-android-7, apt-android-5 ] - steps: - name: Clone repository uses: actions/checkout@v4 + - name: Setup java 17 as required by gradle + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + - name: Build APKs shell: bash {0} - env: - PACKAGE_VARIANT: ${{ matrix.package_variant }} run: | exit_on_error() { echo "$1"; exit 1; } @@ -42,7 +42,7 @@ jobs: fi APK_DIR_PATH="./app/build/outputs/apk/debug" - APK_VERSION_TAG="$RELEASE_VERSION_NAME-${{ env.PACKAGE_VARIANT }}-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it + APK_VERSION_TAG="$RELEASE_VERSION_NAME-github-debug" # Note the "-", GITHUB_SHA will already have "+" before it APK_BASENAME_PREFIX="termux-app_$APK_VERSION_TAG" # Used by attachment steps later @@ -53,7 +53,6 @@ jobs: echo "Building APKs for 'APK_VERSION_TAG' build" export TERMUX_APP_VERSION_NAME="${RELEASE_VERSION_NAME/v/}" # Used by app/build.gradle export TERMUX_APK_VERSION_TAG="$APK_VERSION_TAG" # Used by app/build.gradle - export TERMUX_PACKAGE_VARIANT="${{ env.PACKAGE_VARIANT }}" # Used by app/build.gradle if ! ./gradlew assembleDebug; then exit_on_error "Build failed for '$APK_VERSION_TAG' build." fi diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index f8d33fd4..93e34903 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -16,6 +16,11 @@ jobs: steps: - name: Clone repository uses: actions/checkout@v4 + - name: Setup java 17 as required by gradle + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' - name: Execute tests run: | ./gradlew test diff --git a/LICENSE.md b/LICENSE.md index 4b661702..995cf098 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,4 +3,3 @@ The `termux/termux-app` repository is released under [GPLv3 only](https://www.gn ### Exceptions - [Terminal Emulator for Android](https://github.com/jackpal/Android-Terminal-Emulator) code is used which is released under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) license. Check [`terminal-view`](terminal-view) and [`terminal-emulator`](terminal-emulator) libraries. -- Check [`termux-shared/LICENSE.md`](termux-shared/LICENSE.md) for `termux-shared` library related exceptions. diff --git a/README.md b/README.md index 625a4fc9..b897f81d 100644 --- a/README.md +++ b/README.md @@ -252,10 +252,6 @@ Users must post complete report (optionally without sensitive info) when reporti ## For Maintainers and Contributors -The [termux-shared](termux-shared) library was added in [`v0.109`](https://github.com/termux/termux-app/releases/tag/v0.109). It defines shared constants and utils of the Termux app and its plugins. It was created to allow for the removal of all hardcoded paths in the Termux app. Some of the termux plugins are using this as well and rest will in future. If you are contributing code that is using a constant or a util that may be shared, then define it in `termux-shared` library if it currently doesn't exist and reference it from there. Update the relevant changelogs as well. Pull requests using hardcoded values **will/should not** be accepted. Termux app and plugin specific classes must be added under `com.termux.shared.termux` package and general classes outside it. The [`termux-shared` `LICENSE`](termux-shared/LICENSE.md) must also be checked and updated if necessary when contributing code. The licenses of any external library or code must be honoured. - -The main Termux constants are defined by [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) class. It also contains information on how to fork Termux or build it with your own package name. Changing the package name will require building the bootstrap zip packages and other packages with the new `$PREFIX`, check [Building Packages](https://github.com/termux/termux-packages/wiki/Building-packages) for more info. - Check [Termux Libraries](https://github.com/termux/termux-app/wiki/Termux-Libraries) for how to import termux libraries in plugin apps and [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for how to update termux libraries for plugins. Commit messages **must** use [Conventional Commits](https://www.conventionalcommits.org) specs so that chagelogs can automatically be generated by the [`create-conventional-changelog`](https://github.com/termux/create-conventional-changelog) script, check its repo for further details on the spec. Use the following `types` as `Added: Add foo`, `Added|Fixed: Add foo and fix bar`, `Changed!: Change baz as a breaking change`, etc. You can optionally add a scope as well, like `Fixed(terminal): Some bug`. The space after `:` is necessary. @@ -271,13 +267,3 @@ Commit messages **must** use [Conventional Commits](https://www.conventionalcomm Changelogs for releases are generated based on [Keep a Changelog](https://github.com/olivierlacan/keep-a-changelog) specs. The `versionName` in `build.gradle` files of Termux and its plugin apps must follow the [semantic version `2.0.0` spec](https://semver.org/spec/v2.0.0.html) in the format `major.minor.patch(-prerelease)(+buildmetadata)`. When bumping `versionName` in `build.gradle` files and when creating a tag for new releases on GitHub, make sure to include the patch number as well, like `v0.1.0` instead of just `v0.1`. The `build.gradle` files and `attach_debug_apks_to_release` workflow validates the version as well and the build/attachment will fail if `versionName` does not follow the spec. -## - - - -## Forking - -- Check [`TermuxConstants`](https://github.com/termux/termux-app/blob/master/termux-shared/src/main/java/com/termux/shared/termux/TermuxConstants.java) javadocs for instructions on what changes to make in the app to change package name. -- You also need to recompile bootstrap zip for the new package name. Check [building bootstrap](https://github.com/termux/termux-packages/wiki/For-maintainers#build-bootstrap-archives), [here](https://github.com/termux/termux-app/issues/1983) and [here](https://github.com/termux/termux-app/issues/2081#issuecomment-865280111). -- Currently, not all plugins use `TermuxConstants` from `termux-shared` library and have hardcoded `com.termux` values and will need to be manually patched. -- If forking termux plugins, check [Forking and Local Development](https://github.com/termux/termux-app/wiki/Termux-Libraries#forking-and-local-development) for info on how to use termux libraries for plugins. diff --git a/app/build.gradle b/app/build.gradle index 3df93fef..871c64f1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,18 +2,9 @@ plugins { id "com.android.application" } -ext { - // The packageVariant defines the bootstrap variant that will be included in the app APK. - // This must be supported by com.termux.shared.termux.TermuxBootstrap.PackageVariant or app will - // crash at startup. - // Bootstrap of a different variant must not be manually installed by the user after app installation - // by replacing $PREFIX since app code is dependant on the variant used to build the APK. - // Currently supported values are: [ "apt-android-7" "apt-android-5" ] - packageVariant = System.getenv("TERMUX_PACKAGE_VARIANT") ?: "apt-android-7" // Default: "apt-android-7" -} - android { - compileSdkVersion project.properties.compileSdkVersion.toInteger() + namespace "com.termux" + ndkVersion = System.getenv("JITPACK_NDK_VERSION") ?: project.properties.ndkVersion def appVersionName = System.getenv("TERMUX_APP_VERSION_NAME") ?: "" def apkVersionTag = System.getenv("TERMUX_APK_VERSION_TAG") ?: "" @@ -21,34 +12,25 @@ android { def splitAPKsForReleaseBuilds = System.getenv("TERMUX_SPLIT_APKS_FOR_RELEASE_BUILDS") ?: "0" // F-Droid does not support split APKs #1904 dependencies { - implementation "androidx.annotation:annotation:1.3.0" - implementation "androidx.core:core:1.6.0" - implementation "androidx.drawerlayout:drawerlayout:1.1.1" - implementation "androidx.preference:preference:1.1.1" + implementation "androidx.annotation:annotation:1.7.0" + implementation "androidx.core:core:1.12.0" + implementation "androidx.drawerlayout:drawerlayout:1.2.0" implementation "androidx.viewpager:viewpager:1.0.0" - implementation "com.google.android.material:material:1.4.0" - implementation "com.google.guava:guava:24.1-jre" - implementation "io.noties.markwon:core:$markwonVersion" - implementation "io.noties.markwon:ext-strikethrough:$markwonVersion" - implementation "io.noties.markwon:linkify:$markwonVersion" - implementation "io.noties.markwon:recycler:$markwonVersion" + implementation "com.google.android.material:material:1.10.0" implementation project(":terminal-view") - implementation project(":termux-shared") } defaultConfig { - applicationId "com.termux" minSdkVersion project.properties.minSdkVersion.toInteger() targetSdkVersion project.properties.targetSdkVersion.toInteger() + compileSdk project.properties.compileSdkVersion.toInteger() versionCode 118 versionName "0.118.0" if (appVersionName) versionName = appVersionName validateVersionName(versionName) - buildConfigField "String", "TERMUX_PACKAGE_VARIANT", "\"" + project.ext.packageVariant + "\"" // Used by TermuxApplication class - manifestPlaceholders.TERMUX_PACKAGE_NAME = "com.termux" manifestPlaceholders.TERMUX_APP_NAME = "Termux" manifestPlaceholders.TERMUX_API_APP_NAME = "Termux:API" @@ -97,11 +79,8 @@ android { } compileOptions { - // Flag to enable support for the new language APIs - coreLibraryDesugaringEnabled true - - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } externalNativeBuild { @@ -110,7 +89,7 @@ android { } } - lintOptions { + lint { disable 'ProtectedPermissions' } @@ -130,10 +109,10 @@ android { variant.outputs.all { output -> if (variant.buildType.name == "debug") { def abi = output.getFilter(com.android.build.OutputFile.ABI) - outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : project.ext.packageVariant + "-" + "debug") + "_" + (abi ? abi : "universal") + ".apk") + outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "-" + "debug") + "_" + (abi ? abi : "universal") + ".apk") } else if (variant.buildType.name == "release") { def abi = output.getFilter(com.android.build.OutputFile.ABI) - outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : project.ext.packageVariant + "-" + "release") + "_" + (abi ? abi : "universal") + ".apk") + outputFileName = new File("termux-app_" + (apkVersionTag ? apkVersionTag : "-" + "release") + "_" + (abi ? abi : "universal") + ".apk") } } } @@ -143,7 +122,6 @@ android { dependencies { testImplementation "junit:junit:4.13.2" testImplementation "org.robolectric:robolectric:4.10" - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5" } task versionName { @@ -182,7 +160,8 @@ def downloadBootstrap(String arch, String expectedChecksum, String version) { } } - def remoteUrl = "https://github.com/termux/termux-packages/releases/download/bootstrap-" + version + "/bootstrap-" + arch + ".zip" + // def remoteUrl = "https://github.com/termux/termux-packages/releases/download/bootstrap-" + version + "/bootstrap-" + arch + ".zip" + def remoteUrl = "https://fornwall.me/tmp/bootstrap-" + arch + "-test-v" + version + ".zip" logger.quiet("Downloading " + remoteUrl + " ...") file.parentFile.mkdirs() @@ -212,22 +191,11 @@ clean { task downloadBootstraps() { doLast { - def packageVariant = project.ext.packageVariant - if (packageVariant == "apt-android-7") { - def version = "2022.04.28-r5" + "+" + packageVariant - downloadBootstrap("aarch64", "4a51a7eb209fe82efc24d52e3cccc13165f27377290687cb82038cbd8e948430", version) - downloadBootstrap("arm", "6459a786acbae50d4c8a36fa1c3de6a4dd2d482572f6d54f73274709bd627325", version) - downloadBootstrap("i686", "919d212b2f19e08600938db4079e794e947365022dbfd50ac342c50fcedcd7be", version) - downloadBootstrap("x86_64", "61b02fdc03ea4f5d9da8d8cf018013fdc6659e6da6cbf44e9b24d1c623580b89", version) - } else if (packageVariant == "apt-android-5") { - def version = "2022.04.28-r6" + "+" + packageVariant - downloadBootstrap("aarch64", "913609d439415c828c5640be1b0561467e539cb1c7080662decaaca2fb4820e7", version) - downloadBootstrap("arm", "26bfb45304c946170db69108e5eb6e3641aad751406ce106c80df80cad2eccf8", version) - downloadBootstrap("i686", "46dcfeb5eef67ba765498db9fe4c50dc4690805139aa0dd141a9d8ee0693cd27", version) - downloadBootstrap("x86_64", "615b590679ee6cd885b7fd2ff9473c845e920f9b422f790bb158c63fe42b8481", version) - } else { - throw new GradleException("Unsupported TERMUX_PACKAGE_VARIANT \"" + packageVariant + "\"") - } + def version = "3" + downloadBootstrap("aarch64", "308484efc4400a003a731836f6c33dfa6e5fc04abc27a2268968a3e5f549114b", version) + downloadBootstrap("arm", "0b39a9d53882fb7878cd5b2cf5846e50853e69ebf7df283afcf8ea51af14b322", version) + downloadBootstrap("i686", "ec7a18f5fa17d01cc0aa8e1389b21f3d9b99c8eddd33d4ddde36dd2380f74a07", version) + downloadBootstrap("x86_64", "e7d90df0dcb698c2413c5581b238d425e59fc84e178358a9e56b2101be341279", version) } } @@ -236,3 +204,8 @@ afterEvaluate { variant.javaCompileProvider.get().dependsOn(downloadBootstraps) } } + +// https://stackoverflow.com/questions/75274720/a-failure-occurred-while-executing-appcheckdebugduplicateclasses/ +configurations.implementation { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a01c0389..7365abb1 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -10,8 +10,3 @@ -dontobfuscate #-renamesourcefileattribute SourceFile #-keepattributes SourceFile,LineNumberTable - -# Temp fix for androidx.window:window:1.0.0-alpha09 imported by termux-shared -# https://issuetracker.google.com/issues/189001730 -# https://android-review.googlesource.com/c/platform/frameworks/support/+/1757630 --keep class androidx.window.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4e95702b..7d89e02f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,9 +1,7 @@ - + + - - - + + + + - - - - - - + + + android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar"> + android:theme="@style/Theme.TermuxActivity.DayNight.NoActionBar"> - - - - - - - - - - - - - + android:name=".app.TermuxActivityInternal" + android:targetActivity=".app.TermuxActivity"/> - - - - - - - - + android:taskAffinity="${TERMUX_PACKAGE_NAME}.filereceiver" > @@ -137,12 +100,6 @@ - - - @@ -156,10 +113,10 @@ - + - - - - - - - - - - - - - - - + android:foregroundServiceType="specialUse" + android:permission="" + android:exported="false"> + + - - - - - diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java deleted file mode 100644 index 635f2810..00000000 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ /dev/null @@ -1,287 +0,0 @@ -package com.termux.app; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.Service; -import android.content.Intent; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.IBinder; - -import com.termux.R; -import com.termux.shared.data.DataUtils; -import com.termux.shared.data.IntentUtils; -import com.termux.shared.termux.plugins.TermuxPluginUtils; -import com.termux.shared.termux.file.TermuxFileUtils; -import com.termux.shared.file.filesystem.FileType; -import com.termux.shared.errors.Errno; -import com.termux.shared.errors.Error; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.termux.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE; -import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; -import com.termux.shared.file.FileUtils; -import com.termux.shared.logger.Logger; -import com.termux.shared.notification.NotificationUtils; -import com.termux.shared.shell.command.ExecutionCommand; -import com.termux.shared.shell.command.ExecutionCommand.Runner; - -/** - * A service that receives {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent from third party apps and - * plugins that contains info on command execution and forwards the extras to {@link TermuxService} - * for the actual execution. - * - * Check https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent for more info. - */ -public class RunCommandService extends Service { - - private static final String LOG_TAG = "RunCommandService"; - - class LocalBinder extends Binder { - public final RunCommandService service = RunCommandService.this; - } - - private final IBinder mBinder = new RunCommandService.LocalBinder(); - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - public void onCreate() { - Logger.logVerbose(LOG_TAG, "onCreate"); - runStartForeground(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Logger.logDebug(LOG_TAG, "onStartCommand"); - - if (intent == null) return Service.START_NOT_STICKY; - - // Run again in case service is already started and onCreate() is not called - runStartForeground(); - - Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent)); - - ExecutionCommand executionCommand = new ExecutionCommand(); - executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help, RUN_COMMAND_SERVICE.RUN_COMMAND_API_HELP_URL); - - Error error; - String errmsg; - - // If invalid action passed, then just return - if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) { - errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction()); - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } - - String executableExtra = executionCommand.executable = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH, null); - executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS, null); - - /* - * If intent was sent with `am` command, then normal comma characters may have been replaced - * with alternate characters if a normal comma existed in an argument itself to prevent it - * splitting into multiple arguments by `am` command. - * If `tudo` or `sudo` are used, then simply using their `-r` and `--comma-alternative` command - * options can be used without passing the below extras, but native supports is helpful if - * they are not being used. - * https://github.com/agnostic-apollo/tudo#passing-arguments-using-run_command-intent - * https://android.googlesource.com/platform/frameworks/base/+/21bdaf1/cmds/am/src/com/android/commands/am/Am.java#572 - */ - boolean replaceCommaAlternativeCharsInArguments = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_REPLACE_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, false); - if (replaceCommaAlternativeCharsInArguments) { - String commaAlternativeCharsInArguments = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMA_ALTERNATIVE_CHARS_IN_ARGUMENTS, null); - if (commaAlternativeCharsInArguments == null) - commaAlternativeCharsInArguments = TermuxConstants.COMMA_ALTERNATIVE; - // Replace any commaAlternativeCharsInArguments characters with normal commas - DataUtils.replaceSubStringsInStringArrayItems(executionCommand.arguments, commaAlternativeCharsInArguments, TermuxConstants.COMMA_NORMAL); - } - - executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_STDIN, null); - executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_WORKDIR, null); - - // If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION - executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RUNNER, - (intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName())); - if (Runner.runnerOf(executionCommand.runner) == null) { - errmsg = this.getString(R.string.error_run_command_service_invalid_execution_command_runner, executionCommand.runner); - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } - - executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null); - executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION); - executionCommand.shellName = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SHELL_NAME, null); - executionCommand.shellCreateMode = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_SHELL_CREATE_MODE, null); - executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL, "RUN_COMMAND Execution Intent Command"); - executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION, null); - executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP, null); - executionCommand.isPluginExecutionCommand = true; - executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(RUN_COMMAND_SERVICE.EXTRA_PENDING_INTENT); - executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_DIRECTORY, null); - if (executionCommand.resultConfig.resultDirectoryPath != null) { - executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_RESULT_SINGLE_FILE, false); - executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_BASENAME, null); - executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null); - executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null); - executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, RUN_COMMAND_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null); - } - - // If "allow-external-apps" property to not set to "true", then just return - // We enable force notifications if "allow-external-apps" policy is violated so that the - // user knows someone tried to run a command in termux context, since it may be malicious - // app or imported (tasker) plugin project and not the user himself. If a pending intent is - // also sent, then its creator is also logged and shown. - errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(this, LOG_TAG); - if (errmsg != null) { - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, true); - return stopService(); - } - - - - // If executable is null or empty, then exit here instead of getting canonical path which would expand to "/" - if (executionCommand.executable == null || executionCommand.executable.isEmpty()) { - errmsg = this.getString(R.string.error_run_command_service_mandatory_extra_missing, RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } - - // Get canonical path of executable - executionCommand.executable = TermuxFileUtils.getCanonicalPath(executionCommand.executable, null, true); - - // If executable is not a regular file, or is not readable or executable, then just return - // Setting of missing read and execute permissions is not done - error = FileUtils.validateRegularFileExistenceAndPermissions("executable", executionCommand.executable, null, - FileUtils.APP_EXECUTABLE_FILE_PERMISSIONS, true, true, - false); - if (error != null) { - executionCommand.setStateFailed(error); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } - - - - // If workingDirectory is not null or empty - if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) { - // Get canonical path of workingDirectory - executionCommand.workingDirectory = TermuxFileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true); - - // If workingDirectory is not a directory, or is not readable or writable, then just return - // Creation of missing directory and setting of read, write and execute permissions are only done if workingDirectory is - // under allowed termux working directory paths. - // We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required - // for working directories. - error = TermuxFileUtils.validateDirectoryFileExistenceAndPermissions("working", executionCommand.workingDirectory, - true, true, true, - false, true); - if (error != null) { - executionCommand.setStateFailed(error); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return stopService(); - } - } - - // If the executable passed as the extra was an applet for coreutils/busybox, then we must - // use it instead of the canonical path above since otherwise arguments would be passed to - // coreutils/busybox instead and command would fail. Broken symlinks would already have been - // validated so it should be fine to use it. - executableExtra = TermuxFileUtils.getExpandedTermuxPath(executableExtra); - if (FileUtils.getFileType(executableExtra, false) == FileType.SYMLINK) { - Logger.logVerbose(LOG_TAG, "The executableExtra path \"" + executableExtra + "\" is a symlink so using it instead of the canonical path \"" + executionCommand.executable + "\""); - executionCommand.executable = executableExtra; - } - - executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(executionCommand.executable).build(); - - Logger.logVerboseExtended(LOG_TAG, executionCommand.toString()); - - // Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE - Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri); - execIntent.setClass(this, TermuxService.class); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_STDIN, executionCommand.stdin); - if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_RUNNER, executionCommand.runner); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, DataUtils.getStringFromInteger(executionCommand.backgroundCustomLogLevel, null)); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_SHELL_NAME, executionCommand.shellName); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_SHELL_CREATE_MODE, executionCommand.shellCreateMode); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT, executionCommand.resultConfig.resultPendingIntent); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, executionCommand.resultConfig.resultDirectoryPath); - if (executionCommand.resultConfig.resultDirectoryPath != null) { - execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, executionCommand.resultConfig.resultSingleFile); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, executionCommand.resultConfig.resultFileBasename); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, executionCommand.resultConfig.resultFileOutputFormat); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, executionCommand.resultConfig.resultFileErrorFormat); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, executionCommand.resultConfig.resultFilesSuffix); - } - - // Start TERMUX_SERVICE and pass it execution intent - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - this.startForegroundService(execIntent); - } else { - this.startService(execIntent); - } - - return stopService(); - } - - private int stopService() { - runStopForeground(); - return Service.START_NOT_STICKY; - } - - private void runStartForeground() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - setupNotificationChannel(); - startForeground(TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID, buildNotification()); - } - } - - private void runStopForeground() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - stopForeground(true); - } - } - - private Notification buildNotification() { - // Build the notification - Notification.Builder builder = NotificationUtils.geNotificationBuilder(this, - TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, Notification.PRIORITY_LOW, - TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, null, null, - null, null, NotificationUtils.NOTIFICATION_MODE_SILENT); - if (builder == null) return null; - - // No need to show a timestamp: - builder.setShowWhen(false); - - // Set notification icon - builder.setSmallIcon(R.drawable.ic_service_notification); - - // Set background color for small notification icon - builder.setColor(0xFF607D8B); - - return builder.build(); - } - - private void setupNotificationChannel() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; - - NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID, - TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); - } - -} diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 308d1f0b..ee52395c 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -13,6 +13,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; @@ -23,39 +24,13 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.view.autofill.AutofillManager; import android.widget.EditText; -import android.widget.ImageButton; import android.widget.ListView; -import android.widget.RelativeLayout; import android.widget.Toast; import com.termux.R; -import com.termux.app.api.file.FileReceiverActivity; -import com.termux.app.terminal.TermuxActivityRootView; -import com.termux.app.terminal.TermuxTerminalSessionActivityClient; -import com.termux.app.terminal.io.TermuxTerminalExtraKeys; -import com.termux.shared.activities.ReportActivity; -import com.termux.shared.activity.ActivityUtils; -import com.termux.shared.activity.media.AppCompatActivityUtils; -import com.termux.shared.data.IntentUtils; -import com.termux.shared.android.PermissionUtils; -import com.termux.shared.data.DataUtils; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; -import com.termux.app.activities.HelpActivity; -import com.termux.app.activities.SettingsActivity; -import com.termux.shared.termux.crash.TermuxCrashUtils; -import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; -import com.termux.app.terminal.TermuxSessionsListViewController; -import com.termux.app.terminal.io.TerminalToolbarViewPager; -import com.termux.app.terminal.TermuxTerminalViewClient; -import com.termux.shared.termux.extrakeys.ExtraKeysView; -import com.termux.shared.termux.interact.TextInputDialogUtils; -import com.termux.shared.logger.Logger; -import com.termux.shared.termux.TermuxUtils; -import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; -import com.termux.shared.termux.theme.TermuxThemeUtils; -import com.termux.shared.theme.NightMode; -import com.termux.shared.view.ViewUtils; +import com.termux.app.extrakeys.ExtraKeysView; +import com.termux.app.extrakeys.TermuxTerminalExtraKeys; +import com.termux.app.extrakeys.TerminalToolbarViewPager; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSessionClient; import com.termux.view.TerminalView; @@ -67,7 +42,13 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.drawerlayout.widget.DrawerLayout; import androidx.viewpager.widget.ViewPager; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Properties; /** * A terminal emulator activity. @@ -81,6 +62,26 @@ import java.util.Arrays; */ public final class TermuxActivity extends AppCompatActivity implements ServiceConnection { + public static final String ACTION_RELOAD_STYLE = "com.termux.app.reload_style"; + public static final String ACTION_REQUEST_PERMISSIONS = "com.termux.app.request_storage_permissions"; + public static final String EXTRA_FAILSAFE_SESSION = "com.termux.app.failsafe_session"; + + private static final int CONTEXT_MENU_SELECT_URL_ID = 0; + private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1; + private static final int CONTEXT_MENU_SHARE_SELECTED_TEXT = 10; + private static final int CONTEXT_MENU_AUTOFILL_ID = 2; + private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3; + private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4; + private static final int CONTEXT_MENU_STYLING_ID = 5; + private static final int CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON = 6; + private static final int CONTEXT_MENU_HELP_ID = 7; + private static final int CONTEXT_MENU_REPORT_ID = 8; + + private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input"; + private static final String ARG_ACTIVITY_RECREATED = "activity_recreated"; + + private static final String LOG_TAG = "TermuxActivity"; + /** * The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to * {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in @@ -94,37 +95,17 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo TerminalView mTerminalView; /** - * The {@link TerminalViewClient} interface implementation to allow for communication between - * {@link TerminalView} and {@link TermuxActivity}. + * The {@link TerminalViewClient} interface implementation to allow for communication between + * {@link TerminalView} and {@link TermuxActivity}. */ TermuxTerminalViewClient mTermuxTerminalViewClient; /** - * The {@link TerminalSessionClient} interface implementation to allow for communication between - * {@link TerminalSession} and {@link TermuxActivity}. + * The {@link TerminalSessionClient} interface implementation to allow for communication between + * {@link TerminalSession} and {@link TermuxActivity}. */ TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient; - /** - * Termux app shared preferences manager. - */ - private TermuxAppSharedPreferences mPreferences; - - /** - * Termux app SharedProperties loaded from termux.properties - */ - private TermuxAppSharedProperties mProperties; - - /** - * The root view of the {@link TermuxActivity}. - */ - TermuxActivityRootView mTermuxActivityRootView; - - /** - * The space at the bottom of {@link @mTermuxActivityRootView} of the {@link TermuxActivity}. - */ - View mTermuxActivityBottomSpaceView; - /** * The terminal extra keys view. */ @@ -156,148 +137,84 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo */ private boolean mIsVisible; - /** - * If onResume() was called after onCreate(). - */ - private boolean mIsOnResumeAfterOnCreate = false; - - /** - * If activity was restarted like due to call to {@link #recreate()} after receiving - * {@link TERMUX_ACTIVITY#ACTION_RELOAD_STYLE}, system dark night mode was changed or activity - * was killed by android. - */ - private boolean mIsActivityRecreated = false; - - /** - * The {@link TermuxActivity} is in an invalid state and must not be run. - */ - private boolean mIsInvalidState; - - private int mNavBarHeight; - private float mTerminalToolbarDefaultHeight; + public final TermuxProperties mProperties = new TermuxProperties(); - private static final int CONTEXT_MENU_SELECT_URL_ID = 0; - private static final int CONTEXT_MENU_SHARE_TRANSCRIPT_ID = 1; - private static final int CONTEXT_MENU_SHARE_SELECTED_TEXT = 10; - private static final int CONTEXT_MENU_AUTOFILL_ID = 2; - private static final int CONTEXT_MENU_RESET_TERMINAL_ID = 3; - private static final int CONTEXT_MENU_KILL_PROCESS_ID = 4; - private static final int CONTEXT_MENU_STYLING_ID = 5; - private static final int CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON = 6; - private static final int CONTEXT_MENU_HELP_ID = 7; - private static final int CONTEXT_MENU_SETTINGS_ID = 8; - private static final int CONTEXT_MENU_REPORT_ID = 9; - - private static final String ARG_TERMINAL_TOOLBAR_TEXT_INPUT = "terminal_toolbar_text_input"; - private static final String ARG_ACTIVITY_RECREATED = "activity_recreated"; - - private static final String LOG_TAG = "TermuxActivity"; + public TermuxPreferences mPreferences; @Override public void onCreate(Bundle savedInstanceState) { - Logger.logDebug(LOG_TAG, "onCreate"); - mIsOnResumeAfterOnCreate = true; - - if (savedInstanceState != null) - mIsActivityRecreated = savedInstanceState.getBoolean(ARG_ACTIVITY_RECREATED, false); - - // Delete ReportInfo serialized object files from cache older than 14 days - ReportActivity.deleteReportInfoFilesOlderThanXDays(this, 14, false); - - // Load Termux app SharedProperties from disk - mProperties = TermuxAppSharedProperties.getProperties(); - reloadProperties(); - - setActivityTheme(); - super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + // mIsActivityRecreated = savedInstanceState.getBoolean(ARG_ACTIVITY_RECREATED, false); + } + + mProperties.reloadProperties(); + mPreferences = new TermuxPreferences(this); + setContentView(R.layout.activity_termux); - // Load termux shared preferences - // This will also fail if TermuxConstants.TERMUX_PACKAGE_NAME does not equal applicationId - mPreferences = TermuxAppSharedPreferences.build(this, true); - if (mPreferences == null) { - // An AlertDialog should have shown to kill the app, so we don't continue running activity code - mIsInvalidState = true; - return; - } + mTermuxTerminalSessionActivityClient = new TermuxTerminalSessionActivityClient(this); + mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionActivityClient); - setMargins(); + mTerminalView = findViewById(R.id.terminal_view); + mTerminalView.setTerminalViewClient(mTermuxTerminalViewClient); + mTerminalView.setTextSize(mPreferences.getFontSize()); - mTermuxActivityRootView = findViewById(R.id.activity_termux_root_view); - mTermuxActivityRootView.setActivity(this); - mTermuxActivityBottomSpaceView = findViewById(R.id.activity_termux_bottom_space_view); - mTermuxActivityRootView.setOnApplyWindowInsetsListener(new TermuxActivityRootView.WindowInsetsListener()); - - View content = findViewById(android.R.id.content); - content.setOnApplyWindowInsetsListener((v, insets) -> { - mNavBarHeight = insets.getSystemWindowInsetBottom(); - return insets; - }); - - if (mProperties.isUsingFullScreen()) { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - } - - setTermuxTerminalViewAndClients(); + mTermuxTerminalSessionActivityClient.onCreate(); setTerminalToolbarView(savedInstanceState); - setSettingsButtonView(); + View newSessionButton = findViewById(R.id.new_session_button); + newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionActivityClient.addNewSession(false, null)); + newSessionButton.setOnLongClickListener(v -> { + TermuxMessageDialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null, + R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionActivityClient.addNewSession(false, text), + R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionActivityClient.addNewSession(true, text), + -1, null, null); + return true; + }); - setNewSessionButtonView(); - - setToggleKeyboardView(); + View toggleKeyboardButton = findViewById(R.id.toggle_keyboard_button); + toggleKeyboardButton.setOnClickListener(v -> { + mTermuxTerminalViewClient.onToggleSoftKeyboardRequest(); + getDrawer().closeDrawers(); + }); + toggleKeyboardButton.setOnLongClickListener(v -> { + toggleTerminalToolbar(); + return true; + }); registerForContextMenu(mTerminalView); - FileReceiverActivity.updateFileReceiverActivityComponentsState(this); - - try { - // Start the {@link TermuxService} and make it run regardless of who is bound to it - Intent serviceIntent = new Intent(this, TermuxService.class); + // Start the {@link TermuxService} and make it run regardless of who is bound to it + Intent serviceIntent = new Intent(this, TermuxService.class); + if (Build.VERSION.SDK_INT >= 26) { + startForegroundService(serviceIntent); + } else { startService(serviceIntent); - - // Attempt to bind to the service, this will call the {@link #onServiceConnected(ComponentName, IBinder)} - // callback if it succeeds. - if (!bindService(serviceIntent, this, 0)) - throw new RuntimeException("bindService() failed"); - } catch (Exception e) { - Logger.logStackTraceWithMessage(LOG_TAG,"TermuxActivity failed to start TermuxService", e); - Logger.showToast(this, - getString(e.getMessage() != null && e.getMessage().contains("app is in background") ? - R.string.error_termux_service_start_failed_bg : R.string.error_termux_service_start_failed_general), - true); - mIsInvalidState = true; - return; } + ; - // Send the {@link TermuxConstants#BROADCAST_TERMUX_OPENED} broadcast to notify apps that Termux - // app has been opened. - TermuxUtils.sendTermuxOpenedBroadcast(this); + // Attempt to bind to the service, this will call the {@link #onServiceConnected(ComponentName, IBinder)} + // callback if it succeeds. + if (!bindService(serviceIntent, this, 0)) { + throw new RuntimeException("bindService() failed"); + } } @Override public void onStart() { super.onStart(); - Logger.logDebug(LOG_TAG, "onStart"); - - if (mIsInvalidState) return; - mIsVisible = true; + mTermuxTerminalSessionActivityClient.onStart(); - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.onStart(); - - if (mTermuxTerminalViewClient != null) + if (mTermuxTerminalViewClient != null) { mTermuxTerminalViewClient.onStart(); - - if (mPreferences.isTerminalMarginAdjustmentEnabled()) - addTermuxActivityRootViewGlobalLayoutListener(); + } registerTermuxActivityBroadcastReceiver(); } @@ -306,42 +223,23 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo public void onResume() { super.onResume(); - Logger.logVerbose(LOG_TAG, "onResume"); - - if (mIsInvalidState) return; - - if (mTermuxTerminalSessionActivityClient != null) + if (mTermuxTerminalSessionActivityClient != null) { mTermuxTerminalSessionActivityClient.onResume(); - - if (mTermuxTerminalViewClient != null) - mTermuxTerminalViewClient.onResume(); - - // Check if a crash happened on last run of the app or if a plugin crashed and show a - // notification with the crash details if it did - TermuxCrashUtils.notifyAppCrashFromCrashLogFile(this, LOG_TAG); - - mIsOnResumeAfterOnCreate = false; + } } @Override protected void onStop() { super.onStop(); - Logger.logDebug(LOG_TAG, "onStop"); - - if (mIsInvalidState) return; - mIsVisible = false; - if (mTermuxTerminalSessionActivityClient != null) + if (mTermuxTerminalSessionActivityClient != null) { mTermuxTerminalSessionActivityClient.onStop(); + } - if (mTermuxTerminalViewClient != null) - mTermuxTerminalViewClient.onStop(); + unregisterReceiver(mTermuxActivityBroadcastReceiver); - removeTermuxActivityRootViewGlobalLayoutListener(); - - unregisterTermuxActivityBroadcastReceiver(); getDrawer().closeDrawers(); } @@ -349,10 +247,6 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo public void onDestroy() { super.onDestroy(); - Logger.logDebug(LOG_TAG, "onDestroy"); - - if (mIsInvalidState) return; - if (mTermuxService != null) { // Do not leave service and session clients with references to activity. mTermuxService.unsetTermuxTerminalSessionClient(); @@ -368,17 +262,11 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo @Override public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { - Logger.logVerbose(LOG_TAG, "onSaveInstanceState"); - super.onSaveInstanceState(savedInstanceState); saveTerminalToolbarTextInput(savedInstanceState); savedInstanceState.putBoolean(ARG_ACTIVITY_RECREATED, true); } - - - - /** * Part of the {@link ServiceConnection} interface. The service is bound with * {@link #bindService(Intent, ServiceConnection, int)} in {@link #onCreate(Bundle)} which will cause a call to this @@ -386,40 +274,42 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo */ @Override public void onServiceConnected(ComponentName componentName, IBinder service) { - Logger.logDebug(LOG_TAG, "onServiceConnected"); - mTermuxService = ((TermuxService.LocalBinder) service).service; - setTermuxSessionsListView(); + ListView termuxSessionsListView = findViewById(R.id.terminal_sessions_list); + mTermuxSessionListViewController = new TermuxSessionsListViewController(this, mTermuxService.getTermuxSessions()); + termuxSessionsListView.setAdapter(mTermuxSessionListViewController); + termuxSessionsListView.setOnItemClickListener(mTermuxSessionListViewController); + termuxSessionsListView.setOnItemLongClickListener(mTermuxSessionListViewController); + final Intent intent = getIntent(); + if (intent != null) { + Log.e("termux", "SHORT CLASS: " + intent.getComponent().getShortClassName()); + Log.e("termux", "LONG CLASS: " + intent.getComponent().getClassName()); + } setIntent(null); + boolean isFailSafe = intent.getBooleanExtra(EXTRA_FAILSAFE_SESSION, false); + if (mTermuxService.isTermuxSessionsEmpty()) { - if (mIsVisible) { - TermuxInstaller.setupBootstrapIfNeeded(TermuxActivity.this, () -> { - if (mTermuxService == null) return; // Activity might have been destroyed. - try { - boolean launchFailsafe = false; - if (intent != null && intent.getExtras() != null) { - launchFailsafe = intent.getExtras().getBoolean(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false); - } - mTermuxTerminalSessionActivityClient.addNewSession(launchFailsafe, null); - } catch (WindowManager.BadTokenException e) { - // Activity finished - ignore. - } - }); - } else { - // The service connected while not in foreground - just bail out. - finishActivityIfNotFinishing(); - } + TermuxInstaller.setupBootstrapIfNeeded(TermuxActivity.this, () -> { + if (mTermuxService == null) { + // Activity might have been destroyed. + return; + } + try { + mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null); + } catch (WindowManager.BadTokenException e) { + // Activity finished - ignore. + } + }); } else { // If termux was started from launcher "New session" shortcut and activity is recreated, // then the original intent will be re-delivered, resulting in a new session being re-added // each time. - if (!mIsActivityRecreated && intent != null && Intent.ACTION_RUN.equals(intent.getAction())) { + if (Intent.ACTION_RUN.equals(intent.getAction())) { // Android 7.1 app shortcut from res/xml/shortcuts.xml. - boolean isFailSafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false); mTermuxTerminalSessionActivityClient.addNewSession(isFailSafe, null); } else { mTermuxTerminalSessionActivityClient.setCurrentSession(mTermuxTerminalSessionActivityClient.getCurrentStoredSessionOrLast()); @@ -432,97 +322,26 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo @Override public void onServiceDisconnected(ComponentName name) { - Logger.logDebug(LOG_TAG, "onServiceDisconnected"); - // Respect being stopped from the {@link TermuxService} notification action. finishActivityIfNotFinishing(); } - - - - - - private void reloadProperties() { - mProperties.loadTermuxPropertiesFromDisk(); - - if (mTermuxTerminalViewClient != null) - mTermuxTerminalViewClient.onReloadProperties(); - } - - - - private void setActivityTheme() { - // Update NightMode.APP_NIGHT_MODE - TermuxThemeUtils.setAppNightMode(mProperties.getNightMode()); - - // Set activity night mode. If NightMode.SYSTEM is set, then android will automatically - // trigger recreation of activity when uiMode/dark mode configuration is changed so that - // day or night theme takes affect. - AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true); - } - - private void setMargins() { - RelativeLayout relativeLayout = findViewById(R.id.activity_termux_root_relative_layout); - int marginHorizontal = mProperties.getTerminalMarginHorizontal(); - int marginVertical = mProperties.getTerminalMarginVertical(); - ViewUtils.setLayoutMarginsInDp(relativeLayout, marginHorizontal, marginVertical, marginHorizontal, marginVertical); - } - - - - public void addTermuxActivityRootViewGlobalLayoutListener() { - getTermuxActivityRootView().getViewTreeObserver().addOnGlobalLayoutListener(getTermuxActivityRootView()); - } - - public void removeTermuxActivityRootViewGlobalLayoutListener() { - if (getTermuxActivityRootView() != null) - getTermuxActivityRootView().getViewTreeObserver().removeOnGlobalLayoutListener(getTermuxActivityRootView()); - } - - - - private void setTermuxTerminalViewAndClients() { - // Set termux terminal view and session clients - mTermuxTerminalSessionActivityClient = new TermuxTerminalSessionActivityClient(this); - mTermuxTerminalViewClient = new TermuxTerminalViewClient(this, mTermuxTerminalSessionActivityClient); - - // Set termux terminal view - mTerminalView = findViewById(R.id.terminal_view); - mTerminalView.setTerminalViewClient(mTermuxTerminalViewClient); - - if (mTermuxTerminalViewClient != null) - mTermuxTerminalViewClient.onCreate(); - - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.onCreate(); - } - - private void setTermuxSessionsListView() { - ListView termuxSessionsListView = findViewById(R.id.terminal_sessions_list); - mTermuxSessionListViewController = new TermuxSessionsListViewController(this, mTermuxService.getTermuxSessions()); - termuxSessionsListView.setAdapter(mTermuxSessionListViewController); - termuxSessionsListView.setOnItemClickListener(mTermuxSessionListViewController); - termuxSessionsListView.setOnItemLongClickListener(mTermuxSessionListViewController); - } - - - private void setTerminalToolbarView(Bundle savedInstanceState) { mTermuxTerminalExtraKeys = new TermuxTerminalExtraKeys(this, mTerminalView, mTermuxTerminalViewClient, mTermuxTerminalSessionActivityClient); final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager(); - if (mPreferences.shouldShowTerminalToolbar()) terminalToolbarViewPager.setVisibility(View.VISIBLE); + if (mPreferences.isShowTerminalToolbar()) { + terminalToolbarViewPager.setVisibility(View.VISIBLE); + } ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams(); mTerminalToolbarDefaultHeight = layoutParams.height; setTerminalToolbarHeight(); - String savedTextInput = null; - if (savedInstanceState != null) - savedTextInput = savedInstanceState.getString(ARG_TERMINAL_TOOLBAR_TEXT_INPUT); + String savedTextInput = savedInstanceState == null ? null : + savedInstanceState.getString(ARG_TERMINAL_TOOLBAR_TEXT_INPUT); terminalToolbarViewPager.setAdapter(new TerminalToolbarViewPager.PageAdapter(this, savedTextInput)); terminalToolbarViewPager.addOnPageChangeListener(new TerminalToolbarViewPager.OnPageChangeListener(this, terminalToolbarViewPager)); @@ -535,7 +354,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo ViewGroup.LayoutParams layoutParams = terminalToolbarViewPager.getLayoutParams(); layoutParams.height = Math.round(mTerminalToolbarDefaultHeight * (mTermuxTerminalExtraKeys.getExtraKeysInfo() == null ? 0 : mTermuxTerminalExtraKeys.getExtraKeysInfo().getMatrix().length) * - mProperties.getTerminalToolbarHeightScaleFactor()); + 1 + ); terminalToolbarViewPager.setLayoutParams(layoutParams); } @@ -543,8 +363,8 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo final ViewPager terminalToolbarViewPager = getTerminalToolbarViewPager(); if (terminalToolbarViewPager == null) return; - final boolean showNow = mPreferences.toogleShowTerminalToolbar(); - Logger.showToast(this, (showNow ? getString(R.string.msg_enabling_terminal_toolbar) : getString(R.string.msg_disabling_terminal_toolbar)), true); + final boolean showNow = mPreferences.toggleShowTerminalToolbar(); + TermuxMessageDialogUtils.showToast(this, (showNow ? getString(R.string.msg_enabling_terminal_toolbar) : getString(R.string.msg_disabling_terminal_toolbar))); terminalToolbarViewPager.setVisibility(showNow ? View.VISIBLE : View.GONE); if (showNow && isTerminalToolbarTextInputViewSelected()) { // Focus the text input view if just revealed. @@ -558,47 +378,11 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo final EditText textInputView = findViewById(R.id.terminal_toolbar_text_input); if (textInputView != null) { String textInput = textInputView.getText().toString(); - if (!textInput.isEmpty()) savedInstanceState.putString(ARG_TERMINAL_TOOLBAR_TEXT_INPUT, textInput); + if (!textInput.isEmpty()) + savedInstanceState.putString(ARG_TERMINAL_TOOLBAR_TEXT_INPUT, textInput); } } - - - private void setSettingsButtonView() { - ImageButton settingsButton = findViewById(R.id.settings_button); - settingsButton.setOnClickListener(v -> { - ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class)); - }); - } - - private void setNewSessionButtonView() { - View newSessionButton = findViewById(R.id.new_session_button); - newSessionButton.setOnClickListener(v -> mTermuxTerminalSessionActivityClient.addNewSession(false, null)); - newSessionButton.setOnLongClickListener(v -> { - TextInputDialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null, - R.string.action_create_named_session_confirm, text -> mTermuxTerminalSessionActivityClient.addNewSession(false, text), - R.string.action_new_session_failsafe, text -> mTermuxTerminalSessionActivityClient.addNewSession(true, text), - -1, null, null); - return true; - }); - } - - private void setToggleKeyboardView() { - findViewById(R.id.toggle_keyboard_button).setOnClickListener(v -> { - mTermuxTerminalViewClient.onToggleSoftKeyboardRequest(); - getDrawer().closeDrawers(); - }); - - findViewById(R.id.toggle_keyboard_button).setOnLongClickListener(v -> { - toggleTerminalToolbar(); - return true; - }); - } - - - - - @SuppressLint("RtlHardcoded") @Override public void onBackPressed() { @@ -611,12 +395,14 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo public void finishActivityIfNotFinishing() { // prevent duplicate calls to finish() if called from multiple places - if (!TermuxActivity.this.isFinishing()) { + if (!isFinishing()) { finish(); } } - /** Show a toast and dismiss the last one if still visible. */ + /** + * Show a toast and dismiss the last one if still visible. + */ public void showToast(String text, boolean longDuration) { if (text == null || text.isEmpty()) return; if (mLastToast != null) mLastToast.cancel(); @@ -625,8 +411,6 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo mLastToast.show(); } - - @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { TerminalSession currentSession = getCurrentSession(); @@ -642,20 +426,23 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.action_select_url); menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.action_share_transcript); - if (!DataUtils.isNullOrEmpty(mTerminalView.getStoredSelectedText())) + + if (mTerminalView.getStoredSelectedText() != null) { menu.add(Menu.NONE, CONTEXT_MENU_SHARE_SELECTED_TEXT, Menu.NONE, R.string.action_share_selected_text); + } if (addAutoFillMenu) menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password); menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal); menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning()); menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal); - menu.add(Menu.NONE, CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.action_toggle_keep_screen_on).setCheckable(true).setChecked(mPreferences.shouldKeepScreenOn()); + menu.add(Menu.NONE, CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.action_toggle_keep_screen_on).setCheckable(true).setChecked(mTerminalView.getKeepScreenOn()); menu.add(Menu.NONE, CONTEXT_MENU_HELP_ID, Menu.NONE, R.string.action_open_help); - menu.add(Menu.NONE, CONTEXT_MENU_SETTINGS_ID, Menu.NONE, R.string.action_open_settings); menu.add(Menu.NONE, CONTEXT_MENU_REPORT_ID, Menu.NONE, R.string.action_report_issue); } - /** Hook system menu to show context menu instead. */ + /** + * Hook system menu to show context menu instead. + */ @Override public boolean onCreateOptionsMenu(Menu menu) { mTerminalView.showContextMenu(); @@ -680,7 +467,10 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo requestAutoFill(); return true; case CONTEXT_MENU_RESET_TERMINAL_ID: - onResetTerminalSession(session); + if (session != null) { + session.reset(); + showToast(getResources().getString(R.string.msg_terminal_reset), true); + } return true; case CONTEXT_MENU_KILL_PROCESS_ID: showKillSessionDialog(session); @@ -692,13 +482,7 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo toggleKeepScreenOn(); return true; case CONTEXT_MENU_HELP_ID: - ActivityUtils.startActivity(this, new Intent(this, HelpActivity.class)); - return true; - case CONTEXT_MENU_SETTINGS_ID: - ActivityUtils.startActivity(this, new Intent(this, SettingsActivity.class)); - return true; - case CONTEXT_MENU_REPORT_ID: - mTermuxTerminalViewClient.reportIssueFromTranscript(); + startActivity(new Intent(this, TermuxHelpActivity.class)); return true; default: return super.onContextItemSelected(item); @@ -726,41 +510,37 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo b.show(); } - private void onResetTerminalSession(TerminalSession session) { - if (session != null) { - session.reset(); - showToast(getResources().getString(R.string.msg_terminal_reset), true); - - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.onResetTerminalSession(); - } - } - private void showStylingDialog() { Intent stylingIntent = new Intent(); - stylingIntent.setClassName(TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, TermuxConstants.TERMUX_STYLING.TERMUX_STYLING_ACTIVITY_NAME); + stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity"); try { startActivity(stylingIntent); } catch (ActivityNotFoundException | IllegalArgumentException e) { // The startActivity() call is not documented to throw IllegalArgumentException. // However, crash reporting shows that it sometimes does, so catch it here. + String installationUrl = isInstalledFromGooglePlay() + ? "https://play.google.com/store/apps/details?id=com.termux.styling" + : "https://f-droid.org/en/packages/com.termux.styling"; new AlertDialog.Builder(this).setMessage(getString(R.string.error_styling_not_installed)) .setPositiveButton(R.string.action_styling_install, - (dialog, which) -> ActivityUtils.startActivity(this, new Intent(Intent.ACTION_VIEW, Uri.parse(TermuxConstants.TERMUX_STYLING_FDROID_PACKAGE_URL)))) + (dialog, which) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(installationUrl)))) .setNegativeButton(android.R.string.cancel, null).show(); } } - private void toggleKeepScreenOn() { - if (mTerminalView.getKeepScreenOn()) { - mTerminalView.setKeepScreenOn(false); - mPreferences.setKeepScreenOn(false); - } else { - mTerminalView.setKeepScreenOn(true); - mPreferences.setKeepScreenOn(true); - } + + private boolean isInstalledFromGooglePlay() { + List validInstallers = new ArrayList<>(Arrays.asList("com.android.vending", "com.google.android.feedback")); + final String installer = getPackageManager().getInstallerPackageName(getPackageName()); + return installer != null && validInstallers.contains(installer); } - private void requestAutoFill() { + private void toggleKeepScreenOn() { + boolean newValue = !mTerminalView.getKeepScreenOn(); + mTerminalView.setKeepScreenOn(newValue); + mPreferences.setKeepScreenOn(newValue); + } + + public void requestAutoFill() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { AutofillManager autofillManager = getSystemService(AutofillManager.class); if (autofillManager != null && autofillManager.isEnabled()) { @@ -769,69 +549,6 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo } } - - - /** - * For processes to access primary external storage (/sdcard, /storage/emulated/0, ~/storage/shared), - * termux needs to be granted legacy WRITE_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE permissions - * if targeting targetSdkVersion 30 (android 11) and running on sdk 30 (android 11) and higher. - */ - public void requestStoragePermission(boolean isPermissionCallback) { - new Thread() { - @Override - public void run() { - // Do not ask for permission again - int requestCode = isPermissionCallback ? -1 : PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION; - - // If permission is granted, then also setup storage symlinks. - if(PermissionUtils.checkAndRequestLegacyOrManageExternalStoragePermission( - TermuxActivity.this, requestCode, !isPermissionCallback)) { - if (isPermissionCallback) - Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG, - getString(com.termux.shared.R.string.msg_storage_permission_granted_on_request)); - - TermuxInstaller.setupStorageSymlinks(TermuxActivity.this); - } else { - if (isPermissionCallback) - Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG, - getString(com.termux.shared.R.string.msg_storage_permission_not_granted_on_request)); - } - } - }.start(); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - Logger.logVerbose(LOG_TAG, "onActivityResult: requestCode: " + requestCode + ", resultCode: " + resultCode + ", data: " + IntentUtils.getIntentString(data)); - if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) { - requestStoragePermission(true); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - Logger.logVerbose(LOG_TAG, "onRequestPermissionsResult: requestCode: " + requestCode + ", permissions: " + Arrays.toString(permissions) + ", grantResults: " + Arrays.toString(grantResults)); - if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) { - requestStoragePermission(true); - } - } - - - - public int getNavBarHeight() { - return mNavBarHeight; - } - - public TermuxActivityRootView getTermuxActivityRootView() { - return mTermuxActivityRootView; - } - - public View getTermuxActivityBottomSpaceView() { - return mTermuxActivityBottomSpaceView; - } - public ExtraKeysView getExtraKeysView() { return mExtraKeysView; } @@ -845,12 +562,12 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo } public DrawerLayout getDrawer() { - return (DrawerLayout) findViewById(R.id.drawer_layout); + return findViewById(R.id.drawer_layout); } public ViewPager getTerminalToolbarViewPager() { - return (ViewPager) findViewById(R.id.terminal_toolbar_view_pager); + return findViewById(R.id.terminal_toolbar_view_pager); } public float getTerminalToolbarDefaultHeight() { @@ -874,16 +591,6 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo return mIsVisible; } - public boolean isOnResumeAfterOnCreate() { - return mIsOnResumeAfterOnCreate; - } - - public boolean isActivityRecreated() { - return mIsActivityRecreated; - } - - - public TermuxService getTermuxService() { return mTermuxService; } @@ -892,83 +599,34 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo return mTerminalView; } - public TermuxTerminalViewClient getTermuxTerminalViewClient() { - return mTermuxTerminalViewClient; - } - public TermuxTerminalSessionActivityClient getTermuxTerminalSessionClient() { return mTermuxTerminalSessionActivityClient; } @Nullable public TerminalSession getCurrentSession() { - if (mTerminalView != null) - return mTerminalView.getCurrentSession(); - else - return null; - } - - public TermuxAppSharedPreferences getPreferences() { - return mPreferences; - } - - public TermuxAppSharedProperties getProperties() { - return mProperties; - } - - - - - public static void updateTermuxActivityStyling(Context context, boolean recreateActivity) { - // Make sure that terminal styling is always applied. - Intent stylingIntent = new Intent(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE); - stylingIntent.putExtra(TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY, recreateActivity); - context.sendBroadcast(stylingIntent); + return mTerminalView == null ? null : mTerminalView.getCurrentSession(); } private void registerTermuxActivityBroadcastReceiver() { IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(TERMUX_ACTIVITY.ACTION_NOTIFY_APP_CRASH); - intentFilter.addAction(TERMUX_ACTIVITY.ACTION_RELOAD_STYLE); - intentFilter.addAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS); + intentFilter.addAction(ACTION_RELOAD_STYLE); + intentFilter.addAction(ACTION_REQUEST_PERMISSIONS); - registerReceiver(mTermuxActivityBroadcastReceiver, intentFilter); - } - - private void unregisterTermuxActivityBroadcastReceiver() { - unregisterReceiver(mTermuxActivityBroadcastReceiver); - } - - private void fixTermuxActivityBroadcastReceiverIntent(Intent intent) { - if (intent == null) return; - - String extraReloadStyle = intent.getStringExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE); - if ("storage".equals(extraReloadStyle)) { - intent.removeExtra(TERMUX_ACTIVITY.EXTRA_RELOAD_STYLE); - intent.setAction(TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS); + if (Build.VERSION.SDK_INT >= 33) { + registerReceiver(mTermuxActivityBroadcastReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(mTermuxActivityBroadcastReceiver, intentFilter); } } class TermuxActivityBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (intent == null) return; - if (mIsVisible) { - fixTermuxActivityBroadcastReceiverIntent(intent); - switch (intent.getAction()) { - case TERMUX_ACTIVITY.ACTION_NOTIFY_APP_CRASH: - Logger.logDebug(LOG_TAG, "Received intent to notify app crash"); - TermuxCrashUtils.notifyAppCrashFromCrashLogFile(context, LOG_TAG); - return; - case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE: - Logger.logDebug(LOG_TAG, "Received intent to reload styling"); - reloadActivityStyling(intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_RECREATE_ACTIVITY, true)); - return; - case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS: - Logger.logDebug(LOG_TAG, "Received intent to request storage permissions"); - requestStoragePermission(false); + case ACTION_RELOAD_STYLE: + reloadActivityStyling(); return; default: } @@ -976,49 +634,17 @@ public final class TermuxActivity extends AppCompatActivity implements ServiceCo } } - private void reloadActivityStyling(boolean recreateActivity) { - if (mProperties != null) { - reloadProperties(); + private void reloadActivityStyling() { + mProperties.reloadProperties(); - if (mExtraKeysView != null) { - mExtraKeysView.setButtonTextAllCaps(mProperties.shouldExtraKeysTextBeAllCaps()); - mExtraKeysView.reload(mTermuxTerminalExtraKeys.getExtraKeysInfo(), mTerminalToolbarDefaultHeight); - } - - // Update NightMode.APP_NIGHT_MODE - TermuxThemeUtils.setAppNightMode(mProperties.getNightMode()); + if (mExtraKeysView != null) { + //mExtraKeysView.setButtonTextAllCaps(mProperties.shouldExtraKeysTextBeAllCaps()); + mExtraKeysView.reload(mTermuxTerminalExtraKeys.getExtraKeysInfo(), mTerminalToolbarDefaultHeight); } - setMargins(); setTerminalToolbarHeight(); - FileReceiverActivity.updateFileReceiverActivityComponentsState(this); - if (mTermuxTerminalSessionActivityClient != null) mTermuxTerminalSessionActivityClient.onReloadActivityStyling(); - - if (mTermuxTerminalViewClient != null) - mTermuxTerminalViewClient.onReloadActivityStyling(); - - // To change the activity and drawer theme, activity needs to be recreated. - // It will destroy the activity, including all stored variables and views, and onCreate() - // will be called again. Extra keys input text, terminal sessions and transcripts will be preserved. - if (recreateActivity) { - Logger.logDebug(LOG_TAG, "Recreating activity"); - TermuxActivity.this.recreate(); - } } - - - - public static void startTermuxActivity(@NonNull final Context context) { - ActivityUtils.startActivity(context, newInstance(context)); - } - - public static Intent newInstance(@NonNull final Context context) { - Intent intent = new Intent(context, TermuxActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return intent; - } - } diff --git a/app/src/main/java/com/termux/app/TermuxAppShell.java b/app/src/main/java/com/termux/app/TermuxAppShell.java new file mode 100644 index 00000000..82a1f934 --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxAppShell.java @@ -0,0 +1,135 @@ +package com.termux.app; + +import android.content.Context; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +public final class TermuxAppShell { + public class StreamGobbler extends Thread { + @NonNull + private final String shell; + @NonNull + private final InputStream inputStream; + @NonNull + private final BufferedReader reader; + private static final String LOG_TAG = "termux-tasks"; + + public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream) { + super("TermuxStreamGobbler"); + this.shell = shell; + this.inputStream = inputStream; + reader = new BufferedReader(new InputStreamReader(inputStream)); + } + + @Override + public void run() { + try { + String line; + while ((line = reader.readLine()) != null) { + // TODO: Is this wait necessary? + // TODO: log + try { + this.wait(128); + } catch (InterruptedException e) { + // no action + } + } + } catch (IOException e) { + // reader probably closed, expected exit condition + } + + // make sure our stream is closed and resources will be freed + try { + reader.close(); + } catch (IOException e) { + // read already closed + } + } + } + + private final Process mProcess; + private final TermuxService mAppShellClient; + + private TermuxAppShell(@NonNull final Process process, final TermuxService appShellClient) { + this.mProcess = process; + this.mAppShellClient = appShellClient; + } + + public static TermuxAppShell execute(String executable, + String[] arguments, + @NonNull final TermuxService termuxService) { + final String[] commandArray = TermuxShellUtils.setupShellCommandArguments(executable, arguments); + String[] environmentArray = TermuxShellUtils.setupEnvironment(false); + final Process process; + try { + process = Runtime.getRuntime().exec(commandArray, environmentArray, new File(TermuxConstants.HOME_PATH)); + } catch (IOException e) { + Log.e(TermuxConstants.LOG_TAG, "Error executing task", e); + return null; + } + + final TermuxAppShell appShell = new TermuxAppShell(process, termuxService); + new Thread() { + @Override + public void run() { + try { + appShell.executeInner(termuxService); + } catch (IllegalThreadStateException | InterruptedException e) { + Log.e(TermuxConstants.LOG_TAG, "Error: " + e); + } + } + }.start(); + + return appShell; + } + + private void executeInner(@NonNull final Context context) throws IllegalThreadStateException, InterruptedException { + int mPid = TermuxShellUtils.getPid(mProcess); + + DataOutputStream STDIN = new DataOutputStream(mProcess.getOutputStream()); + StreamGobbler STDOUT = new StreamGobbler(mPid + "-stdout-gobbler", mProcess.getInputStream()); + StreamGobbler STDERR = new StreamGobbler(mPid + "-stderr-gobbler", mProcess.getErrorStream()); + + STDOUT.start(); + STDERR.start(); + + int exitCode = mProcess.waitFor(); + + try { + STDIN.close(); + } catch (IOException e) { + // might be closed already + } + STDOUT.join(); + STDERR.join(); + mProcess.destroy(); + + // TODO: handle exit code, (notify on success, show something more on error)? + + } + + /** + * Kill this {@link TermuxAppShell} by sending a {@link OsConstants#SIGILL} to its {@link #mProcess}. + */ + public void kill() { + int pid = TermuxShellUtils.getPid(mProcess); + try { + // Send SIGKILL to process + Os.kill(pid, OsConstants.SIGKILL); + } catch (ErrnoException e) { + Log.w(TermuxConstants.LOG_TAG, "Failed to send SIGKILL to AppShell with pid " + pid + ": " + e.getMessage()); + } + } + +} diff --git a/app/src/main/java/com/termux/app/TermuxApplication.java b/app/src/main/java/com/termux/app/TermuxApplication.java deleted file mode 100644 index 1123abf6..00000000 --- a/app/src/main/java/com/termux/app/TermuxApplication.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.termux.app; - -import android.app.Application; -import android.content.Context; - -import com.termux.BuildConfig; -import com.termux.shared.errors.Error; -import com.termux.shared.logger.Logger; -import com.termux.shared.termux.TermuxBootstrap; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.termux.crash.TermuxCrashUtils; -import com.termux.shared.termux.file.TermuxFileUtils; -import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; -import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; -import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; -import com.termux.shared.termux.shell.am.TermuxAmSocketServer; -import com.termux.shared.termux.shell.TermuxShellManager; -import com.termux.shared.termux.theme.TermuxThemeUtils; - -public class TermuxApplication extends Application { - - private static final String LOG_TAG = "TermuxApplication"; - - public void onCreate() { - super.onCreate(); - - Context context = getApplicationContext(); - - // Set crash handler for the app - TermuxCrashUtils.setDefaultCrashHandler(this); - - // Set log config for the app - setLogConfig(context); - - Logger.logDebug("Starting Application"); - - // Set TermuxBootstrap.TERMUX_APP_PACKAGE_MANAGER and TermuxBootstrap.TERMUX_APP_PACKAGE_VARIANT - TermuxBootstrap.setTermuxPackageManagerAndVariant(BuildConfig.TERMUX_PACKAGE_VARIANT); - - // Init app wide SharedProperties loaded from termux.properties - TermuxAppSharedProperties properties = TermuxAppSharedProperties.init(context); - - // Init app wide shell manager - TermuxShellManager shellManager = TermuxShellManager.init(context); - - // Set NightMode.APP_NIGHT_MODE - TermuxThemeUtils.setAppNightMode(properties.getNightMode()); - - // Check and create termux files directory. If failed to access it like in case of secondary - // user or external sd card installation, then don't run files directory related code - Error error = TermuxFileUtils.isTermuxFilesDirectoryAccessible(this, true, true); - boolean isTermuxFilesDirectoryAccessible = error == null; - if (isTermuxFilesDirectoryAccessible) { - Logger.logInfo(LOG_TAG, "Termux files directory is accessible"); - - error = TermuxFileUtils.isAppsTermuxAppDirectoryAccessible(true, true); - if (error != null) { - Logger.logErrorExtended(LOG_TAG, "Create apps/termux-app directory failed\n" + error); - return; - } - - // Setup termux-am-socket server - TermuxAmSocketServer.setupTermuxAmSocketServer(context); - } else { - Logger.logErrorExtended(LOG_TAG, "Termux files directory is not accessible\n" + error); - } - - // Init TermuxShellEnvironment constants and caches after everything has been setup including termux-am-socket server - TermuxShellEnvironment.init(this); - - if (isTermuxFilesDirectoryAccessible) { - TermuxShellEnvironment.writeEnvironmentToFile(this); - } - } - - public static void setLogConfig(Context context) { - Logger.setDefaultLogTag(TermuxConstants.TERMUX_APP_NAME); - - // Load the log level from shared preferences and set it to the {@link Logger.CURRENT_LOG_LEVEL} - TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context); - if (preferences == null) return; - preferences.setLogLevel(null, preferences.getLogLevel()); - } - -} diff --git a/app/src/main/java/com/termux/app/TermuxConstants.java b/app/src/main/java/com/termux/app/TermuxConstants.java new file mode 100644 index 00000000..05a64739 --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxConstants.java @@ -0,0 +1,14 @@ +package com.termux.app; + +public class TermuxConstants { + + public static final String LOG_TAG = "termux"; + + public static final String FILES_PATH = "/data/data/com.termux/files"; + public static final String PREFIX_PATH = FILES_PATH + "/usr"; + public static final String BIN_PATH = PREFIX_PATH + "/bin"; + public static final String HOME_PATH = FILES_PATH + "/home"; + + public static final int TERMUX_APP_NOTIFICATION_ID = 1337; + +} diff --git a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java b/app/src/main/java/com/termux/app/TermuxDocumentsProvider.java similarity index 98% rename from app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java rename to app/src/main/java/com/termux/app/TermuxDocumentsProvider.java index 7974d6db..e28f60e4 100644 --- a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java +++ b/app/src/main/java/com/termux/app/TermuxDocumentsProvider.java @@ -1,4 +1,4 @@ -package com.termux.filepicker; +package com.termux.app; import android.content.res.AssetFileDescriptor; import android.database.Cursor; @@ -12,7 +12,6 @@ import android.provider.DocumentsProvider; import android.webkit.MimeTypeMap; import com.termux.R; -import com.termux.shared.termux.TermuxConstants; import java.io.File; import java.io.FileNotFoundException; @@ -35,7 +34,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider { private static final String ALL_MIME_TYPES = "*/*"; - private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR; + private static final File BASE_DIR = new File(TermuxConstants.HOME_PATH); // The default columns to return information about a root if no specific @@ -171,7 +170,7 @@ public class TermuxDocumentsProvider extends DocumentsProvider { // through the whole SD card). boolean isInsideHome; try { - isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.TERMUX_HOME_DIR_PATH); + isInsideHome = file.getCanonicalPath().startsWith(TermuxConstants.HOME_PATH); } catch (IOException e) { isInsideHome = true; } diff --git a/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java b/app/src/main/java/com/termux/app/TermuxFileReceiverActivity.java similarity index 55% rename from app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java rename to app/src/main/java/com/termux/app/TermuxFileReceiverActivity.java index 3ce524f6..c33040b0 100644 --- a/app/src/main/java/com/termux/app/api/file/FileReceiverActivity.java +++ b/app/src/main/java/com/termux/app/TermuxFileReceiverActivity.java @@ -1,30 +1,16 @@ -package com.termux.app.api.file; +package com.termux.app; -import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.OpenableColumns; +import android.util.Log; import android.util.Patterns; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import com.termux.R; -import com.termux.shared.android.PackageUtils; -import com.termux.shared.data.DataUtils; -import com.termux.shared.data.IntentUtils; -import com.termux.shared.net.uri.UriUtils; -import com.termux.shared.interact.MessageDialogUtils; -import com.termux.shared.net.uri.UriScheme; -import com.termux.shared.termux.interact.TextInputDialogUtils; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.termux.TermuxConstants.TERMUX_APP; -import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; -import com.termux.app.TermuxService; -import com.termux.shared.logger.Logger; -import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; -import com.termux.shared.termux.settings.properties.TermuxPropertyConstants; import java.io.ByteArrayInputStream; import java.io.File; @@ -36,11 +22,11 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.regex.Pattern; -public class FileReceiverActivity extends AppCompatActivity { +public class TermuxFileReceiverActivity extends AppCompatActivity { - static final String TERMUX_RECEIVEDIR = TermuxConstants.TERMUX_FILES_DIR_PATH + "/home/downloads"; - static final String EDITOR_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-file-editor"; - static final String URL_OPENER_PROGRAM = TermuxConstants.TERMUX_HOME_DIR_PATH + "/bin/termux-url-opener"; + static final String TERMUX_RECEIVEDIR = TermuxConstants.HOME_PATH + "/downloads"; + static final String EDITOR_PROGRAM = TermuxConstants.HOME_PATH + "/bin/termux-file-editor"; + static final String URL_OPENER_PROGRAM = TermuxConstants.HOME_PATH + "/bin/termux-url-opener"; /** * If the activity should be finished when the name input dialog is dismissed. This is disabled @@ -50,8 +36,6 @@ public class FileReceiverActivity extends AppCompatActivity { */ boolean mFinishOnDismissNameDialog = true; - private static final String API_TAG = TermuxConstants.TERMUX_APP_NAME + "FileReceiver"; - private static final String LOG_TAG = "FileReceiverActivity"; static boolean isSharedTextAnUrl(String sharedText) { @@ -68,9 +52,7 @@ public class FileReceiverActivity extends AppCompatActivity { final String type = intent.getType(); final String scheme = intent.getScheme(); - Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent)); - - final String sharedTitle = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_TITLE, null); + final String sharedTitle = intent.getStringExtra(Intent.EXTRA_TITLE); if (Intent.ACTION_SEND.equals(action) && type != null) { final String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT); @@ -82,7 +64,7 @@ public class FileReceiverActivity extends AppCompatActivity { if (isSharedTextAnUrl(sharedText)) { handleUrlAndFinish(sharedText); } else { - String subject = IntentUtils.getStringExtraIfSet(intent, Intent.EXTRA_SUBJECT, null); + String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT); if (subject == null) subject = sharedTitle; if (subject != null) subject += ".txt"; promptNameAndSave(new ByteArrayInputStream(sharedText.getBytes(StandardCharsets.UTF_8)), subject); @@ -98,14 +80,13 @@ public class FileReceiverActivity extends AppCompatActivity { return; } - if (UriScheme.SCHEME_CONTENT.equals(scheme)) { + if ("content".equals(scheme)) { handleContentUri(dataUri, sharedTitle); - } else if (UriScheme.SCHEME_FILE.equals(scheme)) { - Logger.logVerbose(LOG_TAG, "uri: \"" + dataUri + "\", path: \"" + dataUri.getPath() + "\", fragment: \"" + dataUri.getFragment() + "\""); + } else if ("file".equals(scheme)) { + Log.v(LOG_TAG, "uri: \"" + dataUri + "\", path: \"" + dataUri.getPath() + "\", fragment: \"" + dataUri.getFragment() + "\""); - // Get full path including fragment (anything after last "#") - String path = UriUtils.getUriFilePathWithFragment(dataUri); - if (DataUtils.isNullOrEmpty(path)) { + String path = dataUri.getPath(); + if (path == null || path.isEmpty()) { showErrorDialogAndQuit("File path from data uri is null, empty or invalid."); return; } @@ -125,19 +106,13 @@ public class FileReceiverActivity extends AppCompatActivity { void showErrorDialogAndQuit(String message) { mFinishOnDismissNameDialog = false; - MessageDialogUtils.showMessage(this, - API_TAG, message, - null, (dialog, which) -> finish(), - null, null, - dialog -> finish()); + TermuxMessageDialogUtils.showMessage(this, "Termux", message, null, (dialog, which) -> finish(), null, null, dialog -> finish()); } void handleContentUri(@NonNull final Uri uri, String subjectFromIntent) { try { - Logger.logVerbose(LOG_TAG, "uri: \"" + uri + "\", path: \"" + uri.getPath() + "\", fragment: \"" + uri.getFragment() + "\""); - + Log.v(LOG_TAG, "uri: \"" + uri + "\", path: \"" + uri.getPath() + "\", fragment: \"" + uri.getFragment() + "\""); String attachmentFileName = null; - String[] projection = new String[]{OpenableColumns.DISPLAY_NAME}; try (Cursor c = getContentResolver().query(uri, projection, null, null, null)) { if (c != null && c.moveToFirst()) { @@ -146,19 +121,23 @@ public class FileReceiverActivity extends AppCompatActivity { } } - if (attachmentFileName == null) attachmentFileName = subjectFromIntent; - if (attachmentFileName == null) attachmentFileName = UriUtils.getUriFileBasename(uri, true); + if (attachmentFileName == null) { + attachmentFileName = subjectFromIntent; + } + if (attachmentFileName == null) { + attachmentFileName = new File(uri.getPath()).getName(); + } InputStream in = getContentResolver().openInputStream(uri); promptNameAndSave(in, attachmentFileName); } catch (Exception e) { showErrorDialogAndQuit("Unable to handle shared content:\n\n" + e.getMessage()); - Logger.logStackTraceWithMessage(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e); + Log.e(LOG_TAG, "handleContentUri(uri=" + uri + ") failed", e); } } void promptNameAndSave(final InputStream in, final String attachmentFileName) { - TextInputDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, + TermuxMessageDialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> { File outFile = saveStreamWithName(in, text); if (outFile == null) return; @@ -174,20 +153,20 @@ public class FileReceiverActivity extends AppCompatActivity { //noinspection ResultOfMethodCallIgnored editorProgramFile.setExecutable(true); - final Uri scriptUri = UriUtils.getFileUri(EDITOR_PROGRAM); + final Uri scriptUri = new Uri.Builder().scheme("file").path(EDITOR_PROGRAM).build(); - Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, scriptUri); - executeIntent.setClass(FileReceiverActivity.this, TermuxService.class); - executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()}); + Intent executeIntent = new Intent(TermuxService.ACTION_SERVICE_EXECUTE, scriptUri); + executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class); + executeIntent.putExtra(TermuxService.TERMUX_EXECUTE_EXTRA_ARGUMENTS, new String[]{outFile.getAbsolutePath()}); startService(executeIntent); finish(); }, R.string.action_file_received_open_directory, text -> { if (saveStreamWithName(in, text) == null) return; - Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE); - executeIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, TERMUX_RECEIVEDIR); - executeIntent.setClass(FileReceiverActivity.this, TermuxService.class); + Intent executeIntent = new Intent(TermuxService.ACTION_SERVICE_EXECUTE); + executeIntent.putExtra(TermuxService.TERMUX_EXECUTE_WORKDIR, TERMUX_RECEIVEDIR); + executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class); startService(executeIntent); finish(); }, @@ -199,7 +178,7 @@ public class FileReceiverActivity extends AppCompatActivity { public File saveStreamWithName(InputStream in, String attachmentFileName) { File receiveDir = new File(TERMUX_RECEIVEDIR); - if (DataUtils.isNullOrEmpty(attachmentFileName)) { + if (attachmentFileName == null || attachmentFileName.isEmpty()) { showErrorDialogAndQuit("File name cannot be null or empty"); return null; } @@ -221,7 +200,7 @@ public class FileReceiverActivity extends AppCompatActivity { return outFile; } catch (IOException e) { showErrorDialogAndQuit("Error saving file:\n\n" + e); - Logger.logStackTraceWithMessage(LOG_TAG, "Error saving file", e); + Log.e(LOG_TAG, "Error saving file", e); return null; } } @@ -238,48 +217,13 @@ public class FileReceiverActivity extends AppCompatActivity { //noinspection ResultOfMethodCallIgnored urlOpenerProgramFile.setExecutable(true); - final Uri urlOpenerProgramUri = UriUtils.getFileUri(URL_OPENER_PROGRAM); + final Uri urlOpenerProgramUri = new Uri.Builder().scheme("file").path(URL_OPENER_PROGRAM).build(); - Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri); - executeIntent.setClass(FileReceiverActivity.this, TermuxService.class); - executeIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, new String[]{url}); + Intent executeIntent = new Intent(TermuxService.ACTION_SERVICE_EXECUTE, urlOpenerProgramUri); + executeIntent.setClass(TermuxFileReceiverActivity.this, TermuxService.class); + executeIntent.putExtra(TermuxService.TERMUX_EXECUTE_EXTRA_ARGUMENTS, new String[]{url}); startService(executeIntent); finish(); } - /** - * Update {@link TERMUX_APP#FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on - * {@link TermuxPropertyConstants#KEY_DISABLE_FILE_SHARE_RECEIVER} value and - * {@link TERMUX_APP#FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME} component state depending on - * {@link TermuxPropertyConstants#KEY_DISABLE_FILE_VIEW_RECEIVER} value. - */ - public static void updateFileReceiverActivityComponentsState(@NonNull Context context) { - new Thread() { - @Override - public void run() { - TermuxAppSharedProperties properties = TermuxAppSharedProperties.getProperties(); - - String errmsg; - boolean state; - - state = !properties.isFileShareReceiverDisabled(); - Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state); - errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME, - TERMUX_APP.FILE_SHARE_RECEIVER_ACTIVITY_CLASS_NAME, - state, null, false, false); - if (errmsg != null) - Logger.logError(LOG_TAG, errmsg); - - state = !properties.isFileViewReceiverDisabled(); - Logger.logVerbose(LOG_TAG, "Setting " + TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME + " component state to " + state); - errmsg = PackageUtils.setComponentState(context,TermuxConstants.TERMUX_PACKAGE_NAME, - TERMUX_APP.FILE_VIEW_RECEIVER_ACTIVITY_CLASS_NAME, - state, null, false, false); - if (errmsg != null) - Logger.logError(LOG_TAG, errmsg); - - } - }.start(); - } - } diff --git a/app/src/main/java/com/termux/app/activities/HelpActivity.java b/app/src/main/java/com/termux/app/TermuxHelpActivity.java similarity index 86% rename from app/src/main/java/com/termux/app/activities/HelpActivity.java rename to app/src/main/java/com/termux/app/TermuxHelpActivity.java index a2e4d6a9..f10b5593 100644 --- a/app/src/main/java/com/termux/app/activities/HelpActivity.java +++ b/app/src/main/java/com/termux/app/TermuxHelpActivity.java @@ -1,4 +1,4 @@ -package com.termux.app.activities; +package com.termux.app; import android.content.ActivityNotFoundException; import android.content.Intent; @@ -13,10 +13,10 @@ import android.widget.RelativeLayout; import androidx.appcompat.app.AppCompatActivity; -import com.termux.shared.termux.TermuxConstants; - /** Basic embedded browser for viewing help pages. */ -public final class HelpActivity extends AppCompatActivity { +public final class TermuxHelpActivity extends AppCompatActivity { + + public static final String TERMUX_WIKI_URL = "https://wiki.termux.com"; // Default: "https://wiki.termux.com" WebView mWebView; @@ -35,14 +35,13 @@ public final class HelpActivity extends AppCompatActivity { mWebView = new WebView(this); WebSettings settings = mWebView.getSettings(); settings.setCacheMode(WebSettings.LOAD_NO_CACHE); - settings.setAppCacheEnabled(false); setContentView(progressLayout); mWebView.clearCache(true); mWebView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.equals(TermuxConstants.TERMUX_WIKI_URL) || url.startsWith(TermuxConstants.TERMUX_WIKI_URL + "/")) { + if (url.equals(TERMUX_WIKI_URL) || url.startsWith(TERMUX_WIKI_URL + "/")) { // Inline help. setContentView(progressLayout); return false; @@ -63,7 +62,7 @@ public final class HelpActivity extends AppCompatActivity { setContentView(mWebView); } }); - mWebView.loadUrl(TermuxConstants.TERMUX_WIKI_URL); + mWebView.loadUrl(TERMUX_WIKI_URL); } @Override diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index b1414999..a9b809f3 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -4,24 +4,17 @@ import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; -import android.os.Build; +import android.content.pm.ApplicationInfo; import android.os.Environment; +import android.os.UserHandle; +import android.os.UserManager; +import android.system.ErrnoException; import android.system.Os; +import android.util.Log; import android.util.Pair; import android.view.WindowManager; import com.termux.R; -import com.termux.shared.file.FileUtils; -import com.termux.shared.termux.crash.TermuxCrashUtils; -import com.termux.shared.termux.file.TermuxFileUtils; -import com.termux.shared.interact.MessageDialogUtils; -import com.termux.shared.logger.Logger; -import com.termux.shared.markdown.MarkdownUtils; -import com.termux.shared.errors.Error; -import com.termux.shared.android.PackageUtils; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.termux.TermuxUtils; -import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -33,11 +26,6 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR; -import static com.termux.shared.termux.TermuxConstants.TERMUX_PREFIX_DIR_PATH; -import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR; -import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH; - /** * Install the Termux bootstrap packages if necessary by following the below steps: *

@@ -59,59 +47,42 @@ import static com.termux.shared.termux.TermuxConstants.TERMUX_STAGING_PREFIX_DIR */ final class TermuxInstaller { + private static final String TERMUX_STAGING_PREFIX_DIR_PATH = TermuxConstants.FILES_PATH + "/usr-staging"; // Default: "/data/data/com.termux/files/usr-staging" + private static final String LOG_TAG = "TermuxInstaller"; - /** Performs bootstrap setup if necessary. */ + /** + * Performs bootstrap setup if necessary. + */ static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) { String bootstrapErrorMessage; Error filesDirectoryAccessibleError; - // This will also call Context.getFilesDir(), which should ensure that termux files directory - // is created if it does not already exist - filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true); - boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null; + // Ensure that termux files directory is created if it does not already exist: + new File(activity.getFilesDir(), "home").mkdir(); // Termux can only be run as the primary user (device owner) since only that // account has the expected file system paths. Verify that: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PackageUtils.isCurrentUserThePrimaryUser(activity)) { - bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, - MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false)); - Logger.logError(LOG_TAG, "isFilesDirectoryAccessible: " + isFilesDirectoryAccessible); - Logger.logError(LOG_TAG, bootstrapErrorMessage); - sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage); - MessageDialogUtils.exitAppWithErrorMessage(activity, - activity.getString(R.string.bootstrap_error_title), - bootstrapErrorMessage); + UserManager userManager = (UserManager) activity.getSystemService(Context.USER_SERVICE); + boolean isCurrentUserPrimary = userManager.getSerialNumberForUser(UserHandle.getUserHandleForUid(activity.getApplicationInfo().uid)) == 0; + if (!isCurrentUserPrimary) { + bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message); + TermuxMessageDialogUtils.exitAppWithErrorMessage(activity, activity.getString(R.string.bootstrap_error_title), bootstrapErrorMessage); return; } - if (!isFilesDirectoryAccessible) { - bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError); - //noinspection SdCardPath - if (PackageUtils.isAppInstalledOnExternalStorage(activity) && - !TermuxConstants.TERMUX_FILES_DIR_PATH.equals(activity.getFilesDir().getAbsolutePath().replaceAll("^/data/user/0/", "/data/data/"))) { - bootstrapErrorMessage += "\n\n" + activity.getString(R.string.bootstrap_error_installed_on_portable_sd, - MarkdownUtils.getMarkdownCodeForString(TERMUX_PREFIX_DIR_PATH, false)); - } - - Logger.logError(LOG_TAG, bootstrapErrorMessage); - sendBootstrapCrashReportNotification(activity, bootstrapErrorMessage); - MessageDialogUtils.showMessage(activity, - activity.getString(R.string.bootstrap_error_title), - bootstrapErrorMessage, null); + boolean isInstalledOnExternalStorage = (activity.getApplicationInfo().flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE) != 0; + if (isInstalledOnExternalStorage) { + new AlertDialog.Builder(activity) + .setTitle(R.string.bootstrap_error_installed_on_portable_sd) + .show(); return; } // If prefix directory exists, even if its a symlink to a valid directory and symlink is not broken/dangling - if (FileUtils.directoryFileExists(TERMUX_PREFIX_DIR_PATH, true)) { - if (TermuxFileUtils.isTermuxPrefixDirectoryEmpty()) { - Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" exists but is empty or only contains specific unimportant files."); - } else { - whenDone.run(); - return; - } - } else if (FileUtils.fileExists(TERMUX_PREFIX_DIR_PATH, false)) { - Logger.logInfo(LOG_TAG, "The termux prefix directory \"" + TERMUX_PREFIX_DIR_PATH + "\" does not exist but another file exists at its destination."); + if (new File(TermuxConstants.PREFIX_PATH).exists()) { + whenDone.run(); + return; } final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false); @@ -119,40 +90,19 @@ final class TermuxInstaller { @Override public void run() { try { - Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages."); - - Error error; - // Delete prefix staging directory or any file at its destination - error = FileUtils.deleteFile("termux prefix staging directory", TERMUX_STAGING_PREFIX_DIR_PATH, true); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); + File stagingPrefixFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH); + if (stagingPrefixFile.exists() && !deleteDir(stagingPrefixFile)) { + showBootstrapErrorDialog(activity, whenDone, "Unable to delete old staging area."); return; } - // Delete prefix directory or any file at its destination - error = FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); + File prefixFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH); + if (prefixFile.exists() && !deleteDir(prefixFile)) { + showBootstrapErrorDialog(activity, whenDone, "Unable to delete old PREFIX."); return; } - // Create prefix staging directory if it does not already exist and set required permissions - error = TermuxFileUtils.isTermuxPrefixStagingDirectoryAccessible(true, true); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } - - // Create prefix directory if it does not already exist and set required permissions - error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(true, true); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } - - Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TERMUX_STAGING_PREFIX_DIR_PATH + "\"."); - final byte[] buffer = new byte[8096]; final List> symlinks = new ArrayList<>(50); @@ -170,25 +120,15 @@ final class TermuxInstaller { String oldPath = parts[0]; String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + "/" + parts[1]; symlinks.add(Pair.create(oldPath, newPath)); - - error = ensureDirectoryExists(new File(newPath).getParentFile()); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } } } else { String zipEntryName = zipEntry.getName(); File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName); boolean isDirectory = zipEntry.isDirectory(); - error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile()); - if (error != null) { - showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error)); - return; - } - - if (!isDirectory) { + if (isDirectory) { + targetFile.mkdirs(); + } else { try (FileOutputStream outStream = new FileOutputStream(targetFile)) { int readBytes; while ((readBytes = zipInput.read(buffer)) != -1) @@ -210,22 +150,12 @@ final class TermuxInstaller { Os.symlink(symlink.first, symlink.second); } - Logger.logInfo(LOG_TAG, "Moving termux prefix staging to prefix directory."); - - if (!TERMUX_STAGING_PREFIX_DIR.renameTo(TERMUX_PREFIX_DIR)) { - throw new RuntimeException("Moving termux prefix staging to prefix directory failed"); - } - - Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully."); - - // Recreate env file since termux prefix was wiped earlier - TermuxShellEnvironment.writeEnvironmentToFile(activity); + Os.rename(TERMUX_STAGING_PREFIX_DIR_PATH, TermuxConstants.PREFIX_PATH); activity.runOnUiThread(whenDone); - } catch (final Exception e) { - showBootstrapErrorDialog(activity, whenDone, Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e))); - + Log.e(LOG_TAG, "Error in installation", e); + showBootstrapErrorDialog(activity, whenDone, "Error in installation: " + e.getMessage()); } finally { activity.runOnUiThread(() -> { try { @@ -240,10 +170,7 @@ final class TermuxInstaller { } public static void showBootstrapErrorDialog(Activity activity, Runnable whenDone, String message) { - Logger.logErrorExtended(LOG_TAG, "Bootstrap Error:\n" + message); - - // Send a notification with the exception so that the user knows why bootstrap setup failed - sendBootstrapCrashReportNotification(activity, message); + Log.e(LOG_TAG, "Bootstrap Error: " + message); activity.runOnUiThread(() -> { try { @@ -254,7 +181,7 @@ final class TermuxInstaller { }) .setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> { dialog.dismiss(); - FileUtils.deleteFile("termux prefix directory", TERMUX_PREFIX_DIR_PATH, true); + deleteDir(new File(TermuxConstants.PREFIX_PATH)); TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone); }).show(); } catch (WindowManager.BadTokenException e1) { @@ -263,40 +190,19 @@ final class TermuxInstaller { }); } - private static void sendBootstrapCrashReportNotification(Activity activity, String message) { - final String title = TermuxConstants.TERMUX_APP_NAME + " Bootstrap Error"; - - // Add info of all install Termux plugin apps as well since their target sdk or installation - // on external/portable sd card can affect Termux app files directory access or exec. - TermuxCrashUtils.sendCrashReportNotification(activity, LOG_TAG, - title, null, "## " + title + "\n\n" + message + "\n\n" + - TermuxUtils.getTermuxDebugMarkdownString(activity), - true, false, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES, true); - } - static void setupStorageSymlinks(final Context context) { - final String LOG_TAG = "termux-storage"; - final String title = TermuxConstants.TERMUX_APP_NAME + " Setup Storage Error"; - - Logger.logInfo(LOG_TAG, "Setting up storage symlinks."); + Log.i(LOG_TAG, "Setting up storage symlinks."); new Thread() { public void run() { try { - Error error; - File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR; + File storageDir = new File(TermuxConstants.HOME_PATH + "/storage"); - error = FileUtils.clearDirectory("~/storage", storageDir.getAbsolutePath()); - if (error != null) { - Logger.logErrorAndShowToast(context, LOG_TAG, error.getMessage()); - Logger.logErrorExtended(LOG_TAG, "Setup Storage Error\n" + error.toString()); - TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null, - "## " + title + "\n\n" + Error.getErrorMarkdownString(error), - true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true); - return; + if (!clearDirectory(storageDir)) { + throw new RuntimeException("Unable to clear ~/storage"); } - Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\"."); + Log.i(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\"."); // Get primary storage root "/storage/emulated/0" symlink File sharedDir = Environment.getExternalStorageDirectory(); @@ -342,7 +248,7 @@ final class TermuxInstaller { File dir = dirs[i]; if (dir == null) continue; String symlinkName = "external-" + i; - Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\"."); + Log.i(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\"."); Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath()); } } @@ -354,25 +260,28 @@ final class TermuxInstaller { File dir = dirs[i]; if (dir == null) continue; String symlinkName = "media-" + i; - Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\"."); + Log.i(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\"."); Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath()); } } - - Logger.logInfo(LOG_TAG, "Storage symlinks created successfully."); - } catch (Exception e) { - Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage()); - Logger.logStackTraceWithMessage(LOG_TAG, "Setup Storage Error: Error setting up link", e); - TermuxCrashUtils.sendCrashReportNotification(context, LOG_TAG, title, null, - "## " + title + "\n\n" + Logger.getStackTracesMarkdownString(null, Logger.getStackTracesStringArray(e)), - true, false, TermuxUtils.AppInfoMode.TERMUX_PACKAGE, true); + } catch (ErrnoException e) { + throw new RuntimeException(e); } } }.start(); } - private static Error ensureDirectoryExists(File directory) { - return FileUtils.createDirectoryFile(directory.getAbsolutePath()); + public static boolean deleteDir(File dir) { + if (dir.isDirectory()) { + String[] children = dir.list(); + for (int i = 0; i < children.length; i++) { + boolean success = deleteDir(new File(dir, children[i])); + if (!success) { + return false; + } + } + } + return dir.delete(); } public static byte[] loadZipBytes() { @@ -383,4 +292,12 @@ final class TermuxInstaller { public static native byte[] getZip(); + private static boolean clearDirectory(File directory) { + for (File child : directory.listFiles()) { + if (!clearDirectory(child)) { + return false; + } + } + return directory.delete(); + } } diff --git a/termux-shared/src/main/java/com/termux/shared/termux/interact/TextInputDialogUtils.java b/app/src/main/java/com/termux/app/TermuxMessageDialogUtils.java similarity index 58% rename from termux-shared/src/main/java/com/termux/shared/termux/interact/TextInputDialogUtils.java rename to app/src/main/java/com/termux/app/TermuxMessageDialogUtils.java index d644af72..23e83bb9 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/interact/TextInputDialogUtils.java +++ b/app/src/main/java/com/termux/app/TermuxMessageDialogUtils.java @@ -1,16 +1,58 @@ -package com.termux.shared.termux.interact; +package com.termux.app; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.DialogInterface; +import android.os.Handler; +import android.os.Looper; import android.text.Selection; import android.util.TypedValue; import android.view.KeyEvent; -import android.view.ViewGroup.LayoutParams; +import android.view.ViewGroup; import android.widget.EditText; import android.widget.LinearLayout; +import android.widget.Toast; -public final class TextInputDialogUtils { +public class TermuxMessageDialogUtils { + + public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) { + showMessage(context, titleText, messageText, null, null, null, null, onDismiss); + } + + public static void showMessage(Context context, + String titleText, + String messageText, + String positiveText, + final DialogInterface.OnClickListener onPositiveButton, + String negativeText, + final DialogInterface.OnClickListener onNegativeButton, + final DialogInterface.OnDismissListener onDismiss + ) { + + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setTitle(titleText) + .setMessage(messageText) + .setPositiveButton(positiveText == null ? context.getString(android.R.string.ok) : positiveText, onPositiveButton); + + if (negativeText != null) { + builder.setNegativeButton(negativeText, onNegativeButton); + } + + if (onDismiss != null) { + builder.setOnDismissListener(onDismiss); + } + + builder.show(); + } + + public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) { + showMessage(context, titleText, messageText, dialog -> System.exit(0)); + } + + public static void showToast(Context context, String message) { + new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show()); + } public interface TextSetListener { void onTextSet(String text); @@ -43,7 +85,7 @@ public final class TextInputDialogUtils { LinearLayout layout = new LinearLayout(activity); layout.setOrientation(LinearLayout.VERTICAL); - layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + layout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom); layout.addView(input); @@ -69,4 +111,6 @@ public final class TextInputDialogUtils { dialogHolder[0].show(); } + + } diff --git a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java index f8f21b5d..3c36b2dc 100644 --- a/app/src/main/java/com/termux/app/TermuxOpenReceiver.java +++ b/app/src/main/java/com/termux/app/TermuxOpenReceiver.java @@ -11,16 +11,9 @@ import android.net.Uri; import android.os.Environment; import android.os.ParcelFileDescriptor; import android.provider.MediaStore; +import android.util.Log; import android.webkit.MimeTypeMap; -import com.termux.shared.termux.plugins.TermuxPluginUtils; -import com.termux.shared.data.DataUtils; -import com.termux.shared.data.IntentUtils; -import com.termux.shared.net.uri.UriUtils; -import com.termux.shared.logger.Logger; -import com.termux.shared.net.uri.UriScheme; -import com.termux.shared.termux.TermuxConstants; - import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -35,12 +28,11 @@ public class TermuxOpenReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { final Uri data = intent.getData(); if (data == null) { - Logger.logError(LOG_TAG, "Called without intent data"); + Log.e(LOG_TAG, "Called without intent data"); return; } - Logger.logVerbose(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent)); - Logger.logVerbose(LOG_TAG, "uri: \"" + data + "\", path: \"" + data.getPath() + "\", fragment: \"" + data.getFragment() + "\""); + Log.v(LOG_TAG, "uri: \"" + data + "\", path: \"" + data.getPath() + "\", fragment: \"" + data.getFragment() + "\""); final String contentTypeExtra = intent.getStringExtra("content-type"); final boolean useChooser = intent.getBooleanExtra("chooser", false); @@ -51,12 +43,12 @@ public class TermuxOpenReceiver extends BroadcastReceiver { // Ok. break; default: - Logger.logError(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'"); + Log.e(LOG_TAG, "Invalid action '" + intentAction + "', using 'view'"); break; } String scheme = data.getScheme(); - if (scheme != null && !UriScheme.SCHEME_FILE.equals(scheme)) { + if (scheme != null && !"file".equals(scheme)) { Intent urlIntent = new Intent(intentAction, data); if (intentAction.equals(Intent.ACTION_SEND)) { urlIntent.putExtra(Intent.EXTRA_TEXT, data.toString()); @@ -68,21 +60,21 @@ public class TermuxOpenReceiver extends BroadcastReceiver { try { context.startActivity(urlIntent); } catch (ActivityNotFoundException e) { - Logger.logError(LOG_TAG, "No app handles the url " + data); + Log.e(LOG_TAG, "No app handles the url " + data); } return; } // Get full path including fragment (anything after last "#") - String filePath = UriUtils.getUriFilePathWithFragment(data); - if (DataUtils.isNullOrEmpty(filePath)) { - Logger.logError(LOG_TAG, "filePath is null or empty"); + String filePath = data.getPath(); + if (filePath == null || filePath.isEmpty()) { + Log.e(LOG_TAG, "filePath is null or empty"); return; } final File fileToShare = new File(filePath); if (!(fileToShare.isFile() && fileToShare.canRead())) { - Logger.logError(LOG_TAG, "Not a readable file: '" + fileToShare.getAbsolutePath() + "'"); + Log.e(LOG_TAG, "Not a readable file: '" + fileToShare.getAbsolutePath() + "'"); return; } @@ -104,7 +96,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver { } // Do not create Uri with Uri.parse() and use Uri.Builder().path(), check UriUtils.getUriFilePath(). - Uri uriToShare = UriUtils.getContentUri(TermuxConstants.TERMUX_FILE_SHARE_URI_AUTHORITY, fileToShare.getAbsolutePath()); + Uri uriToShare = new Uri.Builder().scheme("content").authority("com.termux.files").path(fileToShare.getAbsolutePath()).build(); if (Intent.ACTION_SEND.equals(intentAction)) { sendIntent.putExtra(Intent.EXTRA_STREAM, uriToShare); @@ -120,7 +112,7 @@ public class TermuxOpenReceiver extends BroadcastReceiver { try { context.startActivity(sendIntent); } catch (ActivityNotFoundException e) { - Logger.logError(LOG_TAG, "No app handles the url " + data); + Log.e(LOG_TAG, "No app handles the url " + data); } } @@ -195,28 +187,12 @@ public class TermuxOpenReceiver extends BroadcastReceiver { File file = new File(uri.getPath()); try { String path = file.getCanonicalPath(); - String callingPackageName = getCallingPackage(); - Logger.logDebug(LOG_TAG, "Open file request received from " + callingPackageName + " for \"" + path + "\" with mode \"" + mode + "\""); + // String callingPackageName = getCallingPackage(); String storagePath = Environment.getExternalStorageDirectory().getCanonicalPath(); // See https://support.google.com/faqs/answer/7496913: - if (!(path.startsWith(TermuxConstants.TERMUX_FILES_DIR_PATH) || path.startsWith(storagePath))) { + if (!(path.startsWith(TermuxConstants.FILES_PATH) || path.startsWith(storagePath))) { throw new IllegalArgumentException("Invalid path: " + path); } - - // If TermuxConstants.PROP_ALLOW_EXTERNAL_APPS property to not set to "true", then throw exception - String errmsg = TermuxPluginUtils.checkIfAllowExternalAppsPolicyIsViolated(getContext(), LOG_TAG); - if (errmsg != null) { - throw new IllegalArgumentException(errmsg); - } - - // **DO NOT** allow these files to be modified by ContentProvider exposed to external - // apps, since they may silently modify the values for security properties like - // TermuxConstants.PROP_ALLOW_EXTERNAL_APPS set by users without their explicit consent. - if (TermuxConstants.TERMUX_PROPERTIES_FILE_PATHS_LIST.contains(path) || - TermuxConstants.TERMUX_FLOAT_PROPERTIES_FILE_PATHS_LIST.contains(path)) { - mode = "r"; - } - } catch (IOException e) { throw new IllegalArgumentException(e); } diff --git a/app/src/main/java/com/termux/app/TermuxPermissionUtils.java b/app/src/main/java/com/termux/app/TermuxPermissionUtils.java new file mode 100644 index 00000000..cedc7556 --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxPermissionUtils.java @@ -0,0 +1,171 @@ +package com.termux.app; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.PowerManager; +import android.provider.Settings; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class TermuxPermissionUtils { + + public static final int REQUEST_GRANT_STORAGE_PERMISSION = 1000; + public static final int REQUEST_DISABLE_BATTERY_OPTIMIZATIONS = 2000; + public static final int REQUEST_GRANT_DISPLAY_OVER_OTHER_APPS_PERMISSION = 2001; + + private static final String LOG_TAG = "PermissionUtils"; + + + /** + * Check if app has been granted the required permission. + * + * @param context The context for operations. + * @param permission The {@link String} name for permission to check. + * @return Returns {@code true} if permission is granted, otherwise {@code false}. + */ + public static boolean checkPermission(@NonNull Context context, @NonNull String permission) { + return checkPermissions(context, new String[]{permission}); + } + + /** + * Check if app has been granted the required permissions. + * + * @param context The context for operations. + * @param permissions The {@link String[]} names for permissions to check. + * @return Returns {@code true} if permissions are granted, otherwise {@code false}. + */ + public static boolean checkPermissions(@NonNull Context context, @NonNull String[] permissions) { + // checkSelfPermission may return true for permissions not even requested + List permissionsNotRequested = getPermissionsNotRequested(context, permissions); + if (permissionsNotRequested.size() > 0) { + Log.e(LOG_TAG, "Attempted to check for permissions that have not been requested in app manifest: " + permissionsNotRequested); + return false; + } + for (String permission : permissions) { + if (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_DENIED) { + return false; + } + } + return true; + } + + public static boolean requestPermission(@NonNull Context context, @NonNull String permission, int requestCode) { + return requestPermissions(context, new String[]{permission}, requestCode); + } + + public static boolean requestPermissions(@NonNull Context context, @NonNull String[] permissions, int requestCode) { + List permissionsNotRequested = getPermissionsNotRequested(context, permissions); + if (permissionsNotRequested.size() > 0) { + throw new RuntimeException("Requested permissions not in the manifest: " + permissionsNotRequested); + } + + for (String permission : permissions) { + int result = ContextCompat.checkSelfPermission(context, permission); + if (result != PackageManager.PERMISSION_GRANTED) { + Log.i(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions)); + ((Activity) context).requestPermissions(permissions, requestCode); + break; + } + } + + return true; + } + + + /** + * Check if app has requested the required permission in the manifest. + * + * @param context The context for operations. + * @param permission The {@link String} name for permission to check. + * @return Returns {@code true} if permission has been requested, otherwise {@code false}. + */ + public static boolean isPermissionRequested(@NonNull Context context, @NonNull String permission) { + return getPermissionsNotRequested(context, new String[]{permission}).size() == 0; + } + + /** + * Check if app has requested the required permissions or not in the manifest. + * + * @param context The context for operations. + * @param permissions The {@link String[]} names for permissions to check. + * @return Returns {@link List} of permissions that have not been requested. It will have + * size 0 if all permissions have been requested. + */ + @NonNull + public static List getPermissionsNotRequested(@NonNull Context context, @NonNull String[] permissions) { + List permissionsNotRequested = new ArrayList<>(); + Collections.addAll(permissionsNotRequested, permissions); + + PackageInfo packageInfo; + try { + packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + + // If no permissions are requested, then nothing to check + if (packageInfo.requestedPermissions == null || packageInfo.requestedPermissions.length == 0) + return permissionsNotRequested; + + List requestedPermissionsList = Arrays.asList(packageInfo.requestedPermissions); + for (String permission : permissions) { + if (requestedPermissionsList.contains(permission)) { + permissionsNotRequested.remove(permission); + } + } + + return permissionsNotRequested; + } + + public static boolean checkStoragePermission(@NonNull Context context, boolean checkLegacyStoragePermission) { + if (checkLegacyStoragePermission || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return checkPermissions(context, + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}); + } else { + return Environment.isExternalStorageManager(); + } + } + + /** + * Check if {@link Manifest.permission#REQUEST_IGNORE_BATTERY_OPTIMIZATIONS} permission has been + * granted. + * + * @param context The context for operations. + * @return Returns {@code true} if permission is granted, otherwise {@code false}. + */ + public static boolean checkIfBatteryOptimizationsDisabled(@NonNull Context context) { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return powerManager.isIgnoringBatteryOptimizations(context.getPackageName()); + } + + @SuppressLint("BatteryLife") + public static void requestDisableBatteryOptimizations(@NonNull Context context) { + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + context.getPackageName())); + + // Flag must not be passed for activity contexts, otherwise onActivityResult() will not be called with permission grant result. + // Flag must be passed for non-activity contexts like services, otherwise "Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag" exception will be raised. + if (!(context instanceof Activity)) + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + context.startActivity(intent); + } + +} diff --git a/app/src/main/java/com/termux/app/TermuxPreferences.java b/app/src/main/java/com/termux/app/TermuxPreferences.java new file mode 100644 index 00000000..cb5ebe55 --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxPreferences.java @@ -0,0 +1,87 @@ +package com.termux.app; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.TypedValue; + +public class TermuxPreferences { + + private static final int MAX_FONTSIZE = 256; + private static final int MIN_FONTSIZE = 256; + + private static final String PREF_KEEP_SCREEN_ON = "screen_on"; + private static final String PREF_CURRENT_SESSION = "current_session"; + private static final String PREF_FONT_SIZE = "font_size"; + private static final String PREF_SHOW_TOOLBAR = "show_toolbar"; + + private int minFontSize; + private int maxFontSize; + private int defaultFontSize; + + + private final SharedPreferences prefs; + + TermuxPreferences(TermuxActivity activity) { + prefs = activity.getPreferences(Context.MODE_PRIVATE); + setupFontSizeDefaults(activity); + } + + public void setKeepScreenOn(boolean newValue) { + prefs.edit().putBoolean(PREF_KEEP_SCREEN_ON, newValue).apply(); + } + + public boolean isKeepScreenOn() { + return prefs.getBoolean(PREF_KEEP_SCREEN_ON, false); + } + + public void setCurrentSession(String currentSession) { + prefs.edit().putString(PREF_CURRENT_SESSION, currentSession).apply(); + } + + public String getCurrentSession() { + return prefs.getString(PREF_KEEP_SCREEN_ON, null); + } + + public void setShowTerminalToolbar(boolean newValue) { + prefs.edit().putBoolean(PREF_SHOW_TOOLBAR, newValue).apply(); + } + + public boolean isShowTerminalToolbar() { + return prefs.getBoolean(PREF_SHOW_TOOLBAR, true); + } + + public boolean toggleShowTerminalToolbar() { + boolean newValue = !isShowTerminalToolbar(); + prefs.edit().putBoolean(PREF_SHOW_TOOLBAR, newValue).apply(); + return newValue; + } + + public int getFontSize() { + return prefs.getInt(PREF_FONT_SIZE, defaultFontSize); + } + + public int changeFontSize(boolean increase) { + int fontSize = getFontSize(); + + fontSize += (increase ? 1 : -1) * 2; + fontSize = Math.max(MIN_FONTSIZE, Math.min(fontSize, MAX_FONTSIZE)); + + prefs.edit().putInt(PREF_FONT_SIZE, fontSize).apply(); + return fontSize; + } + + private void setupFontSizeDefaults(Context context) { + float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics()); + + // This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size + // to prevent invisible text due to zoom be mistake: + minFontSize = (int) (4f * dipInPixels); + maxFontSize = 256; + + // http://www.google.com/design/spec/style/typography.html#typography-line-height + defaultFontSize = Math.round(12 * dipInPixels); + // Make it divisible by 2 since that is the minimal adjustment step: + if (defaultFontSize % 2 == 1) defaultFontSize--; + } + +} diff --git a/app/src/main/java/com/termux/app/TermuxProperties.java b/app/src/main/java/com/termux/app/TermuxProperties.java new file mode 100644 index 00000000..a3bdfeea --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxProperties.java @@ -0,0 +1,55 @@ +package com.termux.app; + +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +public class TermuxProperties { + + private final Properties properties = new Properties(); + + void reloadProperties() { + properties.clear(); + try { + for (String subPath : new String[]{".termux.properties", ".config/termux.properties"}) { + File propertiesFile = new File(TermuxConstants.HOME_PATH + '/' + subPath); + if (propertiesFile.exists()) { + try (FileInputStream in = new FileInputStream(propertiesFile)) { + try { + properties.load(in); + } catch (Exception e) { + Log.e(TermuxConstants.LOG_TAG, "Error reading termux properties", e); + // TODO: Show toast + } + } + } + } + } catch (IOException e) { + Log.e(TermuxConstants.LOG_TAG, "Failed to reload properties", e); + } + } + + boolean isBackKeyTheEscapeKey() { + return properties.getProperty("back-key", "escape").equalsIgnoreCase("escape"); + } + + boolean isEnforcingCharBasedInput() { + return properties.getProperty("enforce-char-based-input", "false").equalsIgnoreCase("true"); + } + + boolean areVirtualVolumeKeysDisabled() { + return false; // TODO + } + + public String getExtraKeys() { + String defaultValue = "[['ESC','/',{key: '-', popup: '|'},'HOME','UP','END','PGUP'], ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT','PGDN']]"; + return properties.getProperty("extra-keys", defaultValue); + } + + public String getExtraKeysStyle() { + return properties.getProperty("extra-keys-style", "default"); + } +} diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 8025d0bd..40c6c6f9 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -2,58 +2,33 @@ package com.termux.app; import android.annotation.SuppressLint; import android.app.Notification; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.res.Resources; +import android.net.Uri; import android.net.wifi.WifiManager; import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.termux.R; -import com.termux.app.event.SystemEventReceiver; -import com.termux.app.terminal.TermuxTerminalSessionActivityClient; -import com.termux.app.terminal.TermuxTerminalSessionServiceClient; -import com.termux.shared.termux.plugins.TermuxPluginUtils; -import com.termux.shared.data.IntentUtils; -import com.termux.shared.net.uri.UriUtils; -import com.termux.shared.errors.Errno; -import com.termux.shared.shell.ShellUtils; -import com.termux.shared.shell.command.runner.app.AppShell; -import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; -import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; -import com.termux.shared.termux.shell.TermuxShellUtils; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; -import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; -import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; -import com.termux.shared.termux.shell.TermuxShellManager; -import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession; -import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase; -import com.termux.shared.logger.Logger; -import com.termux.shared.notification.NotificationUtils; -import com.termux.shared.android.PermissionUtils; -import com.termux.shared.data.DataUtils; -import com.termux.shared.shell.command.ExecutionCommand; -import com.termux.shared.shell.command.ExecutionCommand.Runner; -import com.termux.shared.shell.command.ExecutionCommand.ShellCreateMode; -import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSessionClient; -import java.util.ArrayList; import java.util.List; /** - * A service holding a list of {@link TermuxSession} in {@link TermuxShellManager#mTermuxSessions} and background {@link AppShell} + * A service holding a list of {@link TermuxSession} in {@link TermuxShellManager#mTermuxSessions} and background {@link TermuxAppShell} * in {@link TermuxShellManager#mTermuxTasks}, showing a foreground notification while running so that it is not terminated. * The user interacts with the session through {@link TermuxActivity}, but this service may outlive * the activity when the user or the system disposes of the activity. In that case the user may @@ -65,7 +40,17 @@ import java.util.List; * Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see * {@link #buildNotification()}. */ -public final class TermuxService extends Service implements AppShell.AppShellClient, TermuxSession.TermuxSessionClient { +public final class TermuxService extends Service { + + public static final String ACTION_STOP_SERVICE = "com.termux.service.action.service_stop"; + public static final String ACTION_SERVICE_EXECUTE = "com.termux.service.action.service_execute"; + public static final String ACTION_WAKE_LOCK = "com.termux.service.action.wake_lock"; + public static final String ACTION_WAKE_UNLOCK = "com.termux.service.action.wake_unlock"; + public static final String TERMUX_EXECUTE_EXTRA_ARGUMENTS = "com.termux.execute.arguments"; + public static final String TERMUX_EXECUTE_WORKDIR = "com.termux.execute.workdir"; + + public static final String NOTIFICATION_CHANNEL_ID = "com.termu.service.notification_channel"; + /** This service is only bound from inside the same process and never uses IPC. */ class LocalBinder extends Binder { @@ -81,17 +66,7 @@ public final class TermuxService extends Service implements AppShell.AppShellCli * that holds activity references for activity related functions. * Note that the service may often outlive the activity, so need to clear this reference. */ - private TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient; - - /** The basic implementation of the {@link TerminalSessionClient} interface to be used by {@link TerminalSession} - * that does not hold activity references and only a service reference. - */ - private final TermuxTerminalSessionServiceClient mTermuxTerminalSessionServiceClient = new TermuxTerminalSessionServiceClient(this); - - /** - * Termux app shared properties manager, loaded from termux.properties - */ - private TermuxAppSharedProperties mProperties; + private TermuxTerminalSessionActivityClient mTerminalSessionClient; /** * Termux app shell manager @@ -102,120 +77,78 @@ public final class TermuxService extends Service implements AppShell.AppShellCli private PowerManager.WakeLock mWakeLock; private WifiManager.WifiLock mWifiLock; - /** If the user has executed the {@link TERMUX_SERVICE#ACTION_STOP_SERVICE} intent. */ boolean mWantsToStop = false; private static final String LOG_TAG = "TermuxService"; @Override public void onCreate() { - Logger.logVerbose(LOG_TAG, "onCreate"); - - // Get Termux app SharedProperties without loading from disk since TermuxApplication handles - // load and TermuxActivity handles reloads - mProperties = TermuxAppSharedProperties.getProperties(); - - mShellManager = TermuxShellManager.getShellManager(); - - runStartForeground(); - - SystemEventReceiver.registerPackageUpdateEvents(this); + mShellManager = new TermuxShellManager(this); } @SuppressLint("Wakelock") @Override public int onStartCommand(Intent intent, int flags, int startId) { - Logger.logDebug(LOG_TAG, "onStartCommand"); + setupNotificationChannel(); + startForeground(TermuxConstants.TERMUX_APP_NOTIFICATION_ID, buildNotification()); - // Run again in case service is already started and onCreate() is not called - runStartForeground(); - - String action = null; - if (intent != null) { - Logger.logVerboseExtended(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent)); - action = intent.getAction(); - } - - if (action != null) { - switch (action) { - case TERMUX_SERVICE.ACTION_STOP_SERVICE: - Logger.logDebug(LOG_TAG, "ACTION_STOP_SERVICE intent received"); + if (intent != null && intent.getAction() != null) { + switch (intent.getAction()) { + case ACTION_STOP_SERVICE: + Log.d(LOG_TAG, "ACTION_STOP_SERVICE intent received"); actionStopService(); break; - case TERMUX_SERVICE.ACTION_WAKE_LOCK: - Logger.logDebug(LOG_TAG, "ACTION_WAKE_LOCK intent received"); + case ACTION_WAKE_LOCK: + Log.d(LOG_TAG, "ACTION_WAKE_LOCK intent received"); actionAcquireWakeLock(); break; - case TERMUX_SERVICE.ACTION_WAKE_UNLOCK: - Logger.logDebug(LOG_TAG, "ACTION_WAKE_UNLOCK intent received"); + case ACTION_WAKE_UNLOCK: + Log.d(LOG_TAG, "ACTION_WAKE_UNLOCK intent received"); actionReleaseWakeLock(true); break; - case TERMUX_SERVICE.ACTION_SERVICE_EXECUTE: - Logger.logDebug(LOG_TAG, "ACTION_SERVICE_EXECUTE intent received"); + case ACTION_SERVICE_EXECUTE: + Log.d(LOG_TAG, "ACTION_SERVICE_EXECUTE intent received"); actionServiceExecute(intent); break; default: - Logger.logError(LOG_TAG, "Invalid action: \"" + action + "\""); + Log.e(LOG_TAG, "Invalid action: \"" + intent.getAction() + "\""); break; } } - // If this service really do get killed, there is no point restarting it automatically - let the user do on next - // start of {@link Term): return Service.START_NOT_STICKY; } @Override public void onDestroy() { - Logger.logVerbose(LOG_TAG, "onDestroy"); - - TermuxShellUtils.clearTermuxTMPDIR(true); - actionReleaseWakeLock(false); - if (!mWantsToStop) + if (!mWantsToStop) { killAllTermuxExecutionCommands(); - - TermuxShellManager.onAppExit(this); - - SystemEventReceiver.unregisterPackageUpdateEvents(this); - - runStopForeground(); + } } @Override public IBinder onBind(Intent intent) { - Logger.logVerbose(LOG_TAG, "onBind"); + Log.v(LOG_TAG, "onBind"); return mBinder; } @Override public boolean onUnbind(Intent intent) { - Logger.logVerbose(LOG_TAG, "onUnbind"); - + Log.v(LOG_TAG, "onUnbind"); // Since we cannot rely on {@link TermuxActivity.onDestroy()} to always complete, // we unset clients here as well if it failed, so that we do not leave service and session // clients with references to the activity. - if (mTermuxTerminalSessionActivityClient != null) + if (mTerminalSessionClient != null) { unsetTermuxTerminalSessionClient(); + } return false; } - /** Make service run in foreground mode. */ - private void runStartForeground() { - setupNotificationChannel(); - startForeground(TermuxConstants.TERMUX_APP_NOTIFICATION_ID, buildNotification()); - } - - /** Make service leave foreground mode. */ - private void runStopForeground() { - stopForeground(true); - } - - /** Request to stop service. */ private void requestStopService() { - Logger.logDebug(LOG_TAG, "Requesting to stop service"); - runStopForeground(); - stopSelf(); + Log.v(LOG_TAG, "Requesting to stop service"); + stopForeground(Service.STOP_FOREGROUND_REMOVE); + stopSelf(-1); } /** Process action to stop service. */ @@ -225,119 +158,46 @@ public final class TermuxService extends Service implements AppShell.AppShellCli requestStopService(); } - /** Kill all TermuxSessions and TermuxTasks by sending SIGKILL to their processes. - * - * For TermuxSessions, all sessions will be killed, whether user manually exited Termux or if - * onDestroy() was directly called because of unintended shutdown. The processing of results - * will only be done if user manually exited termux or if the session was started by a plugin - * which **expects** the result back via a pending intent. - * - * For TermuxTasks, only tasks that were started by a plugin which **expects** the result - * back via a pending intent will be killed, whether user manually exited Termux or if - * onDestroy() was directly called because of unintended shutdown. The processing of results - * will always be done for the tasks that are killed. The remaining processes will keep on - * running until the termux app process is killed by android, like by OOM, so we let them run - * as long as they can. - * - * Some plugin execution commands may not have been processed and added to mTermuxSessions and - * mTermuxTasks lists before the service is killed, so we maintain a separate - * mPendingPluginExecutionCommands list for those, so that we can notify the pending intent - * creators that execution was cancelled. - * - * Note that if user didn't manually exit Termux and if onDestroy() was directly called because - * of unintended shutdown, like android deciding to kill the service, then there will be no - * guarantee that onDestroy() will be allowed to finish and termux app process may be killed before - * it has finished. This means that in those cases some results may not be sent back to their - * creators for plugin commands but we still try to process whatever results can be processed - * despite the unreliable behaviour of onDestroy(). - * - * Note that if don't kill the processes started by plugins which **expect** the result back - * and notify their creators that they have been killed, then they may get stuck waiting for - * the results forever like in case of commands started by Termux:Tasker or RUN_COMMAND intent, - * since once TermuxService has been killed, no result will be sent back. They may still get - * stuck if termux app process gets killed, so for this case reasonable timeout values should - * be used, like in Tasker for the Termux:Tasker actions. - * - * We make copies of each list since items are removed inside the loop. - */ private synchronized void killAllTermuxExecutionCommands() { - boolean processResult; - - Logger.logDebug(LOG_TAG, "Killing TermuxSessions=" + mShellManager.mTermuxSessions.size() + - ", TermuxTasks=" + mShellManager.mTermuxTasks.size() + - ", PendingPluginExecutionCommands=" + mShellManager.mPendingPluginExecutionCommands.size()); - - List termuxSessions = new ArrayList<>(mShellManager.mTermuxSessions); - List termuxTasks = new ArrayList<>(mShellManager.mTermuxTasks); - List pendingPluginExecutionCommands = new ArrayList<>(mShellManager.mPendingPluginExecutionCommands); - - for (int i = 0; i < termuxSessions.size(); i++) { - ExecutionCommand executionCommand = termuxSessions.get(i).getExecutionCommand(); - processResult = mWantsToStop || executionCommand.isPluginExecutionCommandWithPendingResult(); - termuxSessions.get(i).killIfExecuting(this, processResult); - if (!processResult) - mShellManager.mTermuxSessions.remove(termuxSessions.get(i)); + for (TermuxSession session : mShellManager.mTermuxSessions) { + session.killIfExecuting(); } - - - for (int i = 0; i < termuxTasks.size(); i++) { - ExecutionCommand executionCommand = termuxTasks.get(i).getExecutionCommand(); - if (executionCommand.isPluginExecutionCommandWithPendingResult()) - termuxTasks.get(i).killIfExecuting(this, true); - else - mShellManager.mTermuxTasks.remove(termuxTasks.get(i)); - } - - for (int i = 0; i < pendingPluginExecutionCommands.size(); i++) { - ExecutionCommand executionCommand = pendingPluginExecutionCommands.get(i); - if (!executionCommand.shouldNotProcessResults() && executionCommand.isPluginExecutionCommandWithPendingResult()) { - if (executionCommand.setStateFailed(Errno.ERRNO_CANCELLED.getCode(), this.getString(com.termux.shared.R.string.error_execution_cancelled))) { - TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand); - } - } + for (TermuxAppShell session : mShellManager.mTermuxTasks) { + session.kill(); } } - - /** Process action to acquire Power and Wi-Fi WakeLocks. */ @SuppressLint({"WakelockTimeout", "BatteryLife"}) private void actionAcquireWakeLock() { if (mWakeLock != null) { - Logger.logDebug(LOG_TAG, "Ignoring acquiring WakeLocks since they are already held"); + Log.v(LOG_TAG, "Ignoring acquiring WakeLocks since they are already held"); return; } - Logger.logDebug(LOG_TAG, "Acquiring WakeLocks"); - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermuxConstants.TERMUX_APP_NAME.toLowerCase() + ":service-wakelock"); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); mWakeLock.acquire(); // http://tools.android.com/tech-docs/lint-in-studio-2-3#TOC-WifiManager-Leak WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); - mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TermuxConstants.TERMUX_APP_NAME.toLowerCase()); + mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, getClass().getName()); mWifiLock.acquire(); - if (!PermissionUtils.checkIfBatteryOptimizationsDisabled(this)) { - PermissionUtils.requestDisableBatteryOptimizations(this); + if (!TermuxPermissionUtils.checkIfBatteryOptimizationsDisabled(this)) { + TermuxPermissionUtils.requestDisableBatteryOptimizations(this); } updateNotification(); - - Logger.logDebug(LOG_TAG, "WakeLocks acquired successfully"); - } /** Process action to release Power and Wi-Fi WakeLocks. */ private void actionReleaseWakeLock(boolean updateNotification) { if (mWakeLock == null && mWifiLock == null) { - Logger.logDebug(LOG_TAG, "Ignoring releasing WakeLocks since none are already held"); + Log.d(LOG_TAG, "Ignoring releasing WakeLocks since none are already held"); return; } - Logger.logDebug(LOG_TAG, "Releasing WakeLocks"); - if (mWakeLock != null) { mWakeLock.release(); mWakeLock = null; @@ -348,444 +208,157 @@ public final class TermuxService extends Service implements AppShell.AppShellCli mWifiLock = null; } - if (updateNotification) + if (updateNotification) { updateNotification(); - - Logger.logDebug(LOG_TAG, "WakeLocks released successfully"); + } } - /** Process {@link TERMUX_SERVICE#ACTION_SERVICE_EXECUTE} intent to execute a shell command in - * a foreground TermuxSession or in a background TermuxTask. */ private void actionServiceExecute(Intent intent) { - if (intent == null) { - Logger.logError(LOG_TAG, "Ignoring null intent to actionServiceExecute"); + Uri executableUri = intent.getData(); + if (executableUri == null) { + Log.e(TermuxConstants.LOG_TAG, "No executable URI"); return; } - ExecutionCommand executionCommand = new ExecutionCommand(TermuxShellManager.getNextShellId()); + String executable = executableUri.getPath(); + String[] arguments = intent.getStringArrayExtra(TermuxService.TERMUX_EXECUTE_EXTRA_ARGUMENTS); - executionCommand.executableUri = intent.getData(); - executionCommand.isPluginExecutionCommand = true; - - // If EXTRA_RUNNER is passed, use that, otherwise check EXTRA_BACKGROUND and default to Runner.TERMINAL_SESSION - executionCommand.runner = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RUNNER, - (intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false) ? Runner.APP_SHELL.getName() : Runner.TERMINAL_SESSION.getName())); - if (Runner.runnerOf(executionCommand.runner) == null) { - String errmsg = this.getString(R.string.error_termux_service_invalid_execution_command_runner, executionCommand.runner); - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - return; + TermuxAppShell newTermuxTask = TermuxAppShell.execute(executable, arguments, this); + if (newTermuxTask != null) { + mShellManager.mTermuxTasks.add(newTermuxTask); + updateNotification(); } - - if (executionCommand.executableUri != null) { - Logger.logVerbose(LOG_TAG, "uri: \"" + executionCommand.executableUri + "\", path: \"" + executionCommand.executableUri.getPath() + "\", fragment: \"" + executionCommand.executableUri.getFragment() + "\""); - - // Get full path including fragment (anything after last "#") - executionCommand.executable = UriUtils.getUriFilePathWithFragment(executionCommand.executableUri); - executionCommand.arguments = IntentUtils.getStringArrayExtraIfSet(intent, TERMUX_SERVICE.EXTRA_ARGUMENTS, null); - if (Runner.APP_SHELL.equalsRunner(executionCommand.runner)) - executionCommand.stdin = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_STDIN, null); - executionCommand.backgroundCustomLogLevel = IntentUtils.getIntegerExtraIfSet(intent, TERMUX_SERVICE.EXTRA_BACKGROUND_CUSTOM_LOG_LEVEL, null); - } - - executionCommand.workingDirectory = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_WORKDIR, null); - executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, false); - executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION); - executionCommand.shellName = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_SHELL_NAME, null); - executionCommand.shellCreateMode = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_SHELL_CREATE_MODE, null); - executionCommand.commandLabel = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_LABEL, "Execution Intent Command"); - executionCommand.commandDescription = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, null); - executionCommand.commandHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_COMMAND_HELP, null); - executionCommand.pluginAPIHelp = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, null); - executionCommand.resultConfig.resultPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT); - executionCommand.resultConfig.resultDirectoryPath = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_DIRECTORY, null); - if (executionCommand.resultConfig.resultDirectoryPath != null) { - executionCommand.resultConfig.resultSingleFile = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_RESULT_SINGLE_FILE, false); - executionCommand.resultConfig.resultFileBasename = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_BASENAME, null); - executionCommand.resultConfig.resultFileOutputFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_OUTPUT_FORMAT, null); - executionCommand.resultConfig.resultFileErrorFormat = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILE_ERROR_FORMAT, null); - executionCommand.resultConfig.resultFilesSuffix = IntentUtils.getStringExtraIfSet(intent, TERMUX_SERVICE.EXTRA_RESULT_FILES_SUFFIX, null); - } - - if (executionCommand.shellCreateMode == null) - executionCommand.shellCreateMode = ShellCreateMode.ALWAYS.getMode(); - - // Add the execution command to pending plugin execution commands list - mShellManager.mPendingPluginExecutionCommands.add(executionCommand); - - if (Runner.APP_SHELL.equalsRunner(executionCommand.runner)) - executeTermuxTaskCommand(executionCommand); - else if (Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner)) - executeTermuxSessionCommand(executionCommand); - else { - String errmsg = getString(R.string.error_termux_service_unsupported_execution_command_runner, executionCommand.runner); - executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), errmsg); - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - } - } - - - - - - /** Execute a shell command in background TermuxTask. */ - private void executeTermuxTaskCommand(ExecutionCommand executionCommand) { - if (executionCommand == null) return; - - Logger.logDebug(LOG_TAG, "Executing background \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command"); - - // Transform executable path to shell/session name, e.g. "/bin/do-something.sh" => "do-something.sh". - if (executionCommand.shellName == null && executionCommand.executable != null) - executionCommand.shellName = ShellUtils.getExecutableBasename(executionCommand.executable); - - AppShell newTermuxTask = null; - ShellCreateMode shellCreateMode = processShellCreateMode(executionCommand); - if (shellCreateMode == null) return; - if (ShellCreateMode.NO_SHELL_WITH_NAME.equals(shellCreateMode)) { - newTermuxTask = getTermuxTaskForShellName(executionCommand.shellName); - if (newTermuxTask != null) - Logger.logVerbose(LOG_TAG, "Existing TermuxTask with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\""); - else - Logger.logVerbose(LOG_TAG, "No existing TermuxTask with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\""); - } - - if (newTermuxTask == null) - newTermuxTask = createTermuxTask(executionCommand); - } - - /** Create a TermuxTask. */ - @Nullable - public AppShell createTermuxTask(String executablePath, String[] arguments, String stdin, String workingDirectory) { - return createTermuxTask(new ExecutionCommand(TermuxShellManager.getNextShellId(), executablePath, - arguments, stdin, workingDirectory, Runner.APP_SHELL.getName(), false)); - } - - /** Create a TermuxTask. */ - @Nullable - public synchronized AppShell createTermuxTask(ExecutionCommand executionCommand) { - if (executionCommand == null) return null; - - Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask"); - - if (!Runner.APP_SHELL.equalsRunner(executionCommand.runner)) { - Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxTask()"); - return null; - } - - executionCommand.setShellCommandShellEnvironment = true; - - if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE) - Logger.logVerboseExtended(LOG_TAG, executionCommand.toString()); - - AppShell newTermuxTask = AppShell.execute(this, executionCommand, this, - new TermuxShellEnvironment(), null,false); - if (newTermuxTask == null) { - Logger.logError(LOG_TAG, "Failed to execute new TermuxTask command for:\n" + executionCommand.getCommandIdAndLabelLogString()); - // If the execution command was started for a plugin, then process the error - if (executionCommand.isPluginExecutionCommand) - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - else { - Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs"); - Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString()); - } - return null; - } - - mShellManager.mTermuxTasks.add(newTermuxTask); - - // Remove the execution command from the pending plugin execution commands list since it has - // now been processed - if (executionCommand.isPluginExecutionCommand) - mShellManager.mPendingPluginExecutionCommands.remove(executionCommand); - - updateNotification(); - - return newTermuxTask; } /** Callback received when a TermuxTask finishes. */ - @Override - public void onAppShellExited(final AppShell termuxTask) { + public void onAppShellExited(@NonNull final TermuxAppShell termuxTask) { mHandler.post(() -> { - if (termuxTask != null) { - ExecutionCommand executionCommand = termuxTask.getExecutionCommand(); - - Logger.logVerbose(LOG_TAG, "The onTermuxTaskExited() callback called for \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask command"); - - // If the execution command was started for a plugin, then process the results - if (executionCommand != null && executionCommand.isPluginExecutionCommand) - TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand); - - mShellManager.mTermuxTasks.remove(termuxTask); - } - + Log.i(LOG_TAG, "The onTermuxTaskExited() callback called for TermuxTask command"); + mShellManager.mTermuxTasks.remove(termuxTask); updateNotification(); }); } - - - - - /** Execute a shell command in a foreground {@link TermuxSession}. */ - private void executeTermuxSessionCommand(ExecutionCommand executionCommand) { - if (executionCommand == null) return; - - Logger.logDebug(LOG_TAG, "Executing foreground \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command"); - - // Transform executable path to shell/session name, e.g. "/bin/do-something.sh" => "do-something.sh". - if (executionCommand.shellName == null && executionCommand.executable != null) - executionCommand.shellName = ShellUtils.getExecutableBasename(executionCommand.executable); - - TermuxSession newTermuxSession = null; - ShellCreateMode shellCreateMode = processShellCreateMode(executionCommand); - if (shellCreateMode == null) return; - if (ShellCreateMode.NO_SHELL_WITH_NAME.equals(shellCreateMode)) { - newTermuxSession = getTermuxSessionForShellName(executionCommand.shellName); - if (newTermuxSession != null) - Logger.logVerbose(LOG_TAG, "Existing TermuxSession with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\""); - else - Logger.logVerbose(LOG_TAG, "No existing TermuxSession with \"" + executionCommand.shellName + "\" shell name found for shell create mode \"" + shellCreateMode.getMode() + "\""); - } - - if (newTermuxSession == null) - newTermuxSession = createTermuxSession(executionCommand); - if (newTermuxSession == null) return; - - handleSessionAction(DataUtils.getIntFromString(executionCommand.sessionAction, - TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY), - newTermuxSession.getTerminalSession()); - } - /** * Create a {@link TermuxSession}. * Currently called by {@link TermuxTerminalSessionActivityClient#addNewSession(boolean, String)} to add a new {@link TermuxSession}. */ @Nullable - public TermuxSession createTermuxSession(String executablePath, String[] arguments, String stdin, - String workingDirectory, boolean isFailSafe, String sessionName) { - ExecutionCommand executionCommand = new ExecutionCommand(TermuxShellManager.getNextShellId(), - executablePath, arguments, stdin, workingDirectory, Runner.TERMINAL_SESSION.getName(), isFailSafe); - executionCommand.shellName = sessionName; - return createTermuxSession(executionCommand); - } + public TermuxSession createTermuxSession(String executablePath, + String[] arguments, + String stdin, + String workingDirectory, + boolean isFailSafe, + String sessionName) { + TerminalSessionClient sessionClient = new TerminalSessionClient() { + @Override + public void onTextChanged(@NonNull TerminalSession changedSession) { + if (mTerminalSessionClient != null) { + mTerminalSessionClient.onTextChanged(changedSession); + } + } - /** Create a {@link TermuxSession}. */ - @Nullable - public synchronized TermuxSession createTermuxSession(ExecutionCommand executionCommand) { - if (executionCommand == null) return null; + @Override + public void onTitleChanged(@NonNull TerminalSession changedSession) { + if (mTerminalSessionClient != null) { + mTerminalSessionClient.onTitleChanged(changedSession); + } + } - Logger.logDebug(LOG_TAG, "Creating \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession"); + @Override + public void onSessionFinished(@NonNull TerminalSession finishedSession) { + if (mTerminalSessionClient != null) { + mTerminalSessionClient.onSessionFinished(finishedSession); + } - if (!Runner.TERMINAL_SESSION.equalsRunner(executionCommand.runner)) { - Logger.logDebug(LOG_TAG, "Ignoring wrong runner \"" + executionCommand.runner + "\" command passed to createTermuxSession()"); - return null; - } + } - executionCommand.setShellCommandShellEnvironment = true; - executionCommand.terminalTranscriptRows = mProperties.getTerminalTranscriptRows(); + @Override + public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) { + if (mTerminalSessionClient != null) { + mTerminalSessionClient.onCopyTextToClipboard(session, text); + } + } - if (Logger.getLogLevel() >= Logger.LOG_LEVEL_VERBOSE) - Logger.logVerboseExtended(LOG_TAG, executionCommand.toString()); + @Override + public void onPasteTextFromClipboard(@Nullable TerminalSession session) { + if (mTerminalSessionClient != null) { + mTerminalSessionClient.onPasteTextFromClipboard(session); + } + } + + @Override + public void onBell(@NonNull TerminalSession session) { + if (mTerminalSessionClient != null) { + mTerminalSessionClient.onBell(session); + } + } + + @Override + public void onColorsChanged(@NonNull TerminalSession session) { + if (mTerminalSessionClient != null) { + mTerminalSessionClient.onColorsChanged(session); + } + } + + @Override + public void onTerminalCursorStateChange(boolean state) { + if (mTerminalSessionClient != null) { + mTerminalSessionClient.onTerminalCursorStateChange(state); + } + } + }; // If the execution command was started for a plugin, only then will the stdout be set // Otherwise if command was manually started by the user like by adding a new terminal session, // then no need to set stdout - TermuxSession newTermuxSession = TermuxSession.execute(this, executionCommand, getTermuxTerminalSessionClient(), - this, new TermuxShellEnvironment(), null, executionCommand.isPluginExecutionCommand); - if (newTermuxSession == null) { - Logger.logError(LOG_TAG, "Failed to execute new TermuxSession command for:\n" + executionCommand.getCommandIdAndLabelLogString()); - // If the execution command was started for a plugin, then process the error - if (executionCommand.isPluginExecutionCommand) - TermuxPluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand, false); - else { - Logger.logError(LOG_TAG, "Set log level to debug or higher to see error in logs"); - Logger.logErrorPrivateExtended(LOG_TAG, executionCommand.toString()); - } - return null; - } + TermuxSession newTermuxSession = TermuxSession.execute( + sessionClient, + this, + isFailSafe + ); mShellManager.mTermuxSessions.add(newTermuxSession); - // Remove the execution command from the pending plugin execution commands list since it has - // now been processed - if (executionCommand.isPluginExecutionCommand) - mShellManager.mPendingPluginExecutionCommands.remove(executionCommand); - - // Notify {@link TermuxSessionsListViewController} that sessions list has been updated if - // activity in is foreground - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated(); + if (mTerminalSessionClient != null) { + mTerminalSessionClient.termuxSessionListNotifyUpdated(); + } updateNotification(); - // No need to recreate the activity since it likely just started and theme should already have applied - TermuxActivity.updateTermuxActivityStyling(this, false); - return newTermuxSession; } /** Remove a TermuxSession. */ public synchronized int removeTermuxSession(TerminalSession sessionToRemove) { int index = getIndexOfSession(sessionToRemove); - - if (index >= 0) + if (index >= 0) { mShellManager.mTermuxSessions.get(index).finish(); - + } return index; } /** Callback received when a {@link TermuxSession} finishes. */ - @Override - public void onTermuxSessionExited(final TermuxSession termuxSession) { - if (termuxSession != null) { - ExecutionCommand executionCommand = termuxSession.getExecutionCommand(); - - Logger.logVerbose(LOG_TAG, "The onTermuxSessionExited() callback called for \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession command"); - - // If the execution command was started for a plugin, then process the results - if (executionCommand != null && executionCommand.isPluginExecutionCommand) - TermuxPluginUtils.processPluginExecutionCommandResult(this, LOG_TAG, executionCommand); - - mShellManager.mTermuxSessions.remove(termuxSession); - - // Notify {@link TermuxSessionsListViewController} that sessions list has been updated if - // activity in is foreground - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.termuxSessionListNotifyUpdated(); + public void onTermuxSessionExited(@NonNull final TermuxSession termuxSession) { + mShellManager.mTermuxSessions.remove(termuxSession); + if (mTerminalSessionClient != null) { + mTerminalSessionClient.termuxSessionListNotifyUpdated(); } - updateNotification(); } - - - - - private ShellCreateMode processShellCreateMode(@NonNull ExecutionCommand executionCommand) { - if (ShellCreateMode.ALWAYS.equalsMode(executionCommand.shellCreateMode)) - return ShellCreateMode.ALWAYS; // Default - else if (ShellCreateMode.NO_SHELL_WITH_NAME.equalsMode(executionCommand.shellCreateMode)) - if (DataUtils.isNullOrEmpty(executionCommand.shellName)) { - TermuxPluginUtils.setAndProcessPluginExecutionCommandError(this, LOG_TAG, executionCommand, false, - getString(R.string.error_termux_service_execution_command_shell_name_unset, executionCommand.shellCreateMode)); - return null; - } else { - return ShellCreateMode.NO_SHELL_WITH_NAME; - } - else { - TermuxPluginUtils.setAndProcessPluginExecutionCommandError(this, LOG_TAG, executionCommand, false, - getString(R.string.error_termux_service_unsupported_execution_command_shell_create_mode, executionCommand.shellCreateMode)); - return null; - } - } - - /** Process session action for new session. */ - private void handleSessionAction(int sessionAction, TerminalSession newTerminalSession) { - Logger.logDebug(LOG_TAG, "Processing sessionAction \"" + sessionAction + "\" for session \"" + newTerminalSession.mSessionName + "\""); - - switch (sessionAction) { - case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY: - setCurrentStoredTerminalSession(newTerminalSession); - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession); - startTermuxActivity(); - break; - case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY: - if (getTermuxSessionsSize() == 1) - setCurrentStoredTerminalSession(newTerminalSession); - startTermuxActivity(); - break; - case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY: - setCurrentStoredTerminalSession(newTerminalSession); - if (mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.setCurrentSession(newTerminalSession); - break; - case TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY: - if (getTermuxSessionsSize() == 1) - setCurrentStoredTerminalSession(newTerminalSession); - break; - default: - Logger.logError(LOG_TAG, "Invalid sessionAction: \"" + sessionAction + "\". Force using default sessionAction."); - handleSessionAction(TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY, newTerminalSession); - break; - } - } - - /** Launch the {@link }TermuxActivity} to bring it to foreground. */ - private void startTermuxActivity() { - // For android >= 10, apps require Display over other apps permission to start foreground activities - // from background (services). If it is not granted, then TermuxSessions that are started will - // show in Termux notification but will not run until user manually clicks the notification. - if (PermissionUtils.validateDisplayOverOtherAppsPermissionForPostAndroid10(this, true)) { - TermuxActivity.startTermuxActivity(this); - } else { - TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this); - if (preferences == null) return; - if (preferences.arePluginErrorNotificationsEnabled(false)) - Logger.showToast(this, this.getString(R.string.error_display_over_other_apps_permission_not_granted_to_start_terminal), true); - } - } - - - - - - /** If {@link TermuxActivity} has not bound to the {@link TermuxService} yet or is destroyed, then - * interface functions requiring the activity should not be available to the terminal sessions, - * so we just return the {@link #mTermuxTerminalSessionServiceClient}. Once {@link TermuxActivity} bind - * callback is received, it should call {@link #setTermuxTerminalSessionClient} to set the - * {@link TermuxService#mTermuxTerminalSessionActivityClient} so that further terminal sessions are directly - * passed the {@link TermuxTerminalSessionActivityClient} object which fully implements the - * {@link TerminalSessionClient} interface. - * - * @return Returns the {@link TermuxTerminalSessionActivityClient} if {@link TermuxActivity} has bound with - * {@link TermuxService}, otherwise {@link TermuxTerminalSessionServiceClient}. - */ - public synchronized TermuxTerminalSessionClientBase getTermuxTerminalSessionClient() { - if (mTermuxTerminalSessionActivityClient != null) - return mTermuxTerminalSessionActivityClient; - else - return mTermuxTerminalSessionServiceClient; - } - - /** This should be called when {@link TermuxActivity#onServiceConnected} is called to set the - * {@link TermuxService#mTermuxTerminalSessionActivityClient} variable and update the {@link TerminalSession} - * and {@link TerminalEmulator} clients in case they were passed {@link TermuxTerminalSessionServiceClient} - * earlier. - * - * @param termuxTerminalSessionActivityClient The {@link TermuxTerminalSessionActivityClient} object that fully - * implements the {@link TerminalSessionClient} interface. - */ public synchronized void setTermuxTerminalSessionClient(TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) { - mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient; - - for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) - mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionActivityClient); + mTerminalSessionClient = termuxTerminalSessionActivityClient; } - /** This should be called when {@link TermuxActivity} has been destroyed and in {@link #onUnbind(Intent)} - * so that the {@link TermuxService} and {@link TerminalSession} and {@link TerminalEmulator} - * clients do not hold an activity references. - */ - public synchronized void unsetTermuxTerminalSessionClient() { - for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) - mShellManager.mTermuxSessions.get(i).getTerminalSession().updateTerminalSessionClient(mTermuxTerminalSessionServiceClient); - - mTermuxTerminalSessionActivityClient = null; - } - - - - - private Notification buildNotification() { Resources res = getResources(); // Set pending intent to be launched when notification is clicked - Intent notificationIntent = TermuxActivity.newInstance(this); - PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); + Intent notificationIntent = new Intent(this, TermuxActivity.class); + notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); // Set notification text int sessionCount = getTermuxSessionsSize(); @@ -798,54 +371,37 @@ public final class TermuxService extends Service implements AppShell.AppShellCli final boolean wakeLockHeld = mWakeLock != null; if (wakeLockHeld) notificationText += " (wake lock held)"; - // Set notification priority // If holding a wake or wifi lock consider the notification of high priority since it's using power, // otherwise use a low priority int priority = (wakeLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_LOW; - - // Build the notification - Notification.Builder builder = NotificationUtils.geNotificationBuilder(this, - TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, priority, - TermuxConstants.TERMUX_APP_NAME, notificationText, null, - contentIntent, null, NotificationUtils.NOTIFICATION_MODE_SILENT); - if (builder == null) return null; - - // No need to show a timestamp: - builder.setShowWhen(false); - - // Set notification icon - builder.setSmallIcon(R.drawable.ic_service_notification); - - // Set background color for small notification icon - builder.setColor(0xFF607D8B); - - // TermuxSessions are always ongoing - builder.setOngoing(true); - - - // Set Exit button action - Intent exitIntent = new Intent(this, TermuxService.class).setAction(TERMUX_SERVICE.ACTION_STOP_SERVICE); - builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0)); - + Intent exitIntent = new Intent(this, TermuxService.class).setAction(TermuxService.ACTION_STOP_SERVICE); // Set Wakelock button actions - String newWakeAction = wakeLockHeld ? TERMUX_SERVICE.ACTION_WAKE_UNLOCK : TERMUX_SERVICE.ACTION_WAKE_LOCK; + String newWakeAction = wakeLockHeld ? TermuxService.ACTION_WAKE_UNLOCK : TermuxService.ACTION_WAKE_LOCK; Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(newWakeAction); String actionTitle = res.getString(wakeLockHeld ? R.string.notification_action_wake_unlock : R.string.notification_action_wake_lock); - int actionIcon = wakeLockHeld ? android.R.drawable.ic_lock_idle_lock : android.R.drawable.ic_lock_lock; - builder.addAction(actionIcon, actionTitle, PendingIntent.getService(this, 0, toggleWakeLockIntent, 0)); + int wakeLockIcon = wakeLockHeld ? android.R.drawable.ic_lock_idle_lock : android.R.drawable.ic_lock_lock; - - return builder.build(); + return new Notification.Builder(this, NOTIFICATION_CHANNEL_ID) + .setPriority(priority) + .setContentText(notificationText) + .setContentIntent(contentIntent) + .setShowWhen(false) + .setSmallIcon(R.drawable.ic_service_notification) + .setColor(0xFF607D8B) + .setOngoing(true) + .addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, PendingIntent.FLAG_IMMUTABLE)) + .addAction(wakeLockIcon, actionTitle, PendingIntent.getService(this, 0, toggleWakeLockIntent, PendingIntent.FLAG_IMMUTABLE)) + .build(); } private void setupNotificationChannel() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; - - NotificationUtils.setupNotificationChannel(this, TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_ID, - TermuxConstants.TERMUX_APP_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); + NotificationChannel channel = new NotificationChannel(TermuxService.NOTIFICATION_CHANNEL_ID, "Termux", NotificationManager.IMPORTANCE_HIGH); + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); } /** Update the shown foreground service notification after making any changes that affect it. */ @@ -858,16 +414,13 @@ public final class TermuxService extends Service implements AppShell.AppShellCli } } - - - - private void setCurrentStoredTerminalSession(TerminalSession terminalSession) { if (terminalSession == null) return; // Make the newly created session the current one to be displayed - TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this); - if (preferences == null) return; - preferences.setCurrentSession(terminalSession.mHandle); + //TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(this); + //jif (preferences == null) return; + //preferences.setCurrentSession(terminalSession.mHandle); + // TODO } public synchronized boolean isTermuxSessionsEmpty() { @@ -895,7 +448,7 @@ public final class TermuxService extends Service implements AppShell.AppShellCli if (terminalSession == null) return null; for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) { - if (mShellManager.mTermuxSessions.get(i).getTerminalSession().equals(terminalSession)) + if (mShellManager.mTermuxSessions.get(i).mTerminalSession.equals(terminalSession)) return mShellManager.mTermuxSessions.get(i); } @@ -910,7 +463,7 @@ public final class TermuxService extends Service implements AppShell.AppShellCli if (terminalSession == null) return -1; for (int i = 0; i < mShellManager.mTermuxSessions.size(); i++) { - if (mShellManager.mTermuxSessions.get(i).getTerminalSession().equals(terminalSession)) + if (mShellManager.mTermuxSessions.get(i).mTerminalSession.equals(terminalSession)) return i; } return -1; @@ -919,41 +472,19 @@ public final class TermuxService extends Service implements AppShell.AppShellCli public synchronized TerminalSession getTerminalSessionForHandle(String sessionHandle) { TerminalSession terminalSession; for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) { - terminalSession = mShellManager.mTermuxSessions.get(i).getTerminalSession(); + terminalSession = mShellManager.mTermuxSessions.get(i).mTerminalSession; if (terminalSession.mHandle.equals(sessionHandle)) return terminalSession; } return null; } - public synchronized AppShell getTermuxTaskForShellName(String name) { - if (DataUtils.isNullOrEmpty(name)) return null; - AppShell appShell; - for (int i = 0, len = mShellManager.mTermuxTasks.size(); i < len; i++) { - appShell = mShellManager.mTermuxTasks.get(i); - String shellName = appShell.getExecutionCommand().shellName; - if (shellName != null && shellName.equals(name)) - return appShell; - } - return null; - } - - public synchronized TermuxSession getTermuxSessionForShellName(String name) { - if (DataUtils.isNullOrEmpty(name)) return null; - TermuxSession termuxSession; - for (int i = 0, len = mShellManager.mTermuxSessions.size(); i < len; i++) { - termuxSession = mShellManager.mTermuxSessions.get(i); - String shellName = termuxSession.getExecutionCommand().shellName; - if (shellName != null && shellName.equals(name)) - return termuxSession; - } - return null; - } - - - public boolean wantsToStop() { return mWantsToStop; } + + public void unsetTermuxTerminalSessionClient() { + this.mTerminalSessionClient = null; + } } diff --git a/app/src/main/java/com/termux/app/TermuxSession.java b/app/src/main/java/com/termux/app/TermuxSession.java new file mode 100644 index 00000000..5def55b5 --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxSession.java @@ -0,0 +1,105 @@ +package com.termux.app; + +import android.os.Build; +import android.os.Process; + +import androidx.annotation.NonNull; + +import com.termux.terminal.TerminalSession; +import com.termux.terminal.TerminalSessionClient; + +import java.io.File; + +public class TermuxSession { + + public static final String[] LOGIN_SHELL_BINARIES = new String[]{"login", "bash", "zsh", "fish", "sh"}; + + public final TerminalSession mTerminalSession; + private final TermuxService mTermuxService; + + private TermuxSession(@NonNull final TerminalSession terminalSession, final TermuxService termuxService) { + this.mTerminalSession = terminalSession; + this.mTermuxService = termuxService; + } + + public static TermuxSession execute(@NonNull final TerminalSessionClient terminalSessionClient, + final TermuxService termuxSessionClient, + boolean failSafe) { + String executable = null; + if (!failSafe) { + for (String shellBinary : LOGIN_SHELL_BINARIES) { + File shellFile = new File(com.termux.app.TermuxConstants.BIN_PATH, shellBinary); + if (shellFile.canExecute()) { + executable = shellFile.getAbsolutePath(); + break; + } + } + } + + boolean isLoginShell = false; + if (executable == null) { + // Fall back to system shell as last resort: + // Do not start a login shell since ~/.profile may cause startup failure if its invalid. + // /system/bin/sh is provided by mksh (not toybox) and does load .mkshrc but for android its set + // to /system/etc/mkshrc even though its default is ~/.mkshrc. + // So /system/etc/mkshrc must still be valid for failsafe session to start properly. + // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=663 + // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=41 + // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/Android.bp;l=114 + executable = "/system/bin/sh"; + } else { + isLoginShell = true; + } + + String[] commandArgs = TermuxShellUtils.setupShellCommandArguments(executable, new String[0]); + executable = commandArgs[0]; + String processName = (isLoginShell ? "-" : "") + new File(executable).getName(); + + String[] arguments = new String[commandArgs.length]; + arguments[0] = processName; + if (commandArgs.length > 1) { + System.arraycopy(commandArgs, 1, arguments, 1, commandArgs.length - 1); + } + + if (!failSafe) { + // Cannot execute written files directly on Android 10 or later. + String wrappedExecutable = executable; + executable = "/system/bin/linker" + (Process.is64Bit() ? "64" : ""); + + String[] origArguments = arguments; + arguments = new String[commandArgs.length + 2]; + arguments[0] = processName; + arguments[1] = TermuxConstants.BIN_PATH + "/sh"; + arguments[2] = wrappedExecutable; + if (origArguments.length > 1) { + System.arraycopy(origArguments, 1, arguments, 3, origArguments.length - 1); + } + } + + // Setup command environment + String[] environmentArray = TermuxShellUtils.setupEnvironment(failSafe); + + TerminalSession terminalSession = new TerminalSession( + executable, + com.termux.app.TermuxConstants.HOME_PATH, + arguments, + environmentArray, + 4000, + terminalSessionClient + ); + + return new TermuxSession(terminalSession, termuxSessionClient); + } + + public void finish() { + // If process is still running, then ignore the call + if (mTerminalSession.isRunning()) return; + mTermuxService.onTermuxSessionExited(this); + } + + public void killIfExecuting() { + // Send SIGKILL to process + mTerminalSession.finishIfRunning(); + } + +} diff --git a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java b/app/src/main/java/com/termux/app/TermuxSessionsListViewController.java similarity index 81% rename from app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java rename to app/src/main/java/com/termux/app/TermuxSessionsListViewController.java index bf914b97..9fd2210b 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java +++ b/app/src/main/java/com/termux/app/TermuxSessionsListViewController.java @@ -1,4 +1,4 @@ -package com.termux.app.terminal; +package com.termux.app; import android.annotation.SuppressLint; import android.graphics.Color; @@ -16,13 +16,8 @@ import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; import com.termux.R; -import com.termux.app.TermuxActivity; -import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession; -import com.termux.shared.theme.NightMode; -import com.termux.shared.theme.ThemeUtils; import com.termux.terminal.TerminalSession; import java.util.List; @@ -51,20 +46,12 @@ public class TermuxSessionsListViewController extends ArrayAdapter parent, View view, int position, long id) { TermuxSession clickedSession = getItem(position); - mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.getTerminalSession()); + mActivity.getTermuxTerminalSessionClient().setCurrentSession(clickedSession.mTerminalSession); mActivity.getDrawer().closeDrawers(); } @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { final TermuxSession selectedSession = getItem(position); - mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.getTerminalSession()); + mActivity.getTermuxTerminalSessionClient().renameSession(selectedSession.mTerminalSession); return true; } diff --git a/app/src/main/java/com/termux/app/TermuxShellManager.java b/app/src/main/java/com/termux/app/TermuxShellManager.java new file mode 100644 index 00000000..dd715f9e --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxShellManager.java @@ -0,0 +1,37 @@ +package com.termux.app; + +import android.content.Context; +import android.widget.ArrayAdapter; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +public class TermuxShellManager { + + private static int SHELL_ID = 0; + + protected final Context mContext; + + /** + * The foreground TermuxSessions which this service manages. + * Note that this list is observed by an activity, like TermuxActivity.mTermuxSessionListViewController, + * so any changes must be made on the UI thread and followed by a call to + * {@link ArrayAdapter#notifyDataSetChanged()}. + */ + public final List mTermuxSessions = new ArrayList<>(); + + /** + * The background TermuxTasks which this service manages. + */ + public final List mTermuxTasks = new ArrayList<>(); + + public TermuxShellManager(@NonNull Context context) { + mContext = context.getApplicationContext(); + } + + public static synchronized int getNextShellId() { + return SHELL_ID++; + } +} diff --git a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java b/app/src/main/java/com/termux/app/TermuxShellUtils.java similarity index 50% rename from termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java rename to app/src/main/java/com/termux/app/TermuxShellUtils.java index de56a29e..1c8d3219 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/shell/TermuxShellUtils.java +++ b/app/src/main/java/com/termux/app/TermuxShellUtils.java @@ -1,32 +1,20 @@ -package com.termux.shared.termux.shell; +package com.termux.app; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.termux.shared.errors.Error; -import com.termux.shared.file.filesystem.FileTypes; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.file.FileUtils; -import com.termux.shared.logger.Logger; -import com.termux.shared.termux.settings.properties.TermuxAppSharedProperties; - -import org.apache.commons.io.filefilter.TrueFileFilter; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class TermuxShellUtils { - private static final String LOG_TAG = "TermuxShellUtils"; - - /** - * Setup shell command arguments for the execute. The file interpreter may be prefixed to - * command arguments if needed. - */ @NonNull public static String[] setupShellCommandArguments(@NonNull String executable, @Nullable String[] arguments) { // The file to execute may either be: @@ -57,7 +45,7 @@ public class TermuxShellUtils { if (shebangExecutable.startsWith("/usr") || shebangExecutable.startsWith("/bin")) { String[] parts = shebangExecutable.split("/"); String binary = parts[parts.length - 1]; - interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary; + interpreter = TermuxConstants.BIN_PATH + "/" + binary; } break; } @@ -67,7 +55,7 @@ public class TermuxShellUtils { } } else { // No shebang and no ELF, use standard shell. - interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh"; + interpreter = TermuxConstants.BIN_PATH + "/sh"; } } } @@ -82,41 +70,68 @@ public class TermuxShellUtils { return result.toArray(new String[0]); } - /** Clear files under {@link TermuxConstants#TERMUX_TMP_PREFIX_DIR_PATH}. */ - public static void clearTermuxTMPDIR(boolean onlyIfExists) { - // Existence check before clearing may be required since clearDirectory() will automatically - // re-create empty directory if doesn't exist, which should not be done for things like - // termux-reset (d6eb5e35). Moreover, TMPDIR must be a directory and not a symlink, this can - // also allow users who don't want TMPDIR to be cleared automatically on termux exit, since - // it may remove files still being used by background processes (#1159). - if(onlyIfExists && !FileUtils.directoryFileExists(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, false)) - return; - Error error; + public static String[] setupEnvironment(boolean failsafe) { + Map environment = new HashMap<>(); + environment.put("HOME", TermuxConstants.HOME_PATH); + environment.put("LANG", "en_US.UTF-8"); + String tmpDir = TermuxConstants.PREFIX_PATH + "/tmp"; + environment.put("TMP", tmpDir); + environment.put("TMPDIR", tmpDir); + environment.put("COLORTERM", "truecolor"); + environment.put("TERM", "xterm-256color"); + putToEnvIfInSystemEnv(environment, "PATH"); + putToEnvIfInSystemEnv(environment, "ANDROID_ASSETS"); + putToEnvIfInSystemEnv(environment, "ANDROID_DATA"); + putToEnvIfInSystemEnv(environment, "ANDROID_ROOT"); + putToEnvIfInSystemEnv(environment, "ANDROID_STORAGE"); + // EXTERNAL_STORAGE is needed for /system/bin/am to work on at least + // Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3. + // https://cs.android.com/android/_/android/platform/system/core/+/fc000489 + putToEnvIfInSystemEnv(environment, "EXTERNAL_STORAGE"); + putToEnvIfInSystemEnv(environment, "ASEC_MOUNTPOINT"); + putToEnvIfInSystemEnv(environment, "LOOP_MOUNTPOINT"); + putToEnvIfInSystemEnv(environment, "ANDROID_RUNTIME_ROOT"); + putToEnvIfInSystemEnv(environment, "ANDROID_ART_ROOT"); + putToEnvIfInSystemEnv(environment, "ANDROID_I18N_ROOT"); + putToEnvIfInSystemEnv(environment, "ANDROID_TZDATA_ROOT"); + putToEnvIfInSystemEnv(environment, "BOOTCLASSPATH"); + putToEnvIfInSystemEnv(environment, "DEX2OATBOOTCLASSPATH"); + putToEnvIfInSystemEnv(environment, "SYSTEMSERVERCLASSPATH"); - TermuxAppSharedProperties properties = TermuxAppSharedProperties.getProperties(); - int days = properties.getDeleteTMPDIRFilesOlderThanXDaysOnExit(); + if (!failsafe) { + environment.put("LD_PRELOAD", TermuxConstants.PREFIX_PATH + "/lib/libtermux-exec.so"); + environment.put("PATH", TermuxConstants.PREFIX_PATH + "/bin:" + System.getenv("PATH")); + } - // Disable currently until FileUtils.deleteFilesOlderThanXDays() is fixed. - if (days > 0) - days = 0; + List environmentList = new ArrayList<>(environment.values()); + for (Map.Entry entry : environment.entrySet()) { + environmentList.add(entry.getKey() + "=" + entry.getValue()); + } + Collections.sort(environmentList); + return environmentList.toArray(new String[0]); + } - if (days < 0) { - Logger.logInfo(LOG_TAG, "Not clearing termux $TMPDIR"); - } else if (days == 0) { - error = FileUtils.clearDirectory("$TMPDIR", - FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null)); - if (error != null) { - Logger.logErrorExtended(LOG_TAG, "Failed to clear termux $TMPDIR\n" + error); - } - } else { - error = FileUtils.deleteFilesOlderThanXDays("$TMPDIR", - FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null), - TrueFileFilter.INSTANCE, days, true, FileTypes.FILE_TYPE_ANY_FLAGS); - if (error != null) { - Logger.logErrorExtended(LOG_TAG, "Failed to delete files from termux $TMPDIR older than " + days + " days\n" + error); - } + private static void putToEnvIfInSystemEnv(@NonNull Map environment, @NonNull String name) { + String value = System.getenv(name); + if (value != null) { + environment.put(name, value); } } + public static int getPid(Process p) { + try { + Field f = p.getClass().getDeclaredField("pid"); + f.setAccessible(true); + try { + return f.getInt(p); + } finally { + f.setAccessible(false); + } + } catch (Throwable e) { + return -1; + } + } + + } diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java b/app/src/main/java/com/termux/app/TermuxTerminalSessionActivityClient.java similarity index 66% rename from app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java rename to app/src/main/java/com/termux/app/TermuxTerminalSessionActivityClient.java index cd381631..996dc5c1 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionActivityClient.java +++ b/app/src/main/java/com/termux/app/TermuxTerminalSessionActivityClient.java @@ -1,32 +1,28 @@ -package com.termux.app.terminal; +package com.termux.app; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; -import android.content.ClipData; -import android.content.ClipboardManager; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Typeface; import android.media.AudioAttributes; import android.media.SoundPool; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.os.VibrationEffect; +import android.os.Vibrator; import android.text.TextUtils; +import android.util.Log; import android.widget.ListView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.termux.R; -import com.termux.shared.interact.ShareUtils; -import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession; -import com.termux.shared.termux.interact.TextInputDialogUtils; -import com.termux.app.TermuxActivity; -import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase; -import com.termux.shared.termux.TermuxConstants; -import com.termux.app.TermuxService; -import com.termux.shared.termux.settings.properties.TermuxPropertyConstants; -import com.termux.shared.termux.terminal.io.BellHandler; -import com.termux.shared.logger.Logger; import com.termux.terminal.TerminalColors; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSessionClient; @@ -37,8 +33,77 @@ import java.io.FileInputStream; import java.io.InputStream; import java.util.Properties; -/** The {@link TerminalSessionClient} implementation that may require an {@link Activity} for its interface methods. */ -public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionClientBase { +/** + * The {@link TerminalSessionClient} implementation that may require an {@link Activity} for its interface methods. + */ +public final class TermuxTerminalSessionActivityClient implements TerminalSessionClient { + + public static class BellHandler { + private static BellHandler instance = null; + private static final Object lock = new Object(); + + private static final String LOG_TAG = "BellHandler"; + + public static BellHandler getInstance(Context context) { + if (instance == null) { + synchronized (lock) { + if (instance == null) { + instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE)); + } + } + } + return instance; + } + + private static final long DURATION = 50; + private static final long MIN_PAUSE = 3 * DURATION; + + private final Handler handler = new Handler(Looper.getMainLooper()); + private long lastBell = 0; + private final Runnable bellRunnable; + + private BellHandler(final Vibrator vibrator) { + bellRunnable = new Runnable() { + @Override + public void run() { + if (vibrator != null) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(DURATION, VibrationEffect.DEFAULT_AMPLITUDE)); + } else { + vibrator.vibrate(DURATION); + } + } catch (Exception e) { + // Issue on samsung devices on android 8 + // java.lang.NullPointerException: Attempt to read from field 'android.os.VibrationEffect com.android.server.VibratorService$Vibration.mEffect' on a null object reference + Log.e(LOG_TAG, "Failed to run vibrator", e); + } + } + } + }; + } + + public synchronized void doBell() { + long now = now(); + long timeSinceLastBell = now - lastBell; + + if (timeSinceLastBell < 0) { + // there is a next bell pending; don't schedule another one + } else if (timeSinceLastBell < MIN_PAUSE) { + // there was a bell recently, schedule the next one + handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell); + lastBell = lastBell + MIN_PAUSE; + } else { + // the last bell was long ago, do it now + bellRunnable.run(); + lastBell = now; + } + } + + private long now() { + return SystemClock.uptimeMillis(); + } + } private final TermuxActivity mActivity; @@ -63,7 +128,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl } /** - * Should be called when mActivity.onStart() is called + * Called when mActivity.onStart() is called */ public void onStart() { // The service has connected, but data may have changed since we were last in the foreground. @@ -112,13 +177,11 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl checkForFontAndColors(); } - - @Override public void onTextChanged(@NonNull TerminalSession changedSession) { - if (!mActivity.isVisible()) return; - - if (mActivity.getCurrentSession() == changedSession) mActivity.getTerminalView().onScreenUpdated(); + if (mActivity.isVisible() && mActivity.getCurrentSession() == changedSession) { + mActivity.getTerminalView().onScreenUpdated(); + } } @Override @@ -150,13 +213,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl // For plugin commands that expect the result back, we should immediately close the session // and send the result back instead of waiting fo the user to press enter. // The plugin can handle/show errors itself. - boolean isPluginExecutionCommandWithPendingResult = false; TermuxSession termuxSession = service.getTermuxSession(index); - if (termuxSession != null) { - isPluginExecutionCommandWithPendingResult = termuxSession.getExecutionCommand().isPluginExecutionCommandWithPendingResult(); - if (isPluginExecutionCommandWithPendingResult) - Logger.logVerbose(LOG_TAG, "The \"" + finishedSession.mSessionName + "\" session will be force finished automatically since result in pending."); - } if (mActivity.isVisible() && finishedSession != mActivity.getCurrentSession()) { // Show toast for non-current sessions that exit. @@ -168,13 +225,13 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { // On Android TV devices we need to use older behaviour because we may // not be able to have multiple launcher icons. - if (service.getTermuxSessionsSize() > 1 || isPluginExecutionCommandWithPendingResult) { + if (service.getTermuxSessionsSize() > 1) { removeFinishedSession(finishedSession); } } else { // Once we have a separate launcher icon for the failsafe session, it // should be safe to auto-close session on exit code '0' or '130'. - if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130 || isPluginExecutionCommandWithPendingResult) { + if (finishedSession.getExitStatus() == 0 || finishedSession.getExitStatus() == 130) { removeFinishedSession(finishedSession); } } @@ -183,15 +240,14 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl @Override public void onCopyTextToClipboard(@NonNull TerminalSession session, String text) { if (!mActivity.isVisible()) return; - - ShareUtils.copyTextToClipboard(mActivity, text); + TermuxUrlUtils.copyTextToClipboard(mActivity, text); } @Override public void onPasteTextFromClipboard(@Nullable TerminalSession session) { if (!mActivity.isVisible()) return; - String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true); + String text = TermuxUrlUtils.getTextStringFromClipboardIfSet(mActivity, true); if (text != null) mActivity.getTerminalView().mEmulator.paste(text); } @@ -200,18 +256,10 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl public void onBell(@NonNull TerminalSession session) { if (!mActivity.isVisible()) return; - switch (mActivity.getProperties().getBellBehaviour()) { - case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_VIBRATE: - BellHandler.getInstance(mActivity).doBell(); - break; - case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_BEEP: - loadBellSoundPool(); - if (mBellSoundPool != null) - mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); - break; - case TermuxPropertyConstants.IVALUE_BELL_BEHAVIOUR_IGNORE: - // Ignore the bell character. - break; + //BellHandler.getInstance(mActivity).doBell(); + loadBellSoundPool(); + if (mBellSoundPool != null) { + mBellSoundPool.play(mBellSoundId, 1.f, 1.f, 1, 0, 1.f); } } @@ -222,48 +270,13 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl } @Override - public void onTerminalCursorStateChange(boolean enabled) { - // Do not start cursor blinking thread if activity is not visible - if (enabled && !mActivity.isVisible()) { - Logger.logVerbose(LOG_TAG, "Ignoring call to start cursor blinking since activity is not visible"); - return; - } + public void onTerminalCursorStateChange(boolean state) { - // If cursor is to enabled now, then start cursor blinking if blinking is enabled - // otherwise stop cursor blinking - mActivity.getTerminalView().setTerminalCursorBlinkerState(enabled, false); } - @Override - public void setTerminalShellPid(@NonNull TerminalSession terminalSession, int pid) { - TermuxService service = mActivity.getTermuxService(); - if (service == null) return; - - TermuxSession termuxSession = service.getTermuxSessionForTerminalSession(terminalSession); - if (termuxSession != null) - termuxSession.getExecutionCommand().mPid = pid; - } - - /** - * Should be called when mActivity.onResetTerminalSession() is called + * Load mBellSoundPool */ - public void onResetTerminalSession() { - // Ensure blinker starts again after reset if cursor blinking was disabled before reset like - // with "tput civis" which would have called onTerminalCursorStateChange() - mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true); - } - - - - @Override - public Integer getTerminalCursorStyle() { - return mActivity.getProperties().getTerminalCursorStyle(); - } - - - - /** Load mBellSoundPool */ private synchronized void loadBellSoundPool() { if (mBellSoundPool == null) { mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes( @@ -271,15 +284,17 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build(); try { - mBellSoundId = mBellSoundPool.load(mActivity, R.raw.bell, 1); - } catch (Exception e){ + mBellSoundId = mBellSoundPool.load(mActivity, com.termux.R.raw.bell, 1); + } catch (Exception e) { // Catch java.lang.RuntimeException: Unable to resume activity {com.termux/com.termux.app.TermuxActivity}: android.content.res.Resources$NotFoundException: File res/raw/bell.ogg from drawable resource ID - Logger.logStackTraceWithMessage(LOG_TAG, "Failed to load bell sound pool", e); + Log.e(LOG_TAG, "Failed to load bell sound pool", e); } } } - /** Release mBellSoundPool resources */ + /** + * Release mBellSoundPool resources + */ private synchronized void releaseBellSoundPool() { if (mBellSoundPool != null) { mBellSoundPool.release(); @@ -288,11 +303,10 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl } - - /** Try switching to session. */ + /** + * Try switching to session. + */ public void setCurrentSession(TerminalSession session) { - if (session == null) return; - if (mActivity.getTerminalView().attachSession(session)) { // notify about switched session if not already displaying the session notifyOfSessionChange(); @@ -306,11 +320,8 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl void notifyOfSessionChange() { if (!mActivity.isVisible()) return; - - if (!mActivity.getProperties().areTerminalSessionChangeToastsDisabled()) { - TerminalSession session = mActivity.getCurrentSession(); - mActivity.showToast(toToastTitle(session), false); - } + TerminalSession session = mActivity.getCurrentSession(); + mActivity.showToast(toToastTitle(session), false); } public void switchToSession(boolean forward) { @@ -328,7 +339,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl TermuxSession termuxSession = service.getTermuxSession(index); if (termuxSession != null) - setCurrentSession(termuxSession.getTerminalSession()); + setCurrentSession(termuxSession.mTerminalSession); } public void switchToSession(int index) { @@ -337,14 +348,14 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl TermuxSession termuxSession = service.getTermuxSession(index); if (termuxSession != null) - setCurrentSession(termuxSession.getTerminalSession()); + setCurrentSession(termuxSession.mTerminalSession); } @SuppressLint("InflateParams") public void renameSession(final TerminalSession sessionToRename) { if (sessionToRename == null) return; - TextInputDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> { + TermuxMessageDialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> { renameSession(sessionToRename, text); termuxSessionListNotifyUpdated(); }, -1, null, -1, null, null); @@ -353,35 +364,28 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl private void renameSession(TerminalSession sessionToRename, String text) { if (sessionToRename == null) return; sessionToRename.mSessionName = text; - TermuxService service = mActivity.getTermuxService(); - if (service != null) { - TermuxSession termuxSession = service.getTermuxSessionForTerminalSession(sessionToRename); - if (termuxSession != null) - termuxSession.getExecutionCommand().shellName = text; - } } public void addNewSession(boolean isFailSafe, String sessionName) { TermuxService service = mActivity.getTermuxService(); - if (service == null) return; + if (service == null) { + return; + } if (service.getTermuxSessionsSize() >= MAX_SESSIONS) { - new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached) - .setPositiveButton(android.R.string.ok, null).show(); + new AlertDialog.Builder(mActivity) + .setTitle(R.string.title_max_terminals_reached) + .setMessage(R.string.msg_max_terminals_reached) + .setPositiveButton(android.R.string.ok, null) + .show(); } else { TerminalSession currentSession = mActivity.getCurrentSession(); - String workingDirectory; - if (currentSession == null) { - workingDirectory = mActivity.getProperties().getDefaultWorkingDirectory(); - } else { - workingDirectory = currentSession.getCwd(); - } - + String workingDirectory = currentSession == null ? TermuxConstants.HOME_PATH : currentSession.getCwd(); TermuxSession newTermuxSession = service.createTermuxSession(null, null, null, workingDirectory, isFailSafe, sessionName); if (newTermuxSession == null) return; - TerminalSession newTerminalSession = newTermuxSession.getTerminalSession(); + TerminalSession newTerminalSession = newTermuxSession.mTerminalSession; setCurrentSession(newTerminalSession); mActivity.getDrawer().closeDrawers(); @@ -390,44 +394,31 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl public void setCurrentStoredSession() { TerminalSession currentSession = mActivity.getCurrentSession(); - if (currentSession != null) - mActivity.getPreferences().setCurrentSession(currentSession.mHandle); - else - mActivity.getPreferences().setCurrentSession(null); + mActivity.mPreferences.setCurrentSession(currentSession == null ? null : currentSession.mHandle); + ; } - /** The current session as stored or the last one if that does not exist. */ + /** + * The current session as stored or the last one if that does not exist. + */ public TerminalSession getCurrentStoredSessionOrLast() { - TerminalSession stored = getCurrentStoredSession(); - - if (stored != null) { - // If a stored session is in the list of currently running sessions, then return it - return stored; - } else { - // Else return the last session currently running - TermuxService service = mActivity.getTermuxService(); - if (service == null) return null; - - TermuxSession termuxSession = service.getLastTermuxSession(); - if (termuxSession != null) - return termuxSession.getTerminalSession(); - else - return null; - } - } - - private TerminalSession getCurrentStoredSession() { - String sessionHandle = mActivity.getPreferences().getCurrentSession(); - - // If no session is stored in shared preferences - if (sessionHandle == null) + String currentSessionHandle = mActivity.mPreferences.getCurrentSession(); + if (currentSessionHandle == null) { return null; + } // Check if the session handle found matches one of the currently running sessions TermuxService service = mActivity.getTermuxService(); if (service == null) return null; - return service.getTerminalSessionForHandle(sessionHandle); + TerminalSession currentSession = service.getTerminalSessionForHandle(currentSessionHandle); + + if (currentSession == null) { + TermuxSession termuxSession = service.getLastTermuxSession(); + return termuxSession == null ? null : termuxSession.mTerminalSession; + } else { + return currentSession; + } } public void removeFinishedSession(TerminalSession finishedSession) { @@ -446,8 +437,9 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl index = size - 1; } TermuxSession termuxSession = service.getTermuxSession(index); - if (termuxSession != null) - setCurrentSession(termuxSession.getTerminalSession()); + if (termuxSession != null) { + setCurrentSession(termuxSession.mTerminalSession); + } } } @@ -493,8 +485,8 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl public void checkForFontAndColors() { try { - File colorsFile = TermuxConstants.TERMUX_COLOR_PROPERTIES_FILE; - File fontFile = TermuxConstants.TERMUX_FONT_FILE; + File fontFile = new File(TermuxConstants.HOME_PATH + "/.termux/font.ttf"); + File colorsFile = new File(TermuxConstants.HOME_PATH + "/.termux/colors.properties"); final Properties props = new Properties(); if (colorsFile.isFile()) { @@ -513,7 +505,7 @@ public class TermuxTerminalSessionActivityClient extends TermuxTerminalSessionCl final Typeface newTypeface = (fontFile.exists() && fontFile.length() > 0) ? Typeface.createFromFile(fontFile) : Typeface.MONOSPACE; mActivity.getTerminalView().setTypeface(newTypeface); } catch (Exception e) { - Logger.logStackTraceWithMessage(LOG_TAG, "Error in checkForFontAndColors()", e); + Log.e(LOG_TAG, "Error in checkForFontAndColors()", e); } } diff --git a/app/src/main/java/com/termux/app/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/TermuxTerminalViewClient.java new file mode 100644 index 00000000..e95c873c --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxTerminalViewClient.java @@ -0,0 +1,413 @@ +package com.termux.app; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.media.AudioManager; +import android.util.Log; +import android.view.Gravity; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.inputmethod.InputMethodManager; +import android.widget.ListView; + +import com.termux.R; +import com.termux.app.extrakeys.SpecialButton; +import com.termux.terminal.KeyHandler; +import com.termux.terminal.TerminalBuffer; +import com.termux.terminal.TerminalEmulator; +import com.termux.terminal.TerminalSession; +import com.termux.view.TerminalViewClient; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; + +import androidx.drawerlayout.widget.DrawerLayout; + +public final class TermuxTerminalViewClient implements TerminalViewClient { + + final TermuxActivity mActivity; + + final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient; + + /** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */ + boolean mVirtualControlKeyDown, mVirtualFnKeyDown; + + private static final String LOG_TAG = "TermuxTerminalViewClient"; + + public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) { + this.mActivity = activity; + this.mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient; + } + + public TermuxActivity getActivity() { + return mActivity; + } + + /** + * Should be called when mActivity.onStart() is called + */ + public void onStart() { + // Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future + } + + @Override + public float onScale(float scale) { + if (scale < 0.9f || scale > 1.1f) { + boolean increase = scale > 1.f; + changeFontSize(increase); + return 1.0f; + } + return scale; + } + + @Override + public void onSingleTapUp(MotionEvent e) { + TerminalSession session = mActivity.getCurrentSession(); + if (session != null) { + TerminalEmulator term = mActivity.getCurrentSession().getEmulator(); + if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) { + mActivity.getSystemService(InputMethodManager.class).showSoftInput(mActivity.getTerminalView(), 0); + } + } + } + + @Override + public boolean shouldBackButtonBeMappedToEscape() { + return mActivity.mProperties.isBackKeyTheEscapeKey(); + } + + @Override + public boolean shouldEnforceCharBasedInput() { + return mActivity.mProperties.isEnforcingCharBasedInput(); + } + + @Override + public boolean isTerminalViewSelected() { + return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus(); + } + + @Override + public void copyModeChanged(boolean copyMode) { + // Disable drawer while copying. + mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED); + } + + @SuppressLint("RtlHardcoded") + @Override + public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) { + if (handleVirtualKeys(keyCode, e, true)) return true; + + if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) { + mTermuxTerminalSessionActivityClient.removeFinishedSession(currentSession); + return true; + } else if (e.isCtrlPressed() && e.isAltPressed()) { + // Get the unmodified code point: + int unicodeChar = e.getUnicodeChar(0); + + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) { + mTermuxTerminalSessionActivityClient.switchToSession(true); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) { + mTermuxTerminalSessionActivityClient.switchToSession(false); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + mActivity.getDrawer().openDrawer(Gravity.LEFT); + } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { + mActivity.getDrawer().closeDrawers(); + } else if (unicodeChar == 'c'/* create */) { + mTermuxTerminalSessionActivityClient.addNewSession(false, null); + } else if (unicodeChar == 'k'/* keyboard */) { + onToggleSoftKeyboardRequest(); + } else if (unicodeChar == 'm'/* menu */) { + mActivity.getTerminalView().showContextMenu(); + } else if (unicodeChar == 'r'/* rename */) { + mTermuxTerminalSessionActivityClient.renameSession(currentSession); + } else if (unicodeChar == 'u' /* urls */) { + showUrlSelection(); + } else if (unicodeChar == 'v') { + doPaste(); + } else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') { + // We also check for the shifted char here since shift may be required to produce '+', + // see https://github.com/termux/termux-api/issues/2 + changeFontSize(true); + } else if (unicodeChar == '-') { + changeFontSize(false); + } else if (unicodeChar >= '1' && unicodeChar <= '9') { + int index = unicodeChar - '1'; + mTermuxTerminalSessionActivityClient.switchToSession(index); + } + return true; + } + + return false; + + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent e) { + // If emulator is not set, like if bootstrap installation failed and user dismissed the error + // dialog, then just exit the activity, otherwise they will be stuck in a broken state. + if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) { + mActivity.finishActivityIfNotFinishing(); + return true; + } + + return handleVirtualKeys(keyCode, e, false); + } + + /** Handle dedicated volume buttons as virtual keys if applicable. */ + private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { + InputDevice inputDevice = event.getDevice(); + if (mActivity.mProperties.areVirtualVolumeKeysDisabled()) { + return false; + } else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { + // Do not steal dedicated buttons from a full external keyboard. + return false; + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + mVirtualControlKeyDown = down; + return true; + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + mVirtualFnKeyDown = down; + return true; + } + return false; + } + + + + @Override + public boolean readControlKey() { + return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown; + } + + @Override + public boolean readAltKey() { + return readExtraKeysSpecialButton(SpecialButton.ALT); + } + + @Override + public boolean readShiftKey() { + return readExtraKeysSpecialButton(SpecialButton.SHIFT); + } + + @Override + public boolean readFnKey() { + return readExtraKeysSpecialButton(SpecialButton.FN); + } + + public boolean readExtraKeysSpecialButton(SpecialButton specialButton) { + if (mActivity.getExtraKeysView() == null) return false; + Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true); + if (state == null) { + Log.e(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys."); + return false; + } + return state; + } + + @Override + public boolean onLongPress(MotionEvent event) { + return false; + } + + @Override + public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) { + if (mVirtualFnKeyDown) { + int resultingKeyCode = -1; + int resultingCodePoint = -1; + boolean altDown = false; + int lowerCase = Character.toLowerCase(codePoint); + switch (lowerCase) { + // Arrow keys. + case 'w': + resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP; + break; + case 'a': + resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT; + break; + case 's': + resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN; + break; + case 'd': + resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT; + break; + + // Page up and down. + case 'p': + resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP; + break; + case 'n': + resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN; + break; + + // Some special keys: + case 't': + resultingKeyCode = KeyEvent.KEYCODE_TAB; + break; + case 'i': + resultingKeyCode = KeyEvent.KEYCODE_INSERT; + break; + case 'h': + resultingCodePoint = '~'; + break; + + // Special characters to input. + case 'u': + resultingCodePoint = '_'; + break; + case 'l': + resultingCodePoint = '|'; + break; + + // Function keys. + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1; + break; + case '0': + resultingKeyCode = KeyEvent.KEYCODE_F10; + break; + + // Other special keys. + case 'e': + resultingCodePoint = /*Escape*/ 27; + break; + case '.': + resultingCodePoint = /*^.*/ 28; + break; + + case 'b': // alt+b, jumping backward in readline. + case 'f': // alf+f, jumping forward in readline. + case 'x': // alt+x, common in emacs. + resultingCodePoint = lowerCase; + altDown = true; + break; + + // Volume control. + case 'v': + resultingCodePoint = -1; + AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE); + audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI); + break; + + // Writing mode: + case 'q': + case 'k': + mActivity.toggleTerminalToolbar(); + mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420 + break; + + case 'z': // Zecret :) + mActivity.requestAutoFill(); + } + + if (resultingKeyCode != -1) { + TerminalEmulator term = session.getEmulator(); + session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode())); + } else if (resultingCodePoint != -1) { + session.writeCodePoint(altDown, resultingCodePoint); + } + return true; + } else if (ctrlDown) { + if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) { + mTermuxTerminalSessionActivityClient.removeFinishedSession(session); + return true; + } + } + + return false; + } + + public void changeFontSize(boolean increase) { + int newFontSize = mActivity.mPreferences.changeFontSize(increase); + mActivity.getTerminalView().setTextSize(newFontSize); + } + + /** + * Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in + * drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut. + */ + public void onToggleSoftKeyboardRequest() { + mActivity.getSystemService(InputMethodManager.class).toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); + + } + + public void shareSessionTranscript() { + TerminalSession session = mActivity.getCurrentSession(); + if (session == null) return; + TerminalEmulator terminalEmulator = session.getEmulator(); + if (terminalEmulator == null) return; + TerminalBuffer terminalBuffer = terminalEmulator.getScreen(); + if (terminalBuffer == null) return; + String sessionTranscript = terminalBuffer.getTranscriptTextWithoutJoinedLines().trim(); + TermuxUrlUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript), + sessionTranscript, mActivity.getString(R.string.title_share_transcript_with)); + } + + public void shareSelectedText() { + String selectedText = mActivity.getTerminalView().getStoredSelectedText(); + if (selectedText != null && !selectedText.isEmpty()) { + TermuxUrlUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text), + selectedText, mActivity.getString(R.string.title_share_selected_text_with)); + } + } + + public void showUrlSelection() { + TerminalSession session = mActivity.getCurrentSession(); + if (session == null) return; + TerminalEmulator terminalEmulator = session.getEmulator(); + if (terminalEmulator == null) return; + TerminalBuffer terminalBuffer = terminalEmulator.getScreen(); + if (terminalBuffer == null) return; + String sessionTranscript = terminalBuffer.getTranscriptTextWithFullLinesJoined().trim(); + + LinkedHashSet urlSet = TermuxUrlUtils.extractUrls(sessionTranscript); + if (urlSet.isEmpty()) { + new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show(); + return; + } + + final CharSequence[] urls = urlSet.toArray(new CharSequence[0]); + Collections.reverse(Arrays.asList(urls)); // Latest first. + + // Click to copy url to clipboard: + final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> { + String url = (String) urls[which]; + TermuxUrlUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard)); + }).setTitle(R.string.title_select_url_dialog).create(); + + // Long press to open URL: + dialog.setOnShowListener(di -> { + ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it + lv.setOnItemLongClickListener((parent, view, position, id) -> { + dialog.dismiss(); + String url = (String) urls[position]; + TermuxUrlUtils.openUrl(mActivity, url); + return true; + }); + }); + + dialog.show(); + } + + public void doPaste() { + TerminalSession session = mActivity.getCurrentSession(); + if (session == null || !session.isRunning()) { + return; + } + + String text = TermuxUrlUtils.getTextStringFromClipboardIfSet(mActivity, true); + if (text != null) { + session.getEmulator().paste(text); + } + } + +} diff --git a/app/src/main/java/com/termux/app/TermuxUrlUtils.java b/app/src/main/java/com/termux/app/TermuxUrlUtils.java new file mode 100644 index 00000000..4d3c4dd3 --- /dev/null +++ b/app/src/main/java/com/termux/app/TermuxUrlUtils.java @@ -0,0 +1,235 @@ +package com.termux.app; + +import android.content.ActivityNotFoundException; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.Nullable; + +import java.util.LinkedHashSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TermuxUrlUtils { + + public static Pattern URL_MATCH_REGEX; + + public static Pattern getUrlMatchRegex() { + if (URL_MATCH_REGEX != null) return URL_MATCH_REGEX; + + StringBuilder regex_sb = new StringBuilder(); + + regex_sb.append("("); // Begin first matching group. + regex_sb.append("(?:"); // Begin scheme group. + regex_sb.append("dav|"); // The DAV proto. + regex_sb.append("dict|"); // The DICT proto. + regex_sb.append("dns|"); // The DNS proto. + regex_sb.append("file|"); // File path. + regex_sb.append("finger|"); // The Finger proto. + regex_sb.append("ftp(?:s?)|"); // The FTP proto. + regex_sb.append("git|"); // The Git proto. + regex_sb.append("gemini|"); // The Gemini proto. + regex_sb.append("gopher|"); // The Gopher proto. + regex_sb.append("http(?:s?)|"); // The HTTP proto. + regex_sb.append("imap(?:s?)|"); // The IMAP proto. + regex_sb.append("irc(?:[6s]?)|"); // The IRC proto. + regex_sb.append("ip[fn]s|"); // The IPFS proto. + regex_sb.append("ldap(?:s?)|"); // The LDAP proto. + regex_sb.append("pop3(?:s?)|"); // The POP3 proto. + regex_sb.append("redis(?:s?)|"); // The Redis proto. + regex_sb.append("rsync|"); // The Rsync proto. + regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto. + regex_sb.append("sftp|"); // The SFTP proto. + regex_sb.append("smb(?:s?)|"); // The SAMBA proto. + regex_sb.append("smtp(?:s?)|"); // The SMTP proto. + regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto. + regex_sb.append("tcp|"); // The TCP proto. + regex_sb.append("telnet|"); // The Telnet proto. + regex_sb.append("tftp|"); // The TFTP proto. + regex_sb.append("udp|"); // The UDP proto. + regex_sb.append("vnc|"); // The VNC proto. + regex_sb.append("ws(?:s?)"); // The Websocket proto. + regex_sb.append(")://"); // End scheme group. + regex_sb.append(")"); // End first matching group. + + + // Begin second matching group. + regex_sb.append("("); + + // User name and/or password in format 'user:pass@'. + regex_sb.append("(?:\\S+(?::\\S*)?@)?"); + + // Begin host group. + regex_sb.append("(?:"); + + // IP address (from http://www.regular-expressions.info/examples.html). + regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|"); + + // Host name or domain. + regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*){1,}[a-z\\u00a1-\\uffff0-9]{1,}))?|"); + + // Just path. Used in case of 'file://' scheme. + regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)"); + + // End host group. + regex_sb.append(")"); + + // Port number. + regex_sb.append("(?::\\d{1,5})?"); + + // Resource path with optional query string. + regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?"); + + // Fragment. + regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?"); + + // End second matching group. + regex_sb.append(")"); + + URL_MATCH_REGEX = Pattern.compile( + regex_sb.toString(), + Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); + + return URL_MATCH_REGEX; + } + + public static LinkedHashSet extractUrls(String text) { + LinkedHashSet urlSet = new LinkedHashSet<>(); + Matcher matcher = getUrlMatchRegex().matcher(text); + + while (matcher.find()) { + int matchStart = matcher.start(1); + int matchEnd = matcher.end(); + String url = text.substring(matchStart, matchEnd); + urlSet.add(url); + } + + return urlSet; + } + + + /** + * Open a url. + * + * @param context The context for operations. + * @param url The url to open. + */ + public static void openUrl(final Context context, final String url) { + if (context == null || url == null || url.isEmpty()) return; + Uri uri = Uri.parse(url); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + try { + context.startActivity(intent); + } catch (ActivityNotFoundException e) { + // If no activity found to handle intent, show system chooser + openSystemAppChooser(context, intent, context.getString(com.termux.R.string.title_open_url_with)); + } catch (Exception e) { + Log.e(TermuxConstants.LOG_TAG, "Failed to open url \"" + url + "\"", e); + } + } + + public static void openSystemAppChooser(final Context context, final Intent intent, final String title) { + if (context == null) return; + + final Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); + chooserIntent.putExtra(Intent.EXTRA_INTENT, intent); + chooserIntent.putExtra(Intent.EXTRA_TITLE, title); + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + context.startActivity(chooserIntent); + } catch (Exception e) { + Log.e(TermuxConstants.LOG_TAG, "Failed to open system chooser for: " + chooserIntent, e); + } + } + + /** + * Share text. + * + * @param context The context for operations. + * @param subject The subject for sharing. + * @param text The text to share. + * @param title The title for share menu. + */ + public static void shareText(final Context context, final String subject, final String text, @Nullable final String title) { + if (context == null || text == null) return; + + final Intent shareTextIntent = new Intent(Intent.ACTION_SEND); + shareTextIntent.setType("text/plain"); + shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + shareTextIntent.putExtra(Intent.EXTRA_TEXT, text); + + openSystemAppChooser(context, shareTextIntent, (title == null) ? context.getString(com.termux.R.string.title_share_with) : title); + } + + + + /** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel` and `toastString`. */ + public static void copyTextToClipboard(Context context, final String text) { + copyTextToClipboard(context, null, text, null); + } + + /** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel`. */ + public static void copyTextToClipboard(Context context, final String text, final String toastString) { + copyTextToClipboard(context, null, text, toastString); + } + + /** + * Copy the text to primary clip of the clipboard. + * + * @param context The context for operations. + * @param clipDataLabel The label to show to the user describing the copied text. + * @param text The text to copy. + * @param toastString If this is not {@code null} or empty, then a toast is shown if copying to + * clipboard is successful. + */ + public static void copyTextToClipboard(Context context, @Nullable final String clipDataLabel, + final String text, final String toastString) { + ClipboardManager clipboardManager = context.getSystemService(ClipboardManager.class); + clipboardManager.setPrimaryClip(ClipData.newPlainText(clipDataLabel, text)); + if (toastString != null && !toastString.isEmpty()) { + TermuxMessageDialogUtils.showToast(context, toastString); + } + } + + /** + * Wrapper for {@link #getTextFromClipboard(Context, boolean)} that returns primary text {@link String} + * if its set and not empty. + */ + @Nullable + public static String getTextStringFromClipboardIfSet(Context context, boolean coerceToText) { + CharSequence textCharSequence = getTextFromClipboard(context, coerceToText); + if (textCharSequence == null) return null; + String textString = textCharSequence.toString(); + return !textString.isEmpty() ? textString : null; + } + + /** + * Get the text from primary clip of the clipboard. + * + * @param context The context for operations. + * @param coerceToText Whether to call {@link ClipData.Item#coerceToText(Context)} to coerce + * non-text data to text. + * @return Returns the {@link CharSequence} of primary text. This will be `null` if failed to get it. + */ + @Nullable + public static CharSequence getTextFromClipboard(Context context, boolean coerceToText) { + if (context == null) return null; + + ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboardManager == null) return null; + + ClipData clipData = clipboardManager.getPrimaryClip(); + if (clipData == null) return null; + + ClipData.Item clipItem = clipData.getItemAt(0); + if (clipItem == null) return null; + + return coerceToText ? clipItem.coerceToText(context) : clipItem.getText(); + } + + +} diff --git a/app/src/main/java/com/termux/app/activities/SettingsActivity.java b/app/src/main/java/com/termux/app/activities/SettingsActivity.java deleted file mode 100644 index 7ca78483..00000000 --- a/app/src/main/java/com/termux/app/activities/SettingsActivity.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.termux.app.activities; - -import android.content.Context; -import android.os.Bundle; -import android.os.Environment; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; - -import com.termux.R; -import com.termux.shared.activities.ReportActivity; -import com.termux.shared.file.FileUtils; -import com.termux.shared.models.ReportInfo; -import com.termux.app.models.UserAction; -import com.termux.shared.interact.ShareUtils; -import com.termux.shared.android.PackageUtils; -import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; -import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; -import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; -import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences; -import com.termux.shared.android.AndroidUtils; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.termux.TermuxUtils; -import com.termux.shared.activity.media.AppCompatActivityUtils; -import com.termux.shared.theme.NightMode; - -public class SettingsActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - AppCompatActivityUtils.setNightMode(this, NightMode.getAppNightMode().getName(), true); - - setContentView(R.layout.activity_settings); - if (savedInstanceState == null) { - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.settings, new RootPreferencesFragment()) - .commit(); - } - - AppCompatActivityUtils.setToolbar(this, com.termux.shared.R.id.toolbar); - AppCompatActivityUtils.setShowBackButtonInActionBar(this, true); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - public static class RootPreferencesFragment extends PreferenceFragmentCompat { - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - setPreferencesFromResource(R.xml.root_preferences, rootKey); - - new Thread() { - @Override - public void run() { - configureTermuxAPIPreference(context); - configureTermuxFloatPreference(context); - configureTermuxTaskerPreference(context); - configureTermuxWidgetPreference(context); - configureAboutPreference(context); - configureDonatePreference(context); - } - }.start(); - } - - private void configureTermuxAPIPreference(@NonNull Context context) { - Preference termuxAPIPreference = findPreference("termux_api"); - if (termuxAPIPreference != null) { - TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, false); - // If failed to get app preferences, then likely app is not installed, so do not show its preference - termuxAPIPreference.setVisible(preferences != null); - } - } - - private void configureTermuxFloatPreference(@NonNull Context context) { - Preference termuxFloatPreference = findPreference("termux_float"); - if (termuxFloatPreference != null) { - TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, false); - // If failed to get app preferences, then likely app is not installed, so do not show its preference - termuxFloatPreference.setVisible(preferences != null); - } - } - - private void configureTermuxTaskerPreference(@NonNull Context context) { - Preference termuxTaskerPreference = findPreference("termux_tasker"); - if (termuxTaskerPreference != null) { - TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, false); - // If failed to get app preferences, then likely app is not installed, so do not show its preference - termuxTaskerPreference.setVisible(preferences != null); - } - } - - private void configureTermuxWidgetPreference(@NonNull Context context) { - Preference termuxWidgetPreference = findPreference("termux_widget"); - if (termuxWidgetPreference != null) { - TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, false); - // If failed to get app preferences, then likely app is not installed, so do not show its preference - termuxWidgetPreference.setVisible(preferences != null); - } - } - - private void configureAboutPreference(@NonNull Context context) { - Preference aboutPreference = findPreference("about"); - if (aboutPreference != null) { - aboutPreference.setOnPreferenceClickListener(preference -> { - new Thread() { - @Override - public void run() { - String title = "About"; - - StringBuilder aboutString = new StringBuilder(); - aboutString.append(TermuxUtils.getAppInfoMarkdownString(context, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES)); - aboutString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context, true)); - aboutString.append("\n\n").append(TermuxUtils.getImportantLinksMarkdownString(context)); - - String userActionName = UserAction.ABOUT.getName(); - - ReportInfo reportInfo = new ReportInfo(userActionName, - TermuxConstants.TERMUX_APP.TERMUX_SETTINGS_ACTIVITY_NAME, title); - reportInfo.setReportString(aboutString.toString()); - reportInfo.setReportSaveFileLabelAndPath(userActionName, - Environment.getExternalStorageDirectory() + "/" + - FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)); - - ReportActivity.startReportActivity(context, reportInfo); - } - }.start(); - - return true; - }); - } - } - - private void configureDonatePreference(@NonNull Context context) { - Preference donatePreference = findPreference("donate"); - if (donatePreference != null) { - String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context); - if (signingCertificateSHA256Digest != null) { - // If APK is a Google Playstore release, then do not show the donation link - // since Termux isn't exempted from the playstore policy donation links restriction - // Check Fund solicitations: https://pay.google.com/intl/en_in/about/policy/ - String apkRelease = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest); - if (apkRelease == null || apkRelease.equals(TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST)) { - donatePreference.setVisible(false); - return; - } else { - donatePreference.setVisible(true); - } - } - - donatePreference.setOnPreferenceClickListener(preference -> { - ShareUtils.openUrl(context, TermuxConstants.TERMUX_DONATE_URL); - return true; - }); - } - } - } - -} diff --git a/app/src/main/java/com/termux/app/event/SystemEventReceiver.java b/app/src/main/java/com/termux/app/event/SystemEventReceiver.java deleted file mode 100644 index efc710f3..00000000 --- a/app/src/main/java/com/termux/app/event/SystemEventReceiver.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.termux.app.event; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.termux.shared.data.IntentUtils; -import com.termux.shared.logger.Logger; -import com.termux.shared.termux.TermuxUtils; -import com.termux.shared.termux.file.TermuxFileUtils; -import com.termux.shared.termux.shell.command.environment.TermuxShellEnvironment; -import com.termux.shared.termux.shell.TermuxShellManager; - -public class SystemEventReceiver extends BroadcastReceiver { - - private static SystemEventReceiver mInstance; - - private static final String LOG_TAG = "SystemEventReceiver"; - - public static synchronized SystemEventReceiver getInstance() { - if (mInstance == null) { - mInstance = new SystemEventReceiver(); - } - return mInstance; - } - - @Override - public void onReceive(@NonNull Context context, @Nullable Intent intent) { - if (intent == null) return; - Logger.logDebug(LOG_TAG, "Intent Received:\n" + IntentUtils.getIntentString(intent)); - - String action = intent.getAction(); - if (action == null) return; - - switch (action) { - case Intent.ACTION_BOOT_COMPLETED: - onActionBootCompleted(context, intent); - break; - case Intent.ACTION_PACKAGE_ADDED: - case Intent.ACTION_PACKAGE_REMOVED: - case Intent.ACTION_PACKAGE_REPLACED: - onActionPackageUpdated(context, intent); - break; - default: - Logger.logError(LOG_TAG, "Invalid action \"" + action + "\" passed to " + LOG_TAG); - } - } - - public synchronized void onActionBootCompleted(@NonNull Context context, @NonNull Intent intent) { - TermuxShellManager.onActionBootCompleted(context, intent); - } - - public synchronized void onActionPackageUpdated(@NonNull Context context, @NonNull Intent intent) { - Uri data = intent.getData(); - if (data != null && TermuxUtils.isUriDataForTermuxPluginPackage(data)) { - Logger.logDebug(LOG_TAG, intent.getAction().replaceAll("^android.intent.action.", "") + - " event received for \"" + data.toString().replaceAll("^package:", "") + "\""); - if (TermuxFileUtils.isTermuxFilesDirectoryAccessible(context, false, false) == null) - TermuxShellEnvironment.writeEnvironmentToFile(context); - } - } - - - - /** - * Register {@link SystemEventReceiver} to listen to {@link Intent#ACTION_PACKAGE_ADDED}, - * {@link Intent#ACTION_PACKAGE_REMOVED} and {@link Intent#ACTION_PACKAGE_REPLACED} broadcasts. - * They must be registered dynamically and cannot be registered implicitly in - * the AndroidManifest.xml due to Android 8+ restrictions. - * - * https://developer.android.com/guide/components/broadcast-exceptions - */ - public synchronized static void registerPackageUpdateEvents(@NonNull Context context) { - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); - intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); - intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); - intentFilter.addDataScheme("package"); - context.registerReceiver(getInstance(), intentFilter); - } - - public synchronized static void unregisterPackageUpdateEvents(@NonNull Context context) { - context.unregisterReceiver(getInstance()); - } - -} diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeyButton.java b/app/src/main/java/com/termux/app/extrakeys/ExtraKeyButton.java similarity index 99% rename from termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeyButton.java rename to app/src/main/java/com/termux/app/extrakeys/ExtraKeyButton.java index bcec59da..603dd0f6 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeyButton.java +++ b/app/src/main/java/com/termux/app/extrakeys/ExtraKeyButton.java @@ -1,4 +1,4 @@ -package com.termux.shared.termux.extrakeys; +package com.termux.app.extrakeys; import android.text.TextUtils; diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java b/app/src/main/java/com/termux/app/extrakeys/ExtraKeysConstants.java similarity index 99% rename from termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java rename to app/src/main/java/com/termux/app/extrakeys/ExtraKeysConstants.java index b747381c..76ac0390 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysConstants.java +++ b/app/src/main/java/com/termux/app/extrakeys/ExtraKeysConstants.java @@ -1,4 +1,4 @@ -package com.termux.shared.termux.extrakeys; +package com.termux.app.extrakeys; import android.view.KeyEvent; @@ -13,8 +13,8 @@ public class ExtraKeysConstants { public static List PRIMARY_REPETITIVE_KEYS = Arrays.asList( "UP", "DOWN", "LEFT", "RIGHT", "BKSP", "DEL", - "PGUP", "PGDN"); - + "PGUP", "PGDN" + ); /** Defines the {@link KeyEvent} for common keys. */ diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysInfo.java b/app/src/main/java/com/termux/app/extrakeys/ExtraKeysInfo.java similarity index 75% rename from termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysInfo.java rename to app/src/main/java/com/termux/app/extrakeys/ExtraKeysInfo.java index 2f5bb4a7..77ec8924 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysInfo.java +++ b/app/src/main/java/com/termux/app/extrakeys/ExtraKeysInfo.java @@ -1,13 +1,10 @@ -package com.termux.shared.termux.extrakeys; +package com.termux.app.extrakeys; import android.view.View; -import android.widget.Button; import androidx.annotation.NonNull; import com.google.android.material.button.MaterialButton; -import com.termux.shared.termux.extrakeys.ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS; -import com.termux.shared.termux.terminal.io.TerminalExtraKeys; import org.json.JSONArray; import org.json.JSONException; @@ -93,38 +90,12 @@ public class ExtraKeysInfo { */ private final ExtraKeyButton[][] mButtons; - /** - * Initialize {@link ExtraKeysInfo}. - * - * @param propertiesInfo The {@link String} containing the info to create the {@link ExtraKeysInfo}. - * Check the class javadoc for details. - * @param style The style to pass to {@link #getCharDisplayMapForStyle(String)} to get the - * {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the display text - * mapping for the keys if a custom value is not defined by - * {@link ExtraKeyButton#KEY_DISPLAY_NAME} for a key. - * @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the - * aliases for the actual key names. You can create your own or - * optionally pass {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}. - */ - public ExtraKeysInfo(@NonNull String propertiesInfo, String style, + public ExtraKeysInfo(@NonNull String propertiesInfo, + String style, @NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException { mButtons = initExtraKeysInfo(propertiesInfo, getCharDisplayMapForStyle(style), extraKeyAliasMap); } - /** - * Initialize {@link ExtraKeysInfo}. - * - * @param propertiesInfo The {@link String} containing the info to create the {@link ExtraKeysInfo}. - * Check the class javadoc for details. - * @param extraKeyDisplayMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the - * display text mapping for the keys if a custom value is not defined - * by {@link ExtraKeyButton#KEY_DISPLAY_NAME} for a key. You can create - * your own or optionally pass one of the values defined in - * {@link #getCharDisplayMapForStyle(String)}. - * @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the - * aliases for the actual key names. You can create your own or - * optionally pass {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}. - */ public ExtraKeysInfo(@NonNull String propertiesInfo, @NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap, @NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException { @@ -198,15 +169,15 @@ public class ExtraKeysInfo { public static ExtraKeysConstants.ExtraKeyDisplayMap getCharDisplayMapForStyle(String style) { switch (style) { case "arrows-only": - return EXTRA_KEY_DISPLAY_MAPS.ARROWS_ONLY_CHAR_DISPLAY; + return ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.ARROWS_ONLY_CHAR_DISPLAY; case "arrows-all": - return EXTRA_KEY_DISPLAY_MAPS.LOTS_OF_ARROWS_CHAR_DISPLAY; + return ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.LOTS_OF_ARROWS_CHAR_DISPLAY; case "all": - return EXTRA_KEY_DISPLAY_MAPS.FULL_ISO_CHAR_DISPLAY; + return ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.FULL_ISO_CHAR_DISPLAY; case "none": return new ExtraKeysConstants.ExtraKeyDisplayMap(); default: - return EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY; + return ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY; } } diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysView.java b/app/src/main/java/com/termux/app/extrakeys/ExtraKeysView.java similarity index 70% rename from termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysView.java rename to app/src/main/java/com/termux/app/extrakeys/ExtraKeysView.java index 4fbbf1e1..e73990c7 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/ExtraKeysView.java +++ b/app/src/main/java/com/termux/app/extrakeys/ExtraKeysView.java @@ -1,4 +1,4 @@ -package com.termux.shared.termux.extrakeys; +package com.termux.app.extrakeys; import android.annotation.SuppressLint; import android.content.Context; @@ -7,24 +7,11 @@ import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.util.AttributeSet; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.ScheduledExecutorService; - -import java.util.Map; -import java.util.HashMap; -import java.util.stream.Collectors; - import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; -import android.view.ViewGroup; import android.widget.GridLayout; import android.widget.PopupWindow; @@ -32,50 +19,22 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.material.button.MaterialButton; -import com.termux.shared.R; -import com.termux.shared.termux.terminal.io.TerminalExtraKeys; -import com.termux.shared.theme.ThemeUtils; -/** - * A {@link View} showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft - * keyboards. - * - * To use it, add following to a layout file and import it in your activity layout file or inflate - * it with a {@link androidx.viewpager.widget.ViewPager}.: - * {@code - * - * - * } - * - * Then in your activity, get its reference by a call to {@link android.app.Activity#findViewById(int)} - * or {@link LayoutInflater#inflate(int, ViewGroup)} if using {@link androidx.viewpager.widget.ViewPager}. - * Then call {@link #setExtraKeysViewClient(IExtraKeysView)} and pass it the implementation of - * {@link IExtraKeysView} so that you can receive callbacks. You can also override other values set - * in {@link ExtraKeysView#ExtraKeysView(Context, AttributeSet)} by calling the respective functions. - * If you extend {@link ExtraKeysView}, you can also set them in the constructor, but do call super(). - * - * After this you will have to make a call to {@link ExtraKeysView#reload(ExtraKeysInfo, float) and pass - * it the {@link ExtraKeysInfo} to load and display the extra keys. Read its class javadocs for more - * info on how to create it. - * - * Termux app defines the view in res/layout/view_terminal_toolbar_extra_keys and - * inflates it in TerminalToolbarViewPager.instantiateItem() and sets the {@link ExtraKeysView} client - * and calls {@link ExtraKeysView#reload(ExtraKeysInfo). - * The {@link ExtraKeysInfo} is created by TermuxAppSharedProperties.setExtraKeys(). - * Then its got and the view height is adjusted in TermuxActivity.setTerminalToolbarHeight(). - * The client used is TermuxTerminalExtraKeys, which extends - * {@link TerminalExtraKeys } to handle Termux app specific logic and - * leave the rest to the super class. - */ +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + public final class ExtraKeysView extends GridLayout { - /** The client for the {@link ExtraKeysView}. */ + /** + * The client for the {@link ExtraKeysView}. + */ public interface IExtraKeysView { /** @@ -84,22 +43,22 @@ public final class ExtraKeysView extends GridLayout { * However, this is not called for {@link #mSpecialButtons}, whose state can instead be read * via a call to {@link #readSpecialButton(SpecialButton, boolean)}. * - * @param view The view that was clicked. + * @param view The view that was clicked. * @param buttonInfo The {@link ExtraKeyButton} for the button that was clicked. * The button may be a {@link ExtraKeyButton#KEY_MACRO} set which can be * checked with a call to {@link ExtraKeyButton#isMacro()}. - * @param button The {@link MaterialButton} that was clicked. + * @param button The {@link MaterialButton} that was clicked. */ void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, MaterialButton button); /** * This is called by {@link ExtraKeysView} when a button is clicked so that the client - * can perform any hepatic feedback. This is only called in the {@link MaterialButton.OnClickListener} + * can perform any hepatic feedback. This is only called in the {@link OnClickListener} * and not for every repeat. Its also called for {@link #mSpecialButtons}. * - * @param view The view that was clicked. + * @param view The view that was clicked. * @param buttonInfo The {@link ExtraKeyButton} for the button that was clicked. - * @param button The {@link MaterialButton} that was clicked. + * @param button The {@link MaterialButton} that was clicked. * @return Return {@code true} if the client handled the feedback, otherwise {@code false} * so that {@link ExtraKeysView#performExtraKeyButtonHapticFeedback(View, ExtraKeyButton, MaterialButton)} * can handle it depending on system settings. @@ -108,52 +67,47 @@ public final class ExtraKeysView extends GridLayout { } - - /** Defines the default value for {@link #mButtonTextColor} defined by current theme. */ - public static final int ATTR_BUTTON_TEXT_COLOR = R.attr.extraKeysButtonTextColor; - /** Defines the default value for {@link #mButtonActiveTextColor} defined by current theme. */ - public static final int ATTR_BUTTON_ACTIVE_TEXT_COLOR = R.attr.extraKeysButtonActiveTextColor; - /** Defines the default value for {@link #mButtonBackgroundColor} defined by current theme. */ - public static final int ATTR_BUTTON_BACKGROUND_COLOR = R.attr.extraKeysButtonBackgroundColor; - /** Defines the default value for {@link #mButtonActiveBackgroundColor} defined by current theme. */ - public static final int ATTR_BUTTON_ACTIVE_BACKGROUND_COLOR = R.attr.extraKeysButtonActiveBackgroundColor; - - /** Defines the default fallback value for {@link #mButtonTextColor} if {@link #ATTR_BUTTON_TEXT_COLOR} is undefined. */ - public static final int DEFAULT_BUTTON_TEXT_COLOR = 0xFFFFFFFF; - /** Defines the default fallback value for {@link #mButtonActiveTextColor} if {@link #ATTR_BUTTON_ACTIVE_TEXT_COLOR} is undefined. */ - public static final int DEFAULT_BUTTON_ACTIVE_TEXT_COLOR = 0xFF80DEEA; - /** Defines the default fallback value for {@link #mButtonBackgroundColor} if {@link #ATTR_BUTTON_BACKGROUND_COLOR} is undefined. */ - public static final int DEFAULT_BUTTON_BACKGROUND_COLOR = 0x00000000; - /** Defines the default fallback value for {@link #mButtonActiveBackgroundColor} if {@link #ATTR_BUTTON_ACTIVE_BACKGROUND_COLOR} is undefined. */ - public static final int DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR = 0xFF7F7F7F; - - - - /** Defines the minimum allowed duration in milliseconds for {@link #mLongPressTimeout}. */ + /** + * Defines the minimum allowed duration in milliseconds for {@link #mLongPressTimeout}. + */ public static final int MIN_LONG_PRESS_DURATION = 200; - /** Defines the maximum allowed duration in milliseconds for {@link #mLongPressTimeout}. */ + /** + * Defines the maximum allowed duration in milliseconds for {@link #mLongPressTimeout}. + */ public static final int MAX_LONG_PRESS_DURATION = 3000; - /** Defines the fallback duration in milliseconds for {@link #mLongPressTimeout}. */ + /** + * Defines the fallback duration in milliseconds for {@link #mLongPressTimeout}. + */ public static final int FALLBACK_LONG_PRESS_DURATION = 400; - /** Defines the minimum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. */ + /** + * Defines the minimum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. + */ public static final int MIN_LONG_PRESS__REPEAT_DELAY = 5; - /** Defines the maximum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. */ + /** + * Defines the maximum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. + */ public static final int MAX_LONG_PRESS__REPEAT_DELAY = 2000; - /** Defines the default duration in milliseconds for {@link #mLongPressRepeatDelay}. */ + /** + * Defines the default duration in milliseconds for {@link #mLongPressRepeatDelay}. + */ public static final int DEFAULT_LONG_PRESS_REPEAT_DELAY = 80; - - - /** The implementation of the {@link IExtraKeysView} that acts as a client for the {@link ExtraKeysView}. */ + /** + * The implementation of the {@link IExtraKeysView} that acts as a client for the {@link ExtraKeysView}. + */ protected IExtraKeysView mExtraKeysViewClient; - /** The map for the {@link SpecialButton} and their {@link SpecialButtonState}. Defaults to - * the one returned by {@link #getDefaultSpecialButtons(ExtraKeysView)}. */ + /** + * The map for the {@link SpecialButton} and their {@link SpecialButtonState}. Defaults to + * the one returned by {@link #getDefaultSpecialButtons(ExtraKeysView)}. + */ protected Map mSpecialButtons; - /** The keys for the {@link SpecialButton} added to {@link #mSpecialButtons}. This is automatically - * set when the call to {@link #setSpecialButtons(Map)} is made. */ + /** + * The keys for the {@link SpecialButton} added to {@link #mSpecialButtons}. This is automatically + * set when the call to {@link #setSpecialButtons(Map)} is made. + */ protected Set mSpecialButtonsKeys; @@ -166,18 +120,28 @@ public final class ExtraKeysView extends GridLayout { protected List mRepetitiveKeys; - /** The text color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_TEXT_COLOR}. */ + /** + * The text color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_TEXT_COLOR}. + */ protected int mButtonTextColor; - /** The text color for the extra keys button when its active. - * Defaults to {@link #DEFAULT_BUTTON_ACTIVE_TEXT_COLOR}. */ + /** + * The text color for the extra keys button when its active. + * Defaults to {@link #DEFAULT_BUTTON_ACTIVE_TEXT_COLOR}. + */ protected int mButtonActiveTextColor; - /** The background color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_BACKGROUND_COLOR}. */ + /** + * The background color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_BACKGROUND_COLOR}. + */ protected int mButtonBackgroundColor; - /** The background color for the extra keys button when its active. Defaults to - * {@link #DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR}. */ + /** + * The background color for the extra keys button when its active. Defaults to + * {@link #DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR}. + */ protected int mButtonActiveBackgroundColor; - /** Defines whether text for the extra keys button should be all capitalized automatically. */ + /** + * Defines whether text for the extra keys button should be all capitalized automatically. + */ protected boolean mButtonTextAllCaps = true; @@ -199,8 +163,10 @@ public final class ExtraKeysView extends GridLayout { protected int mLongPressRepeatDelay; - /** The popup window shown if {@link ExtraKeyButton#getPopup()} returns a {@code non-null} value - * and a swipe up action is done on an extra key. */ + /** + * The popup window shown if {@link ExtraKeyButton#getPopup()} returns a {@code non-null} value + * and a swipe up action is done on an extra key. + */ protected PopupWindow mPopupWindow; protected ScheduledExecutorService mScheduledExecutor; @@ -215,130 +181,143 @@ public final class ExtraKeysView extends GridLayout { setRepetitiveKeys(ExtraKeysConstants.PRIMARY_REPETITIVE_KEYS); setSpecialButtons(getDefaultSpecialButtons(this)); - setButtonColors( - ThemeUtils.getSystemAttrColor(context, ATTR_BUTTON_TEXT_COLOR, DEFAULT_BUTTON_TEXT_COLOR), - ThemeUtils.getSystemAttrColor(context, ATTR_BUTTON_ACTIVE_TEXT_COLOR, DEFAULT_BUTTON_ACTIVE_TEXT_COLOR), - ThemeUtils.getSystemAttrColor(context, ATTR_BUTTON_BACKGROUND_COLOR, DEFAULT_BUTTON_BACKGROUND_COLOR), - ThemeUtils.getSystemAttrColor(context, ATTR_BUTTON_ACTIVE_BACKGROUND_COLOR, DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR)); - setLongPressTimeout(ViewConfiguration.getLongPressTimeout()); setLongPressRepeatDelay(DEFAULT_LONG_PRESS_REPEAT_DELAY); } - /** Get {@link #mExtraKeysViewClient}. */ + /** + * Get {@link #mExtraKeysViewClient}. + */ public IExtraKeysView getExtraKeysViewClient() { return mExtraKeysViewClient; } - /** Set {@link #mExtraKeysViewClient}. */ + /** + * Set {@link #mExtraKeysViewClient}. + */ public void setExtraKeysViewClient(IExtraKeysView extraKeysViewClient) { mExtraKeysViewClient = extraKeysViewClient; } - /** Get {@link #mRepetitiveKeys}. */ + /** + * Get {@link #mRepetitiveKeys}. + */ public List getRepetitiveKeys() { if (mRepetitiveKeys == null) return null; return mRepetitiveKeys.stream().map(String::new).collect(Collectors.toList()); } - /** Set {@link #mRepetitiveKeys}. Must not be {@code null}. */ + /** + * Set {@link #mRepetitiveKeys}. Must not be {@code null}. + */ public void setRepetitiveKeys(@NonNull List repetitiveKeys) { mRepetitiveKeys = repetitiveKeys; } - /** Get {@link #mSpecialButtons}. */ + /** + * Get {@link #mSpecialButtons}. + */ public Map getSpecialButtons() { if (mSpecialButtons == null) return null; return mSpecialButtons.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - /** Get {@link #mSpecialButtonsKeys}. */ + /** + * Get {@link #mSpecialButtonsKeys}. + */ public Set getSpecialButtonsKeys() { if (mSpecialButtonsKeys == null) return null; return mSpecialButtonsKeys.stream().map(String::new).collect(Collectors.toSet()); } - /** Set {@link #mSpecialButtonsKeys}. Must not be {@code null}. */ + /** + * Set {@link #mSpecialButtonsKeys}. Must not be {@code null}. + */ public void setSpecialButtons(@NonNull Map specialButtons) { mSpecialButtons = specialButtons; mSpecialButtonsKeys = this.mSpecialButtons.keySet().stream().map(SpecialButton::getKey).collect(Collectors.toSet()); } - /** - * Set the {@link ExtraKeysView} button colors. - * - * @param buttonTextColor The value for {@link #mButtonTextColor}. - * @param buttonActiveTextColor The value for {@link #mButtonActiveTextColor}. - * @param buttonBackgroundColor The value for {@link #mButtonBackgroundColor}. - * @param buttonActiveBackgroundColor The value for {@link #mButtonActiveBackgroundColor}. + * Get {@link #mButtonTextColor}. */ - public void setButtonColors(int buttonTextColor, int buttonActiveTextColor, int buttonBackgroundColor, int buttonActiveBackgroundColor) { - mButtonTextColor = buttonTextColor; - mButtonActiveTextColor = buttonActiveTextColor; - mButtonBackgroundColor = buttonBackgroundColor; - mButtonActiveBackgroundColor = buttonActiveBackgroundColor; - } - - - /** Get {@link #mButtonTextColor}. */ public int getButtonTextColor() { return mButtonTextColor; } - /** Set {@link #mButtonTextColor}. */ + /** + * Set {@link #mButtonTextColor}. + */ public void setButtonTextColor(int buttonTextColor) { mButtonTextColor = buttonTextColor; } - /** Get {@link #mButtonActiveTextColor}. */ + /** + * Get {@link #mButtonActiveTextColor}. + */ public int getButtonActiveTextColor() { return mButtonActiveTextColor; } - /** Set {@link #mButtonActiveTextColor}. */ + /** + * Set {@link #mButtonActiveTextColor}. + */ public void setButtonActiveTextColor(int buttonActiveTextColor) { mButtonActiveTextColor = buttonActiveTextColor; } - /** Get {@link #mButtonBackgroundColor}. */ + /** + * Get {@link #mButtonBackgroundColor}. + */ public int getButtonBackgroundColor() { return mButtonBackgroundColor; } - /** Set {@link #mButtonBackgroundColor}. */ + /** + * Set {@link #mButtonBackgroundColor}. + */ public void setButtonBackgroundColor(int buttonBackgroundColor) { mButtonBackgroundColor = buttonBackgroundColor; } - /** Get {@link #mButtonActiveBackgroundColor}. */ + /** + * Get {@link #mButtonActiveBackgroundColor}. + */ public int getButtonActiveBackgroundColor() { return mButtonActiveBackgroundColor; } - /** Set {@link #mButtonActiveBackgroundColor}. */ + /** + * Set {@link #mButtonActiveBackgroundColor}. + */ public void setButtonActiveBackgroundColor(int buttonActiveBackgroundColor) { mButtonActiveBackgroundColor = buttonActiveBackgroundColor; } - /** Set {@link #mButtonTextAllCaps}. */ + /** + * Set {@link #mButtonTextAllCaps}. + */ public void setButtonTextAllCaps(boolean buttonTextAllCaps) { mButtonTextAllCaps = buttonTextAllCaps; } - /** Get {@link #mLongPressTimeout}. */ + /** + * Get {@link #mLongPressTimeout}. + */ public int getLongPressTimeout() { return mLongPressTimeout; } - /** Set {@link #mLongPressTimeout}. */ + /** + * Set {@link #mLongPressTimeout}. + */ public void setLongPressTimeout(int longPressDuration) { if (longPressDuration >= MIN_LONG_PRESS_DURATION && longPressDuration <= MAX_LONG_PRESS_DURATION) { mLongPressTimeout = longPressDuration; @@ -347,12 +326,16 @@ public final class ExtraKeysView extends GridLayout { } } - /** Get {@link #mLongPressRepeatDelay}. */ + /** + * Get {@link #mLongPressRepeatDelay}. + */ public int getLongPressRepeatDelay() { return mLongPressRepeatDelay; } - /** Set {@link #mLongPressRepeatDelay}. */ + /** + * Set {@link #mLongPressRepeatDelay}. + */ public void setLongPressRepeatDelay(int longPressRepeatDelay) { if (mLongPressRepeatDelay >= MIN_LONG_PRESS__REPEAT_DELAY && mLongPressRepeatDelay <= MAX_LONG_PRESS__REPEAT_DELAY) { mLongPressRepeatDelay = longPressRepeatDelay; @@ -362,7 +345,9 @@ public final class ExtraKeysView extends GridLayout { } - /** Get the default map that can be used for {@link #mSpecialButtons}. */ + /** + * Get the default map that can be used for {@link #mSpecialButtons}. + */ @NonNull public Map getDefaultSpecialButtons(ExtraKeysView extraKeysView) { return new HashMap() {{ @@ -374,20 +359,19 @@ public final class ExtraKeysView extends GridLayout { } - /** * Reload this instance of {@link ExtraKeysView} with the info passed in {@code extraKeysInfo}. * * @param extraKeysInfo The {@link ExtraKeysInfo} that defines the necessary info for the extra keys. - * @param heightPx The height in pixels of the parent surrounding the {@link ExtraKeysView}. It must - * be a single child. + * @param heightPx The height in pixels of the parent surrounding the {@link ExtraKeysView}. It must + * be a single child. */ @SuppressLint("ClickableViewAccessibility") public void reload(ExtraKeysInfo extraKeysInfo, float heightPx) { if (extraKeysInfo == null) return; - for(SpecialButtonState state : mSpecialButtons.values()) + for (SpecialButtonState state : mSpecialButtons.values()) state.buttons = new ArrayList<>(); removeAllViews(); @@ -469,13 +453,9 @@ public final class ExtraKeysView extends GridLayout { } }); - LayoutParams param = new GridLayout.LayoutParams(); + LayoutParams param = new LayoutParams(); param.width = 0; - if(Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) { - param.height = (int)(heightPx + 0.5); - } else { - param.height = 0; - } + param.height = 0; param.setMargins(0, 0, 0, 0); param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f); param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f); @@ -487,7 +467,6 @@ public final class ExtraKeysView extends GridLayout { } - public void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, MaterialButton button) { if (mExtraKeysViewClient != null) mExtraKeysViewClient.onExtraKeyButtonClick(view, buttonInfo, button); @@ -515,7 +494,6 @@ public final class ExtraKeysView extends GridLayout { } - public void onAnyExtraKeyButtonClick(View view, @NonNull ExtraKeyButton buttonInfo, MaterialButton button) { if (isSpecialButton(buttonInfo)) { if (mLongPressCount > 0) return; @@ -585,7 +563,6 @@ public final class ExtraKeysView extends GridLayout { } - void showPopup(View view, ExtraKeyButton extraButton) { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); @@ -623,8 +600,9 @@ public final class ExtraKeysView extends GridLayout { } - - /** Check whether a {@link ExtraKeyButton} is a {@link SpecialButton}. */ + /** + * Check whether a {@link ExtraKeyButton} is a {@link SpecialButton}. + */ public boolean isSpecialButton(ExtraKeyButton button) { return mSpecialButtonsKeys.contains(button.getKey()); } @@ -632,12 +610,12 @@ public final class ExtraKeysView extends GridLayout { /** * Read whether {@link SpecialButton} registered in {@link #mSpecialButtons} is active or not. * - * @param specialButton The {@link SpecialButton} to read. + * @param specialButton The {@link SpecialButton} to read. * @param autoSetInActive Set to {@code true} if {@link SpecialButtonState#isActive} should be * set {@code false} if button is not locked. * @return Returns {@code null} if button does not exist in {@link #mSpecialButtons}. If button - * exists, then returns {@code true} if the button is created in {@link ExtraKeysView} - * and is active, otherwise {@code false}. + * exists, then returns {@code true} if the button is created in {@link ExtraKeysView} + * and is active, otherwise {@code false}. */ @Nullable public Boolean readSpecialButton(SpecialButton specialButton, boolean autoSetInActive) { @@ -667,7 +645,6 @@ public final class ExtraKeysView extends GridLayout { } - /** * General util function to compute the longest column length in a matrix. */ diff --git a/app/src/main/java/com/termux/app/terminal/io/KeyboardShortcut.java b/app/src/main/java/com/termux/app/extrakeys/KeyboardShortcut.java similarity index 87% rename from app/src/main/java/com/termux/app/terminal/io/KeyboardShortcut.java rename to app/src/main/java/com/termux/app/extrakeys/KeyboardShortcut.java index 00a832dd..ab8e46b8 100644 --- a/app/src/main/java/com/termux/app/terminal/io/KeyboardShortcut.java +++ b/app/src/main/java/com/termux/app/extrakeys/KeyboardShortcut.java @@ -1,4 +1,4 @@ -package com.termux.app.terminal.io; +package com.termux.app.extrakeys; public class KeyboardShortcut { diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/SpecialButton.java b/app/src/main/java/com/termux/app/extrakeys/SpecialButton.java similarity index 97% rename from termux-shared/src/main/java/com/termux/shared/termux/extrakeys/SpecialButton.java rename to app/src/main/java/com/termux/app/extrakeys/SpecialButton.java index 382377b4..a0298070 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/SpecialButton.java +++ b/app/src/main/java/com/termux/app/extrakeys/SpecialButton.java @@ -1,4 +1,4 @@ -package com.termux.shared.termux.extrakeys; +package com.termux.app.extrakeys; import androidx.annotation.NonNull; diff --git a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/SpecialButtonState.java b/app/src/main/java/com/termux/app/extrakeys/SpecialButtonState.java similarity index 97% rename from termux-shared/src/main/java/com/termux/shared/termux/extrakeys/SpecialButtonState.java rename to app/src/main/java/com/termux/app/extrakeys/SpecialButtonState.java index 606dcc4f..a0d8abce 100644 --- a/termux-shared/src/main/java/com/termux/shared/termux/extrakeys/SpecialButtonState.java +++ b/app/src/main/java/com/termux/app/extrakeys/SpecialButtonState.java @@ -1,4 +1,4 @@ -package com.termux.shared.termux.extrakeys; +package com.termux.app.extrakeys; import com.google.android.material.button.MaterialButton; diff --git a/app/src/main/java/com/termux/app/terminal/io/TerminalToolbarViewPager.java b/app/src/main/java/com/termux/app/extrakeys/TerminalToolbarViewPager.java similarity index 88% rename from app/src/main/java/com/termux/app/terminal/io/TerminalToolbarViewPager.java rename to app/src/main/java/com/termux/app/extrakeys/TerminalToolbarViewPager.java index a526570b..cde8d44d 100644 --- a/app/src/main/java/com/termux/app/terminal/io/TerminalToolbarViewPager.java +++ b/app/src/main/java/com/termux/app/extrakeys/TerminalToolbarViewPager.java @@ -1,4 +1,4 @@ -package com.termux.app.terminal.io; +package com.termux.app.extrakeys; import android.view.LayoutInflater; import android.view.View; @@ -11,7 +11,6 @@ import androidx.viewpager.widget.ViewPager; import com.termux.R; import com.termux.app.TermuxActivity; -import com.termux.shared.termux.extrakeys.ExtraKeysView; import com.termux.terminal.TerminalSession; public class TerminalToolbarViewPager { @@ -45,16 +44,10 @@ public class TerminalToolbarViewPager { layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false); ExtraKeysView extraKeysView = (ExtraKeysView) layout; extraKeysView.setExtraKeysViewClient(mActivity.getTermuxTerminalExtraKeys()); - extraKeysView.setButtonTextAllCaps(mActivity.getProperties().shouldExtraKeysTextBeAllCaps()); + extraKeysView.setButtonTextAllCaps(false); mActivity.setExtraKeysView(extraKeysView); extraKeysView.reload(mActivity.getTermuxTerminalExtraKeys().getExtraKeysInfo(), mActivity.getTerminalToolbarDefaultHeight()); - - // apply extra keys fix if enabled in prefs - if (mActivity.getProperties().isUsingFullScreen() && mActivity.getProperties().isUsingFullScreenWorkAround()) { - FullScreenWorkAround.apply(mActivity); - } - } else { layout = inflater.inflate(R.layout.view_terminal_toolbar_text_input, collection, false); final EditText editText = layout.findViewById(R.id.terminal_toolbar_text_input); @@ -87,11 +80,8 @@ public class TerminalToolbarViewPager { public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) { collection.removeView((View) view); } - } - - public static class OnPageChangeListener extends ViewPager.SimpleOnPageChangeListener { final TermuxActivity mActivity; @@ -108,10 +98,11 @@ public class TerminalToolbarViewPager { mActivity.getTerminalView().requestFocus(); } else { final EditText editText = mTerminalToolbarViewPager.findViewById(R.id.terminal_toolbar_text_input); - if (editText != null) editText.requestFocus(); + if (editText != null) { + editText.requestFocus(); + } } } - } } diff --git a/app/src/main/java/com/termux/app/extrakeys/TermuxTerminalExtraKeys.java b/app/src/main/java/com/termux/app/extrakeys/TermuxTerminalExtraKeys.java new file mode 100644 index 00000000..a13309bf --- /dev/null +++ b/app/src/main/java/com/termux/app/extrakeys/TermuxTerminalExtraKeys.java @@ -0,0 +1,179 @@ +package com.termux.app.extrakeys; + +import android.annotation.SuppressLint; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.drawerlayout.widget.DrawerLayout; + +import com.google.android.material.button.MaterialButton; +import com.termux.app.TermuxActivity; +import com.termux.app.TermuxTerminalSessionActivityClient; +import com.termux.app.TermuxTerminalViewClient; +import com.termux.view.TerminalView; + +import org.json.JSONException; + +import java.util.HashMap; +import java.util.Map; + +public class TermuxTerminalExtraKeys implements ExtraKeysView.IExtraKeysView { + public static Map PRIMARY_KEY_CODES_FOR_STRINGS = new HashMap<>() {{ + put("SPACE", KeyEvent.KEYCODE_SPACE); + put("ESC", KeyEvent.KEYCODE_ESCAPE); + put("TAB", KeyEvent.KEYCODE_TAB); + put("HOME", KeyEvent.KEYCODE_MOVE_HOME); + put("END", KeyEvent.KEYCODE_MOVE_END); + put("PGUP", KeyEvent.KEYCODE_PAGE_UP); + put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN); + put("INS", KeyEvent.KEYCODE_INSERT); + put("DEL", KeyEvent.KEYCODE_FORWARD_DEL); + put("BKSP", KeyEvent.KEYCODE_DEL); + put("UP", KeyEvent.KEYCODE_DPAD_UP); + put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT); + put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT); + put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN); + put("ENTER", KeyEvent.KEYCODE_ENTER); + put("F1", KeyEvent.KEYCODE_F1); + put("F2", KeyEvent.KEYCODE_F2); + put("F3", KeyEvent.KEYCODE_F3); + put("F4", KeyEvent.KEYCODE_F4); + put("F5", KeyEvent.KEYCODE_F5); + put("F6", KeyEvent.KEYCODE_F6); + put("F7", KeyEvent.KEYCODE_F7); + put("F8", KeyEvent.KEYCODE_F8); + put("F9", KeyEvent.KEYCODE_F9); + put("F10", KeyEvent.KEYCODE_F10); + put("F11", KeyEvent.KEYCODE_F11); + put("F12", KeyEvent.KEYCODE_F12); + }}; + + private ExtraKeysInfo mExtraKeysInfo; + + final TermuxActivity mActivity; + final TermuxTerminalViewClient mTermuxTerminalViewClient; + final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient; + + private static final String LOG_TAG = "TermuxTerminalExtraKeys"; + + public TermuxTerminalExtraKeys(TermuxActivity activity, @NonNull TerminalView terminalView, + TermuxTerminalViewClient termuxTerminalViewClient, + TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) { + mActivity = activity; + mTermuxTerminalViewClient = termuxTerminalViewClient; + mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient; + + setExtraKeys(); + } + + + /** + * Set the terminal extra keys and style. + */ + private void setExtraKeys() { + mExtraKeysInfo = null; + + try { + // The mMap stores the extra key and style string values while loading properties + // Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and + // {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)} + String extrakeys = mActivity.mProperties.getExtraKeys(); + String extraKeysStyle = mActivity.mProperties.getExtraKeysStyle(); + + ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle); + if (ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !"default".equals(extraKeysStyle)) { + mActivity.showToast("The style \"" + extraKeysStyle + "\" is invalid. Using default style instead.", true); + extraKeysStyle = "default"; + } + + mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES); + } catch (JSONException e) { + mActivity.showToast("Could not load and set the extra keys property from the properties file: " + e, true); + + //try { + mExtraKeysInfo = null; // TODO: new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE, ExtraKeysConstants.CONTROL_CHARS_ALIASES); + //} catch (JSONException e2) { + // We should be able to handle our own defaults! + //throw new RuntimeException(e2); + //} + } + } + + public ExtraKeysInfo getExtraKeysInfo() { + return mExtraKeysInfo; + } + + @SuppressLint("RtlHardcoded") + public void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) { + if ("KEYBOARD".equals(key)) { + if(mTermuxTerminalViewClient != null) + mTermuxTerminalViewClient.onToggleSoftKeyboardRequest(); + } else if ("DRAWER".equals(key)) { + DrawerLayout drawerLayout = mTermuxTerminalViewClient.getActivity().getDrawer(); + if (drawerLayout.isDrawerOpen(Gravity.LEFT)) + drawerLayout.closeDrawer(Gravity.LEFT); + else + drawerLayout.openDrawer(Gravity.LEFT); + } else if ("PASTE".equals(key)) { + if(mTermuxTerminalSessionActivityClient != null) + mTermuxTerminalSessionActivityClient.onPasteTextFromClipboard(null); + } else if ("SCROLL".equals(key)) { + TerminalView terminalView = mTermuxTerminalViewClient.getActivity().getTerminalView(); + if (terminalView != null && terminalView.mEmulator != null) + terminalView.mEmulator.toggleAutoScrollDisabled(); + } else { + if (PRIMARY_KEY_CODES_FOR_STRINGS.containsKey(key)) { + Integer keyCode = PRIMARY_KEY_CODES_FOR_STRINGS.get(key); + if (keyCode == null) return; + int metaState = 0; + if (ctrlDown) metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON; + if (altDown) metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON; + if (shiftDown) metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON; + if (fnDown) metaState |= KeyEvent.META_FUNCTION_ON; + + KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState); + mActivity.getTerminalView().onKeyDown(keyCode, keyEvent); + } else { + // not a control char + key.codePoints().forEach(codePoint -> { + mActivity.getTerminalView().inputCodePoint(TerminalView.KEY_EVENT_SOURCE_VIRTUAL_KEYBOARD, codePoint, ctrlDown, altDown); + }); + } + } + } + + @Override + public void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, MaterialButton button) { + if (buttonInfo.isMacro()) { + String[] keys = buttonInfo.getKey().split(" "); + boolean ctrlDown = false; + boolean altDown = false; + boolean shiftDown = false; + boolean fnDown = false; + for (String key : keys) { + if (SpecialButton.CTRL.getKey().equals(key)) { + ctrlDown = true; + } else if (SpecialButton.ALT.getKey().equals(key)) { + altDown = true; + } else if (SpecialButton.SHIFT.getKey().equals(key)) { + shiftDown = true; + } else if (SpecialButton.FN.getKey().equals(key)) { + fnDown = true; + } else { + onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown); + ctrlDown = false; altDown = false; shiftDown = false; fnDown = false; + } + } + } else { + onTerminalExtraKeyButtonClick(view, buttonInfo.getKey(), false, false, false, false); + } + } + + @Override + public boolean performExtraKeyButtonHapticFeedback(View view, ExtraKeyButton buttonInfo, MaterialButton button) { + return false; + } +} diff --git a/app/src/main/java/com/termux/app/fragments/settings/TermuxAPIPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/TermuxAPIPreferencesFragment.java deleted file mode 100644 index 8f4a1714..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/TermuxAPIPreferencesFragment.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.termux.app.fragments.settings; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; - -@Keep -public class TermuxAPIPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(TermuxAPIPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_api_preferences, rootKey); - } - -} - -class TermuxAPIPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAPIAppSharedPreferences mPreferences; - - private static TermuxAPIPreferencesDataStore mInstance; - - private TermuxAPIPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAPIAppSharedPreferences.build(context, true); - } - - public static synchronized TermuxAPIPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new TermuxAPIPreferencesDataStore(context); - } - return mInstance; - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/TermuxFloatPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/TermuxFloatPreferencesFragment.java deleted file mode 100644 index a6f25c83..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/TermuxFloatPreferencesFragment.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.termux.app.fragments.settings; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; - -@Keep -public class TermuxFloatPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(TermuxFloatPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_float_preferences, rootKey); - } - -} - -class TermuxFloatPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxFloatAppSharedPreferences mPreferences; - - private static TermuxFloatPreferencesDataStore mInstance; - - private TermuxFloatPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxFloatAppSharedPreferences.build(context, true); - } - - public static synchronized TermuxFloatPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new TermuxFloatPreferencesDataStore(context); - } - return mInstance; - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/TermuxPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/TermuxPreferencesFragment.java deleted file mode 100644 index 448b9c7b..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/TermuxPreferencesFragment.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.termux.app.fragments.settings; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; - -@Keep -public class TermuxPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(TermuxPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_preferences, rootKey); - } - -} - -class TermuxPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAppSharedPreferences mPreferences; - - private static TermuxPreferencesDataStore mInstance; - - private TermuxPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAppSharedPreferences.build(context, true); - } - - public static synchronized TermuxPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new TermuxPreferencesDataStore(context); - } - return mInstance; - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/TermuxTaskerPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/TermuxTaskerPreferencesFragment.java deleted file mode 100644 index 1e028807..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/TermuxTaskerPreferencesFragment.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.termux.app.fragments.settings; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; - -@Keep -public class TermuxTaskerPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(TermuxTaskerPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_tasker_preferences, rootKey); - } - -} - -class TermuxTaskerPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxTaskerAppSharedPreferences mPreferences; - - private static TermuxTaskerPreferencesDataStore mInstance; - - private TermuxTaskerPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxTaskerAppSharedPreferences.build(context, true); - } - - public static synchronized TermuxTaskerPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new TermuxTaskerPreferencesDataStore(context); - } - return mInstance; - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/TermuxWidgetPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/TermuxWidgetPreferencesFragment.java deleted file mode 100644 index ded9c701..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/TermuxWidgetPreferencesFragment.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.termux.app.fragments.settings; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences; - -@Keep -public class TermuxWidgetPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(TermuxWidgetPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_widget_preferences, rootKey); - } - -} - -class TermuxWidgetPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxWidgetAppSharedPreferences mPreferences; - - private static TermuxWidgetPreferencesDataStore mInstance; - - private TermuxWidgetPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxWidgetAppSharedPreferences.build(context, true); - } - - public static synchronized TermuxWidgetPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new TermuxWidgetPreferencesDataStore(context); - } - return mInstance; - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux/DebuggingPreferencesFragment.java deleted file mode 100644 index 8afd568f..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/termux/DebuggingPreferencesFragment.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.termux.app.fragments.settings.termux; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.ListPreference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; -import com.termux.shared.logger.Logger; - -@Keep -public class DebuggingPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_debugging_preferences, rootKey); - - configureLoggingPreferences(context); - } - - private void configureLoggingPreferences(@NonNull Context context) { - PreferenceCategory loggingCategory = findPreference("logging"); - if (loggingCategory == null) return; - - ListPreference logLevelListPreference = findPreference("log_level"); - if (logLevelListPreference != null) { - TermuxAppSharedPreferences preferences = TermuxAppSharedPreferences.build(context, true); - if (preferences == null) return; - - setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel()); - loggingCategory.addPreference(logLevelListPreference); - } - } - - public static ListPreference setLogLevelListPreferenceData(ListPreference logLevelListPreference, Context context, int logLevel) { - if (logLevelListPreference == null) - logLevelListPreference = new ListPreference(context); - - CharSequence[] logLevels = Logger.getLogLevelsArray(); - CharSequence[] logLevelLabels = Logger.getLogLevelLabelsArray(context, logLevels, true); - - logLevelListPreference.setEntryValues(logLevels); - logLevelListPreference.setEntries(logLevelLabels); - - logLevelListPreference.setValue(String.valueOf(logLevel)); - logLevelListPreference.setDefaultValue(Logger.DEFAULT_LOG_LEVEL); - - return logLevelListPreference; - } - -} - -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - if (key == null) return null; - - switch (key) { - case "log_level": - return String.valueOf(mPreferences.getLogLevel()); - default: - return null; - } - } - - @Override - public void putString(String key, @Nullable String value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value)); - } - break; - default: - break; - } - } - - - - @Override - public void putBoolean(String key, boolean value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "terminal_view_key_logging_enabled": - mPreferences.setTerminalViewKeyLoggingEnabled(value); - break; - case "plugin_error_notifications_enabled": - mPreferences.setPluginErrorNotificationsEnabled(value); - break; - case "crash_report_notifications_enabled": - mPreferences.setCrashReportNotificationsEnabled(value); - break; - default: - break; - } - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - if (mPreferences == null) return false; - switch (key) { - case "terminal_view_key_logging_enabled": - return mPreferences.isTerminalViewKeyLoggingEnabled(); - case "plugin_error_notifications_enabled": - return mPreferences.arePluginErrorNotificationsEnabled(false); - case "crash_report_notifications_enabled": - return mPreferences.areCrashReportNotificationsEnabled(false); - default: - return false; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalIOPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalIOPreferencesFragment.java deleted file mode 100644 index f8504f43..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalIOPreferencesFragment.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.termux.app.fragments.settings.termux; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; - -@Keep -public class TerminalIOPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(TerminalIOPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_terminal_io_preferences, rootKey); - } - -} - -class TerminalIOPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAppSharedPreferences mPreferences; - - private static TerminalIOPreferencesDataStore mInstance; - - private TerminalIOPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAppSharedPreferences.build(context, true); - } - - public static synchronized TerminalIOPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new TerminalIOPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - public void putBoolean(String key, boolean value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "soft_keyboard_enabled": - mPreferences.setSoftKeyboardEnabled(value); - break; - case "soft_keyboard_enabled_only_if_no_hardware": - mPreferences.setSoftKeyboardEnabledOnlyIfNoHardware(value); - break; - default: - break; - } - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - if (mPreferences == null) return false; - - switch (key) { - case "soft_keyboard_enabled": - return mPreferences.isSoftKeyboardEnabled(); - case "soft_keyboard_enabled_only_if_no_hardware": - return mPreferences.isSoftKeyboardEnabledOnlyIfNoHardware(); - default: - return false; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalViewPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalViewPreferencesFragment.java deleted file mode 100644 index ff033fd3..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/termux/TerminalViewPreferencesFragment.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.termux.app.fragments.settings.termux; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences; - -@Keep -public class TerminalViewPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(TerminalViewPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_terminal_view_preferences, rootKey); - } - -} - -class TerminalViewPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAppSharedPreferences mPreferences; - - private static TerminalViewPreferencesDataStore mInstance; - - private TerminalViewPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAppSharedPreferences.build(context, true); - } - - public static synchronized TerminalViewPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new TerminalViewPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - public void putBoolean(String key, boolean value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "terminal_margin_adjustment": - mPreferences.setTerminalMarginAdjustment(value); - break; - default: - break; - } - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - if (mPreferences == null) return false; - - switch (key) { - case "terminal_margin_adjustment": - return mPreferences.isTerminalMarginAdjustmentEnabled(); - default: - return false; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux_api/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux_api/DebuggingPreferencesFragment.java deleted file mode 100644 index 908c6ebd..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/termux_api/DebuggingPreferencesFragment.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.termux.app.fragments.settings.termux_api; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.ListPreference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; - -@Keep -public class DebuggingPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_api_debugging_preferences, rootKey); - - configureLoggingPreferences(context); - } - - private void configureLoggingPreferences(@NonNull Context context) { - PreferenceCategory loggingCategory = findPreference("logging"); - if (loggingCategory == null) return; - - ListPreference logLevelListPreference = findPreference("log_level"); - if (logLevelListPreference != null) { - TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context, true); - if (preferences == null) return; - - com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment. - setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true)); - loggingCategory.addPreference(logLevelListPreference); - } - } -} - -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxAPIAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxAPIAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - if (key == null) return null; - - switch (key) { - case "log_level": - return String.valueOf(mPreferences.getLogLevel(true)); - default: - return null; - } - } - - @Override - public void putString(String key, @Nullable String value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); - } - break; - default: - break; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux_float/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux_float/DebuggingPreferencesFragment.java deleted file mode 100644 index 1d815aa0..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/termux_float/DebuggingPreferencesFragment.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.termux.app.fragments.settings.termux_float; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.ListPreference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxFloatAppSharedPreferences; - -@Keep -public class DebuggingPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_float_debugging_preferences, rootKey); - - configureLoggingPreferences(context); - } - - private void configureLoggingPreferences(@NonNull Context context) { - PreferenceCategory loggingCategory = findPreference("logging"); - if (loggingCategory == null) return; - - ListPreference logLevelListPreference = findPreference("log_level"); - if (logLevelListPreference != null) { - TermuxFloatAppSharedPreferences preferences = TermuxFloatAppSharedPreferences.build(context, true); - if (preferences == null) return; - - com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment. - setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true)); - loggingCategory.addPreference(logLevelListPreference); - } - } -} - -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxFloatAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxFloatAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - if (key == null) return null; - - switch (key) { - case "log_level": - return String.valueOf(mPreferences.getLogLevel(true)); - default: - return null; - } - } - - @Override - public void putString(String key, @Nullable String value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); - } - break; - default: - break; - } - } - - @Override - public void putBoolean(String key, boolean value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "terminal_view_key_logging_enabled": - mPreferences.setTerminalViewKeyLoggingEnabled(value, true); - break; - default: - break; - } - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - if (mPreferences == null) return false; - switch (key) { - case "terminal_view_key_logging_enabled": - return mPreferences.isTerminalViewKeyLoggingEnabled(true); - default: - return false; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux_tasker/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux_tasker/DebuggingPreferencesFragment.java deleted file mode 100644 index 7e291839..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/termux_tasker/DebuggingPreferencesFragment.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.termux.app.fragments.settings.termux_tasker; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.ListPreference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxTaskerAppSharedPreferences; - -@Keep -public class DebuggingPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_tasker_debugging_preferences, rootKey); - - configureLoggingPreferences(context); - } - - private void configureLoggingPreferences(@NonNull Context context) { - PreferenceCategory loggingCategory = findPreference("logging"); - if (loggingCategory == null) return; - - ListPreference logLevelListPreference = findPreference("log_level"); - if (logLevelListPreference != null) { - TermuxTaskerAppSharedPreferences preferences = TermuxTaskerAppSharedPreferences.build(context, true); - if (preferences == null) return; - - com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment. - setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true)); - loggingCategory.addPreference(logLevelListPreference); - } - } -} - -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxTaskerAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxTaskerAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - if (key == null) return null; - - switch (key) { - case "log_level": - return String.valueOf(mPreferences.getLogLevel(true)); - default: - return null; - } - } - - @Override - public void putString(String key, @Nullable String value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); - } - break; - default: - break; - } - } - -} diff --git a/app/src/main/java/com/termux/app/fragments/settings/termux_widget/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/termux_widget/DebuggingPreferencesFragment.java deleted file mode 100644 index 50823d54..00000000 --- a/app/src/main/java/com/termux/app/fragments/settings/termux_widget/DebuggingPreferencesFragment.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.termux.app.fragments.settings.termux_widget; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.ListPreference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceDataStore; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; - -import com.termux.R; -import com.termux.shared.termux.settings.preferences.TermuxWidgetAppSharedPreferences; - -@Keep -public class DebuggingPreferencesFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - Context context = getContext(); - if (context == null) return; - - PreferenceManager preferenceManager = getPreferenceManager(); - preferenceManager.setPreferenceDataStore(DebuggingPreferencesDataStore.getInstance(context)); - - setPreferencesFromResource(R.xml.termux_widget_debugging_preferences, rootKey); - - configureLoggingPreferences(context); - } - - private void configureLoggingPreferences(@NonNull Context context) { - PreferenceCategory loggingCategory = findPreference("logging"); - if (loggingCategory == null) return; - - ListPreference logLevelListPreference = findPreference("log_level"); - if (logLevelListPreference != null) { - TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, true); - if (preferences == null) return; - - com.termux.app.fragments.settings.termux.DebuggingPreferencesFragment. - setLogLevelListPreferenceData(logLevelListPreference, context, preferences.getLogLevel(true)); - loggingCategory.addPreference(logLevelListPreference); - } - } -} - -class DebuggingPreferencesDataStore extends PreferenceDataStore { - - private final Context mContext; - private final TermuxWidgetAppSharedPreferences mPreferences; - - private static DebuggingPreferencesDataStore mInstance; - - private DebuggingPreferencesDataStore(Context context) { - mContext = context; - mPreferences = TermuxWidgetAppSharedPreferences.build(context, true); - } - - public static synchronized DebuggingPreferencesDataStore getInstance(Context context) { - if (mInstance == null) { - mInstance = new DebuggingPreferencesDataStore(context); - } - return mInstance; - } - - - - @Override - @Nullable - public String getString(String key, @Nullable String defValue) { - if (mPreferences == null) return null; - if (key == null) return null; - - switch (key) { - case "log_level": - return String.valueOf(mPreferences.getLogLevel(true)); - default: - return null; - } - } - - @Override - public void putString(String key, @Nullable String value) { - if (mPreferences == null) return; - if (key == null) return; - - switch (key) { - case "log_level": - if (value != null) { - mPreferences.setLogLevel(mContext, Integer.parseInt(value), true); - } - break; - default: - break; - } - } - -} diff --git a/app/src/main/java/com/termux/app/models/UserAction.java b/app/src/main/java/com/termux/app/models/UserAction.java deleted file mode 100644 index 1e82255e..00000000 --- a/app/src/main/java/com/termux/app/models/UserAction.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.termux.app.models; - -public enum UserAction { - - ABOUT("about"), - REPORT_ISSUE_FROM_TRANSCRIPT("report issue from transcript"); - - private final String name; - - UserAction(final String name) { - this.name = name; - } - - public String getName() { - return name; - } - -} diff --git a/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java b/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java deleted file mode 100644 index 7e8b0e9d..00000000 --- a/app/src/main/java/com/termux/app/terminal/TermuxActivityRootView.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.termux.app.terminal; - -import android.content.Context; -import android.graphics.Rect; -import android.inputmethodservice.InputMethodService; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.view.WindowInsets; -import android.view.inputmethod.EditorInfo; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import androidx.annotation.Nullable; -import androidx.core.view.WindowInsetsCompat; - -import com.termux.app.TermuxActivity; -import com.termux.shared.logger.Logger; -import com.termux.shared.view.ViewUtils; - - -/** - * The {@link TermuxActivity} relies on {@link android.view.WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE)} - * set by {@link TermuxTerminalViewClient#setSoftKeyboardState(boolean, boolean)} to automatically - * resize the view and push the terminal up when soft keyboard is opened. However, this does not - * always work properly. When `enforce-char-based-input=true` is set in `termux.properties` - * and {@link com.termux.view.TerminalView#onCreateInputConnection(EditorInfo)} sets the inputType - * to `InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS` - * instead of the default `InputType.TYPE_NULL` for termux, some keyboards may still show suggestions. - * Gboard does too, but only when text is copied and clipboard suggestions **and** number keys row - * toggles are enabled in its settings. When number keys row toggle is not enabled, Gboard will still - * show the row but will switch it with suggestions if needed. If its enabled, then number keys row - * is always shown and suggestions are shown in an additional row on top of it. This additional row is likely - * part of the candidates view returned by the keyboard app in {@link InputMethodService#onCreateCandidatesView()}. - * - * With the above configuration, the additional clipboard suggestions row partially covers the - * extra keys/terminal. Reopening the keyboard/activity does not fix the issue. This is either a bug - * in the Android OS where it does not consider the candidate's view height in its calculation to push - * up the view or because Gboard does not include the candidate's view height in the height reported - * to android that should be used, hence causing an overlap. - * - * Gboard logs the following entry to `logcat` when its opened with or without the suggestions bar showing: - * I/KeyboardViewUtil: KeyboardViewUtil.calculateMaxKeyboardBodyHeight():62 leave 500 height for app when screen height:2392, header height:176 and isFullscreenMode:false, so the max keyboard body height is:1716 - * where `keyboard_height = screen_height - height_for_app - header_height` (62 is a hardcoded value in Gboard source code and may be a version number) - * So this may in fact be due to Gboard but https://stackoverflow.com/questions/57567272 suggests - * otherwise. Another similar report https://stackoverflow.com/questions/66761661. - * Also check https://github.com/termux/termux-app/issues/1539. - * - * This overlap may happen even without `enforce-char-based-input=true` for keyboards with extended layouts - * like number row, etc. - * - * To fix these issues, `activity_termux.xml` has the constant 1sp transparent - * `activity_termux_bottom_space_view` View at the bottom. This will appear as a line matching the - * activity theme. When {@link TermuxActivity} {@link ViewTreeObserver.OnGlobalLayoutListener} is - * called when any of the sub view layouts change, like keyboard opening/closing keyboard, - * extra keys/input view switched, etc, we check if the bottom space view is visible or not. - * If its not, then we add a margin to the bottom of the root view, so that the keyboard does not - * overlap the extra keys/terminal, since the margin will push up the view. By default the margin - * added is equal to the height of the hidden part of extra keys/terminal. For Gboard's case, the - * hidden part equals the `header_height`. The updates to margins may cause a jitter in some cases - * when the view is redrawn if the margin is incorrect, but logic has been implemented to avoid that. - */ -public class TermuxActivityRootView extends LinearLayout implements ViewTreeObserver.OnGlobalLayoutListener { - - public TermuxActivity mActivity; - public Integer marginBottom; - public Integer lastMarginBottom; - public long lastMarginBottomTime; - public long lastMarginBottomExtraTime; - - /** Log root view events. */ - private boolean ROOT_VIEW_LOGGING_ENABLED = false; - - private static final String LOG_TAG = "TermuxActivityRootView"; - - private static int mStatusBarHeight; - - public TermuxActivityRootView(Context context) { - super(context); - } - - public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public TermuxActivityRootView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public void setActivity(TermuxActivity activity) { - mActivity = activity; - } - - /** - * Sets whether root view logging is enabled or not. - * - * @param value The boolean value that defines the state. - */ - public void setIsRootViewLoggingEnabled(boolean value) { - ROOT_VIEW_LOGGING_ENABLED = value; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - if (marginBottom != null) { - if (ROOT_VIEW_LOGGING_ENABLED) - Logger.logVerbose(LOG_TAG, "onMeasure: Setting bottom margin to " + marginBottom); - ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams(); - params.setMargins(0, 0, 0, marginBottom); - setLayoutParams(params); - marginBottom = null; - requestLayout(); - } - } - - @Override - public void onGlobalLayout() { - if (mActivity == null || !mActivity.isVisible()) return; - - View bottomSpaceView = mActivity.getTermuxActivityBottomSpaceView(); - if (bottomSpaceView == null) return; - - boolean root_view_logging_enabled = ROOT_VIEW_LOGGING_ENABLED; - - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, ":\nonGlobalLayout:"); - - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) getLayoutParams(); - - // Get the position Rects of the bottom space view and the main window holding it - Rect[] windowAndViewRects = ViewUtils.getWindowAndViewRects(bottomSpaceView, mStatusBarHeight); - if (windowAndViewRects == null) - return; - - Rect windowAvailableRect = windowAndViewRects[0]; - Rect bottomSpaceViewRect = windowAndViewRects[1]; - - // If the bottomSpaceViewRect is inside the windowAvailableRect, then it must be completely visible - //boolean isVisible = windowAvailableRect.contains(bottomSpaceViewRect); // rect.right comparison often fails in landscape - boolean isVisible = ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect); - boolean isVisibleBecauseMargin = (windowAvailableRect.bottom == bottomSpaceViewRect.bottom) && params.bottomMargin > 0; - boolean isVisibleBecauseExtraMargin = ((bottomSpaceViewRect.bottom - windowAvailableRect.bottom) < 0); - - if (root_view_logging_enabled) { - Logger.logVerbose(LOG_TAG, "windowAvailableRect " + ViewUtils.toRectString(windowAvailableRect) + ", bottomSpaceViewRect " + ViewUtils.toRectString(bottomSpaceViewRect)); - Logger.logVerbose(LOG_TAG, "windowAvailableRect.bottom " + windowAvailableRect.bottom + - ", bottomSpaceViewRect.bottom " +bottomSpaceViewRect.bottom + - ", diff " + (bottomSpaceViewRect.bottom - windowAvailableRect.bottom) + ", bottom " + params.bottomMargin + - ", isVisible " + windowAvailableRect.contains(bottomSpaceViewRect) + ", isRectAbove " + ViewUtils.isRectAbove(windowAvailableRect, bottomSpaceViewRect) + - ", isVisibleBecauseMargin " + isVisibleBecauseMargin + ", isVisibleBecauseExtraMargin " + isVisibleBecauseExtraMargin); - } - - // If the bottomSpaceViewRect is visible, then remove the margin if needed - if (isVisible) { - // If visible because of margin, i.e the bottom of bottomSpaceViewRect equals that of windowAvailableRect - // and a margin has been added - // Necessary so that we don't get stuck in an infinite loop since setting margin - // will call OnGlobalLayoutListener again and next time bottom space view - // will be visible and margin will be set to 0, which again will call - // OnGlobalLayoutListener... - // Calling addTermuxActivityRootViewGlobalLayoutListener with a delay fails to - // set appropriate margins when views are changed quickly since some changes - // may be missed. - if (isVisibleBecauseMargin) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Visible due to margin"); - - // Once the view has been redrawn with new margin, we set margin back to 0 so that - // when next time onMeasure() is called, margin 0 is used. This is necessary for - // cases when view has been redrawn with new margin because bottom space view was - // hidden by keyboard and then view was redrawn again due to layout change (like - // keyboard symbol view is switched to), android will add margin below its new position - // if its greater than 0, which was already above the keyboard creating x2x margin. - // Adding time check since moving split screen divider in landscape causes jitter - // and prevents some infinite loops - if ((System.currentTimeMillis() - lastMarginBottomTime) > 40) { - lastMarginBottomTime = System.currentTimeMillis(); - marginBottom = 0; - } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Ignoring restoring marginBottom to 0 since called to quickly"); - } - - return; - } - - boolean setMargin = params.bottomMargin != 0; - - // If visible because of extra margin, i.e the bottom of bottomSpaceViewRect is above that of windowAvailableRect - // onGlobalLayout: windowAvailableRect 1408, bottomSpaceViewRect 1232, diff -176, bottom 0, isVisible true, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false - // onGlobalLayout: Bottom margin already equals 0 - if (isVisibleBecauseExtraMargin) { - // Adding time check since prevents infinite loops, like in landscape mode in freeform mode in Taskbar - if ((System.currentTimeMillis() - lastMarginBottomExtraTime) > 40) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Resetting margin since visible due to extra margin"); - lastMarginBottomExtraTime = System.currentTimeMillis(); - // lastMarginBottom must be invalid. May also happen when keyboards are changed. - lastMarginBottom = null; - setMargin = true; - } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Ignoring resetting margin since visible due to extra margin since called to quickly"); - } - } - - if (setMargin) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Setting bottom margin to 0"); - params.setMargins(0, 0, 0, 0); - setLayoutParams(params); - } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Bottom margin already equals 0"); - // This is done so that when next time onMeasure() is called, lastMarginBottom is used. - // This is done since we **expect** the keyboard to have same dimensions next time layout - // changes, so best set margin while view is drawn the first time, otherwise it will - // cause a jitter when OnGlobalLayoutListener is called with margin 0 and it sets the - // likely same lastMarginBottom again and requesting a redraw. Hopefully, this logic - // works fine for all cases. - marginBottom = lastMarginBottom; - } - } - // ELse find the part of the extra keys/terminal that is hidden and add a margin accordingly - else { - int pxHidden = bottomSpaceViewRect.bottom - windowAvailableRect.bottom; - - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "pxHidden " + pxHidden + ", bottom " + params.bottomMargin); - - boolean setMargin = params.bottomMargin != pxHidden; - - // If invisible despite margin, i.e a margin was added, but the bottom of bottomSpaceViewRect - // is still below that of windowAvailableRect, this will trigger OnGlobalLayoutListener - // again, so that margins are set properly. May happen when toolbar/extra keys is disabled - // and enabled from left drawer, just like case for isVisibleBecauseExtraMargin. - // onMeasure: Setting bottom margin to 176 - // onGlobalLayout: windowAvailableRect 1232, bottomSpaceViewRect 1408, diff 176, bottom 176, isVisible false, isVisibleBecauseMargin false, isVisibleBecauseExtraMargin false - // onGlobalLayout: Bottom margin already equals 176 - if (pxHidden > 0 && params.bottomMargin > 0) { - if (pxHidden != params.bottomMargin) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since not visible due to wrong margin"); - pxHidden = 0; - } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Force setting margin since not visible despite required margin"); - } - setMargin = true; - } - - if (pxHidden < 0) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Force setting margin to 0 since new margin is negative"); - pxHidden = 0; - } - - - if (setMargin) { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Setting bottom margin to " + pxHidden); - params.setMargins(0, 0, 0, pxHidden); - setLayoutParams(params); - lastMarginBottom = pxHidden; - } else { - if (root_view_logging_enabled) - Logger.logVerbose(LOG_TAG, "Bottom margin already equals " + pxHidden); - } - } - } - - public static class WindowInsetsListener implements View.OnApplyWindowInsetsListener { - @Override - public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { - mStatusBarHeight = WindowInsetsCompat.toWindowInsetsCompat(insets).getInsets(WindowInsetsCompat.Type.statusBars()).top; - // Let view window handle insets however it wants - return v.onApplyWindowInsets(insets); - } - } - -} diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionServiceClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionServiceClient.java deleted file mode 100644 index e943bb3a..00000000 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalSessionServiceClient.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.termux.app.terminal; - -import android.app.Service; - -import androidx.annotation.NonNull; - -import com.termux.app.TermuxService; -import com.termux.shared.termux.shell.command.runner.terminal.TermuxSession; -import com.termux.shared.termux.terminal.TermuxTerminalSessionClientBase; -import com.termux.terminal.TerminalSession; -import com.termux.terminal.TerminalSessionClient; - -/** The {@link TerminalSessionClient} implementation that may require a {@link Service} for its interface methods. */ -public class TermuxTerminalSessionServiceClient extends TermuxTerminalSessionClientBase { - - private static final String LOG_TAG = "TermuxTerminalSessionServiceClient"; - - private final TermuxService mService; - - public TermuxTerminalSessionServiceClient(TermuxService service) { - this.mService = service; - } - - @Override - public void setTerminalShellPid(@NonNull TerminalSession terminalSession, int pid) { - TermuxSession termuxSession = mService.getTermuxSessionForTerminalSession(terminalSession); - if (termuxSession != null) - termuxSession.getExecutionCommand().mPid = pid; - } - -} diff --git a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java deleted file mode 100644 index a3d09d3d..00000000 --- a/app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java +++ /dev/null @@ -1,802 +0,0 @@ -package com.termux.app.terminal; - -import android.annotation.SuppressLint; -import android.app.AlertDialog; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.media.AudioManager; -import android.os.Environment; -import android.text.TextUtils; -import android.view.Gravity; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.Toast; - -import com.termux.R; -import com.termux.app.TermuxActivity; -import com.termux.shared.file.FileUtils; -import com.termux.shared.interact.MessageDialogUtils; -import com.termux.shared.interact.ShareUtils; -import com.termux.shared.shell.ShellUtils; -import com.termux.shared.termux.TermuxBootstrap; -import com.termux.shared.termux.terminal.TermuxTerminalViewClientBase; -import com.termux.shared.termux.extrakeys.SpecialButton; -import com.termux.shared.android.AndroidUtils; -import com.termux.shared.termux.TermuxConstants; -import com.termux.shared.activities.ReportActivity; -import com.termux.shared.models.ReportInfo; -import com.termux.app.models.UserAction; -import com.termux.app.terminal.io.KeyboardShortcut; -import com.termux.shared.termux.settings.properties.TermuxPropertyConstants; -import com.termux.shared.data.DataUtils; -import com.termux.shared.logger.Logger; -import com.termux.shared.markdown.MarkdownUtils; -import com.termux.shared.termux.TermuxUtils; -import com.termux.shared.termux.data.TermuxUrlUtils; -import com.termux.shared.view.KeyboardUtils; -import com.termux.shared.view.ViewUtils; -import com.termux.terminal.KeyHandler; -import com.termux.terminal.TerminalEmulator; -import com.termux.terminal.TerminalSession; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; - -import androidx.drawerlayout.widget.DrawerLayout; - -public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase { - - final TermuxActivity mActivity; - - final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient; - - /** Keeping track of the special keys acting as Ctrl and Fn for the soft keyboard and other hardware keys. */ - boolean mVirtualControlKeyDown, mVirtualFnKeyDown; - - private Runnable mShowSoftKeyboardRunnable; - - private boolean mShowSoftKeyboardIgnoreOnce; - private boolean mShowSoftKeyboardWithDelayOnce; - - private boolean mTerminalCursorBlinkerStateAlreadySet; - - private List mSessionShortcuts; - - private static final String LOG_TAG = "TermuxTerminalViewClient"; - - public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) { - this.mActivity = activity; - this.mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient; - } - - public TermuxActivity getActivity() { - return mActivity; - } - - /** - * Should be called when mActivity.onCreate() is called - */ - public void onCreate() { - onReloadProperties(); - - mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize()); - mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn()); - } - - /** - * Should be called when mActivity.onStart() is called - */ - public void onStart() { - // Set {@link TerminalView#TERMINAL_VIEW_KEY_LOGGING_ENABLED} value - // Also required if user changed the preference from {@link TermuxSettings} activity and returns - boolean isTerminalViewKeyLoggingEnabled = mActivity.getPreferences().isTerminalViewKeyLoggingEnabled(); - mActivity.getTerminalView().setIsTerminalViewKeyLoggingEnabled(isTerminalViewKeyLoggingEnabled); - - // Piggyback on the terminal view key logging toggle for now, should add a separate toggle in future - mActivity.getTermuxActivityRootView().setIsRootViewLoggingEnabled(isTerminalViewKeyLoggingEnabled); - ViewUtils.setIsViewUtilsLoggingEnabled(isTerminalViewKeyLoggingEnabled); - } - - /** - * Should be called when mActivity.onResume() is called - */ - public void onResume() { - // Show the soft keyboard if required - setSoftKeyboardState(true, mActivity.isActivityRecreated()); - - mTerminalCursorBlinkerStateAlreadySet = false; - - if (mActivity.getTerminalView().mEmulator != null) { - // Start terminal cursor blinking if enabled - // If emulator is already set, then start blinker now, otherwise wait for onEmulatorSet() - // event to start it. This is needed since onEmulatorSet() may not be called after - // TermuxActivity is started after device display timeout with double tap and not power button. - setTerminalCursorBlinkerState(true); - mTerminalCursorBlinkerStateAlreadySet = true; - } - } - - /** - * Should be called when mActivity.onStop() is called - */ - public void onStop() { - // Stop terminal cursor blinking if enabled - setTerminalCursorBlinkerState(false); - } - - /** - * Should be called when mActivity.reloadProperties() is called - */ - public void onReloadProperties() { - setSessionShortcuts(); - } - - /** - * Should be called when mActivity.reloadActivityStyling() is called - */ - public void onReloadActivityStyling() { - // Show the soft keyboard if required - setSoftKeyboardState(false, true); - - // Start terminal cursor blinking if enabled - setTerminalCursorBlinkerState(true); - } - - /** - * Should be called when {@link com.termux.view.TerminalView#mEmulator} is set - */ - @Override - public void onEmulatorSet() { - if (!mTerminalCursorBlinkerStateAlreadySet) { - // Start terminal cursor blinking if enabled - // We need to wait for the first session to be attached that's set in - // TermuxActivity.onServiceConnected() and then the multiple calls to TerminalView.updateSize() - // where the final one eventually sets the mEmulator when width/height is not 0. Otherwise - // blinker will not start again if TermuxActivity is started again after exiting it with - // double back press. Check TerminalView.setTerminalCursorBlinkerState(). - setTerminalCursorBlinkerState(true); - mTerminalCursorBlinkerStateAlreadySet = true; - } - } - - - - @Override - public float onScale(float scale) { - if (scale < 0.9f || scale > 1.1f) { - boolean increase = scale > 1.f; - changeFontSize(increase); - return 1.0f; - } - return scale; - } - - - - @Override - public void onSingleTapUp(MotionEvent e) { - TerminalEmulator term = mActivity.getCurrentSession().getEmulator(); - - if (mActivity.getProperties().shouldOpenTerminalTranscriptURLOnClick()) { - int[] columnAndRow = mActivity.getTerminalView().getColumnAndRow(e, true); - String wordAtTap = term.getScreen().getWordAtLocation(columnAndRow[0], columnAndRow[1]); - LinkedHashSet urlSet = TermuxUrlUtils.extractUrls(wordAtTap); - - if (!urlSet.isEmpty()) { - String url = (String) urlSet.iterator().next(); - ShareUtils.openUrl(mActivity, url); - return; - } - } - - if (!term.isMouseTrackingActive() && !e.isFromSource(InputDevice.SOURCE_MOUSE)) { - if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) - KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView()); - else - Logger.logVerbose(LOG_TAG, "Not showing soft keyboard onSingleTapUp since its disabled"); - } - } - - @Override - public boolean shouldBackButtonBeMappedToEscape() { - return mActivity.getProperties().isBackKeyTheEscapeKey(); - } - - @Override - public boolean shouldEnforceCharBasedInput() { - return mActivity.getProperties().isEnforcingCharBasedInput(); - } - - @Override - public boolean shouldUseCtrlSpaceWorkaround() { - return mActivity.getProperties().isUsingCtrlSpaceWorkaround(); - } - - @Override - public boolean isTerminalViewSelected() { - return mActivity.getTerminalToolbarViewPager() == null || mActivity.isTerminalViewSelected() || mActivity.getTerminalView().hasFocus(); - } - - - - @Override - public void copyModeChanged(boolean copyMode) { - // Disable drawer while copying. - mActivity.getDrawer().setDrawerLockMode(copyMode ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED : DrawerLayout.LOCK_MODE_UNLOCKED); - } - - - - @SuppressLint("RtlHardcoded") - @Override - public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) { - if (handleVirtualKeys(keyCode, e, true)) return true; - - if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) { - mTermuxTerminalSessionActivityClient.removeFinishedSession(currentSession); - return true; - } else if (!mActivity.getProperties().areHardwareKeyboardShortcutsDisabled() && - e.isCtrlPressed() && e.isAltPressed()) { - // Get the unmodified code point: - int unicodeChar = e.getUnicodeChar(0); - - if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) { - mTermuxTerminalSessionActivityClient.switchToSession(true); - } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) { - mTermuxTerminalSessionActivityClient.switchToSession(false); - } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { - mActivity.getDrawer().openDrawer(Gravity.LEFT); - } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) { - mActivity.getDrawer().closeDrawers(); - } else if (unicodeChar == 'k'/* keyboard */) { - onToggleSoftKeyboardRequest(); - } else if (unicodeChar == 'm'/* menu */) { - mActivity.getTerminalView().showContextMenu(); - } else if (unicodeChar == 'r'/* rename */) { - mTermuxTerminalSessionActivityClient.renameSession(currentSession); - } else if (unicodeChar == 'c'/* create */) { - mTermuxTerminalSessionActivityClient.addNewSession(false, null); - } else if (unicodeChar == 'u' /* urls */) { - showUrlSelection(); - } else if (unicodeChar == 'v') { - doPaste(); - } else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') { - // We also check for the shifted char here since shift may be required to produce '+', - // see https://github.com/termux/termux-api/issues/2 - changeFontSize(true); - } else if (unicodeChar == '-') { - changeFontSize(false); - } else if (unicodeChar >= '1' && unicodeChar <= '9') { - int index = unicodeChar - '1'; - mTermuxTerminalSessionActivityClient.switchToSession(index); - } - return true; - } - - return false; - - } - - - - @Override - public boolean onKeyUp(int keyCode, KeyEvent e) { - // If emulator is not set, like if bootstrap installation failed and user dismissed the error - // dialog, then just exit the activity, otherwise they will be stuck in a broken state. - if (keyCode == KeyEvent.KEYCODE_BACK && mActivity.getTerminalView().mEmulator == null) { - mActivity.finishActivityIfNotFinishing(); - return true; - } - - return handleVirtualKeys(keyCode, e, false); - } - - /** Handle dedicated volume buttons as virtual keys if applicable. */ - private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) { - InputDevice inputDevice = event.getDevice(); - if (mActivity.getProperties().areVirtualVolumeKeysDisabled()) { - return false; - } else if (inputDevice != null && inputDevice.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { - // Do not steal dedicated buttons from a full external keyboard. - return false; - } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { - mVirtualControlKeyDown = down; - return true; - } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { - mVirtualFnKeyDown = down; - return true; - } - return false; - } - - - - @Override - public boolean readControlKey() { - return readExtraKeysSpecialButton(SpecialButton.CTRL) || mVirtualControlKeyDown; - } - - @Override - public boolean readAltKey() { - return readExtraKeysSpecialButton(SpecialButton.ALT); - } - - @Override - public boolean readShiftKey() { - return readExtraKeysSpecialButton(SpecialButton.SHIFT); - } - - @Override - public boolean readFnKey() { - return readExtraKeysSpecialButton(SpecialButton.FN); - } - - public boolean readExtraKeysSpecialButton(SpecialButton specialButton) { - if (mActivity.getExtraKeysView() == null) return false; - Boolean state = mActivity.getExtraKeysView().readSpecialButton(specialButton, true); - if (state == null) { - Logger.logError(LOG_TAG,"Failed to read an unregistered " + specialButton + " special button value from extra keys."); - return false; - } - return state; - } - - @Override - public boolean onLongPress(MotionEvent event) { - return false; - } - - - - @Override - public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) { - if (mVirtualFnKeyDown) { - int resultingKeyCode = -1; - int resultingCodePoint = -1; - boolean altDown = false; - int lowerCase = Character.toLowerCase(codePoint); - switch (lowerCase) { - // Arrow keys. - case 'w': - resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP; - break; - case 'a': - resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT; - break; - case 's': - resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN; - break; - case 'd': - resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT; - break; - - // Page up and down. - case 'p': - resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP; - break; - case 'n': - resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN; - break; - - // Some special keys: - case 't': - resultingKeyCode = KeyEvent.KEYCODE_TAB; - break; - case 'i': - resultingKeyCode = KeyEvent.KEYCODE_INSERT; - break; - case 'h': - resultingCodePoint = '~'; - break; - - // Special characters to input. - case 'u': - resultingCodePoint = '_'; - break; - case 'l': - resultingCodePoint = '|'; - break; - - // Function keys. - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - resultingKeyCode = (codePoint - '1') + KeyEvent.KEYCODE_F1; - break; - case '0': - resultingKeyCode = KeyEvent.KEYCODE_F10; - break; - - // Other special keys. - case 'e': - resultingCodePoint = /*Escape*/ 27; - break; - case '.': - resultingCodePoint = /*^.*/ 28; - break; - - case 'b': // alt+b, jumping backward in readline. - case 'f': // alf+f, jumping forward in readline. - case 'x': // alt+x, common in emacs. - resultingCodePoint = lowerCase; - altDown = true; - break; - - // Volume control. - case 'v': - resultingCodePoint = -1; - AudioManager audio = (AudioManager) mActivity.getSystemService(Context.AUDIO_SERVICE); - audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI); - break; - - // Writing mode: - case 'q': - case 'k': - mActivity.toggleTerminalToolbar(); - mVirtualFnKeyDown=false; // force disable fn key down to restore keyboard input into terminal view, fixes termux/termux-app#1420 - break; - } - - if (resultingKeyCode != -1) { - TerminalEmulator term = session.getEmulator(); - session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode())); - } else if (resultingCodePoint != -1) { - session.writeCodePoint(altDown, resultingCodePoint); - } - return true; - } else if (ctrlDown) { - if (codePoint == 106 /* Ctrl+j or \n */ && !session.isRunning()) { - mTermuxTerminalSessionActivityClient.removeFinishedSession(session); - return true; - } - - List shortcuts = mSessionShortcuts; - if (shortcuts != null && !shortcuts.isEmpty()) { - int codePointLowerCase = Character.toLowerCase(codePoint); - for (int i = shortcuts.size() - 1; i >= 0; i--) { - KeyboardShortcut shortcut = shortcuts.get(i); - if (codePointLowerCase == shortcut.codePoint) { - switch (shortcut.shortcutAction) { - case TermuxPropertyConstants.ACTION_SHORTCUT_CREATE_SESSION: - mTermuxTerminalSessionActivityClient.addNewSession(false, null); - return true; - case TermuxPropertyConstants.ACTION_SHORTCUT_NEXT_SESSION: - mTermuxTerminalSessionActivityClient.switchToSession(true); - return true; - case TermuxPropertyConstants.ACTION_SHORTCUT_PREVIOUS_SESSION: - mTermuxTerminalSessionActivityClient.switchToSession(false); - return true; - case TermuxPropertyConstants.ACTION_SHORTCUT_RENAME_SESSION: - mTermuxTerminalSessionActivityClient.renameSession(mActivity.getCurrentSession()); - return true; - } - } - } - } - } - - return false; - } - - /** - * Set the terminal sessions shortcuts. - */ - private void setSessionShortcuts() { - mSessionShortcuts = new ArrayList<>(); - - // The {@link TermuxPropertyConstants#MAP_SESSION_SHORTCUTS} stores the session shortcut key and action pair - for (Map.Entry entry : TermuxPropertyConstants.MAP_SESSION_SHORTCUTS.entrySet()) { - // The mMap stores the code points for the session shortcuts while loading properties - Integer codePoint = (Integer) mActivity.getProperties().getInternalPropertyValue(entry.getKey(), true); - // If codePoint is null, then session shortcut did not exist in properties or was invalid - // as parsed by {@link #getCodePointForSessionShortcuts(String,String)} - // If codePoint is not null, then get the action for the MAP_SESSION_SHORTCUTS key and - // add the code point to sessionShortcuts - if (codePoint != null) - mSessionShortcuts.add(new KeyboardShortcut(codePoint, entry.getValue())); - } - } - - - - - - public void changeFontSize(boolean increase) { - mActivity.getPreferences().changeFontSize(increase); - mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize()); - } - - - - /** - * Called when user requests the soft keyboard to be toggled via "KEYBOARD" toggle button in - * drawer or extra keys, or with ctrl+alt+k hardware keyboard shortcut. - */ - public void onToggleSoftKeyboardRequest() { - // If soft keyboard toggle behaviour is enable/disabled - if (mActivity.getProperties().shouldEnableDisableSoftKeyboardOnToggle()) { - // If soft keyboard is visible - if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) { - Logger.logVerbose(LOG_TAG, "Disabling soft keyboard on toggle"); - mActivity.getPreferences().setSoftKeyboardEnabled(false); - KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView()); - } else { - // Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after - // switching back from another app if keyboard was previously disabled by user. - // Also request focus, since it wouldn't have been requested at startup by - // setSoftKeyboardState if keyboard was disabled. #2112 - Logger.logVerbose(LOG_TAG, "Enabling soft keyboard on toggle"); - mActivity.getPreferences().setSoftKeyboardEnabled(true); - KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity); - if(mShowSoftKeyboardWithDelayOnce) { - mShowSoftKeyboardWithDelayOnce = false; - mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 500); - mActivity.getTerminalView().requestFocus(); - } else - KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView()); - } - } - // If soft keyboard toggle behaviour is show/hide - else { - // If soft keyboard is disabled by user for Termux - if (!mActivity.getPreferences().isSoftKeyboardEnabled()) { - Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard on toggle"); - KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView()); - } else { - Logger.logVerbose(LOG_TAG, "Showing/Hiding soft keyboard on toggle"); - KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity); - KeyboardUtils.toggleSoftKeyboard(mActivity); - } - } - } - - public void setSoftKeyboardState(boolean isStartup, boolean isReloadTermuxProperties) { - boolean noShowKeyboard = false; - - // Requesting terminal view focus is necessary regardless of if soft keyboard is to be - // disabled or hidden at startup, otherwise if hardware keyboard is attached and user - // starts typing on hardware keyboard without tapping on the terminal first, then a colour - // tint will be added to the terminal as highlight for the focussed view. Test with a light - // theme. For android 8.+, the "defaultFocusHighlightEnabled" attribute is also set to false - // in TerminalView layout to fix the issue. - - // If soft keyboard is disabled by user for Termux (check function docs for Termux behaviour info) - if (KeyboardUtils.shouldSoftKeyboardBeDisabled(mActivity, - mActivity.getPreferences().isSoftKeyboardEnabled(), - mActivity.getPreferences().isSoftKeyboardEnabledOnlyIfNoHardware())) { - Logger.logVerbose(LOG_TAG, "Maintaining disabled soft keyboard"); - KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView()); - mActivity.getTerminalView().requestFocus(); - noShowKeyboard = true; - // Delay is only required if onCreate() is called like when Termux app is exited with - // double back press, not when Termux app is switched back from another app and keyboard - // toggle is pressed to enable keyboard - if (isStartup && mActivity.isOnResumeAfterOnCreate()) - mShowSoftKeyboardWithDelayOnce = true; - } else { - // Set flag to automatically push up TerminalView when keyboard is opened instead of showing over it - KeyboardUtils.setSoftInputModeAdjustResize(mActivity); - - // Clear any previous flags to disable soft keyboard in case setting updated - KeyboardUtils.clearDisableSoftKeyboardFlags(mActivity); - - // If soft keyboard is to be hidden on startup - if (isStartup && mActivity.getProperties().shouldSoftKeyboardBeHiddenOnStartup()) { - Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on startup"); - // Required to keep keyboard hidden when Termux app is switched back from another app - KeyboardUtils.setSoftKeyboardAlwaysHiddenFlags(mActivity); - - KeyboardUtils.hideSoftKeyboard(mActivity, mActivity.getTerminalView()); - mActivity.getTerminalView().requestFocus(); - noShowKeyboard = true; - // Required to keep keyboard hidden on app startup - mShowSoftKeyboardIgnoreOnce = true; - } - } - - mActivity.getTerminalView().setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean hasFocus) { - // Force show soft keyboard if TerminalView or toolbar text input view has - // focus and close it if they don't - boolean textInputViewHasFocus = false; - final EditText textInputView = mActivity.findViewById(R.id.terminal_toolbar_text_input); - if (textInputView != null) textInputViewHasFocus = textInputView.hasFocus(); - - if (hasFocus || textInputViewHasFocus) { - if (mShowSoftKeyboardIgnoreOnce) { - mShowSoftKeyboardIgnoreOnce = false; return; - } - Logger.logVerbose(LOG_TAG, "Showing soft keyboard on focus change"); - } else { - Logger.logVerbose(LOG_TAG, "Hiding soft keyboard on focus change"); - } - - KeyboardUtils.setSoftKeyboardVisibility(getShowSoftKeyboardRunnable(), mActivity, mActivity.getTerminalView(), hasFocus || textInputViewHasFocus); - } - }); - - // Do not force show soft keyboard if termux-reload-settings command was run with hardware keyboard - // or soft keyboard is to be hidden or is disabled - if (!isReloadTermuxProperties && !noShowKeyboard) { - // Request focus for TerminalView - // Also show the keyboard, since onFocusChange will not be called if TerminalView already - // had focus on startup to show the keyboard, like when opening url with context menu - // "Select URL" long press and returning to Termux app with back button. This - // will also show keyboard even if it was closed before opening url. #2111 - Logger.logVerbose(LOG_TAG, "Requesting TerminalView focus and showing soft keyboard"); - mActivity.getTerminalView().requestFocus(); - mActivity.getTerminalView().postDelayed(getShowSoftKeyboardRunnable(), 300); - } - } - - private Runnable getShowSoftKeyboardRunnable() { - if (mShowSoftKeyboardRunnable == null) { - mShowSoftKeyboardRunnable = () -> { - KeyboardUtils.showSoftKeyboard(mActivity, mActivity.getTerminalView()); - }; - } - return mShowSoftKeyboardRunnable; - } - - - - public void setTerminalCursorBlinkerState(boolean start) { - if (start) { - // If set/update the cursor blinking rate is successful, then enable cursor blinker - if (mActivity.getTerminalView().setTerminalCursorBlinkerRate(mActivity.getProperties().getTerminalCursorBlinkRate())) - mActivity.getTerminalView().setTerminalCursorBlinkerState(true, true); - else - Logger.logError(LOG_TAG,"Failed to start cursor blinker"); - } else { - // Disable cursor blinker - mActivity.getTerminalView().setTerminalCursorBlinkerState(false, true); - } - } - - - - public void shareSessionTranscript() { - TerminalSession session = mActivity.getCurrentSession(); - if (session == null) return; - - String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true); - if (transcriptText == null) return; - - // See https://github.com/termux/termux-app/issues/1166. - transcriptText = DataUtils.getTruncatedCommandOutput(transcriptText, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, true, false).trim(); - ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_transcript), - transcriptText, mActivity.getString(R.string.title_share_transcript_with)); - } - - public void shareSelectedText() { - String selectedText = mActivity.getTerminalView().getStoredSelectedText(); - if (DataUtils.isNullOrEmpty(selectedText)) return; - ShareUtils.shareText(mActivity, mActivity.getString(R.string.title_share_selected_text), - selectedText, mActivity.getString(R.string.title_share_selected_text_with)); - } - - public void showUrlSelection() { - TerminalSession session = mActivity.getCurrentSession(); - if (session == null) return; - - String text = ShellUtils.getTerminalSessionTranscriptText(session, true, true); - - LinkedHashSet urlSet = TermuxUrlUtils.extractUrls(text); - if (urlSet.isEmpty()) { - new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show(); - return; - } - - final CharSequence[] urls = urlSet.toArray(new CharSequence[0]); - Collections.reverse(Arrays.asList(urls)); // Latest first. - - // Click to copy url to clipboard: - final AlertDialog dialog = new AlertDialog.Builder(mActivity).setItems(urls, (di, which) -> { - String url = (String) urls[which]; - ShareUtils.copyTextToClipboard(mActivity, url, mActivity.getString(R.string.msg_select_url_copied_to_clipboard)); - }).setTitle(R.string.title_select_url_dialog).create(); - - // Long press to open URL: - dialog.setOnShowListener(di -> { - ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it - lv.setOnItemLongClickListener((parent, view, position, id) -> { - dialog.dismiss(); - String url = (String) urls[position]; - ShareUtils.openUrl(mActivity, url); - return true; - }); - }); - - dialog.show(); - } - - public void reportIssueFromTranscript() { - TerminalSession session = mActivity.getCurrentSession(); - if (session == null) return; - - final String transcriptText = ShellUtils.getTerminalSessionTranscriptText(session, false, true); - if (transcriptText == null) return; - - MessageDialogUtils.showMessage(mActivity, TermuxConstants.TERMUX_APP_NAME + " Report Issue", - mActivity.getString(R.string.msg_add_termux_debug_info), - mActivity.getString(R.string.action_yes), (dialog, which) -> reportIssueFromTranscript(transcriptText, true), - mActivity.getString(R.string.action_no), (dialog, which) -> reportIssueFromTranscript(transcriptText, false), - null); - } - - private void reportIssueFromTranscript(String transcriptText, boolean addTermuxDebugInfo) { - Logger.showToast(mActivity, mActivity.getString(R.string.msg_generating_report), true); - - new Thread() { - @Override - public void run() { - StringBuilder reportString = new StringBuilder(); - - String title = TermuxConstants.TERMUX_APP_NAME + " Report Issue"; - - reportString.append("## Transcript\n"); - reportString.append("\n").append(MarkdownUtils.getMarkdownCodeForString(transcriptText, true)); - reportString.append("\n##\n"); - - if (addTermuxDebugInfo) { - reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_AND_PLUGIN_PACKAGES)); - } else { - reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(mActivity, TermuxUtils.AppInfoMode.TERMUX_PACKAGE)); - } - - reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(mActivity, true)); - - if (TermuxBootstrap.isAppPackageManagerAPT()) { - String termuxAptInfo = TermuxUtils.geAPTInfoMarkdownString(mActivity); - if (termuxAptInfo != null) - reportString.append("\n\n").append(termuxAptInfo); - } - - if (addTermuxDebugInfo) { - String termuxDebugInfo = TermuxUtils.getTermuxDebugMarkdownString(mActivity); - if (termuxDebugInfo != null) - reportString.append("\n\n").append(termuxDebugInfo); - } - - String userActionName = UserAction.REPORT_ISSUE_FROM_TRANSCRIPT.getName(); - - ReportInfo reportInfo = new ReportInfo(userActionName, - TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY_NAME, title); - reportInfo.setReportString(reportString.toString()); - reportInfo.setReportStringSuffix("\n\n" + TermuxUtils.getReportIssueMarkdownString(mActivity)); - reportInfo.setReportSaveFileLabelAndPath(userActionName, - Environment.getExternalStorageDirectory() + "/" + - FileUtils.sanitizeFileName(TermuxConstants.TERMUX_APP_NAME + "-" + userActionName + ".log", true, true)); - - ReportActivity.startReportActivity(mActivity, reportInfo); - } - }.start(); - } - - public void doPaste() { - TerminalSession session = mActivity.getCurrentSession(); - if (session == null) return; - if (!session.isRunning()) return; - - String text = ShareUtils.getTextStringFromClipboardIfSet(mActivity, true); - if (text != null) - session.getEmulator().paste(text); - } - -} diff --git a/app/src/main/java/com/termux/app/terminal/io/FullScreenWorkAround.java b/app/src/main/java/com/termux/app/terminal/io/FullScreenWorkAround.java deleted file mode 100644 index c01f8994..00000000 --- a/app/src/main/java/com/termux/app/terminal/io/FullScreenWorkAround.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.termux.app.terminal.io; - -import android.graphics.Rect; -import android.view.View; -import android.view.ViewGroup; - -import com.termux.app.TermuxActivity; - -/** - * Work around for fullscreen mode in Termux to fix ExtraKeysView not being visible. - * This class is derived from: - * https://stackoverflow.com/questions/7417123/android-how-to-adjust-layout-in-full-screen-mode-when-softkeyboard-is-visible - * and has some additional tweaks - * --- - * For more information, see https://issuetracker.google.com/issues/36911528 - */ -public class FullScreenWorkAround { - private final View mChildOfContent; - private int mUsableHeightPrevious; - private final ViewGroup.LayoutParams mViewGroupLayoutParams; - - private final int mNavBarHeight; - - - public static void apply(TermuxActivity activity) { - new FullScreenWorkAround(activity); - } - - private FullScreenWorkAround(TermuxActivity activity) { - ViewGroup content = activity.findViewById(android.R.id.content); - mChildOfContent = content.getChildAt(0); - mViewGroupLayoutParams = mChildOfContent.getLayoutParams(); - mNavBarHeight = activity.getNavBarHeight(); - mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(this::possiblyResizeChildOfContent); - } - - private void possiblyResizeChildOfContent() { - int usableHeightNow = computeUsableHeight(); - if (usableHeightNow != mUsableHeightPrevious) { - int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight(); - int heightDifference = usableHeightSansKeyboard - usableHeightNow; - if (heightDifference > (usableHeightSansKeyboard / 4)) { - // keyboard probably just became visible - - // ensures that usable layout space does not extend behind the - // soft keyboard, causing the extra keys to not be visible - mViewGroupLayoutParams.height = (usableHeightSansKeyboard - heightDifference) + getNavBarHeight(); - } else { - // keyboard probably just became hidden - mViewGroupLayoutParams.height = usableHeightSansKeyboard; - } - mChildOfContent.requestLayout(); - mUsableHeightPrevious = usableHeightNow; - } - } - - private int getNavBarHeight() { - return mNavBarHeight; - } - - private int computeUsableHeight() { - Rect r = new Rect(); - mChildOfContent.getWindowVisibleDisplayFrame(r); - return (r.bottom - r.top); - } - -} - diff --git a/app/src/main/java/com/termux/app/terminal/io/TermuxTerminalExtraKeys.java b/app/src/main/java/com/termux/app/terminal/io/TermuxTerminalExtraKeys.java deleted file mode 100644 index a38e8cbb..00000000 --- a/app/src/main/java/com/termux/app/terminal/io/TermuxTerminalExtraKeys.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.termux.app.terminal.io; - -import android.annotation.SuppressLint; -import android.view.Gravity; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.drawerlayout.widget.DrawerLayout; - -import com.termux.app.TermuxActivity; -import com.termux.app.terminal.TermuxTerminalSessionActivityClient; -import com.termux.app.terminal.TermuxTerminalViewClient; -import com.termux.shared.logger.Logger; -import com.termux.shared.termux.extrakeys.ExtraKeysConstants; -import com.termux.shared.termux.extrakeys.ExtraKeysInfo; -import com.termux.shared.termux.settings.properties.TermuxPropertyConstants; -import com.termux.shared.termux.settings.properties.TermuxSharedProperties; -import com.termux.shared.termux.terminal.io.TerminalExtraKeys; -import com.termux.view.TerminalView; - -import org.json.JSONException; - -public class TermuxTerminalExtraKeys extends TerminalExtraKeys { - - private ExtraKeysInfo mExtraKeysInfo; - - final TermuxActivity mActivity; - final TermuxTerminalViewClient mTermuxTerminalViewClient; - final TermuxTerminalSessionActivityClient mTermuxTerminalSessionActivityClient; - - private static final String LOG_TAG = "TermuxTerminalExtraKeys"; - - public TermuxTerminalExtraKeys(TermuxActivity activity, @NonNull TerminalView terminalView, - TermuxTerminalViewClient termuxTerminalViewClient, - TermuxTerminalSessionActivityClient termuxTerminalSessionActivityClient) { - super(terminalView); - - mActivity = activity; - mTermuxTerminalViewClient = termuxTerminalViewClient; - mTermuxTerminalSessionActivityClient = termuxTerminalSessionActivityClient; - - setExtraKeys(); - } - - - /** - * Set the terminal extra keys and style. - */ - private void setExtraKeys() { - mExtraKeysInfo = null; - - try { - // The mMap stores the extra key and style string values while loading properties - // Check {@link #getExtraKeysInternalPropertyValueFromValue(String)} and - // {@link #getExtraKeysStyleInternalPropertyValueFromValue(String)} - String extrakeys = (String) mActivity.getProperties().getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS, true); - String extraKeysStyle = (String) mActivity.getProperties().getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE, true); - - ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap = ExtraKeysInfo.getCharDisplayMapForStyle(extraKeysStyle); - if (ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY.equals(extraKeyDisplayMap) && !TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE.equals(extraKeysStyle)) { - Logger.logError(TermuxSharedProperties.LOG_TAG, "The style \"" + extraKeysStyle + "\" for the key \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE + "\" is invalid. Using default style instead."); - extraKeysStyle = TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE; - } - - mExtraKeysInfo = new ExtraKeysInfo(extrakeys, extraKeysStyle, ExtraKeysConstants.CONTROL_CHARS_ALIASES); - } catch (JSONException e) { - Logger.showToast(mActivity, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: " + e.toString(), true); - Logger.logStackTraceWithMessage(LOG_TAG, "Could not load and set the \"" + TermuxPropertyConstants.KEY_EXTRA_KEYS + "\" property from the properties file: ", e); - - try { - mExtraKeysInfo = new ExtraKeysInfo(TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE, ExtraKeysConstants.CONTROL_CHARS_ALIASES); - } catch (JSONException e2) { - Logger.showToast(mActivity, "Can't create default extra keys",true); - Logger.logStackTraceWithMessage(LOG_TAG, "Could create default extra keys: ", e); - mExtraKeysInfo = null; - } - } - } - - public ExtraKeysInfo getExtraKeysInfo() { - return mExtraKeysInfo; - } - - @SuppressLint("RtlHardcoded") - @Override - public void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) { - if ("KEYBOARD".equals(key)) { - if(mTermuxTerminalViewClient != null) - mTermuxTerminalViewClient.onToggleSoftKeyboardRequest(); - } else if ("DRAWER".equals(key)) { - DrawerLayout drawerLayout = mTermuxTerminalViewClient.getActivity().getDrawer(); - if (drawerLayout.isDrawerOpen(Gravity.LEFT)) - drawerLayout.closeDrawer(Gravity.LEFT); - else - drawerLayout.openDrawer(Gravity.LEFT); - } else if ("PASTE".equals(key)) { - if(mTermuxTerminalSessionActivityClient != null) - mTermuxTerminalSessionActivityClient.onPasteTextFromClipboard(null); - } else if ("SCROLL".equals(key)) { - TerminalView terminalView = mTermuxTerminalViewClient.getActivity().getTerminalView(); - if (terminalView != null && terminalView.mEmulator != null) - terminalView.mEmulator.toggleAutoScrollDisabled(); - } else { - super.onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown); - } - } - -} diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml deleted file mode 100644 index d3914191..00000000 --- a/app/src/main/res/layout/activity_settings.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/activity_termux.xml b/app/src/main/res/layout/activity_termux.xml index 484990ff..8fa2f3b8 100644 --- a/app/src/main/res/layout/activity_termux.xml +++ b/app/src/main/res/layout/activity_termux.xml @@ -1,20 +1,13 @@ - - + android:layout_height="match_parent" + android:orientation="vertical" + android:fitsSystemWindows="true" + > - - - - diff --git a/app/src/main/res/layout/preference_markdown_text.xml b/app/src/main/res/layout/preference_markdown_text.xml deleted file mode 100644 index f77049e3..00000000 --- a/app/src/main/res/layout/preference_markdown_text.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/layout/view_terminal_toolbar_extra_keys.xml b/app/src/main/res/layout/view_terminal_toolbar_extra_keys.xml index 7dbd46fd..db84037f 100644 --- a/app/src/main/res/layout/view_terminal_toolbar_extra_keys.xml +++ b/app/src/main/res/layout/view_terminal_toolbar_extra_keys.xml @@ -1,5 +1,5 @@ - @color/black @color/black - + -l @color/black - - true - true true diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 703734ec..85c50743 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -2,4 +2,9 @@ + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 045e125f..1e4c5aef 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,3 +1,23 @@ + #1F000000 + #0F000000 + + #FF000000 + #FFFFFFFF + + #FF0000 + #C4001D + + #EEEEEE + #BDBDBD + #9E9E9E + #424242 + #212121 + + #DC143C + #FC143C + + #0969DA + #58A6FF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 794d8df3..8bcc12c5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,237 +1,93 @@ - - - - - - - - - - ]> - - - &TERMUX_APP_NAME; - &TERMUX_APP_NAME; user - - + Termux + Termux user - Run commands in &TERMUX_APP_NAME; environment - execute arbitrary commands within &TERMUX_APP_NAME; - environment and access files - - + Run commands in the Termux environment + execute arbitrary commands within Termux environment and access files Installing bootstrap packages… Unable to install bootstrap - &TERMUX_APP_NAME; was unable to install the bootstrap packages. + Termux was unable to install the bootstrap packages. Abort Try again - &TERMUX_APP_NAME; can only be run as the primary user. - \nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed - under any path other than:\n%1$s. - &TERMUX_APP_NAME; cannot be installed on - portable/external/removable sd card on your device. - \nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed - under any path other than:\n%1$s. - - + Termux can only be run as the primary user. + Termux cannot be installed on external storage. New session Failsafe Max terminals reached Close down existing ones before creating new. - Set session name Set New named session Create - Keyboard - Enabling Terminal Toolbar Disabling Terminal Toolbar - - Select URL Click URL to copy or long press to open No URL found in the terminal. URL copied to clipboard - Share transcript Terminal transcript Send transcript to: - Share selected text Terminal Text Send selected text to: - Autofill password - Reset Terminal reset - Kill process (%d) Really kill this session? - Style Keep screen on Help Settings - Report Issue Generating Report Add termux debug info to report? - - The &TERMUX_STYLING_APP_NAME; Plugin App is not installed. + The Termux:Styling Plugin App is not installed. Install - + + Share With + Open URL With + The storage permission not granted. + The %1$s file saved successfully at \"%2$s\" Exit Acquire wakelock Release wakelock - + The storage permission granted by user on request + The storage permission not granted by user on request - &TERMUX_APP_NAME; requires - \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. - Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced Invalid execution command runner to TermuxService: `%1$s` Unsupported execution command runner to TermuxService: `%1$s` Unsupported execution command shell create mode to TermuxService: `%1$s` Shell name not set but `%1$s` shell create mode passed - - Invalid intent action to RunCommandService: `%1$s` Invalid execution command runner to RunCommandService: `%1$s` Mandatory extra missing to RunCommandService: \"%1$s\" Visit %1$s for more info on RUN_COMMAND Intent usage. - - Save file in ~/downloads/ Edit Open directory - - Failed to start TermuxService. Check logcat for exception message. Failed to start TermuxService while app is in background due to android bg restrictions. - - - - &TERMUX_APP_NAME; Settings - - - &TERMUX_APP_NAME; - Preferences for &TERMUX_APP_NAME; app - - - Debugging - Preferences for debugging - - - Logging - - - Log Level - - - Terminal View Key Logging - Logs will not have entries for terminal view keys. (Default) - Logcat logs will have entries for terminal view keys. - These are very verbose and should be disabled under normal circumstances or will cause performance issues. - - - Plugin Error Notifications - Disable flashes and notifications for plugin errors. - Show flashes and notifications for plugin errors. (Default) - - - Crash Report Notifications - Disable notifications for crash reports. - Show notifications for crash reports. (Default) - - - - Terminal I/O - Preferences for terminal I/O - - - Keyboard - - - Soft Keyboard Enabled - Soft keyboard will be disabled. - Soft keyboard will be enabled. (Default) - - - Soft Keyboard Only If No Hardware - Soft keyboard will be enabled even if - hardware keyboard is connected. (Default) - Soft keyboard will be enabled only if - no hardware keyboard is connected. - - - - Terminal View - Preferences for terminal view - - - View - - - Terminal Margin Adjustment - Terminal margin adjustment will be disabled. - Terminal margin adjustment will be enabled. - It should be enabled to try to fix the issue where soft keyboard covers part of extra keys/terminal view. - If it causes screen flickering on your devices, then disable it. (Default) - - - - - &TERMUX_API_APP_NAME; - Preferences for &TERMUX_API_APP_NAME; app - - - - - &TERMUX_FLOAT_APP_NAME; - Preferences for &TERMUX_FLOAT_APP_NAME; app - - - - - &TERMUX_TASKER_APP_NAME; - Preferences for &TERMUX_TASKER_APP_NAME; app - - - - - &TERMUX_WIDGET_APP_NAME; - Preferences for &TERMUX_WIDGET_APP_NAME; app - - - - - About - - - Donate - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index bfcfd471..e0942c62 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,11 +7,11 @@ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 71a8ae49..2c91328e 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -4,16 +4,8 @@ https://material.io/develop/android/theming/dark --> - - - - - - - - - - - - - - diff --git a/termux-shared/src/main/res/values/attrs.xml b/termux-shared/src/main/res/values/attrs.xml deleted file mode 100644 index 83ac9a96..00000000 --- a/termux-shared/src/main/res/values/attrs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/termux-shared/src/main/res/values/colors.xml b/termux-shared/src/main/res/values/colors.xml deleted file mode 100644 index 1e4c5aef..00000000 --- a/termux-shared/src/main/res/values/colors.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - #1F000000 - #0F000000 - - #FF000000 - #FFFFFFFF - - #FF0000 - #C4001D - - #EEEEEE - #BDBDBD - #9E9E9E - #424242 - #212121 - - #DC143C - #FC143C - - #0969DA - #58A6FF - diff --git a/termux-shared/src/main/res/values/dimens.xml b/termux-shared/src/main/res/values/dimens.xml deleted file mode 100644 index 35a91fc3..00000000 --- a/termux-shared/src/main/res/values/dimens.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - 16dp - 16dp - - 4dip - 8dip - 16dip - 32dip - diff --git a/termux-shared/src/main/res/values/strings.xml b/termux-shared/src/main/res/values/strings.xml deleted file mode 100644 index d585e24f..00000000 --- a/termux-shared/src/main/res/values/strings.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - ]> - - - - %1$s Directory Absolute Path: \"%2$s\" - - - - - The %1$s (%2$s) app is not installed or is disabled. - Failed To Get Package Context - Failed to get package context for the \"%1$s\" package. - This may be because the app package is not installed or it has different APK signature from the current app. - Check %1$s for more details. - Failed to get %1$s/%2$s component state - Failed to enable %1$s/%2$s component - Failed to enable %1$s/%2$s component - - - - - Please grant requested permission(s) - Failed to request permissions with request code %1$d: %2$s - Attempted to check for permissions that have not been requested in app manifest: %1$s - Attempted to ask for permissions that have not been requested in app manifest: %1$s - The \"%1$s\" package is targeting targetSdkVersion %2$d and is running on android sdk %3$d but has not set requestLegacyExternalStorage to true in app manifest - Requires `DUMP` and `PACKAGE_USAGE_STATS` permission - %1$s requires - \"Display over other apps\" permission to start activities and services from background on Android >= 10. - Grants it from Android Settings -> Apps -> %1$s -> Advanced -> Draw over other apps. - The permission name may be different on different devices, like on Xiaomi, its called - \"Display pop-up windows while running in the background\", check https://dontkillmyapp.com - for device specific issues. - - - - - %1$s requires `allow-external-apps` - property to be set to `true` in `%2$s` file. - - - - - Report Text - **Report Truncated**\n\nReport is too large to view here. - Use `Save To File` option from options menu (3-dots on top right) and view it in an external text editor app.\n\n##\n\n - - - - - Share With - Open URL With - The storage permission not granted. - The %1$s file saved successfully at \"%2$s\" - - - - - Sending SIGKILL to process on user request or because android is killing the execution service - Executable not set \"%1$s\" for shell command - Execution has been cancelled since execution service is being killed - Failed to execute \"%1$s\" termux session command - Failed to execute \"%1$s\" app shell command - Exception received while to executing \"%1$s\" termux session command.\nException: %2$s - Exception received while to executing \"%1$s\" app shell command.\nException: %2$s - - - - - - If you want to report this issue, then copy its text from the options menu (3-dots on top right) and post - an issue on one of the following links. - \n\nIf you are posting a Termux app crash report, then please provide details on what you were doing that - caused the crash and how to reproduce it, if possible. - \n\nIf you are posting an issue on GitHub, then post it in the repository at which the report belongs at. - Issues opened or emails sent with **(partial) screenshots** instead of copied text or a file of this report - **will likely be automatically closed/deleted**. You may optionally remove any device specific info that - you consider private or don\'t want to share or that is not relevant to the issue. - \n\nWe do not provide support for any hacking related tools/scripts. Any questions asked about them over email, - on GitHub or other official termux community forums **will likely be automatically closed/deleted** and may - even result in **temporary or permanent** ban. Check %1$s/wiki/Hacking for details. - - The &TERMUX_APP_NAME; is required by the %1$s app to run termux commands. - The &TERMUX_APP_NAME; app (package context) is not accessible. - The &TERMUX_APP_NAME; app $PREFIX directory is not accessible by the %1$s app. - This may be because you have not installed or setup &TERMUX_APP_NAME; app or - &TERMUX_APP_NAME; app and %1$s app both have different APK signatures because you have managed to install both apps from different sources. - It may also be because &TERMUX_APP_NAME; $PREFIX directory \"&TERMUX_PREFIX_DIR_PATH;\" does not exist or does not have read, - write and execute permissions. - - - - - Yes - No - Copy - Share - Cancel - Save To File - - The storage permission granted by user on request - The storage permission not granted by user on request - - - - - Enable Launcher Icon - Disable Launcher Icon - Enabling %1$s app launcher icon - Disabling %1$s app launcher icon - Launcher Icon Enabled - Launcher Icon will be disabled. - Launcher Icon will be enabled. (Default) - - - - - Log Level - Off - Normal - Debug - Verbose - *Unknown* - Logcat log level set to \"%1$s\" - - diff --git a/termux-shared/src/main/res/values/styles.xml b/termux-shared/src/main/res/values/styles.xml deleted file mode 100644 index 19f5664f..00000000 --- a/termux-shared/src/main/res/values/styles.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/termux-shared/src/main/res/values/themes.xml b/termux-shared/src/main/res/values/themes.xml deleted file mode 100644 index 66e54f21..00000000 --- a/termux-shared/src/main/res/values/themes.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - -