From df08b8ce94e15185e77ec4e618b4575cb178f3a8 Mon Sep 17 00:00:00 2001 From: flaan4me Date: Thu, 30 Dec 2021 14:33:23 -0500 Subject: [PATCH] Initial commit --- .gitattributes | 14 + .github/workflows/main.yml | 36 + .github/workflows/maven-publish.yml | 34 + .gitignore | 7 + .idea/.name | 1 + .idea/compiler.xml | 23 + .idea/copyright/profiles_settings.xml | 3 + .idea/encodings.xml | 5 + .idea/gradle.xml | 25 + .idea/misc.xml | 10 + .idea/modules.xml | 16 + .idea/scopes/scope_settings.xml | 5 + .idea/vcs.xml | 6 + MODULE_LICENSE_APACHE2 | 0 NOTICE | 190 ++ README.md | 32 + artwork/Feature Graphic.xcf | Bin 0 -> 237190 bytes artwork/android-terminal-emulator-512.png | Bin 0 -> 26800 bytes artwork/android-terminal-emulator.svg | 375 +++ build.gradle | 31 + docs/Building.md | 101 + docs/UTF-8-SMP-chars-demo.txt | 2 + docs/UTF-8-demo.txt | 212 ++ docs/atari_small_notice.txt | 11 + docs/notification icon source.png | Bin 0 -> 3999 bytes docs/releaseChecklist.md | 103 + emulatorview/.gitignore | 1 + emulatorview/build.gradle | 18 + emulatorview/src/main/AndroidManifest.xml | 7 + .../emulatorview/BaseTextRenderer.java | 448 ++++ .../emulatorview/Bitmap4x8FontRenderer.java | 158 ++ .../androidterm/emulatorview/ByteQueue.java | 124 + .../androidterm/emulatorview/ColorScheme.java | 145 ++ .../emulatorview/EmulatorDebug.java | 65 + .../emulatorview/EmulatorView.java | 1714 ++++++++++++++ .../emulatorview/GrowableIntArray.java | 29 + .../emulatorview/PaintRenderer.java | 146 ++ .../androidterm/emulatorview/Screen.java | 157 ++ .../androidterm/emulatorview/StyleRow.java | 97 + .../emulatorview/TermKeyListener.java | 750 ++++++ .../androidterm/emulatorview/TermSession.java | 638 ++++++ .../emulatorview/TerminalEmulator.java | 2011 +++++++++++++++++ .../emulatorview/TextRenderer.java | 63 + .../androidterm/emulatorview/TextStyle.java | 44 + .../emulatorview/TranscriptScreen.java | 498 ++++ .../emulatorview/UnicodeTranscript.java | 1141 ++++++++++ .../emulatorview/UpdateCallback.java | 27 + .../compat/AndroidCharacterCompat.java | 29 + .../emulatorview/compat/AndroidCompat.java | 40 + .../compat/ClipboardManagerCompat.java | 9 + .../compat/ClipboardManagerCompatFactory.java | 18 + .../compat/ClipboardManagerCompatV1.java | 29 + .../compat/ClipboardManagerCompatV11.java | 35 + .../compat/KeyCharacterMapCompat.java | 46 + .../emulatorview/compat/KeycodeConstants.java | 490 ++++ .../emulatorview/compat/Patterns.java | 93 + .../androidterm/emulatorview/package.html | 12 + .../res/drawable-nodpi/atari_small_nodpi.png | Bin 0 -> 974 bytes .../src/main/res/drawable/atari_small.png | Bin 0 -> 974 bytes gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++ gradlew.bat | 90 + libtermexec/.gitignore | 1 + libtermexec/build.gradle | 59 + libtermexec/proguard-rules.pro | 17 + .../libtermexec/ApplicationTest.java | 13 + libtermexec/src/main/AndroidManifest.xml | 4 + .../androidterm/libtermexec/v1/ITerminal.aidl | 39 + .../java/jackpal/androidterm/TermExec.java | 132 ++ libtermexec/src/main/jni/process.cpp | 271 +++ libtermexec/src/main/jni/process.h | 34 + samples/intents/.gitignore | 1 + samples/intents/build.gradle | 19 + samples/intents/lint.xml | 5 + samples/intents/src/main/AndroidManifest.xml | 17 + .../sample/intents/IntentSampleActivity.java | 84 + samples/intents/src/main/res/layout/main.xml | 42 + .../src/main/res/values-ja/strings.xml | 13 + .../src/main/res/values-ko/strings.xml | 10 + .../intents/src/main/res/values/strings.xml | 13 + samples/pathbroadcasts/.gitignore | 1 + samples/pathbroadcasts/build.gradle | 28 + .../src/main/AndroidManifest.xml | 20 + samples/pathbroadcasts/src/main/assets/hello | 4 + samples/pathbroadcasts/src/main/assets/ls | 4 + .../sample/pathbroadcasts/PathReceiver.java | 166 ++ .../src/main/res/values/strings.xml | 4 + samples/telnet/.gitignore | 1 + samples/telnet/build.gradle | 28 + samples/telnet/src/main/AndroidManifest.xml | 21 + .../sample/telnet/LaunchActivity.java | 56 + .../sample/telnet/TelnetSession.java | 568 +++++ .../sample/telnet/TermActivity.java | 285 +++ .../src/main/res/layout/launch_activity.xml | 37 + .../src/main/res/layout/term_activity.xml | 39 + .../telnet/src/main/res/values-ko/strings.xml | 12 + .../telnet/src/main/res/values/strings.xml | 12 + settings.gradle | 5 + term/.gitignore | 1 + term/build.gradle | 35 + term/lint.xml | 5 + term/src/main/AndroidManifest.xml | 139 ++ .../jackpal/androidterm/BoundSession.java | 41 + .../main/java/jackpal/androidterm/Exec.java | 43 + .../androidterm/GenericTermSession.java | 236 ++ .../jackpal/androidterm/RemoteInterface.java | 216 ++ .../java/jackpal/androidterm/RunScript.java | 87 + .../java/jackpal/androidterm/RunShortcut.java | 82 + .../jackpal/androidterm/ShellTermSession.java | 222 ++ .../main/java/jackpal/androidterm/Term.java | 1142 ++++++++++ .../java/jackpal/androidterm/TermDebug.java | 35 + .../jackpal/androidterm/TermPreferences.java | 69 + .../java/jackpal/androidterm/TermService.java | 219 ++ .../java/jackpal/androidterm/TermView.java | 57 + .../jackpal/androidterm/TermViewFlipper.java | 304 +++ .../java/jackpal/androidterm/WindowList.java | 163 ++ .../androidterm/WindowListAdapter.java | 114 + .../androidterm/compat/ActionBarCompat.java | 148 ++ .../androidterm/compat/ActivityCompat.java | 47 + .../androidterm/compat/AlertDialogCompat.java | 89 + .../androidterm/compat/AndroidCompat.java | 47 + .../jackpal/androidterm/compat/Base64.java | 750 ++++++ .../androidterm/compat/FileCompat.java | 50 + .../androidterm/compat/MenuItemCompat.java | 25 + .../jackpal/androidterm/compat/PRNGFixes.java | 345 +++ .../compat/ServiceForegroundCompat.java | 107 + .../androidterm/shortcuts/AddShortcut.java | 312 +++ .../androidterm/shortcuts/ColorValue.java | 215 ++ .../androidterm/shortcuts/FSNavigator.java | 539 +++++ .../androidterm/shortcuts/TextIcon.java | 68 + .../jackpal/androidterm/util/SessionList.java | 153 ++ .../androidterm/util/ShortcutEncryption.java | 313 +++ .../androidterm/util/TermSettings.java | 373 +++ term/src/main/jni/common.cpp | 99 + term/src/main/jni/common.h | 48 + term/src/main/jni/fileCompat.cpp | 52 + term/src/main/jni/fileCompat.h | 24 + term/src/main/jni/termExec.cpp | 92 + term/src/main/jni/termExec.h | 24 + .../res/drawable-hdpi-v11/ic_menu_add.png | Bin 0 -> 2194 bytes .../res/drawable-hdpi-v11/ic_menu_back.png | Bin 0 -> 991 bytes .../ic_menu_close_clear_cancel.png | Bin 0 -> 1391 bytes .../res/drawable-hdpi-v11/ic_menu_forward.png | Bin 0 -> 977 bytes .../drawable-hdpi-v11/ic_menu_preferences.png | Bin 0 -> 1851 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 765 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 840 bytes .../res/drawable-hdpi/btn_close_window.png | Bin 0 -> 1010 bytes .../main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 3263 bytes .../main/res/drawable-hdpi/ic_menu_add.png | Bin 0 -> 2891 bytes .../main/res/drawable-hdpi/ic_menu_back.png | Bin 0 -> 1583 bytes .../ic_menu_close_clear_cancel.png | Bin 0 -> 3698 bytes .../res/drawable-hdpi/ic_menu_forward.png | Bin 0 -> 1580 bytes .../res/drawable-hdpi/ic_menu_preferences.png | Bin 0 -> 3106 bytes .../res/drawable-hdpi/ic_menu_windows.png | Bin 0 -> 1550 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 1175 bytes .../res/drawable-ldpi-v11/ic_menu_add.png | Bin 0 -> 1580 bytes .../res/drawable-ldpi-v11/ic_menu_back.png | Bin 0 -> 1071 bytes .../ic_menu_close_clear_cancel.png | Bin 0 -> 1851 bytes .../res/drawable-ldpi-v11/ic_menu_forward.png | Bin 0 -> 1112 bytes .../drawable-ldpi-v11/ic_menu_preferences.png | Bin 0 -> 1601 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 402 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 429 bytes .../main/res/drawable-ldpi/ic_launcher.png | Bin 0 -> 1553 bytes .../main/res/drawable-ldpi/ic_menu_add.png | Bin 0 -> 1580 bytes .../ic_menu_close_clear_cancel.png | Bin 0 -> 1851 bytes .../res/drawable-ldpi/ic_menu_preferences.png | Bin 0 -> 1601 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 532 bytes .../res/drawable-mdpi-v11/ic_menu_add.png | Bin 0 -> 1339 bytes .../res/drawable-mdpi-v11/ic_menu_back.png | Bin 0 -> 779 bytes .../ic_menu_close_clear_cancel.png | Bin 0 -> 932 bytes .../res/drawable-mdpi-v11/ic_menu_forward.png | Bin 0 -> 769 bytes .../drawable-mdpi-v11/ic_menu_preferences.png | Bin 0 -> 1142 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 539 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 561 bytes .../res/drawable-mdpi/btn_close_window.png | Bin 0 -> 607 bytes .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 1964 bytes .../main/res/drawable-mdpi/ic_menu_add.png | Bin 0 -> 2011 bytes .../main/res/drawable-mdpi/ic_menu_back.png | Bin 0 -> 1163 bytes .../ic_menu_close_clear_cancel.png | Bin 0 -> 2425 bytes .../res/drawable-mdpi/ic_menu_forward.png | Bin 0 -> 1163 bytes .../res/drawable-mdpi/ic_menu_preferences.png | Bin 0 -> 2096 bytes .../res/drawable-mdpi/ic_menu_windows.png | Bin 0 -> 1234 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 761 bytes .../res/drawable-xhdpi-v11/ic_menu_add.png | Bin 0 -> 3061 bytes .../res/drawable-xhdpi-v11/ic_menu_back.png | Bin 0 -> 1319 bytes .../ic_menu_close_clear_cancel.png | Bin 0 -> 1709 bytes .../drawable-xhdpi-v11/ic_menu_forward.png | Bin 0 -> 1227 bytes .../ic_menu_preferences.png | Bin 0 -> 2507 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 958 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 1175 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 4168 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 1655 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 6382 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 0 -> 8794 bytes .../main/res/drawable/btn_close_window.png | Bin 0 -> 607 bytes .../main/res/drawable/close_background.xml | 20 + term/src/main/res/drawable/ic_folder.png | Bin 0 -> 1163 bytes term/src/main/res/drawable/ic_folderup.png | Bin 0 -> 1066 bytes term/src/main/res/drawable/ic_launcher.png | Bin 0 -> 1964 bytes term/src/main/res/drawable/ic_menu_add.png | Bin 0 -> 2011 bytes term/src/main/res/drawable/ic_menu_back.png | Bin 0 -> 1163 bytes .../drawable/ic_menu_close_clear_cancel.png | Bin 0 -> 2425 bytes .../src/main/res/drawable/ic_menu_forward.png | Bin 0 -> 1163 bytes .../main/res/drawable/ic_menu_preferences.png | Bin 0 -> 2096 bytes .../src/main/res/drawable/ic_menu_windows.png | Bin 0 -> 1234 bytes .../ic_stat_service_notification_icon.png | Bin 0 -> 761 bytes term/src/main/res/layout/term_activity.xml | 24 + term/src/main/res/layout/window_list_item.xml | 47 + .../res/layout/window_list_new_window.xml | 27 + term/src/main/res/menu/main.xml | 45 + term/src/main/res/values-cs/arrays.xml | 136 ++ term/src/main/res/values-cs/strings.xml | 127 ++ term/src/main/res/values-de/arrays.xml | 141 ++ term/src/main/res/values-de/strings.xml | 164 ++ term/src/main/res/values-es/arrays.xml | 142 ++ term/src/main/res/values-es/strings.xml | 197 ++ term/src/main/res/values-eu/arrays.xml | 137 ++ term/src/main/res/values-eu/strings.xml | 131 ++ term/src/main/res/values-fr/arrays.xml | 115 + term/src/main/res/values-fr/strings.xml | 163 ++ term/src/main/res/values-hu/arrays.xml | 143 ++ term/src/main/res/values-hu/strings.xml | 194 ++ term/src/main/res/values-it/arrays.xml | 142 ++ term/src/main/res/values-it/strings.xml | 166 ++ term/src/main/res/values-iw/arrays.xml | 126 ++ term/src/main/res/values-iw/strings.xml | 117 + term/src/main/res/values-ja/arrays.xml | 136 ++ term/src/main/res/values-ja/strings.xml | 152 ++ term/src/main/res/values-ka/arrays.xml | 136 ++ term/src/main/res/values-ka/strings.xml | 143 ++ term/src/main/res/values-ko/arrays.xml | 141 ++ term/src/main/res/values-ko/strings.xml | 159 ++ term/src/main/res/values-nb/arrays.xml | 142 ++ term/src/main/res/values-nb/strings.xml | 161 ++ term/src/main/res/values-nl/arrays.xml | 141 ++ term/src/main/res/values-nl/strings.xml | 171 ++ term/src/main/res/values-pl/arrays.xml | 136 ++ term/src/main/res/values-pl/strings.xml | 152 ++ term/src/main/res/values-pt-rPT/arrays.xml | 137 ++ term/src/main/res/values-pt-rPT/strings.xml | 74 + term/src/main/res/values-pt/arrays.xml | 137 ++ term/src/main/res/values-pt/strings.xml | 143 ++ term/src/main/res/values-ro/arrays.xml | 134 ++ term/src/main/res/values-ro/strings.xml | 126 ++ term/src/main/res/values-ru/arrays.xml | 111 + term/src/main/res/values-ru/strings.xml | 151 ++ term/src/main/res/values-sk/arrays.xml | 141 ++ term/src/main/res/values-sk/strings.xml | 188 ++ term/src/main/res/values-sr/arrays.xml | 142 ++ term/src/main/res/values-sr/strings.xml | 195 ++ term/src/main/res/values-sv/arrays.xml | 136 ++ term/src/main/res/values-sv/strings.xml | 52 + term/src/main/res/values-tr/arrays.xml | 86 + term/src/main/res/values-tr/strings.xml | 73 + term/src/main/res/values-uk/arrays.xml | 105 + term/src/main/res/values-uk/strings.xml | 116 + term/src/main/res/values-v11/styles.xml | 33 + term/src/main/res/values-v21/colors.xml | 5 + term/src/main/res/values-v21/styles.xml | 21 + term/src/main/res/values-zh-rCN/arrays.xml | 142 ++ term/src/main/res/values-zh-rCN/strings.xml | 158 ++ term/src/main/res/values-zh-rTW/arrays.xml | 136 ++ term/src/main/res/values-zh-rTW/strings.xml | 96 + term/src/main/res/values/arrays.xml | 142 ++ term/src/main/res/values/arraysNoLocalize.xml | 129 ++ term/src/main/res/values/attrs.xml | 22 + term/src/main/res/values/defaults.xml | 27 + term/src/main/res/values/id.xml | 7 + term/src/main/res/values/strings.xml | 197 ++ term/src/main/res/values/styles.xml | 27 + term/src/main/res/xml/preferences.xml | 209 ++ tests/controlSequences/256color.txt | 13 + .../combiningCharReplacement.txt | 10 + tests/controlSequences/combiningChars.txt | 6 + tests/controlSequences/hideCursor.txt | 1 + tests/controlSequences/setTitle.txt | 1 + tests/controlSequences/showCursor.txt | 1 + tests/controlSequences/textStyle.txt | 20 + tests/emulatorview-test/AndroidManifest.xml | 19 + tests/emulatorview-test/ant.properties | 18 + tests/emulatorview-test/proguard-project.txt | 20 + tests/emulatorview-test/project.properties | 14 + .../res/drawable-hdpi/ic_launcher.png | Bin 0 -> 9397 bytes .../res/drawable-ldpi/ic_launcher.png | Bin 0 -> 2729 bytes .../res/drawable-mdpi/ic_launcher.png | Bin 0 -> 5237 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 14383 bytes .../emulatorview-test/res/values/strings.xml | 6 + .../emulatorview/InputConnectionTest.java | 23 + .../emulatorview/ModifierKeyTest.java | 14 + .../emulatorview/TermKeyListenerTest.java | 433 ++++ tests/fullWidthText | 3 + tests/issue145/README.md | 27 + tests/issue145/fuzzer.go | 37 + tests/issue145/issue145repro-2.txt | 1 + tests/issue145/issue145repro.txt | 2 + tests/issue149/colors.go | 30 + tests/wideChars/combining-chars.txt | 57 + tests/wideChars/last-column-wrapping.txt | 4 + tests/wideChars/linkification.txt | 6 + tests/wideChars/overwriting1.txt | 16 + tests/wideChars/overwriting2.txt | 16 + tests/wideChars/overwriting3.txt | 16 + tests/wideChars/overwriting4.txt | 16 + tests/wideChars/overwriting5.txt | 16 + tests/wideChars/sip-chars.txt | 100 + tools/build-debug | 15 + tools/build-release | 18 + tools/import-icons | 30 + tools/increment-version-number | 20 + tools/install-sdk-packages | 23 + tools/push-and-run-debug | 18 + tools/push-and-run-release | 18 + tools/push-samples-debug | 24 + tools/sign-release-build | 52 + 315 files changed, 28815 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/maven-publish.yml create mode 100644 .gitignore create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/scopes/scope_settings.xml create mode 100644 .idea/vcs.xml create mode 100644 MODULE_LICENSE_APACHE2 create mode 100644 NOTICE create mode 100644 README.md create mode 100644 artwork/Feature Graphic.xcf create mode 100644 artwork/android-terminal-emulator-512.png create mode 100644 artwork/android-terminal-emulator.svg create mode 100644 build.gradle create mode 100644 docs/Building.md create mode 100644 docs/UTF-8-SMP-chars-demo.txt create mode 100644 docs/UTF-8-demo.txt create mode 100644 docs/atari_small_notice.txt create mode 100644 docs/notification icon source.png create mode 100644 docs/releaseChecklist.md create mode 100644 emulatorview/.gitignore create mode 100644 emulatorview/build.gradle create mode 100644 emulatorview/src/main/AndroidManifest.xml create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/BaseTextRenderer.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/Bitmap4x8FontRenderer.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/ByteQueue.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/ColorScheme.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/EmulatorDebug.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/EmulatorView.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/GrowableIntArray.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/PaintRenderer.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/Screen.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/StyleRow.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/TermKeyListener.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/TermSession.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/TerminalEmulator.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/TextRenderer.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/TextStyle.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/TranscriptScreen.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/UnicodeTranscript.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/UpdateCallback.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/AndroidCharacterCompat.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/AndroidCompat.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompat.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatFactory.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatV1.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatV11.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/KeyCharacterMapCompat.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/KeycodeConstants.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/Patterns.java create mode 100644 emulatorview/src/main/java/jackpal/androidterm/emulatorview/package.html create mode 100644 emulatorview/src/main/res/drawable-nodpi/atari_small_nodpi.png create mode 100644 emulatorview/src/main/res/drawable/atari_small.png create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 libtermexec/.gitignore create mode 100644 libtermexec/build.gradle create mode 100644 libtermexec/proguard-rules.pro create mode 100644 libtermexec/src/androidTest/java/jackpal/androidterm/libtermexec/ApplicationTest.java create mode 100644 libtermexec/src/main/AndroidManifest.xml create mode 100644 libtermexec/src/main/aidl/jackpal/androidterm/libtermexec/v1/ITerminal.aidl create mode 100644 libtermexec/src/main/java/jackpal/androidterm/TermExec.java create mode 100644 libtermexec/src/main/jni/process.cpp create mode 100644 libtermexec/src/main/jni/process.h create mode 100644 samples/intents/.gitignore create mode 100644 samples/intents/build.gradle create mode 100644 samples/intents/lint.xml create mode 100644 samples/intents/src/main/AndroidManifest.xml create mode 100644 samples/intents/src/main/java/jackpal/androidterm/sample/intents/IntentSampleActivity.java create mode 100644 samples/intents/src/main/res/layout/main.xml create mode 100644 samples/intents/src/main/res/values-ja/strings.xml create mode 100644 samples/intents/src/main/res/values-ko/strings.xml create mode 100644 samples/intents/src/main/res/values/strings.xml create mode 100644 samples/pathbroadcasts/.gitignore create mode 100644 samples/pathbroadcasts/build.gradle create mode 100644 samples/pathbroadcasts/src/main/AndroidManifest.xml create mode 100755 samples/pathbroadcasts/src/main/assets/hello create mode 100755 samples/pathbroadcasts/src/main/assets/ls create mode 100644 samples/pathbroadcasts/src/main/java/jackpal/androidterm/sample/pathbroadcasts/PathReceiver.java create mode 100644 samples/pathbroadcasts/src/main/res/values/strings.xml create mode 100644 samples/telnet/.gitignore create mode 100644 samples/telnet/build.gradle create mode 100644 samples/telnet/src/main/AndroidManifest.xml create mode 100644 samples/telnet/src/main/java/jackpal/androidterm/sample/telnet/LaunchActivity.java create mode 100644 samples/telnet/src/main/java/jackpal/androidterm/sample/telnet/TelnetSession.java create mode 100644 samples/telnet/src/main/java/jackpal/androidterm/sample/telnet/TermActivity.java create mode 100644 samples/telnet/src/main/res/layout/launch_activity.xml create mode 100644 samples/telnet/src/main/res/layout/term_activity.xml create mode 100644 samples/telnet/src/main/res/values-ko/strings.xml create mode 100644 samples/telnet/src/main/res/values/strings.xml create mode 100644 settings.gradle create mode 100644 term/.gitignore create mode 100644 term/build.gradle create mode 100644 term/lint.xml create mode 100644 term/src/main/AndroidManifest.xml create mode 100644 term/src/main/java/jackpal/androidterm/BoundSession.java create mode 100644 term/src/main/java/jackpal/androidterm/Exec.java create mode 100644 term/src/main/java/jackpal/androidterm/GenericTermSession.java create mode 100644 term/src/main/java/jackpal/androidterm/RemoteInterface.java create mode 100644 term/src/main/java/jackpal/androidterm/RunScript.java create mode 100644 term/src/main/java/jackpal/androidterm/RunShortcut.java create mode 100644 term/src/main/java/jackpal/androidterm/ShellTermSession.java create mode 100644 term/src/main/java/jackpal/androidterm/Term.java create mode 100644 term/src/main/java/jackpal/androidterm/TermDebug.java create mode 100644 term/src/main/java/jackpal/androidterm/TermPreferences.java create mode 100644 term/src/main/java/jackpal/androidterm/TermService.java create mode 100644 term/src/main/java/jackpal/androidterm/TermView.java create mode 100644 term/src/main/java/jackpal/androidterm/TermViewFlipper.java create mode 100644 term/src/main/java/jackpal/androidterm/WindowList.java create mode 100644 term/src/main/java/jackpal/androidterm/WindowListAdapter.java create mode 100644 term/src/main/java/jackpal/androidterm/compat/ActionBarCompat.java create mode 100644 term/src/main/java/jackpal/androidterm/compat/ActivityCompat.java create mode 100644 term/src/main/java/jackpal/androidterm/compat/AlertDialogCompat.java create mode 100644 term/src/main/java/jackpal/androidterm/compat/AndroidCompat.java create mode 100644 term/src/main/java/jackpal/androidterm/compat/Base64.java create mode 100644 term/src/main/java/jackpal/androidterm/compat/FileCompat.java create mode 100644 term/src/main/java/jackpal/androidterm/compat/MenuItemCompat.java create mode 100644 term/src/main/java/jackpal/androidterm/compat/PRNGFixes.java create mode 100644 term/src/main/java/jackpal/androidterm/compat/ServiceForegroundCompat.java create mode 100644 term/src/main/java/jackpal/androidterm/shortcuts/AddShortcut.java create mode 100644 term/src/main/java/jackpal/androidterm/shortcuts/ColorValue.java create mode 100644 term/src/main/java/jackpal/androidterm/shortcuts/FSNavigator.java create mode 100644 term/src/main/java/jackpal/androidterm/shortcuts/TextIcon.java create mode 100644 term/src/main/java/jackpal/androidterm/util/SessionList.java create mode 100644 term/src/main/java/jackpal/androidterm/util/ShortcutEncryption.java create mode 100644 term/src/main/java/jackpal/androidterm/util/TermSettings.java create mode 100644 term/src/main/jni/common.cpp create mode 100644 term/src/main/jni/common.h create mode 100644 term/src/main/jni/fileCompat.cpp create mode 100644 term/src/main/jni/fileCompat.h create mode 100644 term/src/main/jni/termExec.cpp create mode 100644 term/src/main/jni/termExec.h create mode 100644 term/src/main/res/drawable-hdpi-v11/ic_menu_add.png create mode 100644 term/src/main/res/drawable-hdpi-v11/ic_menu_back.png create mode 100644 term/src/main/res/drawable-hdpi-v11/ic_menu_close_clear_cancel.png create mode 100644 term/src/main/res/drawable-hdpi-v11/ic_menu_forward.png create mode 100644 term/src/main/res/drawable-hdpi-v11/ic_menu_preferences.png create mode 100644 term/src/main/res/drawable-hdpi-v11/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-hdpi-v9/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-hdpi/btn_close_window.png create mode 100644 term/src/main/res/drawable-hdpi/ic_launcher.png create mode 100644 term/src/main/res/drawable-hdpi/ic_menu_add.png create mode 100644 term/src/main/res/drawable-hdpi/ic_menu_back.png create mode 100644 term/src/main/res/drawable-hdpi/ic_menu_close_clear_cancel.png create mode 100644 term/src/main/res/drawable-hdpi/ic_menu_forward.png create mode 100644 term/src/main/res/drawable-hdpi/ic_menu_preferences.png create mode 100644 term/src/main/res/drawable-hdpi/ic_menu_windows.png create mode 100644 term/src/main/res/drawable-hdpi/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-ldpi-v11/ic_menu_add.png create mode 100644 term/src/main/res/drawable-ldpi-v11/ic_menu_back.png create mode 100644 term/src/main/res/drawable-ldpi-v11/ic_menu_close_clear_cancel.png create mode 100644 term/src/main/res/drawable-ldpi-v11/ic_menu_forward.png create mode 100644 term/src/main/res/drawable-ldpi-v11/ic_menu_preferences.png create mode 100644 term/src/main/res/drawable-ldpi-v11/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-ldpi-v9/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-ldpi/ic_launcher.png create mode 100644 term/src/main/res/drawable-ldpi/ic_menu_add.png create mode 100644 term/src/main/res/drawable-ldpi/ic_menu_close_clear_cancel.png create mode 100644 term/src/main/res/drawable-ldpi/ic_menu_preferences.png create mode 100644 term/src/main/res/drawable-ldpi/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-mdpi-v11/ic_menu_add.png create mode 100644 term/src/main/res/drawable-mdpi-v11/ic_menu_back.png create mode 100644 term/src/main/res/drawable-mdpi-v11/ic_menu_close_clear_cancel.png create mode 100644 term/src/main/res/drawable-mdpi-v11/ic_menu_forward.png create mode 100644 term/src/main/res/drawable-mdpi-v11/ic_menu_preferences.png create mode 100644 term/src/main/res/drawable-mdpi-v11/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-mdpi-v9/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-mdpi/btn_close_window.png create mode 100644 term/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 term/src/main/res/drawable-mdpi/ic_menu_add.png create mode 100644 term/src/main/res/drawable-mdpi/ic_menu_back.png create mode 100644 term/src/main/res/drawable-mdpi/ic_menu_close_clear_cancel.png create mode 100644 term/src/main/res/drawable-mdpi/ic_menu_forward.png create mode 100644 term/src/main/res/drawable-mdpi/ic_menu_preferences.png create mode 100644 term/src/main/res/drawable-mdpi/ic_menu_windows.png create mode 100644 term/src/main/res/drawable-mdpi/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-xhdpi-v11/ic_menu_add.png create mode 100644 term/src/main/res/drawable-xhdpi-v11/ic_menu_back.png create mode 100644 term/src/main/res/drawable-xhdpi-v11/ic_menu_close_clear_cancel.png create mode 100644 term/src/main/res/drawable-xhdpi-v11/ic_menu_forward.png create mode 100644 term/src/main/res/drawable-xhdpi-v11/ic_menu_preferences.png create mode 100644 term/src/main/res/drawable-xhdpi-v11/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-xhdpi-v9/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 term/src/main/res/drawable-xhdpi/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 term/src/main/res/drawable-xxxhdpi/ic_launcher.png create mode 100644 term/src/main/res/drawable/btn_close_window.png create mode 100644 term/src/main/res/drawable/close_background.xml create mode 100644 term/src/main/res/drawable/ic_folder.png create mode 100644 term/src/main/res/drawable/ic_folderup.png create mode 100644 term/src/main/res/drawable/ic_launcher.png create mode 100644 term/src/main/res/drawable/ic_menu_add.png create mode 100644 term/src/main/res/drawable/ic_menu_back.png create mode 100644 term/src/main/res/drawable/ic_menu_close_clear_cancel.png create mode 100644 term/src/main/res/drawable/ic_menu_forward.png create mode 100644 term/src/main/res/drawable/ic_menu_preferences.png create mode 100644 term/src/main/res/drawable/ic_menu_windows.png create mode 100644 term/src/main/res/drawable/ic_stat_service_notification_icon.png create mode 100644 term/src/main/res/layout/term_activity.xml create mode 100644 term/src/main/res/layout/window_list_item.xml create mode 100644 term/src/main/res/layout/window_list_new_window.xml create mode 100644 term/src/main/res/menu/main.xml create mode 100644 term/src/main/res/values-cs/arrays.xml create mode 100644 term/src/main/res/values-cs/strings.xml create mode 100644 term/src/main/res/values-de/arrays.xml create mode 100644 term/src/main/res/values-de/strings.xml create mode 100644 term/src/main/res/values-es/arrays.xml create mode 100644 term/src/main/res/values-es/strings.xml create mode 100644 term/src/main/res/values-eu/arrays.xml create mode 100644 term/src/main/res/values-eu/strings.xml create mode 100644 term/src/main/res/values-fr/arrays.xml create mode 100644 term/src/main/res/values-fr/strings.xml create mode 100644 term/src/main/res/values-hu/arrays.xml create mode 100644 term/src/main/res/values-hu/strings.xml create mode 100644 term/src/main/res/values-it/arrays.xml create mode 100644 term/src/main/res/values-it/strings.xml create mode 100644 term/src/main/res/values-iw/arrays.xml create mode 100644 term/src/main/res/values-iw/strings.xml create mode 100644 term/src/main/res/values-ja/arrays.xml create mode 100644 term/src/main/res/values-ja/strings.xml create mode 100644 term/src/main/res/values-ka/arrays.xml create mode 100644 term/src/main/res/values-ka/strings.xml create mode 100644 term/src/main/res/values-ko/arrays.xml create mode 100644 term/src/main/res/values-ko/strings.xml create mode 100644 term/src/main/res/values-nb/arrays.xml create mode 100644 term/src/main/res/values-nb/strings.xml create mode 100644 term/src/main/res/values-nl/arrays.xml create mode 100644 term/src/main/res/values-nl/strings.xml create mode 100644 term/src/main/res/values-pl/arrays.xml create mode 100644 term/src/main/res/values-pl/strings.xml create mode 100644 term/src/main/res/values-pt-rPT/arrays.xml create mode 100644 term/src/main/res/values-pt-rPT/strings.xml create mode 100644 term/src/main/res/values-pt/arrays.xml create mode 100644 term/src/main/res/values-pt/strings.xml create mode 100644 term/src/main/res/values-ro/arrays.xml create mode 100644 term/src/main/res/values-ro/strings.xml create mode 100644 term/src/main/res/values-ru/arrays.xml create mode 100644 term/src/main/res/values-ru/strings.xml create mode 100644 term/src/main/res/values-sk/arrays.xml create mode 100644 term/src/main/res/values-sk/strings.xml create mode 100644 term/src/main/res/values-sr/arrays.xml create mode 100644 term/src/main/res/values-sr/strings.xml create mode 100644 term/src/main/res/values-sv/arrays.xml create mode 100644 term/src/main/res/values-sv/strings.xml create mode 100644 term/src/main/res/values-tr/arrays.xml create mode 100644 term/src/main/res/values-tr/strings.xml create mode 100644 term/src/main/res/values-uk/arrays.xml create mode 100644 term/src/main/res/values-uk/strings.xml create mode 100644 term/src/main/res/values-v11/styles.xml create mode 100644 term/src/main/res/values-v21/colors.xml create mode 100644 term/src/main/res/values-v21/styles.xml create mode 100644 term/src/main/res/values-zh-rCN/arrays.xml create mode 100644 term/src/main/res/values-zh-rCN/strings.xml create mode 100644 term/src/main/res/values-zh-rTW/arrays.xml create mode 100644 term/src/main/res/values-zh-rTW/strings.xml create mode 100644 term/src/main/res/values/arrays.xml create mode 100644 term/src/main/res/values/arraysNoLocalize.xml create mode 100644 term/src/main/res/values/attrs.xml create mode 100644 term/src/main/res/values/defaults.xml create mode 100644 term/src/main/res/values/id.xml create mode 100644 term/src/main/res/values/strings.xml create mode 100644 term/src/main/res/values/styles.xml create mode 100644 term/src/main/res/xml/preferences.xml create mode 100644 tests/controlSequences/256color.txt create mode 100644 tests/controlSequences/combiningCharReplacement.txt create mode 100644 tests/controlSequences/combiningChars.txt create mode 100644 tests/controlSequences/hideCursor.txt create mode 100644 tests/controlSequences/setTitle.txt create mode 100644 tests/controlSequences/showCursor.txt create mode 100644 tests/controlSequences/textStyle.txt create mode 100644 tests/emulatorview-test/AndroidManifest.xml create mode 100644 tests/emulatorview-test/ant.properties create mode 100644 tests/emulatorview-test/proguard-project.txt create mode 100644 tests/emulatorview-test/project.properties create mode 100644 tests/emulatorview-test/res/drawable-hdpi/ic_launcher.png create mode 100644 tests/emulatorview-test/res/drawable-ldpi/ic_launcher.png create mode 100644 tests/emulatorview-test/res/drawable-mdpi/ic_launcher.png create mode 100644 tests/emulatorview-test/res/drawable-xhdpi/ic_launcher.png create mode 100644 tests/emulatorview-test/res/values/strings.xml create mode 100644 tests/emulatorview-test/src/jackpal/androidterm/emulatorview/InputConnectionTest.java create mode 100644 tests/emulatorview-test/src/jackpal/androidterm/emulatorview/ModifierKeyTest.java create mode 100644 tests/emulatorview-test/src/jackpal/androidterm/emulatorview/TermKeyListenerTest.java create mode 100644 tests/fullWidthText create mode 100644 tests/issue145/README.md create mode 100644 tests/issue145/fuzzer.go create mode 100644 tests/issue145/issue145repro-2.txt create mode 100644 tests/issue145/issue145repro.txt create mode 100644 tests/issue149/colors.go create mode 100644 tests/wideChars/combining-chars.txt create mode 100644 tests/wideChars/last-column-wrapping.txt create mode 100644 tests/wideChars/linkification.txt create mode 100644 tests/wideChars/overwriting1.txt create mode 100644 tests/wideChars/overwriting2.txt create mode 100644 tests/wideChars/overwriting3.txt create mode 100644 tests/wideChars/overwriting4.txt create mode 100644 tests/wideChars/overwriting5.txt create mode 100644 tests/wideChars/sip-chars.txt create mode 100755 tools/build-debug create mode 100755 tools/build-release create mode 100755 tools/import-icons create mode 100755 tools/increment-version-number create mode 100755 tools/install-sdk-packages create mode 100755 tools/push-and-run-debug create mode 100755 tools/push-and-run-release create mode 100755 tools/push-samples-debug create mode 100755 tools/sign-release-build diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..421502f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Normalize line endings for text files. +# This only affects how files are stored within the repo. Git will +# automatically convert line endings to the platform choice when checking out, +# and automatically convert them back to the normal form when checking in. + +* text=auto + +# Ensure XML files are treated a text files. + +*.xml text + +# Use java diff syntax for java files. + +*.java diff=java diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..6a2b91b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,36 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + pull_request: + branches: [ master ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! + + # Runs a set of commands using the runners shell + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml new file mode 100644 index 0000000..18dd937 --- /dev/null +++ b/.github/workflows/maven-publish.yml @@ -0,0 +1,34 @@ +# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created +# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path + +name: Maven Package + +on: + release: + types: [created] + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + server-id: github # Value of the distributionManagement/repository/id field of the pom.xml + settings-path: ${{ github.workspace }} # location for the settings.xml file + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Publish to GitHub Packages Apache Maven + run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c8ca10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +*.iml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..d6b2332 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Android-Terminal-Emulator \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..217af47 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..e206d70 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..6c701da --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,25 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..59436c9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a7403e3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-2008, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3501e5e --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +#Terminal Emulator for Android + +*Note:* Terminal Emulator for Android development is complete. I am not +accepting pull requests any more. + +Terminal Emulator for Android is a terminal emulator for communicating with the +built-in Android shell. It emulates a reasonably large subset of Digital +Equipment Corporation VT-100 terminal codes, so that programs like "vi", "Emacs" +and "NetHack" will display properly. + +This application was previously named "Android Terminal Emulator". Same great +application, just with a new name. (The change was made at the request of the +Android trademark owner.) + +This code is based on the "Term" application which is included in the Android +Open Source Project. (Which I also wrote. :-) ) + +[Download the Terminal Emulator for Android from Google Play](https://play.google.com/store/apps/details?id=jackpal.androidterm) + +If you are unable to use the Play Store, you can also +[download from GitHub](https://jackpal.github.io/Android-Terminal-Emulator/) + +See [Building](docs/Building.md) for build instructions. + +Got questions? Please check out the +[FAQ](http://github.com/jackpal/Android-Terminal-Emulator/wiki/Frequently-Asked-Questions) +and/or the [Google+ Android Terminal Emulator Community](https://plus.google.com/u/0/communities/106164413936367578283) +before emailing or adding an issue. Thanks! + +Please see the +[Recent Updates](http://github.com/jackpal/Android-Terminal-Emulator/wiki/Recent-Updates) +page for recent updates. diff --git a/artwork/Feature Graphic.xcf b/artwork/Feature Graphic.xcf new file mode 100644 index 0000000000000000000000000000000000000000..38c8670ca80bf3106d06271feec79b3b6a7afce3 GIT binary patch literal 237190 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 WkKeC>->-q+uYu3g8u)_Kq5lsSk`EvN literal 0 HcmV?d00001 diff --git a/artwork/android-terminal-emulator-512.png b/artwork/android-terminal-emulator-512.png new file mode 100644 index 0000000000000000000000000000000000000000..2b4ec5616eb8cc6bb9b4597aa220b59b8edbb26d GIT binary patch literal 26800 zcmb5VbySpJ)HZwr64D?kDIp=)0U`qM zc5ra!3IGzw51G{ssWJ_WwO`^tg5r;D?O9s%E~1Uax!u?0g)7 zfPerY7f-i0_IBQmLS8;jxyQ2i0e~IQR8cev%=_Ez;?8B^zk8kfUGD1%vWGsgrMv95 z`#7zf+gxsv!J?HkP?i-Z6NiMA=sg+f7hyaGJW_W&%u6N_fg~W=F3HJV+k&%?)BNbz zu03n*qx*P ztX0fue=IW__byUZj{^9OxSpjbKq2AIdnf88J&VnW)31#Khx^Ja(u*wTD(kNhN2EvJH)f# zWB=t88UVIHFB@i^jle5((*kCrZ*u#zC1X2Zang_k_2_}*6NVvrO4xx6@F(-XjDdsO zosFH96__Pd8oe62`*d|)4#PveT=WExX}~%`ZmLSkq;4Tn)6*CeO$lm){g3<_NknmQ zmVr81azM6kMRCK~&gF8ff_?_ND-At5b{&H79a3lDMBo6B+YQYy?Vv3iiJ2MHGcKRJ zjq81)OP3Z#HPQc_Js8emH!4_%fj&1RfuWaAFcW9+)C}Lmcfi`f4!y1#wzKs%XEjx| zzfZjI^#7jv1JkGI^Xl~t6giaahE*L(wK`&L!vpYu@Uv5RUF(z)_O*v&(CAlPM4<0& z`vGYw<%TIMAyDut$RiAX^z&d99U$M1ZrWt~I-}}|5AgUO%LT#5o>=5SpE8K}EMCF) zE?!SFsy+DM3mwy+Xk6{eMfRt9S|gW}*q)#8OXGrzVNg8RZckoRg-=ZA1)#s_o_(hw zmgec-!h=7G#RFDF+Pa>Iwzc1k2lNEJ*+-}wbUye0w>haNa;UQ7nH{#wjDh~F@ErAW zz4ph957`oI_?Q92yPEub^nSoO>`eM(fbaT8~NMOf~y%QZF0<%?4Og( z@#D=Z@g%(eotA=*#&g>^`n!tUrtm_q4nsancr^aINO|Ph{E4r>1ZGl0rtz-?*=-*ID>)^Y7IJ(-S{>m zn-@rxfrWRYZ=&UyxdoGSx{=GU8_aiz94770@x7Yg1VHHRb}6>Lv*{>gT~DFr78&>- z@_$T!_n+x|c20R+UR-VPGyIHggE8MY^3XVTfV@+N%~Mwfxl`=SMgIGD#88Jb(0 zx<6L3h3`JJWr%~zv>4O26oy}J!310LUX~DUrX#PJIxU{AR4rV>S{sr-2fX=|li&Y$ zLX6JTqyYwvh>lkI*+*h6vq!Tyqi*7e;O|mllqZH7y~8w_mW0vB`&VAyMM--Oq%sx{ z{>Wx}heq2MmW&gXjEmfNb^Wt27jZm8=i4G#THe?d$U|c5gN)bZ*XT;vTs4awthP1H0TCMi9&zQ`69aa~7I z%ta?Sv)%Ppm9uZ5^%hF7CQU3@`lnSi{Gyc2m}0fPg@HWY^6L$(t*|#0)}E?+ygWRf zhTkuugOB-jtr3s=bxG)ZD{1izcXcl#zT!3&eVkERGafLWk@@xK_8rLar*LeOws`5Lg+ z*JO(AC@ouu@Sa|?!1o5-P(QArG2Q6sd+XAqcp|^?;Xs{sX#i#$43uh)FgS!*!u-yd9wS;#B+mB4q9}DUjq# z!!B@1q3~M(2m6XPG*_R}s(=ld6R7vF;<+-Q0||V3?ymt`l9m4y z!gM3^lH%vGqd*h<)Uf*x4_I>;^jbq?MmY9L{hGRt!&z)R-AAhjO1H(VB9CD46YX6V zxDC!+W;1`TZ14~#`4~?{#)yi|^vPsN;~IRFr}f{ztZUGlT`axH%#2}FAzk>S%*{QG zRRWo1RKP}2{U}uF-4EkN1ogff1n4Pa|02t#)wTPRYh`Q;DPvr@w$UX|t_xlLO#~R2 z0}e|B8g(eaxlFQ=X9cJ)#pU;5Sc-6_GHPj}--0zdE6jVxas70@RUe1Oj+fn7{vPF$ zfAYD)aD%%M4TZ~fS(wSvLMLOg8SnH)V~Zgv;-&O4xnE`BeTo4`0I(uw@WwlEyx8 zyM|JUMtq^^7Vtec6%d=ad}%QW3*#i$4A^AY@tLsy{Tq>J$L1v`o;q9>Mt$c$dAHs> zwm3Xcou1r5HtZn9X(483HErXSh;2!;#91ni%##nRuK~(rweZpXldX|b`nuOx_<4E= z=5q!XM8~dOudRg}ijwqLwWd!Vi!Y@OU8C}BpP_#5B(nKAG6dA?>e|3%vrSKbiah4vJOmK{Lrcd569D5H1E_iWRP2>7baUuB ztzt>$C!kVV46}LfWLn=85CMj>F`2F1zqQ!Obc`7Gj!zw?=?9ePyCr&2Os$Q}_bfy9 zMRQqFnr`>+m*cjuFBDX-kMfV^v}ziTnlX7Rqz!35@8m6!D;&W4rtsY8NGAs<W`_Zj%# zFEiLe(PAL3Iej%V)VbtQPJjP3K@NjanKXd1va+tPPwu`iE*tmW>quXCObl?W+oq?F z?T%DXuogbvkVfah@&z$*Y13;i&UI40fq5z8oJ3Q7>g9p|<>BydRZo&_d8A4`LDtzlILcNbJWNB64al)&u&8Tfd2td z&h?^GQ>@O|XdzMX@6FKU3r^j6CE>kh$b&MGqylgD?R)LH+&ybGd|Zy_Z`H##iev`*>s=i##*bI5U+llxLphp9e1k8U-HWhM&9Gxoqlrsn`L*?@ zezOAK|B7NOuh;I5>lxY|!wB5@N%CgbhdebUWdzcRbBcO=F&~tXcyoFS5Ogls68ttD zBg?iIb@Vlx*b~u_^wyWjv!;)HC-tjFf4;4G@O1NuzN~8+!{Gq z@`ea|a|D=*EiFb`;C0U}LwcLwe4%I`QsI@ZL47g4f!&cmb_Uaoj7KAKLn28Uq?>`( z4dT#Od0j43BBW-ST#Mw*3-Cq2kApRNEqXmXRAdRtOscg}O}`m<^Xk5O;|m*Q8Rue( z>-W5&b06ftG&&O(a7^yvIboBCxbOhRqjX{dg44d!h-TY}*pFa_`8Y96oV*CA0eZ{6 zYp977RU;0!+NAcO$qOI($RYlcC;Gg&O4ZN^BcQ0Qd=8z-+-`~G(tutsUkEQhfY7>^v!LdIzzvR*r_GJ zZr|9&%Mb{EwUD7s;@BHWvIzaIF+FUSsco$c2S(L8;lN zBx-nb-;7j$valY!h9Ie}1e4q4<`S>#E=#%SXdQ~Z8>1sDhO=?f`hY|iVo&@yyUHRH z%-dDm`UjXPhed*iuXaR5#Z$4nJcV}kO)PEk!QM?Urgc4yddb~^QmRlH*$4`50=y?h z#=Rw)w>H&Mw8ZG(!+IP&LN?XS%ja=mGPYRnp|RQ<@Ui*SH0_q4%V? zu&wOGEB}m%B`b*n4S)h;>o3(mB*2m$hWRfGm-op&@39<hRU7b z-E0_|u%nvxr(zMgVar7E(_XB1QpOnDgUEy*uDo=#H*9%|-=|2zNq4)c89(X=UW80M z9w0-{GSVpezRBFWQj0xKXhu?)>Z5Ec+b>p+8 zkZ!!Pq32kyKkN~~C}my19(oe1$1nc7;XZ3S1V5lB$5<{!@!QtdhX9O8Yr(qo$0?+? z_0=qqK=NobzuvNN%J`^_8qv~G((I`!prCI{p85gd>Pu>qobmt+XIHiq;0-6#W+#L?u?_B-bXrp+KXG>lfHb39Z`OH&pJgY%S?$>Q&RBmBdBzAKQnh={I zIeknt{0MC%utaD~dXv;XpsJbUd{!~`0&q^4M><*5PuS)u4)U%vwT{pK=qbWL<>$)6{{X;i`h zhh5Tz-+KQc-rkAxCJ*pVrEMC9;rzsRN6z3dX?9!+_+yTWj{FBmr|XwE;MfNYM3w$O zjD3%vvaDUvCfektN7I~?S^-6eTdx=hI$qp|gX6jaYAEyz3vhXxu)a);9XF5ehoe&Y zSXpj!Yg5Vb09D$@1$+kB*+=TXQ_q5c;Fb5=UVh?fIb`SLf&kJdMHL*x-N(QLnL!jQ{>80ZyC-Omf zvF^(0fsK3~#z|#;Rv>njoeJ1%o{`p$w%I?V^oV_HicKO%_$%z|y^jY7%fZ3SNbkyQ>krM)0dREpcPiKSS z@Z<+m#->&NyUgsLS35Ojaa~-E`>z z^{K~T?8YL%-jZEM8=y|$-Bic4?yb@C-06?;#pE8>GtzW23Y+r4JqnIheBdbA53zR3 zzS21p-zZZv8rqTAW3ktMn}tvKg48cCVPa4pC~|{H&6sTdwiT9VI9B&Cj&%+Ay{0T2iRi zia0+gOe2F})LA^A>j#Pfh-5Elj1Br!id4x05pZ<{M_k_N2^NhT|IEmhp6QwuQ`&-f z9wUu??LLSglOyPA0u)l86qlFe#UKj5T>L|*0g03^ANLL=ULy&P?~$#`-+P-RfNx`B zTGSpUn~kIQL$#!EsCr|p8oLB;wQ#d0_RTd(|5z3^A%NTd>|!#|Z(^p$x&x`p>}jpW zEc;328;(Nc)}z}9!aD|{9iFJoX_!_!S!I9!M|oyIruXe$1^m`w^fPqm5#lj=OqVY~ zrLc7iSR*E;LQ4o>2G*xxz7lV;;EdsHh0MTK5yQA{>7vRJjr76p5uF2Zym}mDJ)EA5 z5ExFN4C_UHqX<+%Nm`;0<8-p3K&o^1bQ6k+4F%pmPEmo(3xn5(P((5Mog!XJyyXi` zvS!&50le#cTFH8ggl}C?Vvo-cuTBZJJ*hP)4S4{PJwkId}MF zjrXF@00jalyh->JhRtRB*0`0V0()C*ak)K>#dma*TJiNU<*{Kr*pvQrMh*z<8Fml0 zkhwS>N-@%tBtrUSq9?Hz#bPYqz56w57t-wtoX&T)G+oUSvw3&{8$()i zfRxnzq#GvB=9?R#x{=)xQpTF37icCDK5TRZILdNH;P-yU$|h!+x26uSnqRmO>R^$y za>#ryoUPuIdv#6&g|BG~ZKTfDE?{%iL3steI0^*e^x?hs;GRQwaNC0zfI_F#>d$ra&E3 z(B%n#*R9j{yN}Z8dx!zwJWI#^jg$Jo&t>8r8@tAmrNTW#fEnVA{h}0R7J3m?xg#%bs?+O|S_2;fe_rDnb%YC(6fwk&%Iu zKH>`OIVToSI981KMO%+}F0vU%#*+1CcyF)6zZ%R`%!c6#tSy8%J_Hd@s_+M@H%ME!V?6M@)t}?TYMM9CcbCWxiZ8+d*_ytqKp{|O;(2) z`<9EnAg=X&j;;25@f)Cwh&shvp@X&+O=N{f<;uodS3{x7jsinlixpjA{AFhk+bvriO;N9S4zJ!z@)+=mi-6ZAUr-o;OZr2iW}?r~XX zdTYaDGpMYI_Z=T-^{BZ2y%kL1TmxbhKTqP&L5uwpE^7>Z{cKEOjPO%4Uee1pz@6#p zHR@nm=f8)P0vOyQ%0r9by;=FLTZoV;toB~gHB*EoLGN?DdxT{xlx`$cdWWk@e2uz^DB!^xO65#to=>xu2+$+;#Hn=W968{8I^pDh%Ox51>WjWXpgC&RPeRxM^0{DwA*sG}p|FEvvy9Y3?itJ_Tr z+2ADmXMo`xmsgGS=cAeNna6Pp3-}DLj<03n`tQc{-~E{BfvfE`ZV&07dBy#zJh;An zj%eTl0sR zS@%UH$5Tg>`LnuB2nXF#@FF;P)rd+OtS-o8RkfF+HdiG{nYL-&+Wj}>pR|s^Z?}nN zW~;_-P3oPZY~k0Uppm0rRbxXJ+FLzw(9yb-kLGrdVNw2>letR%RN1~A^UqW zOmb>830EK@2Z)QzsGQxy6ES@}B6vSa!ua@>2Wqb6`+sTi4B1#kdt?h>8vh15w8Awc zW4={mQ6kzTXyw%LiL0S?uN}|Tj@!pZro!vnO^crj-2_(@A0V6ryn(fdm-xLR?iUx` z3U0&g?5Pv@i_RV)(Se~PW31h&r4AbIH#a2;=BPKH-zkRE!w;~etS`{<-w4}-c1etk zYKdDIiCFCrJ6XJZi@gJfZuP?LWuL{3hHG!%01|6k-X$FSEP?#isMH3JqN-p<>ElqG zNdfsLz4v&YxxSNh(~myU1h-PvVd@?-$PVOF$kPg8vVHwwN~^t-U68RX2e>!jjpH&- zF9~n+(<&n$SC4Ru{5}0=A|Uk(hOB=mdGT8BMXSE^ zk05Ac%^T9AqkH%ZRFNroHQssPv4|0VnLvY<*rq+)I}Ur$thKg5(pv71-*49`Jmg#;~|{-0ie%bsV-7p{^WN8_=n2wJ79DhPePoG?}55m8f6F{e=f#+Y-YdYq2tKHZaGYg+xN^Ehy8qB$vW zq?V_4EF zDOu9NUQ@_N@_EBQ2!rFCU4suhtj6_hlo3L}`>zX-c)QXX!(ZjFP!sgL(X2p8)eG-~ zT1s`5PkxK6mye`%ed(YcaEyoAz8n_}baO53pJ4czpXi|`K~M#eQX`^r6g(yS_L z^MFFeo|AzE#B}N;(YyPLJ6>|~$vfeML7S(6mvum@gr?z$FI6XIP`^+zVBa_BV6D0W z7a%Sv)HJ^IQH1e48Ch9Imb5qFn4wyf1K_*USo0-IG$F30?{$|z>lOzTqH28k;l0nN zC1)1Efmpuf$5WaoK6#idzYsa*dEvhRhw3#F6bxSe>E>U!t*c%&_x0^j;%cp4AOk)x z0)4&34v$#(+j0N!14ROec@<{)D4xzIEcu&6@#JS%{Me(B4PBP7#Y9squg;CA*&>H58^xHI9$$tj11E2uieQX*QUW;!JKd2;SYsi8#V<8B?4f-a>tSmJ9}% zC2CMRrb9 z_QeI{ISaW;?_=7T?$+CDGrySrYHU*;lW<(}3TBt%8J(V*e;R?m|MP*YM|YQNReu8- z8+*JFRwa0CpbwCdiB~P#!s#QPPJ_6%*#7m_%QIWed@#rDDzCo9iv0OcL4egaViV}i zJlz%)b8Bg}teD6k(FmHGMjHHhF0Uh}XPcE#4qVVdK}g{4?8b}a3XbWjvedEvlc%g& zVq*9hk3GRs8&*H}8_p_l7OSh8*nNO|ySHweF^OAjXz+GnzrdED*! zK-)hgzMpV!FI=$10mm`V=}i-^1ycs~vvToMBR760fVfvvS+{#XWB(beT;w8!B56dgu+!oHBP|p8<{lr9V>SsHVS$7EmQ@x8C z;>BLX+{H0>gJK6!N!~g$b*uh7N-v|L zO(xFs!AH{*PcG~D<~>&CF6hc=4_)(qL}%{{ap>x7#~;s@pWp%+0qhTlk!e~4_!$p- z2&}@!>}UdIC|6IiqC%#$@G24F?E?QUQAN zKJR6cY=f&Rz8h>!bJ2nRlY5@umv=VHf;I##C#*hiuG$vRqsGBrZN59gQ}NgbBi=SeEOkNZEGKL3Gfd(=e>;2v0!9Bqy^@QR(x6Vn{@i z=$m@B{tJhO#L`6e_M23qg1dl3U^@W3Z^p&{@9R>vu}EOT@88ut#M4K=Hpbec)J?Ef za>A6P+iY{4qlmrw|FSS5S;ZbeQR3R}%kUoBMGM-XPy_9woW&J(lg2meuxd{)I;dUd zmIX-4eKxokon3NijB*fHD=SsKe=Vy|eGE?zuwu>)MHBtbJ8gmcE4zYO|YfK>Fv|OHLDsknOrF`=@w* zIK2%s^TQm_5~Tdbg*W??Z-ts(t0W8r$?U%lH^yY5hbjd~+f4p}O$9bv!ea2{BEgWl zy2Fe2X06iinSiaI%ac=2xc|Cm+3GBG*KP1so2h!_S57P21>QsyY6V`N6on=mV#R)V zUdNR)MyiJICBrf0yM$=vEICz?j?K}-2VAKtEaY4tK0=qngg-7rE}~EI z1Tf*>MwOu*A*w95oCOYI3DWkBN`LR7qy6JXEMl zqeDM`b=#KSJtvItW>4F?YA6k%DYpG4Cv1jNweT4x2Gb8pViscA3w2pSLjA7%ziBeo zftQ8C%)s|=eYHal69ZE!Fo$@d&9gaCnkOnDZK%70!XO5oXg{>BPIb1FyQsLvk9wew z(-{Z@oA&T~?#1#EkOL})Yn!BRnmp!y9{yXlP{Q>e?b9-PCK}HpZ$KGPeUOaAfO=S} znLD8%e%DyPU~4PC2xNAs8p=i%CPM}P)?AAu--pdr$O6~1Umb(-;U6(QI zb_M%w_qu_pR!1VNGae%w&F4N@8#9d^N!wH}%%%~Oki=qkNfn9{DA58YS%>u<97w&S zZ+LaLapoUMLcZ+HG0{QIXOZ%j+RrSf?lUO_=8nzV6vQh`JRV9b`1utAS-A^#NiL{=!(YBT;vj{f==@unj% zC}#s;XXE;ECHyj^lllA`1tE1nXIbM*vX++f!Jy6UNBo2W82p7lmZ)Vd^sIEx(>J~W zX=Nn*yC1)1>$rPmfr=T!4I$vW%KdG zRt&+>(vZ7QXPv0CX|Vug@Vp6PL9VmGZm_kk;3I7|wE5K8GvDXpY54OSJpMS439~S~ zx9ke3#P%*XfFyxC!Q zA-x+mAIU~}e5KJ8vE<|Pmp?&_(_~f=m7A|#NRB$~Z{CIh?1razuf4?>d#Bpdo3lIp z?#5C(AKvdgdn16UHS-;QXF6cB`5@@{PvNoto&FdKbdX$(gP2B8)ukkc2#}yeS^nyF z8$Q;Vr!DmHnl99~K;9`CU(Uns41WZd*JT9{+x)*Hu+I=MT43nkIwEpit9&+>X4vjD zgrp4=2mUx@Kxx6CkD69Z{atwvBLNzzI^bey49$y*2pC)yMD$>3x1DHL7flhbmYDy? z;-c~w@vAnw=tHt(ao$HZglntDkTnnZwbp@+eIxQ;g65Ze zG9{BvSY6#>X6?B)4S9V*0yGY;5Ozymx$XsinXIn1o7+2r?pUl7g22|g9*M+w5{jUs z$hV$7mgSkQDqk(+V z-ZCm6eW&r&W0?fI;Hppkkpr*qSLIVbnpb<1*wi2CtCo!Dw4XBEs0%v4E)_$L?j>aW zk-KmTZlpjWUt6Qylaxey_ z#zm!@Ezem$_Z@DW3>)hew}&B1s^hPUAzs!L>%5E;KBW75O++ePuj5{8h}wJjm*WX~ z+~cLnBii+e)`m#HyEq=FO3l|W01S}Hg?K>@sdHd2td1Pxm#H$y$ zm+8R~L=%%ix^XHJQ|dtZqhId;Svs|Pk5HeBI_>#&b)enliT?qFZc+6STp7AD>iev9 z!!RS0h!7BZ;JUZa8u@m1R>bG9pom(y!8l?zQh^$TV)(|s$6eOX%9#kl==cP+YpRrE z4(i7W5*>-&4s2FF`R@%@zf%CnvcOk0r`VqpOz0e;EJ0O>@fq1q+lcmQ+w}v zw!yw|W+WjMGfQmD0{)t2{+{3lS>7(ys={XJtleT<<|7xDFgkof;6s{dE2ud%w3JI}i7ZWo6a?mZSO7uiKyVJwxgrfTT zm!zzWMd7W#6sX7Ngi0DFOH1BN@9&AkbT1qHJsE81WEuu9B&4%!ZkexdC4A|to#_XX z<66wfqZz$f8O|zS#qGStju(k#Woql&1%t!nmq)5! zZLC*3)DLSfy3IH67NO38GCB3Su6NX$$bAIuA&nYryo6b$;4;zZXCJ3Nkd%Y zyOwj9efHd<;NZ?HZZL4vcUr|A)rkFN8^~Z0J#0nbDPY&`MaE-@hNMI=K3tcA`t-ue z>V>M`a3a{3Sk)I7w?7qnsoGCkJlK9)FvnjFGLFuK?d{uYh}Or%hwDA%4+wI8ep5aDmUoS3egm6}1||;Hw9xTO;uvy${jcxd{hZd~l+jSB@78~N+4ACx zmyw-Y_x()n~axWpv^KBzLNL9*L_d@n4Sf*xD#epD& zAR-;ztCG)-#Otz!6#f>@9i8e8BD3$dsA}w!BBzL}o89q@C7)M*F;g`kbX_^(rM@1t zISPZ`jq`cQ;O{D|_Ji)l3mzQctqMaf)AXEKb>D%jV5`(4oT$1lWO3x<(`W-dmks2D zG$Q*0?ZQ)^g-D> z`EOlwCF4;sC7$b}=?j@e=y)766*-Tdg>>pdN6-ZzqZI?5oy3;y;4if^c3y2<@prFH zvH4bYlm?VZJ0n|jEmM!Fx^9KZ2aUH#6$m=W+XBQ{oIOLLKYUo)SDN16p85Uy*KdE` z$IQHxGD<<)_gxQm^ex&V%zUMXD?qJ6MZr&L{muc?EB#dw5@2pI)!y$btJ{d5nMIBA zhTgZw_>5cBe>8k4Jid{msaAJ8pO$^f9^Y-XmI8Ia!C9OL(;>wJ@1o*2@pzxr;h!43p%LZfZJ?@^SXxSH zVscR5I2SV*`;aT0yv@)`FTSU*B8vR_!|A6g58Ia)9j88T%zXL?$c$#E3>ksx+-qi}X$@|9R-=LUk)NGw!PgE(Uba$C+J%nE5-= z@y^blRF(S$2;#NByt;$;w*MPBSC!i*RpZf@im*chk~cMGtCbT{mtfiQ+?feLbPFGm zsGa9FJeZRF)p7PW*xKRi{Ny#W+atdqNFs|!s@|+f{7`uS=#~OvU)Qrn!}+R@&Cfm{ zo(@Y0PR$4w{ZoCFQyV%=5^n;I%q*Rw9P^xWRB(!A4-7C=SMOf4lsUb zeV6_&G^e|p0hFu%Sof|gD1Gf6otV&;kkYj~w%Q$pxy}7!$jXzX_nDk1d4rWd7Mh^{ zXTgPObIUN&NhR%34p&z?(NCndL0@s2z$~csMacf**^7l0q9thZF+H5%%>lT;afVcS z1;>}qXb#waWt~l5OH%0)!qh5pU7o}!um}0%+Cqg>Zf@whLKY*tmnWJENmz?Jos23y z(wJG}MC8VYISv#*5&}~0X~NCt%sMfU(Q$ps^n2(%mQaqq(&yhNr@aw?NB-s~W;O*d zF$RzGKViP{m8N&)@P_bgPL=`ICt1b5b8jy=Hl@~dZlUtqlI5eF)t1duk)+yM{TL1; z?w9YM`@y|pW{oZT7)D%&F)oq?Y=0Xj{L9-X-)6Q-CuF#jobpmIcPy){R+ zWsi;G5$)K9@YH*r@9uRMhp8dR0{fr_#+J(_V!6&d5Iezp8ss?8KMQ|9-T2m>|6GKp z4WjbVk&%IoiP~UhJ5KKp7mJADzCImP)MUA3M-px-{tCq4OKmTFFs-a0w->G>2`hc+ z32LLn^Kv6wks6?(U5f8AxlMl?!Pl!l9f=*Z>5=rwx8^!KIniCp>I$t*h|aJNR= zfRz!Ujm$l##s>YjgR!qfW7E2drbr-(FmHRUmG3TJ25;!T)II;nTG0C$It|tba6J>l zrM#?d9v#aU+~WI_PdzR3^c&u&i+*5bh|P3-iJF$A(`W0&iQqE&mrr>K#|2C-|Ay*! z4_QY9gORa};XLWzP(Va{I=Z`?`iDv2r7jLRa7Rh=qA5FXZ&;sW%qi#xF+`zHln%-_ zBy0aY-#^_R+`E_`Z^Ks*yZ2-OUh1DEg+KmzMS55kG@CQkV2My>2%W$3psrE+oKIE8nwkP z@%zT~9Brz~0N+V&|#A=`2~KF1Y07M z0yk}>s{GVey>&&b&9`^^4^8`3wj}8#Ou#G^=3s9v&c*okl7WeZD#Ui~an}UJ%C|P0 zh{8LN+?r&50YQn+{+xsow~9w-%tyjw3kSIH5^?ft3*JPMQ4n18kJS1Q?zeRByl?>s zNx0CEjZHMSkfaTBDO@C7@Kmvs0$e@qs{XIDB;2+S$0slZ`EsR1pEy<&t%;g% zbpvYYT!@Nv2MPmF^bV>SZ#5j)`Z+27EQ!Ttjd{Iv>a_fV6G!tC z4^ShI1LX@O?ZQ)=ehJj;7!P@{JHhpRu?8#Jz_>f9lMQ>vFg@%O15?b=p5KPg(*jfM z#v0HY{n51c<>>+^$SV2HB6eu2tWjYcD8AAH@ty2xXu+rQ*jlg^TWWOeW^&QY;>8d3X7{X@bYQI?xLiQiWdH?opMD%c-*d={hw>SxFx}wwD-s54@|4vzr4YC>0RjtmV6t z%LXlZ-ufKaNQ1v=%mdNS3OzPHAy)PsFUOQSUo&^m#>xJ;g);*l{5b6t$8+TdmxN?A z@a8Wa`8}#>AyHNER1ZzfjN02G++vjP;?L~sm2(;LJ|EKlU0@8L9xL><7~BA@xNZaL zhm53?)TTWcZI$~y_(eG5bL6#FSYq(l3cVjfhgoVoBDdd%BYA&G>JVNgvN3;-y*VAJgC-~6%VC51*y-n%b>upeJHoUq~jbtg`yH*OZ&G zD)PM=8t@%v8c1K{28t-{;pfjkW_otmYNzUyaibzmKs@g+kks%BrHvffLphqgga^DT5#a~B z`fMeYrDxKULeX<$2-Yw2Z@ywpum|g4kA$FLdi&*y6IiRm&-!BBoTqd+-Xn2k(^Pr~yHA)1s2Wt%a6BurI1s8kIc>-+ByIB30E^O4M!Pa4S6x~|7{ z@!!J2T`*urT2@f+hVWR1|4s& z)MMWfDA+Mc@r zF%{DN;QlL72~-QufX$d@M8O!>g8pX`0nC$}52+&=qVA-t?G%dybUQLc-|6FVB3L<% zV&t6lA)=Q<34;r!qg7T9l1l;hLUl^PcpD?I zh&BA(BaHm!0jC~*spY&-jMo3x*Hu46^?dESfV2YAjUT!j6p1AiK@cPa2`P~->DpaD z8k7_zq(Qnnl~h2wJ4CuWmUsC60WZJp+_`giXXc(c=XsuUXyBqm0Jll?{K0W50a(?d z$ElfxK897?lp5V;XdT~24|bte5f%MPr~LNi$F-i91Xvh~z#S#K*13fKM$6~h#fP`x z7x17vlJ;|NchV=v%YanoH|5z=EshGnL=pP`ke~6`6$X#XH+&S}uE5BYW|<(DpAz5jW2vNo z^K?r6oT)F|!v_aAhje~S+l9r`K*$qer3a2R>EAtKS`8SjHW@_fz~h++Q>x?ULDwgm z?)R&L6U~11oG$2TQoVT_41%b{fY+H;|8#qkwTb-##rIP!zc0Sw=Tb=pr~5#K9GE?- zW~X`5+`2H3X$)Oos|}{DRmIGa`NDA)ad3+-;^%iQ_Q$8m()U&309txR4SlCsvvJyZcIb;NC%lK;z6|j9p@nW;HgIZNNPjCJhJfg7Vayv?21%{$u=@bS0cVtr_)cbJ*4g)dnSv`7-Lr6ig~M~J3Zvio zI(^Z!^EANj1x8PwR{Boc2m(dW8m&jf)YMOg)5F{aObLb)m2%{>+Lhc}K;n&PS02)7 zWP~rxVIj|2+<+Z0c!{3^KqOyTt+smnItF7&;-U)n3&Fe$d{&*_va|5qrcKdXBJi|$ zRw^?rXCXwXFl{vD8DO{O47V~`(bj6ry(Spte3*gdK` z-`$nb=1EgEjfVz`Eg~{-gEOVt>@gAweu= za)Rurmt0*@+s}pU^ISi47*%o#VfGBSFHKW5d(KlPVchDnk`0-|F7CH=WV^dG$=S^| z3Ot{K_m3iIhWzNLYetH}j&|*=An!>6N;nsnrqA|@K{*nSCZA>!q7DTz^1bjttWxZe zi*peso#wX}>(u7}q8xLFq~(#8X&`@*cir~47SCD|fT9vGOShN#W$XN(xloSVpD{-| zKk_O$fl;8ZUTzYGx9|WyjY|MnLWv0U=!P?1?GZ(4bLwPYHRE*0P4# z9>~Zv)@Ypq92j#jS*(lQ(m@W0^~rG@ZX)RI0KkfGBg*^1rLB8MFP<|FTM8 zj4utFB3@9QV193PhQVK-OGq>r<+-&zX)K$<5g|sPbcG9c-Hk$9!r5l+bTk4IS%3;d zedouM>|6{K%JT!Tdjc#?i=Xbzk@&)y7uo-2c-_JV=3yPFG(je2E33X({=kBegn{m8 z8#Q9my%?amdwv8P{cj0+&zDb$j2~;Vjp<0`D?1ar2xcg7lji^$hK-GX0~3-(Q+I!O zs2#!jKBUGaZP1(UCs=`TC3uTsEiDsYZU+fSl0XXoc~do_TMGrOtNmI9L(k$`Azmrovm?v02-Fw7w)DGEh|exJfsZ`tP``mKZZ<6 z_pGVRvy&`$zQCP($W;0rBHR){EDOw*nOly0759^LR|t3_>AAag*5U{7YlWQ;9p|qa z8R$EIAPUpw=O-hVj|*%7Nf*8|mtbk-Eq}^%H^cs=V?@XNUFW*HfYt&i%@+@;4{e$1 zvgQtU31&x=yeg5WneKa>G&D`lyNMXL=w!Kp(VnCdtmqU1W=>V)*ywC9g6oka;qrc4 zgU7|1Z&!fCctJc7Q5%)76Q+^xIWG&x8y1#^pw2HQ`|fQ9_5qjPPLfw0{3b^ok^UVw zDxQ~4lII)pl8Cdrba)(}lt_U6b36m8e6ql5FsnTx`8Jf=4Nei?{8N9;*>mn7(LK6v ztluAbpWANf>lgfJ>PJ#fC&hQva@;GQRcPa+K#KSN6|1d#8OSelDK<7<&@m|?R#iI? z;hpMB>+MZBuWXzg?RBwLWG#;Wv7>=SpUMJ7NV&OMyUqhbLK!)Zm3JVm%m0?;4=Z8xiD;KS`rW^(eE9gmSHh*uVE3|` z<%1RZ`f1CDIcTN34l=9m)+K8oI%>6h1?=0 zA(;m^yeemv5$fKnL=luW@6!W}GUdi}E(Bjwq*ZKhYo8u4dHgqDTH_ZNO|aBj(4{Dg z1Ok-H>7dg`z^%vDMe6;6K)}eog18AS;@+sK(ebzVBm;>cBO>wdM((Q53v6Cdnp@@v z;7C{2-ys(GG%$UmVjL=c|B|lJcRH|Y6eP*N+Oob8s;=%46qIIaoDq)NQKtDb`0V^r z{|aWjbS%2H6&G3&Z*l$VGmU`oFlTS^{5qpo1S;>BS$~%$IplKG$hC*X7A?-y_`~Es ze{nPUe~6vW96;34)*NvemDTY1qjR&tF}INSYyDl@1i*AFCi4a#(=yU`EY|c$W6Nd7Dc2kLg)&pI6a(Wv-W{bc0~8m<&@R81Af zj@fjbmcG%}kqQ5uZ?69JnQLcIB*55MaI>y?dfKlH{79MT4Vxgnd;bG_Zg>tW?0&ww z2@I2gSniwf@QdT_2f*@eC2T@NE#QBJZ!Tg}mW#Z-rmS5NpZ>FAzH&)Ea{X--z3wwTU9DnUtV8agrcSS;^Iqm|JK0l~ z`rM8nI1a2;`*fb*y8erY4{ed1!-4zYzm_*EI`{bM+CMT#$0AyNP4NjbI zF;QV*+9PBAd2RtL9g@|G`*y|_J{h^Wl<@wS5}Zhin*xqi2{K-^Rg7YXIm?&B5%FgE zCx64N#rZ%5Be3fAINyeSgBvjvqzXz7zAlHC+RE42KMeSmxe>^y&Ucj>!}l2C|ynM{tvYQ%cir$H^zI!CWsk5nzFNdfGGwB_MQd|F5vj#{+fTLQU$Y#q&_Nu=Uc7L6PAfB%F~f$!z3o8%K!rZD%vOVFZU{d4xW7%5!V(y zF(Ja-MbtGwhCmI0Ih}jTK!Sw6J2ZQ2HE^Nn<=M$fD# zwE!Jw720{1)^66KM7{>hRek+>>g!!ry=gYE;1jn4Sz3R6%Jgh=t|g&ensT&?;CqoF z$5z6f!cOJiVgmz5)wOc?K|0sx%WC`X1&n|qfBCBZYk6K;v8XEn*6E^0lJOd`YG3|p za|O`pvTW%J1SpA!7Z_f98*cGuIP6)WKVebSP4%}6*n%^<$oN=I=L{BBfVc^3(Fg7l z+U8SQM!DYt33hOc+bkbuWo5GxJ$5!g8G%QtIjleKhW>ZoOE0##pRa+cgN#78JJ)Lme+C=ieH-eMkIdp`Pzo-&0dAgc+0`)SME66AX1icXaf_%i)@uk zm5Jol{CA3pYp|VKF)kShfZ6>!|2|ajVK#2$uJ|W5Kbzdz@C;?66izp6;wPW;H-pmZ zTsJm|l-Rz{_^D%{4k^tB5+5GtfGy6`(>nd@O`aP~QVxIvx&udKuFGT{uI}%36(n8r zl_dhq%7E4rw?_=<54c9H6;HX3js|^YBaNO}rJ6EoJ$4kV<9~)R99L7Sd9z9D5vJ=x zRAO|uts09xh>#X={sTZ&UJeRL(I0(aWE!-WrnnIavNBm&O#tAX;Bk)HS<>UX!#`UG z%s?1~%bPGr31WJ?g30R*8$Qe3v5!sw@LvL8{=_UFr70K1{Evky#qTj*Lthv$L%yAn z?gw|=khMMCZPnCz#lQTwLVsdDoS;lsulg38<0#Oj)L>Y?djR5^Cz|JK?FU|s@3MKPsqRk2Xe1a!Z^#Wu_kvIby zTfbOQ$n)G04=V;Bk5A2p34!0v!r+Re) zT>At-*6AQuCQ?=Sdr@kD)xlEaFTf_gkzZ&8Xa<08UXBX@vauBS<+CY5Tf3cV{vh`O zH+qd>DO;a|dpb&Tl9;4K5~RLj&1YX}ekNcAN(!KHPt?x^302i%`K3*oLG6g66$wIY zB9h4MjI6f=*?(yprz{TqF(LkipH=o$V!wOFU%n|`V#tt=rB+?U>8^Ybj(iCymau$s ztq?>4&hm3QY)FBPq-8nWexM+R=SpXxCl2{rsLdc^YX1i{WVqKSnW6J!9q^4g04Jgj zVmX(&12Oz5UTWwZEBWctks4*hgC=x-AoMge)=)PYmiB@FAK^8ZDtN1ewv}4%f&zPW zboU^wrD;#O{;-O@f1wUK*PNS#nFMrDZRqYlY$;k4*z{Rb@g1oqm42Ae*F7I7h1M@{ z9)@LLvTs3U0YFClW^~0sgREt^jNm+OZQJQ3b>|~cCl24}lrqh|ZGZxSGPr#I8{8j= ziSk+4yuv?nV4^?-6FUO*$RrFPFe+z>ZYXh@@a8?tm8nD&P)`tQN`eGh{zz~B(GKm# zK}VjeJMeaT3S_4+2g8fGKhRzk7w)bI3jxxP*eMi@(w>flTF0|k8b?dL0B~47i&vY~ ze4{mOLA4)s53;>HEAOd2A=LhJhOw*qej||C2=_v%o%PuF`;>HBw;vobPQ9{Xyx~1I zFP&)c7iSkg&WPq$xMo?R$OYF26n0YT41{n$LEcRFDJRl5&&p62K z46d2oK48kJn;H^2^OnnafPZ_z6kM>lz2Nl_QpZ-46KPLV7uidR3KzLZ1S84g+=C4K z*uSY*hR6yMpQe8C7P&~eZ6~+~Wwpktiis$s>4uE^rGfU8+jgRRP*@OqLU@|sc{n`4 zi`Y5(#k}TzfXUhq7>e0L=ogMeEV9f}oyebWXblOTOKA5HswMsgPllTfQ3hhWK3MxN zMG)RW6qF8LL&?`5bVA7JN+?})FkCKHg2XlU&zH0%oVK+4cqwpNhj?`2&zXTU}? z7GK&^*?EuEh-%fEkJ9&&ML<3B0%{eAZf>=kD7AZaVVWlD)bn(>{=Qq%+*$f6Q5#v1 zU20Ra##AoFUj{7o_xE|tMf#isP{J}WO9j=q(hBq;4wggCUreW(^+-l*v_Nl1R@Hh$ zcWMYa50TyC|L!XWe}9HFB*6VAOP=pOdb#C{0f~>GS;F@sX`jsz{~RJt)n?FsXDYt# zLT!HR9#uTwV|M@!!+B<$Kq|7Y&&=MhW|Yh2xewo7$pWSxBVoGV^ie07D3U4%5~|{s zAATWnPt0_Y&lOFdDG6~z9<^4(dR`P=2?s#ae=g8eCG*&Or(80T*c+ZCG_@}rY^??? z37uh+7cMUvK34H+ZYpQa=Fdn0R$N_EFz&%c?kQThMa}uWURrQBg|}X8`Lt!@l$!jE zYob|1q70!XG?{d@b;N6T*?YQfoK}30C}*L$a!H2b-E>7AYj2m%Q?U;YhMrC5rT%z! zJzIKr;-N2{06o>(i3d~DuLKMROVYdfaEGmzV3m@iQzE4e&r|WQLvylQYM=?W4X$aX ze>A004nKYiL(z^{=4GFZC&hSrg3PmNx|P4FCg`qX_x;9DZ>Dc8xa`?$wPqa1z!&La z0JG6aZBfZCpClyOQ~jx`7lVg@GqS=iuB#=~k)yEpR0Qz$(3<6$e%=~Krr#Ej*Ipm? zUUFKzq5II=>-Sr4uj#U2KV%J4Pb*;OVb&UHn?c{BBBhcuHG9Klm%qqDi<4hVCtCdy z8#mQI^^!BLn3y2T0>+X7P#jG2_ahvolNSdSK)Tmp$7N{F(e zS+lz8*SPD7rK@x)Vi?mtxyHF{PCf>_Dl6j+qhjce6h=g5#N9i3Ew$%A9_Eb;Sf+Mb|L zEwC#$j(bq30pIxz7K6S_h2hmo;+OUk1u`z<8 z2L;!sVUzS-vC&wP6FG_6p*yuX8qR6e!!4}G6>lnNjAQ%do!26g=jwmoef~@ONc)@iOwxvTqZc>R z4K))Fi!VPew+Q!yS7s?_xa%cMRdMCYF0nm{t=`dw`_E;t<=|Iqvr=;Dbq7vAQ$|Tw zq263X{by;LOY~Ab)3Wg46ZH-uva~=H8*gHU?|J~KRyAvn(38U#`{gs2JH6|ebt^Fj z8p1u3v#)o6E6d_;@h3TDbmvCqpBNS*4e?pV;cvjT@+^$JzEH}Y^Nj!6XQSKbdu{{2 z2IHshN+^y_=0u72EyBnw)ZtgR<+<&nn5`v!c-_OVJLh{$J zlZZtopNF4#ll-lR{U}V?(DAYz9c?kBu6Q^8=9-ccxx4!~cR3iD^Pt2;IQluLRKCpq zqG0cp-taHlu-D9+)E%Zz#{|PYQS7g~x*xh(CATgW!t&nN_=Rd^K0gd`OS6)y9y~M< zC%+xO=n~DHgszdKYAW2FpAzl6d9H)LY`{4h?!wu)!SD%ri@{`e6q+iTE53Vd<+957 zDp5bVgLLO%xC3gRq6>qn2n!wW7w{chRwfY_4R0!9ywb^AM~<%Di1I>;r&ndt7b^}= z#5m4hKoL$G7o6gqYi3<)R{iaGvxu9Lzf*CA%$~2_N367e_HWAU5XGdpguSYQzLZa5 zPEg6p)Z<>k&9t{JjYRJ_a_&vX^Ff|Y*CV9JY-XGc!FeR}pe>BUd22y)YWz^N>3m0A zB&nITOE^3fm7C26FLvFr;bh&HD@cV;=E6eUqw1tDW|=rA*`LjD0Sn3 z6R=}qU9A#txAK!*mCb55{`ge6@w-K27wL(Os@j-5@3|Nl6rLLQ{ywK&tf$EWv0i#z z9)~v>sS91Rdm*=wB9Bg`%xB%-&Xi9=**lQcwQ(!Oa|rjMFQAo3p{@SY(t9G5p*Xg& z#TF<1#3`o~V(;Gw{k-;WzWr-WR)}?W-|(V4Hudw7)nEKaeJpI1QPi3ek2%Ntx{rr8 zPpkqb=If0H`!0VoGoG@F9-iOHf4PqI=?yL5t=8P9$EotRXt{7>)UxolnBAnv$x(rR zyH>mBBXM%RRo85ugtT2FsDvg2^s9{KI3k>Mz6AWTj ztSjw@w2=B^7AR7owIrkh1FBNQQ0K?GRCRKL(wZ??As?knh0@c&3M?p+wDbn0DM+{h2GCI%zOk~h(?K<9LL@FL(vfy0nsc%Dlet}5{?g9kyBtB>R`tSuE=m&@h-JNsE@SqyPmdI;sH z+V|&qOhf^g37>S{2TETJ-Z=jf@uP)MxL@?2amMN!YvZ`t9^Tc z1i3?q)4P@T(+b@cW5S9H$~g4^Gi;ePxK16vGi;XiSoY*@C2`bBBlBczjEJOQwmi(8rA*2N@vj7kHI2vs;8G8>OjI z2{o`FXd(m0EcW+j!|UfX*Gr7|uxK!eZHd!Z%#IlUB8ehZi;6&>@6jrJB{<8P1q>BJ zHx!l0t$3(H`}mu6&u=?-KiOA`f~_++n;z||xgYte>zz#gGf1L^*=kg>ZN2V8XYN-D zTnNq^`9gZ6_iCyGZJXbe>BwyP+K&{~pL*T*LD0)=d)b{af-C+H7B9#+I*aOkdu-o9 z;eL1l=J!|H-#ZSlsJBIR)hB}ehc1^*I9&CG#9+iP2K~d^?!jEeKR3G@^;ExVvqBZ0 zj%Q*-Do^%Z#Bv|Yx82vogh9B$RrS6O&(>|Lhhk5OT*)wqptyV$s5n{jb>&b3x0JZ3 za7jZO^C%es1dY``mdp=!21RlDH=YLs1lWuIeH9P(yc%L>SG$@GUN`uQ`flV$9eojy z@$7 zUyA?hPc0eYUwGb{1S=Z*8sRHCZP>`P9;)2mA}&;HP?pC2>8QtXm>ck~;7DdCa z<0^*KB}eyvfRWp=$ zyf<4!#~l}PH6(7-!nrQ&7CU&;P``As_la@%&!3M1_wYcyu9%*G$H-F#j%a0v`jq&& zS|7IsXML%;&-IY9q$g#hXbV@KccvYMhSl}%c7w$J8OiqUCf;%>1lbZ00pRTtT^hE- zjB~`#@s2Ohsdr#%#i-Hk+MV@K4N)!SV_d=@Lckx%vh z;+*hDy1`zs+4{p2=gomlNz0Pw$)_vZ>AAV$WyO2zt#i!+1z`Vbf%& zbzbXRt%oM`GI4yUm_LElGX1XP(u+!rz9ZFvaWIuIz?)t4w^p2&sEZQ^q7n>ADNu<8#N%>f1mSnd-HT7q-G5zrdo`=z1@NhL0KOf zd+D!3;C)E^W`tY68L}VNZ{TCUC;R0%?^k1ELdon|Df_Mr>o#8f?oM&-#p&)Ky7^6q zsdkAb76fl2H`E!fz>9n#HS&}B>8Zm)`ti~8gj8A&?ydS!TT`?S&~4sfc7CqWn0QU! zH8?jnD^Xjc)70R`xmp?ogjg3~KO6j)_v`B;)jrm@+%rG=b1Pks@_H<=(T{sC;zvaC zo6jO?Ww^U8i?e+7IlHJbXAPZ>cXnQ>xhK>wKOUXyB*cK=%j99j8tW17VmbTRW0bRo zrfd!AQr|^p&Nq9!Tfi|HDT_%odd2UGb5)R)Y4UlR!Djd)J^jYiVBW_Qx2g`hM`d43 z1r-H@i!mX%he3brrdPO3c=7u{|Dnm`^2tX0;@y{5h{)gjh~=TL^XDcLcM=MoH(bp7 z9$tL%9Gf{3ehf_DEAwoSah`g9H95>Ua-r*5r-iPNc_>v`AaJA9^^kT>iWu~HXZ|KK zcPcecaW7$G1VeqNL&r$QF2c7%xe$jC0#$t9rnY#T9N8R8TFyT1;ZN~Csb8-0ouR78 zZlHoe16zehL6Ki`H|ejkv4Qh?IX!Nl9Ok+6{MBmc(=kj4CE=|$)KN4yQXf#L2j5lI+F-NCkl`_bwYqMzzIJ06iS zcnpJ>cwW)GFFX&MetqVZiHTHtdmk0UtyBEam;D>!=~!~b*th7B*)<6mq#jt9mGo`7 zF>!78Jb{~CEvVVJ^_2o2jN>$0XKtd(OmW}&X_?Vsp}WSAS!6LOcx9=|gyw~ZicUWM zjHJZIiH$5Cc=}%oRCqi(jy{(!wn04U^qhLeUg#oMt~mO(1QQxKDtsFh>C&a_{*$n%8 ryJG2=DfyD~H%`D=_W%5)kzdQ?KRqRiGcGo{?WXkPg+j5MvH$-8E8%sQ literal 0 HcmV?d00001 diff --git a/artwork/android-terminal-emulator.svg b/artwork/android-terminal-emulator.svg new file mode 100644 index 0000000..4d13696 --- /dev/null +++ b/artwork/android-terminal-emulator.svg @@ -0,0 +1,375 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..30baf09 --- /dev/null +++ b/build.gradle @@ -0,0 +1,31 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +task wrapper (type:Wrapper) { + gradleVersion = '2.2.1' + distributionUrl = 'https://services.gradle.org/distributions/gradle-2.2.1-all.zip' +} + +buildscript { + repositories { + jcenter() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0' + } +} + +allprojects { + repositories { + jcenter() + } +} + +subprojects { + def androidHome + + if ((androidHome = System.env.'ANDROID_HOME') + && (androidHome = androidHome as File).exists() + && androidHome.canWrite()) + apply plugin: 'android-sdk-manager' +} \ No newline at end of file diff --git a/docs/Building.md b/docs/Building.md new file mode 100644 index 0000000..3d04c68 --- /dev/null +++ b/docs/Building.md @@ -0,0 +1,101 @@ +Building +======== + +To keep from typing "Terminal Emulator for Android" over and over again, this +document will use the abbreviation "TEA" to stand for "Terminal +Emulator for Android". + + +Download the Software Needed to Build Terminal Emulator for Android +------------------------------------------------------------------- + +TEA is built using: + + + [Android Studio](http://developer.android.com/sdk) 1.0 or newer + + [Android NDK](http://developer.android.com/tools/sdk/ndk/) r10d or newer + + +Telling Gradle where to find the Android NDK and SDK +---------------------------------------------------- + +Android Studio and the gradle build tool need to know where to find the NDK and +SDK on your computer. + +Create a file local.properties in the root directiory of the TEA project that +contains this text: + + ndk.dir=path/to/ndk + sdk.dir=path/to/sdk + +On my personal dev machine the file looks like this, but of course it will +be different on your machine, depending upon your OS, user name, directory +tree, and version of the NDK that you have installed. + + ndk.dir=/Users/jack/code/android-ndk-r10d + sdk.dir=/Users/jack/Library/Android/sdk + +In addition, if you are building from the command line, the scripts in the +"tools" directory expect the environment variable ANDROID_SDK_ROOT to be +defined. + +On my personal dev machine I have this line in my .profile: + + export ANDROID_SDK_ROOT=/Users/jack/Library/Android/sdk + +Installing required SDK Packages +-------------------------------- + +In order to build, in addition to a current SDK version, +TEA requires the Android 3.0 (API 11) version of the Android SDK +to be installed. + +You can install it by running the following command-line script: + + tools/install-sdk-packages + +Or you can run Android Studio and choose Configure > SDK Manager, then +choose the "Android 3.0 (API 11) > SDK Platform" package. + +Building TEA +------------ + +You can build TEA two ways: + + 1. Using the Android Studio IDE + 2. Using the "gradlew" command line tool + +Using Android Studio is convenient for development. Using "gradlew" is +convenient for automated testing and publishing. + + +Building TEA with Android Studio +-------------------------------- + + 1. Open Android Studio + 2. Choose "Open an existing Android Studio project" from the "Quick Start" + wizard. + 3. Choose the top-level TEA directory. (If you installed the source code from + github, this directory will be named Android-Terminal-Emulator). + 4. Use the Android Studio menu "Run : Run 'term'" to build and run the app. + + +Building TEA from the command line +---------------------------------- + + 0. Make sure a file local.properties exists at the root of the TEA source + tree. Android Studio will create this file automaticaly. If you don't + want to run Android Studio, you can create this file manually with the + paths of your local sdk and ndk installations. For my machine that's: + + sdk.dir=/Users/jack/Library/Android/sdk + ndk.dir=/Users/jack/code/android-ndk-r10d + + 1. Open a command line shell window and navigate to the main TEA directory. + + 2. Build + + $ ./tools/build-debug + + 3. Copy the built executable to a device: + + $ ./tools/push-and-run-debug diff --git a/docs/UTF-8-SMP-chars-demo.txt b/docs/UTF-8-SMP-chars-demo.txt new file mode 100644 index 0000000..3a2e48b --- /dev/null +++ b/docs/UTF-8-SMP-chars-demo.txt @@ -0,0 +1,2 @@ +These are some characters in the Supplementary Multilingual Plane: +𝄞 𝄴𝅘𝅥𝅯𝅗𝅥 𝑥𝄽 diff --git a/docs/UTF-8-demo.txt b/docs/UTF-8-demo.txt new file mode 100644 index 0000000..4363f27 --- /dev/null +++ b/docs/UTF-8-demo.txt @@ -0,0 +1,212 @@ + +UTF-8 encoded sample plain-text file +‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ + +Markus Kuhn [ˈmaʳkʊs kuːn] — 2002-07-25 + + +The ASCII compatible UTF-8 encoding used in this plain-text file +is defined in Unicode, ISO 10646-1, and RFC 2279. + + +Using Unicode/UTF-8, you can write in emails and source code things such as + +Mathematics and sciences: + + ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ⎧⎡⎛┌─────┐⎞⎤⎫ + ⎪⎢⎜│a²+b³ ⎟⎥⎪ + ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β), ⎪⎢⎜│───── ⎟⎥⎪ + ⎪⎢⎜⎷ c₈ ⎟⎥⎪ + ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ, ⎨⎢⎜ ⎟⎥⎬ + ⎪⎢⎜ ∞ ⎟⎥⎪ + ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (⟦A⟧ ⇔ ⟪B⟫), ⎪⎢⎜ ⎲ ⎟⎥⎪ + ⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪ + 2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm ⎩⎣⎝i=1 ⎠⎦⎭ + +Linguistics and dictionaries: + + ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn + Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ] + +APL: + + ((V⍳V)=⍳⍴V)/V←,V ⌷←⍳→⍴∆∇⊃‾⍎⍕⌈ + +Nicer typography in plain text files: + + ╔══════════════════════════════════════════╗ + ║ ║ + ║ • ‘single’ and “double” quotes ║ + ║ ║ + ║ • Curly apostrophes: “We’ve been here” ║ + ║ ║ + ║ • Latin-1 apostrophe and accents: '´` ║ + ║ ║ + ║ • ‚deutsche‘ „Anführungszeichen“ ║ + ║ ║ + ║ • †, ‡, ‰, •, 3–4, —, −5/+5, ™, … ║ + ║ ║ + ║ • ASCII safety test: 1lI|, 0OD, 8B ║ + ║ ╭─────────╮ ║ + ║ • the euro symbol: │ 14.95 € │ ║ + ║ ╰─────────╯ ║ + ╚══════════════════════════════════════════╝ + +Combining characters: + + STARGΛ̊TE SG-1, a = v̇ = r̈, a⃑ ⊥ b⃑ + +Greek (in Polytonic): + + The Greek anthem: + + Σὲ γνωρίζω ἀπὸ τὴν κόψη + τοῦ σπαθιοῦ τὴν τρομερή, + σὲ γνωρίζω ἀπὸ τὴν ὄψη + ποὺ μὲ βία μετράει τὴ γῆ. + + ᾿Απ᾿ τὰ κόκκαλα βγαλμένη + τῶν ῾Ελλήνων τὰ ἱερά + καὶ σὰν πρῶτα ἀνδρειωμένη + χαῖρε, ὦ χαῖρε, ᾿Ελευθεριά! + + From a speech of Demosthenes in the 4th century BC: + + Οὐχὶ ταὐτὰ παρίσταταί μοι γιγνώσκειν, ὦ ἄνδρες ᾿Αθηναῖοι, + ὅταν τ᾿ εἰς τὰ πράγματα ἀποβλέψω καὶ ὅταν πρὸς τοὺς + λόγους οὓς ἀκούω· τοὺς μὲν γὰρ λόγους περὶ τοῦ + τιμωρήσασθαι Φίλιππον ὁρῶ γιγνομένους, τὰ δὲ πράγματ᾿ + εἰς τοῦτο προήκοντα, ὥσθ᾿ ὅπως μὴ πεισόμεθ᾿ αὐτοὶ + πρότερον κακῶς σκέψασθαι δέον. οὐδέν οὖν ἄλλο μοι δοκοῦσιν + οἱ τὰ τοιαῦτα λέγοντες ἢ τὴν ὑπόθεσιν, περὶ ἧς βουλεύεσθαι, + οὐχὶ τὴν οὖσαν παριστάντες ὑμῖν ἁμαρτάνειν. ἐγὼ δέ, ὅτι μέν + ποτ᾿ ἐξῆν τῇ πόλει καὶ τὰ αὑτῆς ἔχειν ἀσφαλῶς καὶ Φίλιππον + τιμωρήσασθαι, καὶ μάλ᾿ ἀκριβῶς οἶδα· ἐπ᾿ ἐμοῦ γάρ, οὐ πάλαι + γέγονεν ταῦτ᾿ ἀμφότερα· νῦν μέντοι πέπεισμαι τοῦθ᾿ ἱκανὸν + προλαβεῖν ἡμῖν εἶναι τὴν πρώτην, ὅπως τοὺς συμμάχους + σώσομεν. ἐὰν γὰρ τοῦτο βεβαίως ὑπάρξῃ, τότε καὶ περὶ τοῦ + τίνα τιμωρήσεταί τις καὶ ὃν τρόπον ἐξέσται σκοπεῖν· πρὶν δὲ + τὴν ἀρχὴν ὀρθῶς ὑποθέσθαι, μάταιον ἡγοῦμαι περὶ τῆς + τελευτῆς ὁντινοῦν ποιεῖσθαι λόγον. + + Δημοσθένους, Γ´ ᾿Ολυνθιακὸς + +Georgian: + + From a Unicode conference invitation: + + გთხოვთ ახლავე გაიაროთ რეგისტრაცია Unicode-ის მეათე საერთაშორისო + კონფერენციაზე დასასწრებად, რომელიც გაიმართება 10-12 მარტს, + ქ. მაინცში, გერმანიაში. კონფერენცია შეჰკრებს ერთად მსოფლიოს + ექსპერტებს ისეთ დარგებში როგორიცაა ინტერნეტი და Unicode-ი, + ინტერნაციონალიზაცია და ლოკალიზაცია, Unicode-ის გამოყენება + ოპერაციულ სისტემებსა, და გამოყენებით პროგრამებში, შრიფტებში, + ტექსტების დამუშავებასა და მრავალენოვან კომპიუტერულ სისტემებში. + +Russian: + + From a Unicode conference invitation: + + Зарегистрируйтесь сейчас на Десятую Международную Конференцию по + Unicode, которая состоится 10-12 марта 1997 года в Майнце в Германии. + Конференция соберет широкий круг экспертов по вопросам глобального + Интернета и Unicode, локализации и интернационализации, воплощению и + применению Unicode в различных операционных системах и программных + приложениях, шрифтах, верстке и многоязычных компьютерных системах. + +Thai (UCS Level 2): + + Excerpt from a poetry on The Romance of The Three Kingdoms (a Chinese + classic 'San Gua'): + + [----------------------------|------------------------] + ๏ แผ่นดินฮั่นเสื่อมโทรมแสนสังเวช พระปกเกศกองบู๊กู้ขึ้นใหม่ + สิบสองกษัตริย์ก่อนหน้าแลถัดไป สององค์ไซร้โง่เขลาเบาปัญญา + ทรงนับถือขันทีเป็นที่พึ่ง บ้านเมืองจึงวิปริตเป็นนักหนา + โฮจิ๋นเรียกทัพทั่วหัวเมืองมา หมายจะฆ่ามดชั่วตัวสำคัญ + เหมือนขับไสไล่เสือจากเคหา รับหมาป่าเข้ามาเลยอาสัญ + ฝ่ายอ้องอุ้นยุแยกให้แตกกัน ใช้สาวนั้นเป็นชนวนชื่นชวนใจ + พลันลิฉุยกุยกีกลับก่อเหตุ ช่างอาเพศจริงหนาฟ้าร้องไห้ + ต้องรบราฆ่าฟันจนบรรลัย ฤๅหาใครค้ำชูกู้บรรลังก์ ฯ + + (The above is a two-column text. If combining characters are handled + correctly, the lines of the second column should be aligned with the + | character above.) + +Ethiopian: + + Proverbs in the Amharic language: + + ሰማይ አይታረስ ንጉሥ አይከሰስ። + ብላ ካለኝ እንደአባቴ በቆመጠኝ። + ጌጥ ያለቤቱ ቁምጥና ነው። + ደሀ በሕልሙ ቅቤ ባይጠጣ ንጣት በገደለው። + የአፍ ወለምታ በቅቤ አይታሽም። + አይጥ በበላ ዳዋ ተመታ። + ሲተረጉሙ ይደረግሙ። + ቀስ በቀስ፥ ዕንቁላል በእግሩ ይሄዳል። + ድር ቢያብር አንበሳ ያስር። + ሰው እንደቤቱ እንጅ እንደ ጉረቤቱ አይተዳደርም። + እግዜር የከፈተውን ጉሮሮ ሳይዘጋው አይድርም። + የጎረቤት ሌባ፥ ቢያዩት ይስቅ ባያዩት ያጠልቅ። + ሥራ ከመፍታት ልጄን ላፋታት። + ዓባይ ማደሪያ የለው፥ ግንድ ይዞ ይዞራል። + የእስላም አገሩ መካ የአሞራ አገሩ ዋርካ። + ተንጋሎ ቢተፉ ተመልሶ ባፉ። + ወዳጅህ ማር ቢሆን ጨርስህ አትላሰው። + እግርህን በፍራሽህ ልክ ዘርጋ። + +Runes: + + ᚻᛖ ᚳᚹᚫᚦ ᚦᚫᛏ ᚻᛖ ᛒᚢᛞᛖ ᚩᚾ ᚦᚫᛗ ᛚᚪᚾᛞᛖ ᚾᚩᚱᚦᚹᛖᚪᚱᛞᚢᛗ ᚹᛁᚦ ᚦᚪ ᚹᛖᛥᚫ + + (Old English, which transcribed into Latin reads 'He cwaeth that he + bude thaem lande northweardum with tha Westsae.' and means 'He said + that he lived in the northern land near the Western Sea.') + +Braille: + + ⡌⠁⠧⠑ ⠼⠁⠒ ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌ + + ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠙⠑⠁⠙⠒ ⠞⠕ ⠃⠑⠛⠔ ⠺⠊⠹⠲ ⡹⠻⠑ ⠊⠎ ⠝⠕ ⠙⠳⠃⠞ + ⠱⠁⠞⠑⠧⠻ ⠁⠃⠳⠞ ⠹⠁⠞⠲ ⡹⠑ ⠗⠑⠛⠊⠌⠻ ⠕⠋ ⠙⠊⠎ ⠃⠥⠗⠊⠁⠇ ⠺⠁⠎ + ⠎⠊⠛⠝⠫ ⠃⠹ ⠹⠑ ⠊⠇⠻⠛⠹⠍⠁⠝⠂ ⠹⠑ ⠊⠇⠻⠅⠂ ⠹⠑ ⠥⠝⠙⠻⠞⠁⠅⠻⠂ + ⠁⠝⠙ ⠹⠑ ⠡⠊⠑⠋ ⠍⠳⠗⠝⠻⠲ ⡎⠊⠗⠕⠕⠛⠑ ⠎⠊⠛⠝⠫ ⠊⠞⠲ ⡁⠝⠙ + ⡎⠊⠗⠕⠕⠛⠑⠰⠎ ⠝⠁⠍⠑ ⠺⠁⠎ ⠛⠕⠕⠙ ⠥⠏⠕⠝ ⠰⡡⠁⠝⠛⠑⠂ ⠋⠕⠗ ⠁⠝⠹⠹⠔⠛ ⠙⠑ + ⠡⠕⠎⠑ ⠞⠕ ⠏⠥⠞ ⠙⠊⠎ ⠙⠁⠝⠙ ⠞⠕⠲ + + ⡕⠇⠙ ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ + + ⡍⠔⠙⠖ ⡊ ⠙⠕⠝⠰⠞ ⠍⠑⠁⠝ ⠞⠕ ⠎⠁⠹ ⠹⠁⠞ ⡊ ⠅⠝⠪⠂ ⠕⠋ ⠍⠹ + ⠪⠝ ⠅⠝⠪⠇⠫⠛⠑⠂ ⠱⠁⠞ ⠹⠻⠑ ⠊⠎ ⠏⠜⠞⠊⠊⠥⠇⠜⠇⠹ ⠙⠑⠁⠙ ⠁⠃⠳⠞ + ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ ⡊ ⠍⠊⠣⠞ ⠙⠁⠧⠑ ⠃⠑⠲ ⠔⠊⠇⠔⠫⠂ ⠍⠹⠎⠑⠇⠋⠂ ⠞⠕ + ⠗⠑⠛⠜⠙ ⠁ ⠊⠕⠋⠋⠔⠤⠝⠁⠊⠇ ⠁⠎ ⠹⠑ ⠙⠑⠁⠙⠑⠌ ⠏⠊⠑⠊⠑ ⠕⠋ ⠊⠗⠕⠝⠍⠕⠝⠛⠻⠹ + ⠔ ⠹⠑ ⠞⠗⠁⠙⠑⠲ ⡃⠥⠞ ⠹⠑ ⠺⠊⠎⠙⠕⠍ ⠕⠋ ⠳⠗ ⠁⠝⠊⠑⠌⠕⠗⠎ + ⠊⠎ ⠔ ⠹⠑ ⠎⠊⠍⠊⠇⠑⠆ ⠁⠝⠙ ⠍⠹ ⠥⠝⠙⠁⠇⠇⠪⠫ ⠙⠁⠝⠙⠎ + ⠩⠁⠇⠇ ⠝⠕⠞ ⠙⠊⠌⠥⠗⠃ ⠊⠞⠂ ⠕⠗ ⠹⠑ ⡊⠳⠝⠞⠗⠹⠰⠎ ⠙⠕⠝⠑ ⠋⠕⠗⠲ ⡹⠳ + ⠺⠊⠇⠇ ⠹⠻⠑⠋⠕⠗⠑ ⠏⠻⠍⠊⠞ ⠍⠑ ⠞⠕ ⠗⠑⠏⠑⠁⠞⠂ ⠑⠍⠏⠙⠁⠞⠊⠊⠁⠇⠇⠹⠂ ⠹⠁⠞ + ⡍⠜⠇⠑⠹ ⠺⠁⠎ ⠁⠎ ⠙⠑⠁⠙ ⠁⠎ ⠁ ⠙⠕⠕⠗⠤⠝⠁⠊⠇⠲ + + (The first couple of paragraphs of "A Christmas Carol" by Dickens) + +Compact font selection example text: + + ABCDEFGHIJKLMNOPQRSTUVWXYZ /0123456789 + abcdefghijklmnopqrstuvwxyz £©µÀÆÖÞßéöÿ + –—‘“”„†•…‰™œŠŸž€ ΑΒΓΔΩαβγδω АБВГДабвгд + ∀∂∈ℝ∧∪≡∞ ↑↗↨↻⇣ ┐┼╔╘░►☺♀ fi�⑀₂ἠḂӥẄɐː⍎אԱა + +Greetings in various languages: + + Hello world, Καλημέρα κόσμε, コンニチハ + +Box drawing alignment tests: █ + ▉ + ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳ + ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳ + ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳ + ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳ + ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎ + ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏ + ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ ▗▄▖▛▀▜ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█ + ▝▀▘▙▄▟ diff --git a/docs/atari_small_notice.txt b/docs/atari_small_notice.txt new file mode 100644 index 0000000..afa8539 --- /dev/null +++ b/docs/atari_small_notice.txt @@ -0,0 +1,11 @@ +COMMENT Copyright (c) 1999, Thomas A. Fine +COMMENT +COMMENT License to copy, modify, and distribute for both commercial and +COMMENT non-commercial use is herby granted, provided this notice +COMMENT is preserved. +COMMENT +COMMENT Email to my last name at head.cfa.harvard.edu +COMMENT http://hea-www.harvard.edu/~fine/ +COMMENT +COMMENT Produced with bdfedit, a tcl/tk font editing program +COMMENT written by Thomas A. Fine \ No newline at end of file diff --git a/docs/notification icon source.png b/docs/notification icon source.png new file mode 100644 index 0000000000000000000000000000000000000000..03afa71fbf9b01b24e79f50e5c2c2ca0001f31b1 GIT binary patch literal 3999 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 FV1jwWY`Xve literal 0 HcmV?d00001 diff --git a/docs/releaseChecklist.md b/docs/releaseChecklist.md new file mode 100644 index 0000000..a826e3f --- /dev/null +++ b/docs/releaseChecklist.md @@ -0,0 +1,103 @@ +## Terminal Emulator for Android Release Checklist + +# Test on 1.6 Donut API 4 + +(Lowest supported level -- will be dropped soon.) + +# Test on 2.1 Eclair API 7 + +# Test on 2.2 Froyo API 8 + +# Test on 2.3 Gingerbread API 10 + +(Still popular with cheap phones.) + +# Test on 4.3 Jelly Bean API 18 + +# Test on 4.4 Kit Kat API 19 + +# Test on 5.1 Lollipop API 22 + +(Or whatever latest is.) + +# Test with Swype + +(Has to be on a real device, Swype beta won't run on an emulator.) + +# Update ./term/src/main/AndroidManifest.xml version number + + tools/increment-version-number + +# Commit changes + + git commit -a -m "Increment version number to v1.0.xx" + +# Tag git branch with version number + + git tag v1.0.xx + +# Push git to repository + + git push + git push --tags + +# Build release apk + + tools/build-release + +(Will only work if you have the signing keys for the app.) + +# Publish to the Google Play Store + + open https://play.google.com/apps/publish + +The Android Developer Console Publishing UI is error prone: + +1) Click on the "Terminal Emulator for Android" link. + +2) Click on the APK files tab + +3) Upload your new APK. + +4) Activate it by clicking on the Activate link + +5) Click on the "Save" button. + +6) Click on the "Product Details button". + +7) Fill in the "Listing Details" for the new version. + +8) Click on the "Save" button + +9) Visit https://play.google.com/apps/publish and verify that the new version is listed as the current version. + +10) Verify that Google Play Store is serving the new version +(check the "What's New" portion.) + +https://play.google.com/store/apps/details?id=jackpal.androidterm + +(Note, it can take several hours for the app to appear in the store.) + +# Update the Terminal Emulator for Android Wiki + + open https://github.com/jackpal/Android-Terminal-Emulator/wiki/Recent-Updates + +# Publish a new pre-compiled version of the APK for people who can't access Market. + +Github serves pages out of branch gh-pages , directory downloads/Term.apk +Also update the version number in index.html + + cp ./term/build/outputs/apk/Term.apk /tmp + git checkout gh-pages + mv /tmp/Term.apk downloads/Term.apk + git add downloads/Term.apk + subl index.html + # Update version save index.html + git add index.html + git commit -m "Update to version v1.0.xx" + git push + git checkout master + +Public URL is http://jackpal.github.com/Android-Terminal-Emulator/downloads/Term.apk + + diff --git a/emulatorview/.gitignore b/emulatorview/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/emulatorview/.gitignore @@ -0,0 +1 @@ +/build diff --git a/emulatorview/build.gradle b/emulatorview/build.gradle new file mode 100644 index 0000000..c2f7adb --- /dev/null +++ b/emulatorview/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + minSdkVersion 4 + targetSdkVersion 22 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} diff --git a/emulatorview/src/main/AndroidManifest.xml b/emulatorview/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bd07ec0 --- /dev/null +++ b/emulatorview/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/BaseTextRenderer.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/BaseTextRenderer.java new file mode 100644 index 0000000..5542ec1 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/BaseTextRenderer.java @@ -0,0 +1,448 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; + +abstract class BaseTextRenderer implements TextRenderer { + protected boolean mReverseVideo; + + protected int[] mPalette; + + protected static final int[] sXterm256Paint = { + // 16 original colors + // First 8 are dim + 0xff000000, // black + 0xffcd0000, // dim red + 0xff00cd00, // dim green + 0xffcdcd00, // dim yellow + 0xff0000ee, // dim blue + 0xffcd00cd, // dim magenta + 0xff00cdcd, // dim cyan + 0xffe5e5e5, // dim white + // second 8 are bright + 0xff7f7f7f, // medium grey + 0xffff0000, // bright red + 0xff00ff00, // bright green + 0xffffff00, // bright yellow + 0xff5c5cff, // light blue + 0xffff00ff, // bright magenta + 0xff00ffff, // bright cyan + 0xffffffff, // bright white + + // 216 color cube, six shades of each color + 0xff000000, + 0xff00005f, + 0xff000087, + 0xff0000af, + 0xff0000d7, + 0xff0000ff, + 0xff005f00, + 0xff005f5f, + 0xff005f87, + 0xff005faf, + 0xff005fd7, + 0xff005fff, + 0xff008700, + 0xff00875f, + 0xff008787, + 0xff0087af, + 0xff0087d7, + 0xff0087ff, + 0xff00af00, + 0xff00af5f, + 0xff00af87, + 0xff00afaf, + 0xff00afd7, + 0xff00afff, + 0xff00d700, + 0xff00d75f, + 0xff00d787, + 0xff00d7af, + 0xff00d7d7, + 0xff00d7ff, + 0xff00ff00, + 0xff00ff5f, + 0xff00ff87, + 0xff00ffaf, + 0xff00ffd7, + 0xff00ffff, + 0xff5f0000, + 0xff5f005f, + 0xff5f0087, + 0xff5f00af, + 0xff5f00d7, + 0xff5f00ff, + 0xff5f5f00, + 0xff5f5f5f, + 0xff5f5f87, + 0xff5f5faf, + 0xff5f5fd7, + 0xff5f5fff, + 0xff5f8700, + 0xff5f875f, + 0xff5f8787, + 0xff5f87af, + 0xff5f87d7, + 0xff5f87ff, + 0xff5faf00, + 0xff5faf5f, + 0xff5faf87, + 0xff5fafaf, + 0xff5fafd7, + 0xff5fafff, + 0xff5fd700, + 0xff5fd75f, + 0xff5fd787, + 0xff5fd7af, + 0xff5fd7d7, + 0xff5fd7ff, + 0xff5fff00, + 0xff5fff5f, + 0xff5fff87, + 0xff5fffaf, + 0xff5fffd7, + 0xff5fffff, + 0xff870000, + 0xff87005f, + 0xff870087, + 0xff8700af, + 0xff8700d7, + 0xff8700ff, + 0xff875f00, + 0xff875f5f, + 0xff875f87, + 0xff875faf, + 0xff875fd7, + 0xff875fff, + 0xff878700, + 0xff87875f, + 0xff878787, + 0xff8787af, + 0xff8787d7, + 0xff8787ff, + 0xff87af00, + 0xff87af5f, + 0xff87af87, + 0xff87afaf, + 0xff87afd7, + 0xff87afff, + 0xff87d700, + 0xff87d75f, + 0xff87d787, + 0xff87d7af, + 0xff87d7d7, + 0xff87d7ff, + 0xff87ff00, + 0xff87ff5f, + 0xff87ff87, + 0xff87ffaf, + 0xff87ffd7, + 0xff87ffff, + 0xffaf0000, + 0xffaf005f, + 0xffaf0087, + 0xffaf00af, + 0xffaf00d7, + 0xffaf00ff, + 0xffaf5f00, + 0xffaf5f5f, + 0xffaf5f87, + 0xffaf5faf, + 0xffaf5fd7, + 0xffaf5fff, + 0xffaf8700, + 0xffaf875f, + 0xffaf8787, + 0xffaf87af, + 0xffaf87d7, + 0xffaf87ff, + 0xffafaf00, + 0xffafaf5f, + 0xffafaf87, + 0xffafafaf, + 0xffafafd7, + 0xffafafff, + 0xffafd700, + 0xffafd75f, + 0xffafd787, + 0xffafd7af, + 0xffafd7d7, + 0xffafd7ff, + 0xffafff00, + 0xffafff5f, + 0xffafff87, + 0xffafffaf, + 0xffafffd7, + 0xffafffff, + 0xffd70000, + 0xffd7005f, + 0xffd70087, + 0xffd700af, + 0xffd700d7, + 0xffd700ff, + 0xffd75f00, + 0xffd75f5f, + 0xffd75f87, + 0xffd75faf, + 0xffd75fd7, + 0xffd75fff, + 0xffd78700, + 0xffd7875f, + 0xffd78787, + 0xffd787af, + 0xffd787d7, + 0xffd787ff, + 0xffd7af00, + 0xffd7af5f, + 0xffd7af87, + 0xffd7afaf, + 0xffd7afd7, + 0xffd7afff, + 0xffd7d700, + 0xffd7d75f, + 0xffd7d787, + 0xffd7d7af, + 0xffd7d7d7, + 0xffd7d7ff, + 0xffd7ff00, + 0xffd7ff5f, + 0xffd7ff87, + 0xffd7ffaf, + 0xffd7ffd7, + 0xffd7ffff, + 0xffff0000, + 0xffff005f, + 0xffff0087, + 0xffff00af, + 0xffff00d7, + 0xffff00ff, + 0xffff5f00, + 0xffff5f5f, + 0xffff5f87, + 0xffff5faf, + 0xffff5fd7, + 0xffff5fff, + 0xffff8700, + 0xffff875f, + 0xffff8787, + 0xffff87af, + 0xffff87d7, + 0xffff87ff, + 0xffffaf00, + 0xffffaf5f, + 0xffffaf87, + 0xffffafaf, + 0xffffafd7, + 0xffffafff, + 0xffffd700, + 0xffffd75f, + 0xffffd787, + 0xffffd7af, + 0xffffd7d7, + 0xffffd7ff, + 0xffffff00, + 0xffffff5f, + 0xffffff87, + 0xffffffaf, + 0xffffffd7, + 0xffffffff, + + // 24 grey scale ramp + 0xff080808, + 0xff121212, + 0xff1c1c1c, + 0xff262626, + 0xff303030, + 0xff3a3a3a, + 0xff444444, + 0xff4e4e4e, + 0xff585858, + 0xff626262, + 0xff6c6c6c, + 0xff767676, + 0xff808080, + 0xff8a8a8a, + 0xff949494, + 0xff9e9e9e, + 0xffa8a8a8, + 0xffb2b2b2, + 0xffbcbcbc, + 0xffc6c6c6, + 0xffd0d0d0, + 0xffdadada, + 0xffe4e4e4, + 0xffeeeeee + }; + + static final ColorScheme defaultColorScheme = + new ColorScheme(0xffcccccc, 0xff000000); + + private final Paint mCursorScreenPaint; + private final Paint mCopyRedToAlphaPaint; + private final Paint mCursorPaint; + private final Paint mCursorStrokePaint; + private final Path mShiftCursor; + private final Path mAltCursor; + private final Path mCtrlCursor; + private final Path mFnCursor; + private RectF mTempSrc; + private RectF mTempDst; + private Matrix mScaleMatrix; + private float mLastCharWidth; + private float mLastCharHeight; + private static final Matrix.ScaleToFit mScaleType = Matrix.ScaleToFit.FILL; + + private Bitmap mCursorBitmap; + private Bitmap mWorkBitmap; + private int mCursorBitmapCursorMode = -1; + + public BaseTextRenderer(ColorScheme scheme) { + if (scheme == null) { + scheme = defaultColorScheme; + } + setDefaultColors(scheme); + + mCursorScreenPaint = new Paint(); + mCursorScreenPaint.setColor(scheme.getCursorBackColor()); + + // Cursor paint and cursor stroke paint are used to draw a grayscale mask that's converted + // to an alpha8 texture. Only the red channel's value matters. + mCursorPaint = new Paint(); + mCursorPaint.setColor(0xff909090); // Opaque lightgray + mCursorPaint.setAntiAlias(true); + + mCursorStrokePaint = new Paint(mCursorPaint); + mCursorStrokePaint.setStrokeWidth(0.1f); + mCursorStrokePaint.setStyle(Paint.Style.STROKE); + + mCopyRedToAlphaPaint = new Paint(); + ColorMatrix cm = new ColorMatrix(); + cm.set(new float[] { + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0 }); + mCopyRedToAlphaPaint.setColorFilter(new ColorMatrixColorFilter(cm)); + + mShiftCursor = new Path(); + mShiftCursor.lineTo(0.5f, 0.33f); + mShiftCursor.lineTo(1.0f, 0.0f); + + mAltCursor = new Path(); + mAltCursor.moveTo(0.0f, 1.0f); + mAltCursor.lineTo(0.5f, 0.66f); + mAltCursor.lineTo(1.0f, 1.0f); + + mCtrlCursor = new Path(); + mCtrlCursor.moveTo(0.0f, 0.25f); + mCtrlCursor.lineTo(1.0f, 0.5f); + mCtrlCursor.lineTo(0.0f, 0.75f); + + mFnCursor = new Path(); + mFnCursor.moveTo(1.0f, 0.25f); + mFnCursor.lineTo(0.0f, 0.5f); + mFnCursor.lineTo(1.0f, 0.75f); + + // For creating the transform when the terminal resizes + mTempSrc = new RectF(); + mTempSrc.set(0.0f, 0.0f, 1.0f, 1.0f); + mTempDst = new RectF(); + mScaleMatrix = new Matrix(); + } + + public void setReverseVideo(boolean reverseVideo) { + mReverseVideo = reverseVideo; + } + + private void setDefaultColors(ColorScheme scheme) { + mPalette = cloneDefaultColors(); + mPalette[TextStyle.ciForeground] = scheme.getForeColor(); + mPalette[TextStyle.ciBackground] = scheme.getBackColor(); + mPalette[TextStyle.ciCursorForeground] = scheme.getCursorForeColor(); + mPalette[TextStyle.ciCursorBackground] = scheme.getCursorBackColor(); + } + + private static int[] cloneDefaultColors() { + int length = sXterm256Paint.length; + int[] clone = new int[TextStyle.ciColorLength]; + System.arraycopy(sXterm256Paint, 0, clone, 0, length); + return clone; + } + + protected void drawCursorImp(Canvas canvas, float x, float y, float charWidth, float charHeight, + int cursorMode) { + if (cursorMode == 0) { + canvas.drawRect(x, y - charHeight, x + charWidth, y, mCursorScreenPaint); + return; + } + + // Fancy cursor. Draw an offscreen cursor shape, then blit it on screen. + + // Has the character size changed? + + if (charWidth != mLastCharWidth || charHeight != mLastCharHeight) { + mLastCharWidth = charWidth; + mLastCharHeight = charHeight; + mTempDst.set(0.0f, 0.0f, charWidth, charHeight); + mScaleMatrix.setRectToRect(mTempSrc, mTempDst, mScaleType); + mCursorBitmap = Bitmap.createBitmap((int) charWidth, (int) charHeight, + Bitmap.Config.ALPHA_8); + mWorkBitmap = Bitmap.createBitmap((int) charWidth, (int) charHeight, + Bitmap.Config.ARGB_8888); + mCursorBitmapCursorMode = -1; + } + + // Has the cursor mode changed ? + + if (cursorMode != mCursorBitmapCursorMode) { + mCursorBitmapCursorMode = cursorMode; + mWorkBitmap.eraseColor(0xffffffff); + Canvas workCanvas = new Canvas(mWorkBitmap); + workCanvas.concat(mScaleMatrix); + drawCursorHelper(workCanvas, mShiftCursor, cursorMode, MODE_SHIFT_SHIFT); + drawCursorHelper(workCanvas, mAltCursor, cursorMode, MODE_ALT_SHIFT); + drawCursorHelper(workCanvas, mCtrlCursor, cursorMode, MODE_CTRL_SHIFT); + drawCursorHelper(workCanvas, mFnCursor, cursorMode, MODE_FN_SHIFT); + + mCursorBitmap.eraseColor(0); + Canvas bitmapCanvas = new Canvas(mCursorBitmap); + bitmapCanvas.drawBitmap(mWorkBitmap, 0, 0, mCopyRedToAlphaPaint); + } + + canvas.drawBitmap(mCursorBitmap, x, y - charHeight, mCursorScreenPaint); + } + + private void drawCursorHelper(Canvas canvas, Path path, int mode, int shift) { + switch ((mode >> shift) & MODE_MASK) { + case MODE_ON: + canvas.drawPath(path, mCursorStrokePaint); + break; + case MODE_LOCKED: + canvas.drawPath(path, mCursorPaint); + break; + } + } +} + diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/Bitmap4x8FontRenderer.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/Bitmap4x8FontRenderer.java new file mode 100644 index 0000000..f2282fd --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/Bitmap4x8FontRenderer.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +import jackpal.androidterm.emulatorview.compat.AndroidCompat; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; + + +class Bitmap4x8FontRenderer extends BaseTextRenderer { + private final static int kCharacterWidth = 4; + private final static int kCharacterHeight = 8; + private Bitmap mFont; + private int mCurrentForeColor; + private int mCurrentBackColor; + private float[] mColorMatrix; + private Paint mPaint; + private static final float BYTE_SCALE = 1.0f / 255.0f; + + public Bitmap4x8FontRenderer(Resources resources, ColorScheme scheme) { + super(scheme); + int fontResource = AndroidCompat.SDK <= 3 ? R.drawable.atari_small + : R.drawable.atari_small_nodpi; + mFont = BitmapFactory.decodeResource(resources,fontResource); + mPaint = new Paint(); + mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + } + + public float getCharacterWidth() { + return kCharacterWidth; + } + + public int getCharacterHeight() { + return kCharacterHeight; + } + + public int getTopMargin() { + return 0; + } + + public void drawTextRun(Canvas canvas, float x, float y, + int lineOffset, int runWidth, char[] text, int index, int count, + boolean selectionStyle, int textStyle, + int cursorOffset, int cursorIndex, int cursorIncr, int cursorWidth, int cursorMode) { + int foreColor = TextStyle.decodeForeColor(textStyle); + int backColor = TextStyle.decodeBackColor(textStyle); + int effect = TextStyle.decodeEffect(textStyle); + + boolean inverse = mReverseVideo ^ + ((effect & (TextStyle.fxInverse | TextStyle.fxItalic)) != 0); + if (inverse) { + int temp = foreColor; + foreColor = backColor; + backColor = temp; + } + + boolean bold = ((effect & TextStyle.fxBold) != 0); + if (bold && foreColor < 8) { + // In 16-color mode, bold also implies bright foreground colors + foreColor += 8; + } + boolean blink = ((effect & TextStyle.fxBlink) != 0); + if (blink && backColor < 8) { + // In 16-color mode, blink also implies bright background colors + backColor += 8; + } + + if (selectionStyle) { + backColor = TextStyle.ciCursorBackground; + } + + boolean invisible = (effect & TextStyle.fxInvisible) != 0; + + if (invisible) { + foreColor = backColor; + } + + drawTextRunHelper(canvas, x, y, lineOffset, text, index, count, foreColor, backColor); + + // The cursor is too small to show the cursor mode. + if (lineOffset <= cursorOffset && cursorOffset < (lineOffset + count)) { + drawTextRunHelper(canvas, x, y, cursorOffset, text, cursorOffset-lineOffset, 1, + TextStyle.ciCursorForeground, TextStyle.ciCursorBackground); + } + } + + private void drawTextRunHelper(Canvas canvas, float x, float y, int lineOffset, char[] text, + int index, int count, int foreColor, int backColor) { + setColorMatrix(mPalette[foreColor], mPalette[backColor]); + int destX = (int) x + kCharacterWidth * lineOffset; + int destY = (int) y; + Rect srcRect = new Rect(); + Rect destRect = new Rect(); + destRect.top = (destY - kCharacterHeight); + destRect.bottom = destY; + boolean drawSpaces = mPalette[backColor] != mPalette[TextStyle.ciBackground]; + for (int i = 0; i < count; i++) { + // XXX No Unicode support in bitmap font + char c = text[i + index]; + if ((c < 128) && ((c != 32) || drawSpaces)) { + int cellX = c & 31; + int cellY = (c >> 5) & 3; + int srcX = cellX * kCharacterWidth; + int srcY = cellY * kCharacterHeight; + srcRect.set(srcX, srcY, + srcX + kCharacterWidth, srcY + kCharacterHeight); + destRect.left = destX; + destRect.right = destX + kCharacterWidth; + canvas.drawBitmap(mFont, srcRect, destRect, mPaint); + } + destX += kCharacterWidth; + } + } + + private void setColorMatrix(int foreColor, int backColor) { + if ((foreColor != mCurrentForeColor) + || (backColor != mCurrentBackColor) + || (mColorMatrix == null)) { + mCurrentForeColor = foreColor; + mCurrentBackColor = backColor; + if (mColorMatrix == null) { + mColorMatrix = new float[20]; + mColorMatrix[18] = 1.0f; // Just copy Alpha + } + for (int component = 0; component < 3; component++) { + int rightShift = (2 - component) << 3; + int fore = 0xff & (foreColor >> rightShift); + int back = 0xff & (backColor >> rightShift); + int delta = back - fore; + mColorMatrix[component * 6] = delta * BYTE_SCALE; + mColorMatrix[component * 5 + 4] = fore; + } + mPaint.setColorFilter(new ColorMatrixColorFilter(mColorMatrix)); + } + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/ByteQueue.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/ByteQueue.java new file mode 100644 index 0000000..fa0608e --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/ByteQueue.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +/** + * A multi-thread-safe produce-consumer byte array. + * Only allows one producer and one consumer. + */ + +class ByteQueue { + public ByteQueue(int size) { + mBuffer = new byte[size]; + } + + public int getBytesAvailable() { + synchronized(this) { + return mStoredBytes; + } + } + + public int read(byte[] buffer, int offset, int length) + throws InterruptedException { + if (length + offset > buffer.length) { + throw + new IllegalArgumentException("length + offset > buffer.length"); + } + if (length < 0) { + throw + new IllegalArgumentException("length < 0"); + + } + if (length == 0) { + return 0; + } + synchronized(this) { + while (mStoredBytes == 0) { + wait(); + } + int totalRead = 0; + int bufferLength = mBuffer.length; + boolean wasFull = bufferLength == mStoredBytes; + while (length > 0 && mStoredBytes > 0) { + int oneRun = Math.min(bufferLength - mHead, mStoredBytes); + int bytesToCopy = Math.min(length, oneRun); + System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy); + mHead += bytesToCopy; + if (mHead >= bufferLength) { + mHead = 0; + } + mStoredBytes -= bytesToCopy; + length -= bytesToCopy; + offset += bytesToCopy; + totalRead += bytesToCopy; + } + if (wasFull) { + notify(); + } + return totalRead; + } + } + + /** + * Attempt to write the specified portion of the provided buffer to + * the queue. Returns the number of bytes actually written to the queue; + * it is the caller's responsibility to check whether all of the data + * was written and repeat the call to write() if necessary. + */ + public int write(byte[] buffer, int offset, int length) + throws InterruptedException { + if (length + offset > buffer.length) { + throw + new IllegalArgumentException("length + offset > buffer.length"); + } + if (length < 0) { + throw + new IllegalArgumentException("length < 0"); + + } + if (length == 0) { + return 0; + } + synchronized(this) { + int bufferLength = mBuffer.length; + boolean wasEmpty = mStoredBytes == 0; + while(bufferLength == mStoredBytes) { + wait(); + } + int tail = mHead + mStoredBytes; + int oneRun; + if (tail >= bufferLength) { + tail = tail - bufferLength; + oneRun = mHead - tail; + } else { + oneRun = bufferLength - tail; + } + int bytesToCopy = Math.min(oneRun, length); + System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy); + offset += bytesToCopy; + mStoredBytes += bytesToCopy; + if (wasEmpty) { + notify(); + } + return bytesToCopy; + } + } + + private byte[] mBuffer; + private int mHead; + private int mStoredBytes; +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/ColorScheme.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/ColorScheme.java new file mode 100644 index 0000000..d81f1f1 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/ColorScheme.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2012 Steven Luo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +/** + * A class describing a color scheme for an {@link EmulatorView}. + *

+ * EmulatorView supports changing its default foreground, + * background, and cursor colors. Passing a ColorScheme to + * {@link EmulatorView#setColorScheme setColorScheme} will cause the + * EmulatorView to use the specified colors as its defaults. + *

+ * Cursor colors can be omitted when specifying a color scheme; if no cursor + * colors are specified, ColorScheme will automatically select + * suitable cursor colors for you. + * + * @see EmulatorView#setColorScheme + */ + +public class ColorScheme { + private int foreColor; + private int backColor; + private int cursorForeColor; + private int cursorBackColor; + final private static int sDefaultCursorBackColor = 0xff808080; + + private void setDefaultCursorColors() { + cursorBackColor = sDefaultCursorBackColor; + // Use the foreColor unless the foreColor is too similar to the cursorBackColor + int foreDistance = distance(foreColor, cursorBackColor); + int backDistance = distance(backColor, cursorBackColor); + if (foreDistance * 2 >= backDistance) { + cursorForeColor = foreColor; + } else { + cursorForeColor = backColor; + } + } + + private static int distance(int a, int b) { + return channelDistance(a, b, 0) * 3 + channelDistance(a, b, 1) * 5 + + channelDistance(a, b, 2); + } + + private static int channelDistance(int a, int b, int channel) { + return Math.abs(getChannel(a, channel) - getChannel(b, channel)); + } + + private static int getChannel(int color, int channel) { + return 0xff & (color >> ((2 - channel) * 8)); + } + + /** + * Creates a ColorScheme object. + * + * @param foreColor The foreground color as an ARGB hex value. + * @param backColor The background color as an ARGB hex value. + */ + public ColorScheme(int foreColor, int backColor) { + this.foreColor = foreColor; + this.backColor = backColor; + setDefaultCursorColors(); + } + + /** + * Creates a ColorScheme object. + * + * @param foreColor The foreground color as an ARGB hex value. + * @param backColor The background color as an ARGB hex value. + * @param cursorForeColor The cursor foreground color as an ARGB hex value. + * @param cursorBackColor The cursor foreground color as an ARGB hex value. + */ + public ColorScheme(int foreColor, int backColor, int cursorForeColor, int cursorBackColor) { + this.foreColor = foreColor; + this.backColor = backColor; + this.cursorForeColor = cursorForeColor; + this.cursorBackColor = cursorBackColor; + } + + /** + * Creates a ColorScheme object from an array. + * + * @param scheme An integer array { foreColor, backColor, + * optionalCursorForeColor, optionalCursorBackColor }. + */ + public ColorScheme(int[] scheme) { + int schemeLength = scheme.length; + if (schemeLength != 2 && schemeLength != 4) { + throw new IllegalArgumentException(); + } + this.foreColor = scheme[0]; + this.backColor = scheme[1]; + if (schemeLength == 2) { + setDefaultCursorColors(); + } else { + this.cursorForeColor = scheme[2]; + this.cursorBackColor = scheme[3]; + } + } + + /** + * @return This ColorScheme's foreground color as an ARGB + * hex value. + */ + public int getForeColor() { + return foreColor; + } + + /** + * @return This ColorScheme's background color as an ARGB + * hex value. + */ + public int getBackColor() { + return backColor; + } + + /** + * @return This ColorScheme's cursor foreground color as an ARGB + * hex value. + */ + public int getCursorForeColor() { + return cursorForeColor; + } + + /** + * @return This ColorScheme's cursor background color as an ARGB + * hex value. + */ + public int getCursorBackColor() { + return cursorBackColor; + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/EmulatorDebug.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/EmulatorDebug.java new file mode 100644 index 0000000..ff84c0c --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/EmulatorDebug.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +/** + * Debug settings. + */ + +class EmulatorDebug { + /** + * Set to true to add debugging code and logging. + */ + public static final boolean DEBUG = false; + + /** + * Set to true to log IME calls. + */ + public static final boolean LOG_IME = DEBUG & false; + + /** + * Set to true to log each character received from the remote process to the + * android log, which makes it easier to debug some kinds of problems with + * emulating escape sequences and control codes. + */ + public static final boolean LOG_CHARACTERS_FLAG = DEBUG & false; + + /** + * Set to true to log unknown escape sequences. + */ + public static final boolean LOG_UNKNOWN_ESCAPE_SEQUENCES = DEBUG & false; + + /** + * The tag we use when logging, so that our messages can be distinguished + * from other messages in the log. Public because it's used by several + * classes. + */ + public static final String LOG_TAG = "EmulatorView"; + + public static String bytesToString(byte[] data, int base, int length) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < length; i++) { + byte b = data[base + i]; + if (b < 32 || b > 126) { + buf.append(String.format("\\x%02x", b)); + } else { + buf.append((char)b); + } + } + return buf.toString(); + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/EmulatorView.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/EmulatorView.java new file mode 100644 index 0000000..ba192f9 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/EmulatorView.java @@ -0,0 +1,1714 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +import jackpal.androidterm.emulatorview.compat.ClipboardManagerCompat; +import jackpal.androidterm.emulatorview.compat.ClipboardManagerCompatFactory; +import jackpal.androidterm.emulatorview.compat.KeycodeConstants; +import jackpal.androidterm.emulatorview.compat.Patterns; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Hashtable; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.URLSpan; +import android.text.util.Linkify; +import android.text.util.Linkify.MatchFilter; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; +import android.widget.Scroller; + +/** + * A view on a {@link TermSession}. Displays the terminal emulator's screen, + * provides access to its scrollback buffer, and passes input through to the + * terminal emulator. + *

+ * If this view is inflated from an XML layout, you need to call {@link + * #attachSession attachSession} and {@link #setDensity setDensity} before using + * the view. If creating this view from code, use the {@link + * #EmulatorView(Context, TermSession, DisplayMetrics)} constructor, which will + * take care of this for you. + */ +public class EmulatorView extends View implements GestureDetector.OnGestureListener { + private final static String TAG = "EmulatorView"; + private final static boolean LOG_KEY_EVENTS = false; + private final static boolean LOG_IME = false; + + /** + * We defer some initialization until we have been layed out in the view + * hierarchy. The boolean tracks when we know what our size is. + */ + private boolean mKnownSize; + + // Set if initialization was deferred because a TermSession wasn't attached + private boolean mDeferInit = false; + + private int mVisibleWidth; + private int mVisibleHeight; + + private TermSession mTermSession; + + /** + * Total width of each character, in pixels + */ + private float mCharacterWidth; + + /** + * Total height of each character, in pixels + */ + private int mCharacterHeight; + + /** + * Top-of-screen margin + */ + private int mTopOfScreenMargin; + + /** + * Used to render text + */ + private TextRenderer mTextRenderer; + + /** + * Text size. Zero means 4 x 8 font. + */ + private int mTextSize = 10; + + private int mCursorBlink; + + /** + * Color scheme (default foreground/background colors). + */ + private ColorScheme mColorScheme = BaseTextRenderer.defaultColorScheme; + + private Paint mForegroundPaint; + + private Paint mBackgroundPaint; + + private boolean mUseCookedIme; + + /** + * Our terminal emulator. + */ + private TerminalEmulator mEmulator; + + /** + * The number of rows of text to display. + */ + private int mRows; + + /** + * The number of columns of text to display. + */ + private int mColumns; + + /** + * The number of columns that are visible on the display. + */ + + private int mVisibleColumns; + + /* + * The number of rows that are visible on the view + */ + private int mVisibleRows; + + /** + * The top row of text to display. Ranges from -activeTranscriptRows to 0 + */ + private int mTopRow; + + private int mLeftColumn; + + private static final int CURSOR_BLINK_PERIOD = 1000; + + private boolean mCursorVisible = true; + + private boolean mIsSelectingText = false; + + private boolean mBackKeySendsCharacter = false; + private int mControlKeyCode; + private int mFnKeyCode; + private boolean mIsControlKeySent = false; + private boolean mIsFnKeySent = false; + + private boolean mMouseTracking; + + private float mDensity; + + private float mScaledDensity; + private static final int SELECT_TEXT_OFFSET_Y = -40; + private int mSelXAnchor = -1; + private int mSelYAnchor = -1; + private int mSelX1 = -1; + private int mSelY1 = -1; + private int mSelX2 = -1; + private int mSelY2 = -1; + + /** + * Routing alt and meta keyCodes away from the IME allows Alt key processing to work on + * the Asus Transformer TF101. + * It doesn't seem to harm anything else, but it also doesn't seem to be + * required on other platforms. + * + * This test should be refined as we learn more. + */ + private final static boolean sTrapAltAndMeta = Build.MODEL.contains("Transformer TF101"); + + private Runnable mBlinkCursor = new Runnable() { + public void run() { + if (mCursorBlink != 0) { + mCursorVisible = ! mCursorVisible; + mHandler.postDelayed(this, CURSOR_BLINK_PERIOD); + } else { + mCursorVisible = true; + } + // Perhaps just invalidate the character with the cursor. + invalidate(); + } + }; + + private GestureDetector mGestureDetector; + private GestureDetector.OnGestureListener mExtGestureListener; + private Scroller mScroller; + private Runnable mFlingRunner = new Runnable() { + public void run() { + if (mScroller.isFinished()) { + return; + } + // Check whether mouse tracking was turned on during fling. + if (isMouseTrackingActive()) { + return; + } + + boolean more = mScroller.computeScrollOffset(); + int newTopRow = mScroller.getCurrY(); + if (newTopRow != mTopRow) { + mTopRow = newTopRow; + invalidate(); + } + + if (more) { + post(this); + } + + } + }; + + /** + * + * A hash table of underlying URLs to implement clickable links. + */ + private Hashtable mLinkLayer = new Hashtable(); + + /** + * Accept links that start with http[s]: + */ + private static class HttpMatchFilter implements MatchFilter { + public boolean acceptMatch(CharSequence s, int start, int end) { + return startsWith(s, start, end, "http:") || + startsWith(s, start, end, "https:"); + } + + private boolean startsWith(CharSequence s, int start, int end, + String prefix) { + int prefixLen = prefix.length(); + int fragmentLen = end - start; + if (prefixLen > fragmentLen) { + return false; + } + for (int i = 0; i < prefixLen; i++) { + if (s.charAt(start + i) != prefix.charAt(i)) { + return false; + } + } + return true; + } + } + + private static MatchFilter sHttpMatchFilter = new HttpMatchFilter(); + + /** + * Convert any URLs in the current row into a URLSpan, + * and store that result in a hash table of URLSpan entries. + * + * @param row The number of the row to check for links + * @return The number of lines in a multi-line-wrap set of links + */ + private int createLinks(int row) + { + TranscriptScreen transcriptScreen = mEmulator.getScreen(); + char [] line = transcriptScreen.getScriptLine(row); + int lineCount = 1; + + //Nothing to do if there's no text. + if(line == null) + return lineCount; + + /* If this is not a basic line, the array returned from getScriptLine() + * could have arbitrary garbage at the end -- find the point at which + * the line ends and only include that in the text to linkify. + * + * XXX: The fact that the array returned from getScriptLine() on a + * basic line contains no garbage is an implementation detail -- the + * documented behavior explicitly allows garbage at the end! */ + int lineLen; + boolean textIsBasic = transcriptScreen.isBasicLine(row); + if (textIsBasic) { + lineLen = line.length; + } else { + // The end of the valid data is marked by a NUL character + for (lineLen = 0; line[lineLen] != 0; ++lineLen); + } + + SpannableStringBuilder textToLinkify = new SpannableStringBuilder(new String(line, 0, lineLen)); + + boolean lineWrap = transcriptScreen.getScriptLineWrap(row); + + //While the current line has a wrap + while (lineWrap) + { + //Get next line + int nextRow = row + lineCount; + line = transcriptScreen.getScriptLine(nextRow); + + //If next line is blank, don't try and append + if(line == null) + break; + + boolean lineIsBasic = transcriptScreen.isBasicLine(nextRow); + if (textIsBasic && !lineIsBasic) { + textIsBasic = lineIsBasic; + } + if (lineIsBasic) { + lineLen = line.length; + } else { + // The end of the valid data is marked by a NUL character + for (lineLen = 0; line[lineLen] != 0; ++lineLen); + } + + textToLinkify.append(new String(line, 0, lineLen)); + + //Check if line after next is wrapped + lineWrap = transcriptScreen.getScriptLineWrap(nextRow); + ++lineCount; + } + + Linkify.addLinks(textToLinkify, Patterns.WEB_URL, + null, sHttpMatchFilter, null); + URLSpan [] urls = textToLinkify.getSpans(0, textToLinkify.length(), URLSpan.class); + if(urls.length > 0) + { + int columns = mColumns; + + //re-index row to 0 if it is negative + int screenRow = row - mTopRow; + + //Create and initialize set of links + URLSpan [][] linkRows = new URLSpan[lineCount][]; + for(int i=0; i= columns) { + ++startRow; + startCol %= columns; + } + } + + endRow = startRow; + endCol = startCol; + for (int i = spanStart; i < spanEnd; ++i) { + char c = textToLinkify.charAt(i); + if (Character.isHighSurrogate(c)) { + ++i; + endCol += UnicodeTranscript.charWidth(c, textToLinkify.charAt(i)); + } else { + endCol += UnicodeTranscript.charWidth(c); + } + if (endCol >= columns) { + ++endRow; + endCol %= columns; + } + } + } + + //Fill linkRows with the URL where appropriate + for(int i=startRow; i <= endRow; ++i) + { + int runStart = (i == startRow) ? startCol: 0; + int runEnd = (i == endRow) ? endCol : mColumns - 1; + + Arrays.fill(linkRows[i], runStart, runEnd + 1, url); + } + } + + //Add links into the link layer for later retrieval + for(int i=0; i newY; mLastY--) { + sendMouseEventCode(mMotionEvent, 64); + } + + if (more) { + post(this); + } + } + }; + private MouseTrackingFlingRunner mMouseTrackingFlingRunner = new MouseTrackingFlingRunner(); + + private float mScrollRemainder; + private TermKeyListener mKeyListener; + + private String mImeBuffer = ""; + + /** + * Our message handler class. Implements a periodic callback. + */ + private final Handler mHandler = new Handler(); + + /** + * Called by the TermSession when the contents of the view need updating + */ + private UpdateCallback mUpdateNotify = new UpdateCallback() { + public void onUpdate() { + if ( mIsSelectingText ) { + int rowShift = mEmulator.getScrollCounter(); + mSelY1 -= rowShift; + mSelY2 -= rowShift; + mSelYAnchor -= rowShift; + } + mEmulator.clearScrollCounter(); + ensureCursorVisible(); + invalidate(); + } + }; + + /** + * Create an EmulatorView for a {@link TermSession}. + * + * @param context The {@link Context} for the view. + * @param session The {@link TermSession} this view will be displaying. + * @param metrics The {@link DisplayMetrics} of the screen on which the view + * will be displayed. + */ + public EmulatorView(Context context, TermSession session, DisplayMetrics metrics) { + super(context); + attachSession(session); + setDensity(metrics); + commonConstructor(context); + } + + /** + * Constructor called when inflating this view from XML. + *

+ * You should call {@link #attachSession attachSession} and {@link + * #setDensity setDensity} before using an EmulatorView created + * using this constructor. + */ + public EmulatorView(Context context, AttributeSet attrs) { + super(context, attrs); + commonConstructor(context); + } + + /** + * Constructor called when inflating this view from XML with a + * default style set. + *

+ * You should call {@link #attachSession attachSession} and {@link + * #setDensity setDensity} before using an EmulatorView created + * using this constructor. + */ + public EmulatorView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + commonConstructor(context); + } + + private void commonConstructor(Context context) { + // TODO: See if we want to use the API level 11 constructor to get new flywheel feature. + mScroller = new Scroller(context); + mMouseTrackingFlingRunner.mScroller = new Scroller(context); + } + + /** + * Attach a {@link TermSession} to this view. + * + * @param session The {@link TermSession} this view will be displaying. + */ + public void attachSession(TermSession session) { + mTextRenderer = null; + mForegroundPaint = new Paint(); + mBackgroundPaint = new Paint(); + mTopRow = 0; + mLeftColumn = 0; + mGestureDetector = new GestureDetector(this); + // mGestureDetector.setIsLongpressEnabled(false); + setVerticalScrollBarEnabled(true); + setFocusable(true); + setFocusableInTouchMode(true); + + mTermSession = session; + + mKeyListener = new TermKeyListener(session); + session.setKeyListener(mKeyListener); + + // Do init now if it was deferred until a TermSession was attached + if (mDeferInit) { + mDeferInit = false; + mKnownSize = true; + initialize(); + } + } + + /** + * Update the screen density for the screen on which the view is displayed. + * + * @param metrics The {@link DisplayMetrics} of the screen. + */ + public void setDensity(DisplayMetrics metrics) { + if (mDensity == 0) { + // First time we've known the screen density, so update font size + mTextSize = (int) (mTextSize * metrics.density); + } + mDensity = metrics.density; + mScaledDensity = metrics.scaledDensity; + } + + /** + * Inform the view that it is now visible on screen. + */ + public void onResume() { + updateSize(false); + if (mCursorBlink != 0) { + mHandler.postDelayed(mBlinkCursor, CURSOR_BLINK_PERIOD); + } + if (mKeyListener != null) { + mKeyListener.onResume(); + } + } + + /** + * Inform the view that it is no longer visible on the screen. + */ + public void onPause() { + if (mCursorBlink != 0) { + mHandler.removeCallbacks(mBlinkCursor); + } + if (mKeyListener != null) { + mKeyListener.onPause(); + } + } + + /** + * Set this EmulatorView's color scheme. + * + * @param scheme The {@link ColorScheme} to use (use null for the default + * scheme). + * @see TermSession#setColorScheme + * @see ColorScheme + */ + public void setColorScheme(ColorScheme scheme) { + if (scheme == null) { + mColorScheme = BaseTextRenderer.defaultColorScheme; + } else { + mColorScheme = scheme; + } + updateText(); + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + outAttrs.inputType = mUseCookedIme ? + EditorInfo.TYPE_CLASS_TEXT : + EditorInfo.TYPE_NULL; + return new BaseInputConnection(this, true) { + /** + * Used to handle composing text requests + */ + private int mCursor; + private int mComposingTextStart; + private int mComposingTextEnd; + private int mSelectedTextStart; + private int mSelectedTextEnd; + + private void sendText(CharSequence text) { + int n = text.length(); + char c; + try { + for(int i = 0; i < n; i++) { + c = text.charAt(i); + if (Character.isHighSurrogate(c)) { + int codePoint; + if (++i < n) { + codePoint = Character.toCodePoint(c, text.charAt(i)); + } else { + // Unicode Replacement Glyph, aka white question mark in black diamond. + codePoint = '\ufffd'; + } + mapAndSend(codePoint); + } else { + mapAndSend(c); + } + } + } catch (IOException e) { + Log.e(TAG, "error writing ", e); + } + } + + private void mapAndSend(int c) throws IOException { + int result = mKeyListener.mapControlChar(c); + if (result < TermKeyListener.KEYCODE_OFFSET) { + mTermSession.write(result); + } else { + mKeyListener.handleKeyCode(result - TermKeyListener.KEYCODE_OFFSET, null, getKeypadApplicationMode()); + } + clearSpecialKeyStatus(); + } + + public boolean beginBatchEdit() { + if (LOG_IME) { + Log.w(TAG, "beginBatchEdit"); + } + setImeBuffer(""); + mCursor = 0; + mComposingTextStart = 0; + mComposingTextEnd = 0; + return true; + } + + public boolean clearMetaKeyStates(int arg0) { + if (LOG_IME) { + Log.w(TAG, "clearMetaKeyStates " + arg0); + } + return false; + } + + public boolean commitCompletion(CompletionInfo arg0) { + if (LOG_IME) { + Log.w(TAG, "commitCompletion " + arg0); + } + return false; + } + + public boolean endBatchEdit() { + if (LOG_IME) { + Log.w(TAG, "endBatchEdit"); + } + return true; + } + + public boolean finishComposingText() { + if (LOG_IME) { + Log.w(TAG, "finishComposingText"); + } + sendText(mImeBuffer); + setImeBuffer(""); + mComposingTextStart = 0; + mComposingTextEnd = 0; + mCursor = 0; + return true; + } + + public int getCursorCapsMode(int reqModes) { + if (LOG_IME) { + Log.w(TAG, "getCursorCapsMode(" + reqModes + ")"); + } + int mode = 0; + if ((reqModes & TextUtils.CAP_MODE_CHARACTERS) != 0) { + mode |= TextUtils.CAP_MODE_CHARACTERS; + } + return mode; + } + + public ExtractedText getExtractedText(ExtractedTextRequest arg0, + int arg1) { + if (LOG_IME) { + Log.w(TAG, "getExtractedText" + arg0 + "," + arg1); + } + return null; + } + + public CharSequence getTextAfterCursor(int n, int flags) { + if (LOG_IME) { + Log.w(TAG, "getTextAfterCursor(" + n + "," + flags + ")"); + } + int len = Math.min(n, mImeBuffer.length() - mCursor); + if (len <= 0 || mCursor < 0 || mCursor >= mImeBuffer.length()) { + return ""; + } + return mImeBuffer.substring(mCursor, mCursor + len); + } + + public CharSequence getTextBeforeCursor(int n, int flags) { + if (LOG_IME) { + Log.w(TAG, "getTextBeforeCursor(" + n + "," + flags + ")"); + } + int len = Math.min(n, mCursor); + if (len <= 0 || mCursor < 0 || mCursor >= mImeBuffer.length()) { + return ""; + } + return mImeBuffer.substring(mCursor-len, mCursor); + } + + public boolean performContextMenuAction(int arg0) { + if (LOG_IME) { + Log.w(TAG, "performContextMenuAction" + arg0); + } + return true; + } + + public boolean performPrivateCommand(String arg0, Bundle arg1) { + if (LOG_IME) { + Log.w(TAG, "performPrivateCommand" + arg0 + "," + arg1); + } + return true; + } + + public boolean reportFullscreenMode(boolean arg0) { + if (LOG_IME) { + Log.w(TAG, "reportFullscreenMode" + arg0); + } + return true; + } + + public boolean commitCorrection (CorrectionInfo correctionInfo) { + if (LOG_IME) { + Log.w(TAG, "commitCorrection"); + } + return true; + } + + public boolean commitText(CharSequence text, int newCursorPosition) { + if (LOG_IME) { + Log.w(TAG, "commitText(\"" + text + "\", " + newCursorPosition + ")"); + } + clearComposingText(); + sendText(text); + setImeBuffer(""); + mCursor = 0; + return true; + } + + private void clearComposingText() { + int len = mImeBuffer.length(); + if (mComposingTextStart > len || mComposingTextEnd > len) { + mComposingTextEnd = mComposingTextStart = 0; + return; + } + setImeBuffer(mImeBuffer.substring(0, mComposingTextStart) + + mImeBuffer.substring(mComposingTextEnd)); + if (mCursor < mComposingTextStart) { + // do nothing + } else if (mCursor < mComposingTextEnd) { + mCursor = mComposingTextStart; + } else { + mCursor -= mComposingTextEnd - mComposingTextStart; + } + mComposingTextEnd = mComposingTextStart = 0; + } + + public boolean deleteSurroundingText(int leftLength, int rightLength) { + if (LOG_IME) { + Log.w(TAG, "deleteSurroundingText(" + leftLength + + "," + rightLength + ")"); + } + if (leftLength > 0) { + for (int i = 0; i < leftLength; i++) { + sendKeyEvent( + new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); + } + } else if ((leftLength == 0) && (rightLength == 0)) { + // Delete key held down / repeating + sendKeyEvent( + new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); + } + // TODO: handle forward deletes. + return true; + } + + public boolean performEditorAction(int actionCode) { + if (LOG_IME) { + Log.w(TAG, "performEditorAction(" + actionCode + ")"); + } + if (actionCode == EditorInfo.IME_ACTION_UNSPECIFIED) { + // The "return" key has been pressed on the IME. + sendText("\r"); + } + return true; + } + + public boolean sendKeyEvent(KeyEvent event) { + if (LOG_IME) { + Log.w(TAG, "sendKeyEvent(" + event + ")"); + } + // Some keys are sent here rather than to commitText. + // In particular, del and the digit keys are sent here. + // (And I have reports that the HTC Magic also sends Return here.) + // As a bit of defensive programming, handle every key. + dispatchKeyEvent(event); + return true; + } + + public boolean setComposingText(CharSequence text, int newCursorPosition) { + if (LOG_IME) { + Log.w(TAG, "setComposingText(\"" + text + "\", " + newCursorPosition + ")"); + } + int len = mImeBuffer.length(); + if (mComposingTextStart > len || mComposingTextEnd > len) { + return false; + } + setImeBuffer(mImeBuffer.substring(0, mComposingTextStart) + + text + mImeBuffer.substring(mComposingTextEnd)); + mComposingTextEnd = mComposingTextStart + text.length(); + mCursor = newCursorPosition > 0 ? mComposingTextEnd + newCursorPosition - 1 + : mComposingTextStart - newCursorPosition; + return true; + } + + public boolean setSelection(int start, int end) { + if (LOG_IME) { + Log.w(TAG, "setSelection" + start + "," + end); + } + int length = mImeBuffer.length(); + if (start == end && start > 0 && start < length) { + mSelectedTextStart = mSelectedTextEnd = 0; + mCursor = start; + } else if (start < end && start > 0 && end < length) { + mSelectedTextStart = start; + mSelectedTextEnd = end; + mCursor = start; + } + return true; + } + + public boolean setComposingRegion(int start, int end) { + if (LOG_IME) { + Log.w(TAG, "setComposingRegion " + start + "," + end); + } + if (start < end && start > 0 && end < mImeBuffer.length()) { + clearComposingText(); + mComposingTextStart = start; + mComposingTextEnd = end; + } + return true; + } + + public CharSequence getSelectedText(int flags) { + if (LOG_IME) { + Log.w(TAG, "getSelectedText " + flags); + } + int len = mImeBuffer.length(); + if (mSelectedTextEnd >= len || mSelectedTextStart > mSelectedTextEnd) { + return ""; + } + return mImeBuffer.substring(mSelectedTextStart, mSelectedTextEnd+1); + } + + }; + } + + private void setImeBuffer(String buffer) { + if (!buffer.equals(mImeBuffer)) { + invalidate(); + } + mImeBuffer = buffer; + } + + /** + * Get the terminal emulator's keypad application mode. + */ + public boolean getKeypadApplicationMode() { + return mEmulator.getKeypadApplicationMode(); + } + + /** + * Set a {@link android.view.GestureDetector.OnGestureListener + * GestureDetector.OnGestureListener} to receive gestures performed on this + * view. Can be used to implement additional + * functionality via touch gestures or override built-in gestures. + * + * @param listener The {@link + * android.view.GestureDetector.OnGestureListener + * GestureDetector.OnGestureListener} which will receive + * gestures. + */ + public void setExtGestureListener(GestureDetector.OnGestureListener listener) { + mExtGestureListener = listener; + } + + /** + * Compute the vertical range that the vertical scrollbar represents. + */ + @Override + protected int computeVerticalScrollRange() { + return mEmulator.getScreen().getActiveRows(); + } + + /** + * Compute the vertical extent of the horizontal scrollbar's thumb within + * the vertical range. This value is used to compute the length of the thumb + * within the scrollbar's track. + */ + @Override + protected int computeVerticalScrollExtent() { + return mRows; + } + + /** + * Compute the vertical offset of the vertical scrollbar's thumb within the + * horizontal range. This value is used to compute the position of the thumb + * within the scrollbar's track. + */ + @Override + protected int computeVerticalScrollOffset() { + return mEmulator.getScreen().getActiveRows() + mTopRow - mRows; + } + + /** + * Call this to initialize the view. + */ + private void initialize() { + TermSession session = mTermSession; + + updateText(); + + mEmulator = session.getEmulator(); + session.setUpdateCallback(mUpdateNotify); + + requestFocus(); + } + + /** + * Get the {@link TermSession} corresponding to this view. + * + * @return The {@link TermSession} object for this view. + */ + public TermSession getTermSession() { + return mTermSession; + } + + /** + * Get the width of the visible portion of this view. + * + * @return The width of the visible portion of this view, in pixels. + */ + public int getVisibleWidth() { + return mVisibleWidth; + } + + /** + * Get the height of the visible portion of this view. + * + * @return The height of the visible portion of this view, in pixels. + */ + public int getVisibleHeight() { + return mVisibleHeight; + } + + /** + * Gets the visible number of rows for the view, useful when updating Ptysize with the correct number of rows/columns + * @return The rows for the visible number of rows, this is calculate in updateSize(int w, int h), please call + * updateSize(true) if the view changed, to get the correct calculation before calling this. + */ + public int getVisibleRows() + { + return mVisibleRows; + } + + /** + * Gets the visible number of columns for the view, again useful to get when updating PTYsize + * @return the columns for the visisble view, please call updateSize(true) to re-calculate this if the view has changed + */ + public int getVisibleColumns() + { + return mVisibleColumns; + } + + + /** + * Page the terminal view (scroll it up or down by delta + * screenfuls). + * + * @param delta The number of screens to scroll. Positive means scroll down, + * negative means scroll up. + */ + public void page(int delta) { + mTopRow = + Math.min(0, Math.max(-(mEmulator.getScreen() + .getActiveTranscriptRows()), mTopRow + mRows * delta)); + invalidate(); + } + + /** + * Page the terminal view horizontally. + * + * @param deltaColumns the number of columns to scroll. Positive scrolls to + * the right. + */ + public void pageHorizontal(int deltaColumns) { + mLeftColumn = + Math.max(0, Math.min(mLeftColumn + deltaColumns, mColumns + - mVisibleColumns)); + invalidate(); + } + + /** + * Sets the text size, which in turn sets the number of rows and columns. + * + * @param fontSize the new font size, in density-independent pixels. + */ + public void setTextSize(int fontSize) { + mTextSize = (int) (fontSize * mDensity); + updateText(); + } + + /** + * Sets the IME mode ("cooked" or "raw"). + * + * @param useCookedIME Whether the IME should be used in cooked mode. + */ + public void setUseCookedIME(boolean useCookedIME) { + mUseCookedIme = useCookedIME; + } + + /** + * Returns true if mouse events are being sent as escape sequences to the terminal. + */ + public boolean isMouseTrackingActive() { + return mEmulator.getMouseTrackingMode() != 0 && mMouseTracking; + } + + /** + * Send a single mouse event code to the terminal. + */ + private void sendMouseEventCode(MotionEvent e, int button_code) { + int x = (int)(e.getX() / mCharacterWidth) + 1; + int y = (int)((e.getY()-mTopOfScreenMargin) / mCharacterHeight) + 1; + // Clip to screen, and clip to the limits of 8-bit data. + boolean out_of_bounds = + x < 1 || y < 1 || + x > mColumns || y > mRows || + x > 255-32 || y > 255-32; + //Log.d(TAG, "mouse button "+x+","+y+","+button_code+",oob="+out_of_bounds); + if(button_code < 0 || button_code > 255-32) { + Log.e(TAG, "mouse button_code out of range: "+button_code); + return; + } + if(!out_of_bounds) { + byte[] data = { + '\033', '[', 'M', + (byte)(32 + button_code), + (byte)(32 + x), + (byte)(32 + y) }; + mTermSession.write(data, 0, data.length); + } + } + + // Begin GestureDetector.OnGestureListener methods + + public boolean onSingleTapUp(MotionEvent e) { + if (mExtGestureListener != null && mExtGestureListener.onSingleTapUp(e)) { + return true; + } + + if (isMouseTrackingActive()) { + sendMouseEventCode(e, 0); // BTN1 press + sendMouseEventCode(e, 3); // release + } + + requestFocus(); + return true; + } + + public void onLongPress(MotionEvent e) { + // XXX hook into external gesture listener + showContextMenu(); + } + + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + if (mExtGestureListener != null && mExtGestureListener.onScroll(e1, e2, distanceX, distanceY)) { + return true; + } + + distanceY += mScrollRemainder; + int deltaRows = (int) (distanceY / mCharacterHeight); + mScrollRemainder = distanceY - deltaRows * mCharacterHeight; + + if (isMouseTrackingActive()) { + // Send mouse wheel events to terminal. + for (; deltaRows>0; deltaRows--) { + sendMouseEventCode(e1, 65); + } + for (; deltaRows<0; deltaRows++) { + sendMouseEventCode(e1, 64); + } + return true; + } + + mTopRow = + Math.min(0, Math.max(-(mEmulator.getScreen() + .getActiveTranscriptRows()), mTopRow + deltaRows)); + invalidate(); + + return true; + } + + public void onSingleTapConfirmed(MotionEvent e) { + } + + public boolean onJumpTapDown(MotionEvent e1, MotionEvent e2) { + // Scroll to bottom + mTopRow = 0; + invalidate(); + return true; + } + + public boolean onJumpTapUp(MotionEvent e1, MotionEvent e2) { + // Scroll to top + mTopRow = -mEmulator.getScreen().getActiveTranscriptRows(); + invalidate(); + return true; + } + + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + if (mExtGestureListener != null && mExtGestureListener.onFling(e1, e2, velocityX, velocityY)) { + return true; + } + + mScrollRemainder = 0.0f; + if (isMouseTrackingActive()) { + mMouseTrackingFlingRunner.fling(e1, velocityX, velocityY); + } else { + float SCALE = 0.25f; + mScroller.fling(0, mTopRow, + -(int) (velocityX * SCALE), -(int) (velocityY * SCALE), + 0, 0, + -mEmulator.getScreen().getActiveTranscriptRows(), 0); + // onScroll(e1, e2, 0.1f * velocityX, -0.1f * velocityY); + post(mFlingRunner); + } + return true; + } + + public void onShowPress(MotionEvent e) { + if (mExtGestureListener != null) { + mExtGestureListener.onShowPress(e); + } + } + + public boolean onDown(MotionEvent e) { + if (mExtGestureListener != null && mExtGestureListener.onDown(e)) { + return true; + } + mScrollRemainder = 0.0f; + return true; + } + + // End GestureDetector.OnGestureListener methods + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mIsSelectingText) { + return onTouchEventWhileSelectingText(ev); + } else { + return mGestureDetector.onTouchEvent(ev); + } + } + + private boolean onTouchEventWhileSelectingText(MotionEvent ev) { + int action = ev.getAction(); + int cx = (int)(ev.getX() / mCharacterWidth); + int cy = Math.max(0, + (int)((ev.getY() + SELECT_TEXT_OFFSET_Y * mScaledDensity) + / mCharacterHeight) + mTopRow); + switch (action) { + case MotionEvent.ACTION_DOWN: + mSelXAnchor = cx; + mSelYAnchor = cy; + mSelX1 = cx; + mSelY1 = cy; + mSelX2 = mSelX1; + mSelY2 = mSelY1; + break; + case MotionEvent.ACTION_MOVE: + case MotionEvent.ACTION_UP: + int minx = Math.min(mSelXAnchor, cx); + int maxx = Math.max(mSelXAnchor, cx); + int miny = Math.min(mSelYAnchor, cy); + int maxy = Math.max(mSelYAnchor, cy); + mSelX1 = minx; + mSelY1 = miny; + mSelX2 = maxx; + mSelY2 = maxy; + if (action == MotionEvent.ACTION_UP) { + ClipboardManagerCompat clip = ClipboardManagerCompatFactory + .getManager(getContext().getApplicationContext()); + clip.setText(getSelectedText().trim()); + toggleSelectingText(); + } + invalidate(); + break; + default: + toggleSelectingText(); + invalidate(); + break; + } + return true; + } + + /** + * Called when a key is pressed in the view. + * + * @param keyCode The keycode of the key which was pressed. + * @param event A {@link KeyEvent} describing the event. + * @return Whether the event was handled. + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (LOG_KEY_EVENTS) { + Log.w(TAG, "onKeyDown " + keyCode); + } + if (handleControlKey(keyCode, true)) { + return true; + } else if (handleFnKey(keyCode, true)) { + return true; + } else if (isSystemKey(keyCode, event)) { + if (! isInterceptedSystemKey(keyCode) ) { + // Don't intercept the system keys + return super.onKeyDown(keyCode, event); + } + } + + // Translate the keyCode into an ASCII character. + + try { + int oldCombiningAccent = mKeyListener.getCombiningAccent(); + int oldCursorMode = mKeyListener.getCursorMode(); + mKeyListener.keyDown(keyCode, event, getKeypadApplicationMode(), + TermKeyListener.isEventFromToggleDevice(event)); + if (mKeyListener.getCombiningAccent() != oldCombiningAccent + || mKeyListener.getCursorMode() != oldCursorMode) { + invalidate(); + } + } catch (IOException e) { + // Ignore I/O exceptions + } + return true; + } + + /** Do we want to intercept this system key? */ + private boolean isInterceptedSystemKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_BACK && mBackKeySendsCharacter; + } + + /** + * Called when a key is released in the view. + * + * @param keyCode The keycode of the key which was released. + * @param event A {@link KeyEvent} describing the event. + * @return Whether the event was handled. + */ + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (LOG_KEY_EVENTS) { + Log.w(TAG, "onKeyUp " + keyCode); + } + if (handleControlKey(keyCode, false)) { + return true; + } else if (handleFnKey(keyCode, false)) { + return true; + } else if (isSystemKey(keyCode, event)) { + // Don't intercept the system keys + if ( ! isInterceptedSystemKey(keyCode) ) { + return super.onKeyUp(keyCode, event); + } + } + + mKeyListener.keyUp(keyCode, event); + clearSpecialKeyStatus(); + return true; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (sTrapAltAndMeta) { + boolean altEsc = mKeyListener.getAltSendsEsc(); + boolean altOn = (event.getMetaState() & KeyEvent.META_ALT_ON) != 0; + boolean metaOn = (event.getMetaState() & KeyEvent.META_META_ON) != 0; + boolean altPressed = (keyCode == KeyEvent.KEYCODE_ALT_LEFT) + || (keyCode == KeyEvent.KEYCODE_ALT_RIGHT); + boolean altActive = mKeyListener.isAltActive(); + if (altEsc && (altOn || altPressed || altActive || metaOn)) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + return onKeyDown(keyCode, event); + } else { + return onKeyUp(keyCode, event); + } + } + } + + if (handleHardwareControlKey(keyCode, event)) { + return true; + } + + if (mKeyListener.isCtrlActive()) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + return onKeyDown(keyCode, event); + } else { + return onKeyUp(keyCode, event); + } + } + + return super.onKeyPreIme(keyCode, event); + }; + + private boolean handleControlKey(int keyCode, boolean down) { + if (keyCode == mControlKeyCode) { + if (LOG_KEY_EVENTS) { + Log.w(TAG, "handleControlKey " + keyCode); + } + mKeyListener.handleControlKey(down); + invalidate(); + return true; + } + return false; + } + + private boolean handleHardwareControlKey(int keyCode, KeyEvent event) { + if (keyCode == KeycodeConstants.KEYCODE_CTRL_LEFT || + keyCode == KeycodeConstants.KEYCODE_CTRL_RIGHT) { + if (LOG_KEY_EVENTS) { + Log.w(TAG, "handleHardwareControlKey " + keyCode); + } + boolean down = event.getAction() == KeyEvent.ACTION_DOWN; + mKeyListener.handleHardwareControlKey(down); + invalidate(); + return true; + } + return false; + } + + private boolean handleFnKey(int keyCode, boolean down) { + if (keyCode == mFnKeyCode) { + if (LOG_KEY_EVENTS) { + Log.w(TAG, "handleFnKey " + keyCode); + } + mKeyListener.handleFnKey(down); + invalidate(); + return true; + } + return false; + } + + private boolean isSystemKey(int keyCode, KeyEvent event) { + return event.isSystem(); + } + + private void clearSpecialKeyStatus() { + if (mIsControlKeySent) { + mIsControlKeySent = false; + mKeyListener.handleControlKey(false); + invalidate(); + } + if (mIsFnKeySent) { + mIsFnKeySent = false; + mKeyListener.handleFnKey(false); + invalidate(); + } + } + + private void updateText() { + ColorScheme scheme = mColorScheme; + if (mTextSize > 0) { + mTextRenderer = new PaintRenderer(mTextSize, scheme); + } + else { + mTextRenderer = new Bitmap4x8FontRenderer(getResources(), scheme); + } + + mForegroundPaint.setColor(scheme.getForeColor()); + mBackgroundPaint.setColor(scheme.getBackColor()); + mCharacterWidth = mTextRenderer.getCharacterWidth(); + mCharacterHeight = mTextRenderer.getCharacterHeight(); + + updateSize(true); + } + + /** + * This is called during layout when the size of this view has changed. If + * you were just added to the view hierarchy, you're called with the old + * values of 0. + */ + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (mTermSession == null) { + // Not ready, defer until TermSession is attached + mDeferInit = true; + return; + } + + if (!mKnownSize) { + mKnownSize = true; + initialize(); + } else { + updateSize(false); + } + } + + private void updateSize(int w, int h) { + mColumns = Math.max(1, (int) (((float) w) / mCharacterWidth)); + mVisibleColumns = Math.max(1, (int) (((float) mVisibleWidth) / mCharacterWidth)); + + mTopOfScreenMargin = mTextRenderer.getTopMargin(); + mRows = Math.max(1, (h - mTopOfScreenMargin) / mCharacterHeight); + mVisibleRows = Math.max(1, (mVisibleHeight - mTopOfScreenMargin) / mCharacterHeight); + mTermSession.updateSize(mColumns, mRows); + + // Reset our paging: + mTopRow = 0; + mLeftColumn = 0; + + invalidate(); + } + + /** + * Update the view's idea of its size. + * + * @param force Whether a size adjustment should be performed even if the + * view's size has not changed. + */ + public void updateSize(boolean force) { + //Need to clear saved links on each display refresh + mLinkLayer.clear(); + if (mKnownSize) { + int w = getWidth(); + int h = getHeight(); + // Log.w("Term", "(" + w + ", " + h + ")"); + if (force || w != mVisibleWidth || h != mVisibleHeight) { + mVisibleWidth = w; + mVisibleHeight = h; + updateSize(mVisibleWidth, mVisibleHeight); + } + } + } + + /** + * Draw the view to the provided {@link Canvas}. + * + * @param canvas The {@link Canvas} to draw the view to. + */ + @Override + protected void onDraw(Canvas canvas) { + updateSize(false); + + if (mEmulator == null) { + // Not ready yet + return; + } + + int w = getWidth(); + int h = getHeight(); + + boolean reverseVideo = mEmulator.getReverseVideo(); + mTextRenderer.setReverseVideo(reverseVideo); + + Paint backgroundPaint = + reverseVideo ? mForegroundPaint : mBackgroundPaint; + canvas.drawRect(0, 0, w, h, backgroundPaint); + float x = -mLeftColumn * mCharacterWidth; + float y = mCharacterHeight + mTopOfScreenMargin; + int endLine = mTopRow + mRows; + int cx = mEmulator.getCursorCol(); + int cy = mEmulator.getCursorRow(); + boolean cursorVisible = mCursorVisible && mEmulator.getShowCursor(); + String effectiveImeBuffer = mImeBuffer; + int combiningAccent = mKeyListener.getCombiningAccent(); + if (combiningAccent != 0) { + effectiveImeBuffer += String.valueOf((char) combiningAccent); + } + int cursorStyle = mKeyListener.getCursorMode(); + + int linkLinesToSkip = 0; //for multi-line links + + for (int i = mTopRow; i < endLine; i++) { + int cursorX = -1; + if (i == cy && cursorVisible) { + cursorX = cx; + } + int selx1 = -1; + int selx2 = -1; + if ( i >= mSelY1 && i <= mSelY2 ) { + if ( i == mSelY1 ) { + selx1 = mSelX1; + } + if ( i == mSelY2 ) { + selx2 = mSelX2; + } else { + selx2 = mColumns; + } + } + mEmulator.getScreen().drawText(i, canvas, x, y, mTextRenderer, cursorX, selx1, selx2, effectiveImeBuffer, cursorStyle); + y += mCharacterHeight; + //if no lines to skip, create links for the line being drawn + if(linkLinesToSkip == 0) + linkLinesToSkip = createLinks(i); + + //createLinks always returns at least 1 + --linkLinesToSkip; + } + } + + private void ensureCursorVisible() { + mTopRow = 0; + if (mVisibleColumns > 0) { + int cx = mEmulator.getCursorCol(); + int visibleCursorX = mEmulator.getCursorCol() - mLeftColumn; + if (visibleCursorX < 0) { + mLeftColumn = cx; + } else if (visibleCursorX >= mVisibleColumns) { + mLeftColumn = (cx - mVisibleColumns) + 1; + } + } + } + + /** + * Toggle text selection mode in the view. + */ + public void toggleSelectingText() { + mIsSelectingText = ! mIsSelectingText; + setVerticalScrollBarEnabled( ! mIsSelectingText ); + if ( ! mIsSelectingText ) { + mSelX1 = -1; + mSelY1 = -1; + mSelX2 = -1; + mSelY2 = -1; + } + } + + /** + * Whether the view is currently in text selection mode. + */ + public boolean getSelectingText() { + return mIsSelectingText; + } + + /** + * Get selected text. + * + * @return A {@link String} with the selected text. + */ + public String getSelectedText() { + return mEmulator.getSelectedText(mSelX1, mSelY1, mSelX2, mSelY2); + } + + /** + * Send a Ctrl key event to the terminal. + */ + public void sendControlKey() { + mIsControlKeySent = true; + mKeyListener.handleControlKey(true); + invalidate(); + } + + /** + * Send an Fn key event to the terminal. The Fn modifier key can be used to + * generate various special characters and escape codes. + */ + public void sendFnKey() { + mIsFnKeySent = true; + mKeyListener.handleFnKey(true); + invalidate(); + } + + /** + * Set the key code to be sent when the Back key is pressed. + */ + public void setBackKeyCharacter(int keyCode) { + mKeyListener.setBackKeyCharacter(keyCode); + mBackKeySendsCharacter = (keyCode != 0); + } + + /** + * Set whether to prepend the ESC keycode to the character when when pressing + * the ALT Key. + * @param flag + */ + public void setAltSendsEsc(boolean flag) { + mKeyListener.setAltSendsEsc(flag); + } + + /** + * Set the keycode corresponding to the Ctrl key. + */ + public void setControlKeyCode(int keyCode) { + mControlKeyCode = keyCode; + } + + /** + * Set the keycode corresponding to the Fn key. + */ + public void setFnKeyCode(int keyCode) { + mFnKeyCode = keyCode; + } + + public void setTermType(String termType) { + mKeyListener.setTermType(termType); + } + + /** + * Set whether mouse events should be sent to the terminal as escape codes. + */ + public void setMouseTracking(boolean flag) { + mMouseTracking = flag; + } + + + /** + * Get the URL for the link displayed at the specified screen coordinates. + * + * @param x The x coordinate being queried (from 0 to screen width) + * @param y The y coordinate being queried (from 0 to screen height) + * @return The URL for the link at the specified screen coordinates, or + * null if no link exists there. + */ + public String getURLat(float x, float y) + { + float w = getWidth(); + float h = getHeight(); + + //Check for division by zero + //If width or height is zero, there are probably no links around, so return null. + if(w == 0 || h == 0) + return null; + + //Get fraction of total screen + float x_pos = x / w; + float y_pos = y / h; + + //Convert to integer row/column index + int row = (int)Math.floor(y_pos * mRows); + int col = (int)Math.floor(x_pos * mColumns); + + //Grab row from link layer + URLSpan [] linkRow = mLinkLayer.get(row); + URLSpan link; + + //If row exists, and link exists at column, return it + if(linkRow != null && (link = linkRow[col]) != null) + return link.getURL(); + else + return null; + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/GrowableIntArray.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/GrowableIntArray.java new file mode 100644 index 0000000..9576ae7 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/GrowableIntArray.java @@ -0,0 +1,29 @@ +package jackpal.androidterm.emulatorview; + +class GrowableIntArray { + GrowableIntArray(int initalCapacity) { + mData = new int[initalCapacity]; + mLength = 0; + } + + void append(int i) { + if (mLength + 1 > mData.length) { + int newLength = Math.max((mData.length * 3) >> 1, 16); + int[] temp = new int[newLength]; + System.arraycopy(mData, 0, temp, 0, mLength); + mData = temp; + } + mData[mLength++] = i; + } + + int length() { + return mLength; + } + + int at(int index) { + return mData[index]; + } + + int[] mData; + int mLength; +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/PaintRenderer.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/PaintRenderer.java new file mode 100644 index 0000000..4b2137a --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/PaintRenderer.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.util.FloatMath; + + +class PaintRenderer extends BaseTextRenderer { + public PaintRenderer(int fontSize, ColorScheme scheme) { + super(scheme); + mTextPaint = new Paint(); + mTextPaint.setTypeface(Typeface.MONOSPACE); + mTextPaint.setAntiAlias(true); + mTextPaint.setTextSize(fontSize); + + mCharHeight = (int) FloatMath.ceil(mTextPaint.getFontSpacing()); + mCharAscent = (int) FloatMath.ceil(mTextPaint.ascent()); + mCharDescent = mCharHeight + mCharAscent; + mCharWidth = mTextPaint.measureText(EXAMPLE_CHAR, 0, 1); + } + + public void drawTextRun(Canvas canvas, float x, float y, int lineOffset, + int runWidth, char[] text, int index, int count, + boolean selectionStyle, int textStyle, + int cursorOffset, int cursorIndex, int cursorIncr, int cursorWidth, int cursorMode) { + int foreColor = TextStyle.decodeForeColor(textStyle); + int backColor = TextStyle.decodeBackColor(textStyle); + int effect = TextStyle.decodeEffect(textStyle); + + boolean inverse = mReverseVideo ^ + (effect & (TextStyle.fxInverse | TextStyle.fxItalic)) != 0; + if (inverse) { + int temp = foreColor; + foreColor = backColor; + backColor = temp; + } + + if (selectionStyle) { + backColor = TextStyle.ciCursorBackground; + } + + boolean blink = (effect & TextStyle.fxBlink) != 0; + if (blink && backColor < 8) { + backColor += 8; + } + mTextPaint.setColor(mPalette[backColor]); + + float left = x + lineOffset * mCharWidth; + canvas.drawRect(left, y + mCharAscent - mCharDescent, + left + runWidth * mCharWidth, y, + mTextPaint); + + boolean cursorVisible = lineOffset <= cursorOffset && cursorOffset < (lineOffset + runWidth); + float cursorX = 0; + if (cursorVisible) { + cursorX = x + cursorOffset * mCharWidth; + drawCursorImp(canvas, (int) cursorX, y, cursorWidth * mCharWidth, mCharHeight, cursorMode); + } + + boolean invisible = (effect & TextStyle.fxInvisible) != 0; + if (!invisible) { + boolean bold = (effect & TextStyle.fxBold) != 0; + boolean underline = (effect & TextStyle.fxUnderline) != 0; + if (bold) { + mTextPaint.setFakeBoldText(true); + } + if (underline) { + mTextPaint.setUnderlineText(true); + } + int textPaintColor; + if (foreColor < 8 && bold) { + // In 16-color mode, bold also implies bright foreground colors + textPaintColor = mPalette[foreColor+8]; + } else { + textPaintColor = mPalette[foreColor]; + } + mTextPaint.setColor(textPaintColor); + + float textOriginY = y - mCharDescent; + + if (cursorVisible) { + // Text before cursor + int countBeforeCursor = cursorIndex - index; + int countAfterCursor = count - (countBeforeCursor + cursorIncr); + if (countBeforeCursor > 0){ + canvas.drawText(text, index, countBeforeCursor, left, textOriginY, mTextPaint); + } + // Text at cursor + mTextPaint.setColor(mPalette[TextStyle.ciCursorForeground]); + canvas.drawText(text, cursorIndex, cursorIncr, cursorX, + textOriginY, mTextPaint); + // Text after cursor + if (countAfterCursor > 0) { + mTextPaint.setColor(textPaintColor); + canvas.drawText(text, cursorIndex + cursorIncr, countAfterCursor, + cursorX + cursorWidth * mCharWidth, + textOriginY, mTextPaint); + } + } else { + canvas.drawText(text, index, count, left, textOriginY, mTextPaint); + } + if (bold) { + mTextPaint.setFakeBoldText(false); + } + if (underline) { + mTextPaint.setUnderlineText(false); + } + } + } + + public int getCharacterHeight() { + return mCharHeight; + } + + public float getCharacterWidth() { + return mCharWidth; + } + + public int getTopMargin() { + return mCharDescent; + } + + private Paint mTextPaint; + private float mCharWidth; + private int mCharHeight; + private int mCharAscent; + private int mCharDescent; + private static final char[] EXAMPLE_CHAR = {'X'}; +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/Screen.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/Screen.java new file mode 100644 index 0000000..84110be --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/Screen.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +/** + * An abstract screen interface. A terminal screen stores lines of text. (The + * reason to abstract it is to allow different implementations, and to hide + * implementation details from clients.) + */ +interface Screen { + + /** + * Set line wrap flag for a given row. Affects how lines are logically + * wrapped when changing screen size or converting to a transcript. + */ + void setLineWrap(int row); + + /** + * Store a Unicode code point into the screen at location (x, y) + * + * @param x X coordinate (also known as column) + * @param y Y coordinate (also known as row) + * @param codePoint Unicode code point to store + * @param style the text style + */ + void set(int x, int y, int codePoint, int style); + + /** + * Store byte b into the screen at location (x, y) + * + * @param x X coordinate (also known as column) + * @param y Y coordinate (also known as row) + * @param b ASCII character to store + * @param style the text style + */ + void set(int x, int y, byte b, int style); + + /** + * Scroll the screen down one line. To scroll the whole screen of a 24 line + * screen, the arguments would be (0, 24). + * + * @param topMargin First line that is scrolled. + * @param bottomMargin One line after the last line that is scrolled. + * @param style the style for the newly exposed line. + */ + void scroll(int topMargin, int bottomMargin, int style); + + /** + * Block copy characters from one position in the screen to another. The two + * positions can overlap. All characters of the source and destination must + * be within the bounds of the screen, or else an InvalidParemeterException + * will be thrown. + * + * @param sx source X coordinate + * @param sy source Y coordinate + * @param w width + * @param h height + * @param dx destination X coordinate + * @param dy destination Y coordinate + */ + void blockCopy(int sx, int sy, int w, int h, int dx, int dy); + + /** + * Block set characters. All characters must be within the bounds of the + * screen, or else and InvalidParemeterException will be thrown. Typically + * this is called with a "val" argument of 32 to clear a block of + * characters. + * + * @param sx source X + * @param sy source Y + * @param w width + * @param h height + * @param val value to set. + * @param style the text style + */ + void blockSet(int sx, int sy, int w, int h, int val, int style); + + /** + * Get the contents of the transcript buffer as a text string. + * + * @return the contents of the transcript buffer. + */ + String getTranscriptText(); + + /** + * Get the contents of the transcript buffer as a text string with color + * information. + * + * @param colors A GrowableIntArray which will hold the colors. + * @return the contents of the transcript buffer. + */ + String getTranscriptText(GrowableIntArray colors); + + /** + * Get the selected text inside transcript buffer as a text string. + * @param x1 Selection start + * @param y1 Selection start + * @param x2 Selection end + * @param y2 Selection end + * @return the contents of the transcript buffer. + */ + String getSelectedText(int x1, int y1, int x2, int y2); + + /** + * Get the selected text inside transcript buffer as a text string with + * color information. + * + * @param colors A StringBuilder which will hold the colors. + * @param x1 Selection start + * @param y1 Selection start + * @param x2 Selection end + * @param y2 Selection end + * @return the contents of the transcript buffer. + */ + String getSelectedText(GrowableIntArray colors, int x1, int y1, int x2, int y2); + + /** + * Get the number of "active" (in-use) screen rows, including any rows in a + * scrollback buffer. + */ + int getActiveRows(); + + /** + * Try to resize the screen without losing its contents. + * + * @param columns + * @param rows + * @param cursor An int[2] containing the current cursor position + * { col, row }. If the resize succeeds, the array will be + * updated to reflect the new location. + * @return Whether the resize succeeded. If the operation fails, save the + * contents of the screen and then use the standard resize. + */ + boolean fastResize(int columns, int rows, int[] cursor); + + /** + * Resize the screen + * @param columns + * @param rows + * @param style + */ + void resize(int columns, int rows, int style); +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/StyleRow.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/StyleRow.java new file mode 100644 index 0000000..de9484a --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/StyleRow.java @@ -0,0 +1,97 @@ +package jackpal.androidterm.emulatorview; + +/** + * Utility class for dealing with text style lines. + * + * We pack color and formatting information for a particular character into an + * int -- see the TextStyle class for details. The simplest way of storing + * that information for a screen row would be to use an array of int -- but + * given that we only use the lower three bytes of the int to store information, + * that effectively wastes one byte per character -- nearly 8 KB per 100 lines + * with an 80-column transcript. + * + * Instead, we use an array of bytes and store the bytes of each int + * consecutively in big-endian order. + */ +final class StyleRow { + private int mStyle; + private int mColumns; + /** Initially null, will be allocated when needed. */ + private byte[] mData; + + StyleRow(int style, int columns) { + mStyle = style; + mColumns = columns; + } + + void set(int column, int style) { + if (style == mStyle && mData == null) { + return; + } + ensureData(); + setStyle(column, style); + } + + int get(int column) { + if (mData == null) { + return mStyle; + } + return getStyle(column); + } + + boolean isSolidStyle() { + return mData == null; + } + + int getSolidStyle() { + if (mData != null) { + throw new IllegalArgumentException("Not a solid style"); + } + return mStyle; + } + + void copy(int start, StyleRow dst, int offset, int len) { + // fast case + if (mData == null && dst.mData == null && start == 0 && offset == 0 + && len == mColumns) { + dst.mStyle = mStyle; + return; + } + // There are other potentially fast cases, but let's just treat them + // all the same for simplicity. + ensureData(); + dst.ensureData(); + System.arraycopy(mData, 3*start, dst.mData, 3*offset, 3*len); + + } + + void ensureData() { + if (mData == null) { + allocate(); + } + } + + private void allocate() { + mData = new byte[3*mColumns]; + for (int i = 0; i < mColumns; i++) { + setStyle(i, mStyle); + } + } + + private int getStyle(int column) { + int index = 3 * column; + byte[] line = mData; + return line[index] & 0xff | (line[index+1] & 0xff) << 8 + | (line[index+2] & 0xff) << 16; + } + + private void setStyle(int column, int value) { + int index = 3 * column; + byte[] line = mData; + line[index] = (byte) (value & 0xff); + line[index+1] = (byte) ((value >> 8) & 0xff); + line[index+2] = (byte) ((value >> 16) & 0xff); + } + + +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TermKeyListener.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TermKeyListener.java new file mode 100644 index 0000000..eb2ae60 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TermKeyListener.java @@ -0,0 +1,750 @@ +package jackpal.androidterm.emulatorview; + +import jackpal.androidterm.emulatorview.compat.AndroidCompat; +import jackpal.androidterm.emulatorview.compat.KeyCharacterMapCompat; + +import java.io.IOException; +import java.util.Map; +import java.util.HashMap; + +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import static jackpal.androidterm.emulatorview.compat.KeycodeConstants.*; + +/** + * An ASCII key listener. Supports control characters and escape. Keeps track of + * the current state of the alt, shift, fn, and control keys. + * + */ +class TermKeyListener { + private final static String TAG = "TermKeyListener"; + private static final boolean LOG_MISC = false; + private static final boolean LOG_KEYS = false; + private static final boolean LOG_COMBINING_ACCENT = false; + + /** Disabled for now because it interferes with ALT processing on phones with physical keyboards. */ + private final static boolean SUPPORT_8_BIT_META = false; + + private static final int KEYMOD_ALT = 0x80000000; + private static final int KEYMOD_CTRL = 0x40000000; + private static final int KEYMOD_SHIFT = 0x20000000; + /** Means this maps raw scancode */ + private static final int KEYMOD_SCAN = 0x10000000; + + private static Map mKeyMap; + + private String[] mKeyCodes = new String[256]; + private String[] mAppKeyCodes = new String[256]; + + private void initKeyCodes() { + mKeyMap = new HashMap(); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_DPAD_LEFT, "\033[1;2D"); + mKeyMap.put(KEYMOD_ALT | KEYCODE_DPAD_LEFT, "\033[1;3D"); + mKeyMap.put(KEYMOD_ALT | KEYMOD_SHIFT | KEYCODE_DPAD_LEFT, "\033[1;4D"); + mKeyMap.put(KEYMOD_CTRL | KEYCODE_DPAD_LEFT, "\033[1;5D"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_SHIFT | KEYCODE_DPAD_LEFT, "\033[1;6D"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_ALT | KEYCODE_DPAD_LEFT, "\033[1;7D"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_ALT | KEYMOD_SHIFT | KEYCODE_DPAD_LEFT, "\033[1;8D"); + + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT, "\033[1;2C"); + mKeyMap.put(KEYMOD_ALT | KEYCODE_DPAD_RIGHT, "\033[1;3C"); + mKeyMap.put(KEYMOD_ALT | KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT, "\033[1;4C"); + mKeyMap.put(KEYMOD_CTRL | KEYCODE_DPAD_RIGHT, "\033[1;5C"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT, "\033[1;6C"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_ALT | KEYCODE_DPAD_RIGHT, "\033[1;7C"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_ALT | KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT, "\033[1;8C"); + + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_DPAD_UP, "\033[1;2A"); + mKeyMap.put(KEYMOD_ALT | KEYCODE_DPAD_UP, "\033[1;3A"); + mKeyMap.put(KEYMOD_ALT | KEYMOD_SHIFT | KEYCODE_DPAD_UP, "\033[1;4A"); + mKeyMap.put(KEYMOD_CTRL | KEYCODE_DPAD_UP, "\033[1;5A"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_SHIFT | KEYCODE_DPAD_UP, "\033[1;6A"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_ALT | KEYCODE_DPAD_UP, "\033[1;7A"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_ALT | KEYMOD_SHIFT | KEYCODE_DPAD_UP, "\033[1;8A"); + + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_DPAD_DOWN, "\033[1;2B"); + mKeyMap.put(KEYMOD_ALT | KEYCODE_DPAD_DOWN, "\033[1;3B"); + mKeyMap.put(KEYMOD_ALT | KEYMOD_SHIFT | KEYCODE_DPAD_DOWN, "\033[1;4B"); + mKeyMap.put(KEYMOD_CTRL | KEYCODE_DPAD_DOWN, "\033[1;5B"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_SHIFT | KEYCODE_DPAD_DOWN, "\033[1;6B"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_ALT | KEYCODE_DPAD_DOWN, "\033[1;7B"); + mKeyMap.put(KEYMOD_CTRL | KEYMOD_ALT | KEYMOD_SHIFT | KEYCODE_DPAD_DOWN, "\033[1;8B"); + + //^[[3~ + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_FORWARD_DEL, "\033[3;2~"); + mKeyMap.put(KEYMOD_ALT | KEYCODE_FORWARD_DEL, "\033[3;3~"); + mKeyMap.put(KEYMOD_CTRL | KEYCODE_FORWARD_DEL, "\033[3;5~"); + + //^[[2~ + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_INSERT, "\033[2;2~"); + mKeyMap.put(KEYMOD_ALT | KEYCODE_INSERT, "\033[2;3~"); + mKeyMap.put(KEYMOD_CTRL | KEYCODE_INSERT, "\033[2;5~"); + + mKeyMap.put(KEYMOD_CTRL | KEYCODE_MOVE_HOME, "\033[1;5H"); + mKeyMap.put(KEYMOD_CTRL | KEYCODE_MOVE_END, "\033[1;5F"); + + mKeyMap.put(KEYMOD_ALT | KEYCODE_ENTER, "\033\r"); + mKeyMap.put(KEYMOD_CTRL | KEYCODE_ENTER, "\n"); + // Duh, so special... + mKeyMap.put(KEYMOD_CTRL | KEYCODE_SPACE, "\000"); + + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F1, "\033[1;2P"); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F2, "\033[1;2Q"); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F3, "\033[1;2R"); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F4, "\033[1;2S"); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F5, "\033[15;2~"); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F6, "\033[17;2~"); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F7, "\033[18;2~"); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F8, "\033[19;2~"); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F9, "\033[20;2~"); + mKeyMap.put(KEYMOD_SHIFT | KEYCODE_F10, "\033[21;2~"); + + mKeyCodes[KEYCODE_DPAD_CENTER] = "\015"; + mKeyCodes[KEYCODE_DPAD_UP] = "\033[A"; + mKeyCodes[KEYCODE_DPAD_DOWN] = "\033[B"; + mKeyCodes[KEYCODE_DPAD_RIGHT] = "\033[C"; + mKeyCodes[KEYCODE_DPAD_LEFT] = "\033[D"; + setFnKeys("vt100"); + mKeyCodes[KEYCODE_SYSRQ] = "\033[32~"; // Sys Request / Print + // Is this Scroll lock? mKeyCodes[Cancel] = "\033[33~"; + mKeyCodes[KEYCODE_BREAK] = "\033[34~"; // Pause/Break + + mKeyCodes[KEYCODE_TAB] = "\011"; + mKeyCodes[KEYCODE_ENTER] = "\015"; + mKeyCodes[KEYCODE_ESCAPE] = "\033"; + + mKeyCodes[KEYCODE_INSERT] = "\033[2~"; + mKeyCodes[KEYCODE_FORWARD_DEL] = "\033[3~"; + // Home/End keys are set by setFnKeys() + mKeyCodes[KEYCODE_PAGE_UP] = "\033[5~"; + mKeyCodes[KEYCODE_PAGE_DOWN] = "\033[6~"; + mKeyCodes[KEYCODE_DEL]= "\177"; + mKeyCodes[KEYCODE_NUM_LOCK] = "\033OP"; + mKeyCodes[KEYCODE_NUMPAD_DIVIDE] = "/"; + mKeyCodes[KEYCODE_NUMPAD_MULTIPLY] = "*"; + mKeyCodes[KEYCODE_NUMPAD_SUBTRACT] = "-"; + mKeyCodes[KEYCODE_NUMPAD_ADD] = "+"; + mKeyCodes[KEYCODE_NUMPAD_ENTER] = "\015"; + mKeyCodes[KEYCODE_NUMPAD_EQUALS] = "="; + mKeyCodes[KEYCODE_NUMPAD_COMMA] = ","; +/* + mKeyCodes[KEYCODE_NUMPAD_DOT] = "."; + mKeyCodes[KEYCODE_NUMPAD_0] = "0"; + mKeyCodes[KEYCODE_NUMPAD_1] = "1"; + mKeyCodes[KEYCODE_NUMPAD_2] = "2"; + mKeyCodes[KEYCODE_NUMPAD_3] = "3"; + mKeyCodes[KEYCODE_NUMPAD_4] = "4"; + mKeyCodes[KEYCODE_NUMPAD_5] = "5"; + mKeyCodes[KEYCODE_NUMPAD_6] = "6"; + mKeyCodes[KEYCODE_NUMPAD_7] = "7"; + mKeyCodes[KEYCODE_NUMPAD_8] = "8"; + mKeyCodes[KEYCODE_NUMPAD_9] = "9"; +*/ + // Keypad is used for cursor/func keys + mKeyCodes[KEYCODE_NUMPAD_DOT] = mKeyCodes[KEYCODE_FORWARD_DEL]; + mKeyCodes[KEYCODE_NUMPAD_0] = mKeyCodes[KEYCODE_INSERT]; + mKeyCodes[KEYCODE_NUMPAD_1] = mKeyCodes[KEYCODE_MOVE_END]; + mKeyCodes[KEYCODE_NUMPAD_2] = mKeyCodes[KEYCODE_DPAD_DOWN]; + mKeyCodes[KEYCODE_NUMPAD_3] = mKeyCodes[KEYCODE_PAGE_DOWN]; + mKeyCodes[KEYCODE_NUMPAD_4] = mKeyCodes[KEYCODE_DPAD_LEFT]; + mKeyCodes[KEYCODE_NUMPAD_5] = "5"; + mKeyCodes[KEYCODE_NUMPAD_6] = mKeyCodes[KEYCODE_DPAD_RIGHT]; + mKeyCodes[KEYCODE_NUMPAD_7] = mKeyCodes[KEYCODE_MOVE_HOME]; + mKeyCodes[KEYCODE_NUMPAD_8] = mKeyCodes[KEYCODE_DPAD_UP]; + mKeyCodes[KEYCODE_NUMPAD_9] = mKeyCodes[KEYCODE_PAGE_UP]; + + +// mAppKeyCodes[KEYCODE_DPAD_UP] = "\033OA"; +// mAppKeyCodes[KEYCODE_DPAD_DOWN] = "\033OB"; +// mAppKeyCodes[KEYCODE_DPAD_RIGHT] = "\033OC"; +// mAppKeyCodes[KEYCODE_DPAD_LEFT] = "\033OD"; + mAppKeyCodes[KEYCODE_NUMPAD_DIVIDE] = "\033Oo"; + mAppKeyCodes[KEYCODE_NUMPAD_MULTIPLY] = "\033Oj"; + mAppKeyCodes[KEYCODE_NUMPAD_SUBTRACT] = "\033Om"; + mAppKeyCodes[KEYCODE_NUMPAD_ADD] = "\033Ok"; + mAppKeyCodes[KEYCODE_NUMPAD_ENTER] = "\033OM"; + mAppKeyCodes[KEYCODE_NUMPAD_EQUALS] = "\033OX"; + mAppKeyCodes[KEYCODE_NUMPAD_DOT] = "\033On"; + mAppKeyCodes[KEYCODE_NUMPAD_COMMA] = "\033Ol"; + mAppKeyCodes[KEYCODE_NUMPAD_0] = "\033Op"; + mAppKeyCodes[KEYCODE_NUMPAD_1] = "\033Oq"; + mAppKeyCodes[KEYCODE_NUMPAD_2] = "\033Or"; + mAppKeyCodes[KEYCODE_NUMPAD_3] = "\033Os"; + mAppKeyCodes[KEYCODE_NUMPAD_4] = "\033Ot"; + mAppKeyCodes[KEYCODE_NUMPAD_5] = "\033Ou"; + mAppKeyCodes[KEYCODE_NUMPAD_6] = "\033Ov"; + mAppKeyCodes[KEYCODE_NUMPAD_7] = "\033Ow"; + mAppKeyCodes[KEYCODE_NUMPAD_8] = "\033Ox"; + mAppKeyCodes[KEYCODE_NUMPAD_9] = "\033Oy"; + } + + public void setCursorKeysApplicationMode(boolean val) { + if (LOG_MISC) { + Log.d(EmulatorDebug.LOG_TAG, "CursorKeysApplicationMode=" + val); + } + if (val) { + mKeyCodes[KEYCODE_NUMPAD_8] = mKeyCodes[KEYCODE_DPAD_UP] = "\033OA"; + mKeyCodes[KEYCODE_NUMPAD_2] = mKeyCodes[KEYCODE_DPAD_DOWN] = "\033OB"; + mKeyCodes[KEYCODE_NUMPAD_6] = mKeyCodes[KEYCODE_DPAD_RIGHT] = "\033OC"; + mKeyCodes[KEYCODE_NUMPAD_4] = mKeyCodes[KEYCODE_DPAD_LEFT] = "\033OD"; + } else { + mKeyCodes[KEYCODE_NUMPAD_8] = mKeyCodes[KEYCODE_DPAD_UP] = "\033[A"; + mKeyCodes[KEYCODE_NUMPAD_2] = mKeyCodes[KEYCODE_DPAD_DOWN] = "\033[B"; + mKeyCodes[KEYCODE_NUMPAD_6] = mKeyCodes[KEYCODE_DPAD_RIGHT] = "\033[C"; + mKeyCodes[KEYCODE_NUMPAD_4] = mKeyCodes[KEYCODE_DPAD_LEFT] = "\033[D"; + } + } + + /** + * The state engine for a modifier key. Can be pressed, released, locked, + * and so on. + * + */ + private class ModifierKey { + + private int mState; + + private static final int UNPRESSED = 0; + + private static final int PRESSED = 1; + + private static final int RELEASED = 2; + + private static final int USED = 3; + + private static final int LOCKED = 4; + + /** + * Construct a modifier key. UNPRESSED by default. + * + */ + public ModifierKey() { + mState = UNPRESSED; + } + + public void onPress() { + switch (mState) { + case PRESSED: + // This is a repeat before use + break; + case RELEASED: + mState = LOCKED; + break; + case USED: + // This is a repeat after use + break; + case LOCKED: + mState = UNPRESSED; + break; + default: + mState = PRESSED; + break; + } + } + + public void onRelease() { + switch (mState) { + case USED: + mState = UNPRESSED; + break; + case PRESSED: + mState = RELEASED; + break; + default: + // Leave state alone + break; + } + } + + public void adjustAfterKeypress() { + switch (mState) { + case PRESSED: + mState = USED; + break; + case RELEASED: + mState = UNPRESSED; + break; + default: + // Leave state alone + break; + } + } + + public boolean isActive() { + return mState != UNPRESSED; + } + + public int getUIMode() { + switch (mState) { + default: + case UNPRESSED: + return TextRenderer.MODE_OFF; + case PRESSED: + case RELEASED: + case USED: + return TextRenderer.MODE_ON; + case LOCKED: + return TextRenderer.MODE_LOCKED; + } + } + } + + private ModifierKey mAltKey = new ModifierKey(); + + private ModifierKey mCapKey = new ModifierKey(); + + private ModifierKey mControlKey = new ModifierKey(); + + private ModifierKey mFnKey = new ModifierKey(); + + private int mCursorMode; + + private boolean mHardwareControlKey; + + private TermSession mTermSession; + + private int mBackKeyCode; + private boolean mAltSendsEsc; + + private int mCombiningAccent; + + // Map keycodes out of (above) the Unicode code point space. + static public final int KEYCODE_OFFSET = 0xA00000; + + /** + * Construct a term key listener. + * + */ + public TermKeyListener(TermSession termSession) { + mTermSession = termSession; + initKeyCodes(); + updateCursorMode(); + } + + public void setBackKeyCharacter(int code) { + mBackKeyCode = code; + } + + public void setAltSendsEsc(boolean flag) { + mAltSendsEsc = flag; + } + + public void handleHardwareControlKey(boolean down) { + mHardwareControlKey = down; + } + + public void onPause() { + // Ensure we don't have any left-over modifier state when switching + // views. + mHardwareControlKey = false; + } + + public void onResume() { + // Nothing special. + } + + public void handleControlKey(boolean down) { + if (down) { + mControlKey.onPress(); + } else { + mControlKey.onRelease(); + } + updateCursorMode(); + } + + public void handleFnKey(boolean down) { + if (down) { + mFnKey.onPress(); + } else { + mFnKey.onRelease(); + } + updateCursorMode(); + } + + public void setTermType(String termType) { + setFnKeys(termType); + } + + private void setFnKeys(String termType) { + // These key assignments taken from the debian squeeze terminfo database. + if (termType.equals("xterm")) { + mKeyCodes[KEYCODE_NUMPAD_7] = mKeyCodes[KEYCODE_MOVE_HOME] = "\033OH"; + mKeyCodes[KEYCODE_NUMPAD_1] = mKeyCodes[KEYCODE_MOVE_END] = "\033OF"; + } else { + mKeyCodes[KEYCODE_NUMPAD_7] = mKeyCodes[KEYCODE_MOVE_HOME] = "\033[1~"; + mKeyCodes[KEYCODE_NUMPAD_1] = mKeyCodes[KEYCODE_MOVE_END] = "\033[4~"; + } + if (termType.equals("vt100")) { + mKeyCodes[KEYCODE_F1] = "\033OP"; // VT100 PF1 + mKeyCodes[KEYCODE_F2] = "\033OQ"; // VT100 PF2 + mKeyCodes[KEYCODE_F3] = "\033OR"; // VT100 PF3 + mKeyCodes[KEYCODE_F4] = "\033OS"; // VT100 PF4 + // the following keys are in the database, but aren't on a real vt100. + mKeyCodes[KEYCODE_F5] = "\033Ot"; + mKeyCodes[KEYCODE_F6] = "\033Ou"; + mKeyCodes[KEYCODE_F7] = "\033Ov"; + mKeyCodes[KEYCODE_F8] = "\033Ol"; + mKeyCodes[KEYCODE_F9] = "\033Ow"; + mKeyCodes[KEYCODE_F10] = "\033Ox"; + // The following keys are not in database. + mKeyCodes[KEYCODE_F11] = "\033[23~"; + mKeyCodes[KEYCODE_F12] = "\033[24~"; + } else if (termType.startsWith("linux")) { + mKeyCodes[KEYCODE_F1] = "\033[[A"; + mKeyCodes[KEYCODE_F2] = "\033[[B"; + mKeyCodes[KEYCODE_F3] = "\033[[C"; + mKeyCodes[KEYCODE_F4] = "\033[[D"; + mKeyCodes[KEYCODE_F5] = "\033[[E"; + mKeyCodes[KEYCODE_F6] = "\033[17~"; + mKeyCodes[KEYCODE_F7] = "\033[18~"; + mKeyCodes[KEYCODE_F8] = "\033[19~"; + mKeyCodes[KEYCODE_F9] = "\033[20~"; + mKeyCodes[KEYCODE_F10] = "\033[21~"; + mKeyCodes[KEYCODE_F11] = "\033[23~"; + mKeyCodes[KEYCODE_F12] = "\033[24~"; + } else { + // default + // screen, screen-256colors, xterm, anything new + mKeyCodes[KEYCODE_F1] = "\033OP"; // VT100 PF1 + mKeyCodes[KEYCODE_F2] = "\033OQ"; // VT100 PF2 + mKeyCodes[KEYCODE_F3] = "\033OR"; // VT100 PF3 + mKeyCodes[KEYCODE_F4] = "\033OS"; // VT100 PF4 + mKeyCodes[KEYCODE_F5] = "\033[15~"; + mKeyCodes[KEYCODE_F6] = "\033[17~"; + mKeyCodes[KEYCODE_F7] = "\033[18~"; + mKeyCodes[KEYCODE_F8] = "\033[19~"; + mKeyCodes[KEYCODE_F9] = "\033[20~"; + mKeyCodes[KEYCODE_F10] = "\033[21~"; + mKeyCodes[KEYCODE_F11] = "\033[23~"; + mKeyCodes[KEYCODE_F12] = "\033[24~"; + } + } + + public int mapControlChar(int ch) { + return mapControlChar(mHardwareControlKey || mControlKey.isActive(), mFnKey.isActive(), ch); + } + + public int mapControlChar(boolean control, boolean fn, int ch) { + int result = ch; + if (control) { + // Search is the control key. + if (result >= 'a' && result <= 'z') { + result = (char) (result - 'a' + '\001'); + } else if (result >= 'A' && result <= 'Z') { + result = (char) (result - 'A' + '\001'); + } else if (result == ' ' || result == '2') { + result = 0; + } else if (result == '[' || result == '3') { + result = 27; // ^[ (Esc) + } else if (result == '\\' || result == '4') { + result = 28; + } else if (result == ']' || result == '5') { + result = 29; + } else if (result == '^' || result == '6') { + result = 30; // control-^ + } else if (result == '_' || result == '7') { + result = 31; + } else if (result == '8') { + result = 127; // DEL + } else if (result == '9') { + result = KEYCODE_OFFSET + KEYCODE_F11; + } else if (result == '0') { + result = KEYCODE_OFFSET + KEYCODE_F12; + } + } else if (fn) { + if (result == 'w' || result == 'W') { + result = KEYCODE_OFFSET + KeyEvent.KEYCODE_DPAD_UP; + } else if (result == 'a' || result == 'A') { + result = KEYCODE_OFFSET + KeyEvent.KEYCODE_DPAD_LEFT; + } else if (result == 's' || result == 'S') { + result = KEYCODE_OFFSET + KeyEvent.KEYCODE_DPAD_DOWN; + } else if (result == 'd' || result == 'D') { + result = KEYCODE_OFFSET + KeyEvent.KEYCODE_DPAD_RIGHT; + } else if (result == 'p' || result == 'P') { + result = KEYCODE_OFFSET + KEYCODE_PAGE_UP; + } else if (result == 'n' || result == 'N') { + result = KEYCODE_OFFSET + KEYCODE_PAGE_DOWN; + } else if (result == 't' || result == 'T') { + result = KEYCODE_OFFSET + KeyEvent.KEYCODE_TAB; + } else if (result == 'l' || result == 'L') { + result = '|'; + } else if (result == 'u' || result == 'U') { + result = '_'; + } else if (result == 'e' || result == 'E') { + result = 27; // ^[ (Esc) + } else if (result == '.') { + result = 28; // ^\ + } else if (result > '0' && result <= '9') { + // F1-F9 + result = (char)(result + KEYCODE_OFFSET + KEYCODE_F1 - 1); + } else if (result == '0') { + result = KEYCODE_OFFSET + KEYCODE_F10; + } else if (result == 'i' || result == 'I') { + result = KEYCODE_OFFSET + KEYCODE_INSERT; + } else if (result == 'x' || result == 'X') { + result = KEYCODE_OFFSET + KEYCODE_FORWARD_DEL; + } else if (result == 'h' || result == 'H') { + result = KEYCODE_OFFSET + KEYCODE_MOVE_HOME; + } else if (result == 'f' || result == 'F') { + result = KEYCODE_OFFSET + KEYCODE_MOVE_END; + } + } + + if (result > -1) { + mAltKey.adjustAfterKeypress(); + mCapKey.adjustAfterKeypress(); + mControlKey.adjustAfterKeypress(); + mFnKey.adjustAfterKeypress(); + updateCursorMode(); + } + + return result; + } + + /** + * Handle a keyDown event. + * + * @param keyCode the keycode of the keyDown event + * + */ + public void keyDown(int keyCode, KeyEvent event, boolean appMode, + boolean allowToggle) throws IOException { + if (LOG_KEYS) { + Log.i(TAG, "keyDown(" + keyCode + "," + event + "," + appMode + "," + allowToggle + ")"); + } + if (handleKeyCode(keyCode, event, appMode)) { + return; + } + int result = -1; + boolean chordedCtrl = false; + boolean setHighBit = false; + switch (keyCode) { + case KeyEvent.KEYCODE_ALT_RIGHT: + case KeyEvent.KEYCODE_ALT_LEFT: + if (allowToggle) { + mAltKey.onPress(); + updateCursorMode(); + } + break; + + case KeyEvent.KEYCODE_SHIFT_LEFT: + case KeyEvent.KEYCODE_SHIFT_RIGHT: + if (allowToggle) { + mCapKey.onPress(); + updateCursorMode(); + } + break; + + case KEYCODE_CTRL_LEFT: + case KEYCODE_CTRL_RIGHT: + // Ignore the control key. + return; + + case KEYCODE_CAPS_LOCK: + // Ignore the capslock key. + return; + + case KEYCODE_FUNCTION: + // Ignore the function key. + return; + + case KeyEvent.KEYCODE_BACK: + result = mBackKeyCode; + break; + + default: { + int metaState = event.getMetaState(); + chordedCtrl = ((META_CTRL_ON & metaState) != 0); + boolean effectiveCaps = allowToggle && + (mCapKey.isActive()); + boolean effectiveAlt = allowToggle && mAltKey.isActive(); + int effectiveMetaState = metaState & (~META_CTRL_MASK); + if (effectiveCaps) { + effectiveMetaState |= KeyEvent.META_SHIFT_ON; + } + if (!allowToggle && (effectiveMetaState & META_ALT_ON) != 0) { + effectiveAlt = true; + } + if (effectiveAlt) { + if (mAltSendsEsc) { + mTermSession.write(new byte[]{0x1b},0,1); + effectiveMetaState &= ~KeyEvent.META_ALT_MASK; + } else if (SUPPORT_8_BIT_META) { + setHighBit = true; + effectiveMetaState &= ~KeyEvent.META_ALT_MASK; + } else { + // Legacy behavior: Pass Alt through to allow composing characters. + effectiveMetaState |= KeyEvent.META_ALT_ON; + } + } + + // Note: The Hacker keyboard IME key labeled Alt actually sends Meta. + + + if ((metaState & KeyEvent.META_META_ON) != 0) { + if (mAltSendsEsc) { + mTermSession.write(new byte[]{0x1b},0,1); + effectiveMetaState &= ~KeyEvent.META_META_MASK; + } else { + if (SUPPORT_8_BIT_META) { + setHighBit = true; + effectiveMetaState &= ~KeyEvent.META_META_MASK; + } + } + } + result = event.getUnicodeChar(effectiveMetaState); + + if ((result & KeyCharacterMap.COMBINING_ACCENT) != 0) { + if (LOG_COMBINING_ACCENT) { + Log.i(TAG, "Got combining accent " + result); + } + mCombiningAccent = result & KeyCharacterMap.COMBINING_ACCENT_MASK; + return; + } + if (mCombiningAccent != 0) { + int unaccentedChar = result; + result = KeyCharacterMap.getDeadChar(mCombiningAccent, unaccentedChar); + if (LOG_COMBINING_ACCENT) { + Log.i(TAG, "getDeadChar(" + mCombiningAccent + ", " + unaccentedChar + ") -> " + result); + } + mCombiningAccent = 0; + } + + break; + } + } + + boolean effectiveControl = chordedCtrl || mHardwareControlKey || (allowToggle && mControlKey.isActive()); + boolean effectiveFn = allowToggle && mFnKey.isActive(); + + result = mapControlChar(effectiveControl, effectiveFn, result); + + if (result >= KEYCODE_OFFSET) { + handleKeyCode(result - KEYCODE_OFFSET, null, appMode); + } else if (result >= 0) { + if (setHighBit) { + result |= 0x80; + } + mTermSession.write(result); + } + } + + public int getCombiningAccent() { + return mCombiningAccent; + } + + public int getCursorMode() { + return mCursorMode; + } + + private void updateCursorMode() { + mCursorMode = getCursorModeHelper(mCapKey, TextRenderer.MODE_SHIFT_SHIFT) + | getCursorModeHelper(mAltKey, TextRenderer.MODE_ALT_SHIFT) + | getCursorModeHelper(mControlKey, TextRenderer.MODE_CTRL_SHIFT) + | getCursorModeHelper(mFnKey, TextRenderer.MODE_FN_SHIFT); + } + + private static int getCursorModeHelper(ModifierKey key, int shift) { + return key.getUIMode() << shift; + } + + static boolean isEventFromToggleDevice(KeyEvent event) { + if (AndroidCompat.SDK < 11) { + return true; + } + KeyCharacterMapCompat kcm = KeyCharacterMapCompat.wrap( + KeyCharacterMap.load(event.getDeviceId())); + return kcm.getModifierBehaviour() == + KeyCharacterMapCompat.MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED; + } + + public boolean handleKeyCode(int keyCode, KeyEvent event, boolean appMode) throws IOException { + String code = null; + if (event != null) { + int keyMod = 0; + // META_CTRL_ON was added only in API 11, so don't use it, + // use our own tracking of Ctrl key instead. + // (event.getMetaState() & META_CTRL_ON) != 0 + if (mHardwareControlKey || mControlKey.isActive()) { + keyMod |= KEYMOD_CTRL; + } + if ((event.getMetaState() & META_ALT_ON) != 0) { + keyMod |= KEYMOD_ALT; + } + if ((event.getMetaState() & META_SHIFT_ON) != 0) { + keyMod |= KEYMOD_SHIFT; + } + // First try to map scancode + code = mKeyMap.get(event.getScanCode() | KEYMOD_SCAN | keyMod); + if (code == null) { + code = mKeyMap.get(keyCode | keyMod); + } + } + + if (code == null && keyCode >= 0 && keyCode < mKeyCodes.length) { + if (appMode) { + code = mAppKeyCodes[keyCode]; + } + if (code == null) { + code = mKeyCodes[keyCode]; + } + } + + if (code != null) { + if (EmulatorDebug.LOG_CHARACTERS_FLAG) { + byte[] bytes = code.getBytes(); + Log.d(EmulatorDebug.LOG_TAG, "Out: '" + EmulatorDebug.bytesToString(bytes, 0, bytes.length) + "'"); + } + mTermSession.write(code); + return true; + } + return false; + } + + /** + * Handle a keyUp event. + * + * @param keyCode the keyCode of the keyUp event + */ + public void keyUp(int keyCode, KeyEvent event) { + boolean allowToggle = isEventFromToggleDevice(event); + switch (keyCode) { + case KeyEvent.KEYCODE_ALT_LEFT: + case KeyEvent.KEYCODE_ALT_RIGHT: + if (allowToggle) { + mAltKey.onRelease(); + updateCursorMode(); + } + break; + case KeyEvent.KEYCODE_SHIFT_LEFT: + case KeyEvent.KEYCODE_SHIFT_RIGHT: + if (allowToggle) { + mCapKey.onRelease(); + updateCursorMode(); + } + break; + + case KEYCODE_CTRL_LEFT: + case KEYCODE_CTRL_RIGHT: + // ignore control keys. + break; + + default: + // Ignore other keyUps + break; + } + } + + public boolean getAltSendsEsc() { + return mAltSendsEsc; + } + + public boolean isAltActive() { + return mAltKey.isActive(); + } + + public boolean isCtrlActive() { + return mControlKey.isActive(); + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TermSession.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TermSession.java new file mode 100644 index 0000000..4d588e5 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TermSession.java @@ -0,0 +1,638 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CodingErrorAction; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +/** + * A terminal session, consisting of a VT100 terminal emulator and its + * input and output streams. + *

+ * You need to supply an {@link InputStream} and {@link OutputStream} to + * provide input and output to the terminal. For a locally running + * program, these would typically point to a tty; for a telnet program + * they might point to a network socket. Reader and writer threads will be + * spawned to do I/O to these streams. All other operations, including + * processing of input and output in {@link #processInput processInput} and + * {@link #write(byte[], int, int) write}, will be performed on the main thread. + *

+ * Call {@link #setTermIn} and {@link #setTermOut} to connect the input and + * output streams to the emulator. When all of your initialization is + * complete, your initial screen size is known, and you're ready to + * start VT100 emulation, call {@link #initializeEmulator} or {@link + * #updateSize} with the number of rows and columns the terminal should + * initially have. (If you attach the session to an {@link EmulatorView}, + * the view will take care of setting the screen size and initializing the + * emulator for you.) + *

+ * When you're done with the session, you should call {@link #finish} on it. + * This frees emulator data from memory, stops the reader and writer threads, + * and closes the attached I/O streams. + */ +public class TermSession { + public void setKeyListener(TermKeyListener l) { + mKeyListener = l; + } + + private TermKeyListener mKeyListener; + + private ColorScheme mColorScheme = BaseTextRenderer.defaultColorScheme; + private UpdateCallback mNotify; + + private OutputStream mTermOut; + private InputStream mTermIn; + + private String mTitle; + + private TranscriptScreen mTranscriptScreen; + private TerminalEmulator mEmulator; + + private boolean mDefaultUTF8Mode; + + private Thread mReaderThread; + private ByteQueue mByteQueue; + private byte[] mReceiveBuffer; + + private Thread mWriterThread; + private ByteQueue mWriteQueue; + private Handler mWriterHandler; + + private CharBuffer mWriteCharBuffer; + private ByteBuffer mWriteByteBuffer; + private CharsetEncoder mUTF8Encoder; + + // Number of rows in the transcript + private static final int TRANSCRIPT_ROWS = 10000; + + private static final int NEW_INPUT = 1; + private static final int NEW_OUTPUT = 2; + private static final int FINISH = 3; + private static final int EOF = 4; + + /** + * Callback to be invoked when a {@link TermSession} finishes. + * + * @see TermSession#setUpdateCallback + */ + public interface FinishCallback { + /** + * Callback function to be invoked when a {@link TermSession} finishes. + * + * @param session The TermSession which has finished. + */ + void onSessionFinish(TermSession session); + } + private FinishCallback mFinishCallback; + + private boolean mIsRunning = false; + private Handler mMsgHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (!mIsRunning) { + return; + } + if (msg.what == NEW_INPUT) { + readFromProcess(); + } else if (msg.what == EOF) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + onProcessExit(); + } + }); + } + } + }; + + private UpdateCallback mTitleChangedListener; + + public TermSession() { + this(false); + } + + public TermSession(final boolean exitOnEOF) { + mWriteCharBuffer = CharBuffer.allocate(2); + mWriteByteBuffer = ByteBuffer.allocate(4); + mUTF8Encoder = Charset.forName("UTF-8").newEncoder(); + mUTF8Encoder.onMalformedInput(CodingErrorAction.REPLACE); + mUTF8Encoder.onUnmappableCharacter(CodingErrorAction.REPLACE); + + mReceiveBuffer = new byte[4 * 1024]; + mByteQueue = new ByteQueue(4 * 1024); + mReaderThread = new Thread() { + private byte[] mBuffer = new byte[4096]; + + @Override + public void run() { + try { + while(true) { + int read = mTermIn.read(mBuffer); + if (read == -1) { + // EOF -- process exited + break; + } + int offset = 0; + while (read > 0) { + int written = mByteQueue.write(mBuffer, + offset, read); + offset += written; + read -= written; + mMsgHandler.sendMessage( + mMsgHandler.obtainMessage(NEW_INPUT)); + } + } + } catch (IOException e) { + } catch (InterruptedException e) { + } + + if (exitOnEOF) mMsgHandler.sendMessage(mMsgHandler.obtainMessage(EOF)); + } + }; + mReaderThread.setName("TermSession input reader"); + + mWriteQueue = new ByteQueue(4096); + mWriterThread = new Thread() { + private byte[] mBuffer = new byte[4096]; + + @Override + public void run() { + Looper.prepare(); + + mWriterHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (msg.what == NEW_OUTPUT) { + writeToOutput(); + } else if (msg.what == FINISH) { + Looper.myLooper().quit(); + } + } + }; + + // Drain anything in the queue from before we started + writeToOutput(); + + Looper.loop(); + } + + private void writeToOutput() { + ByteQueue writeQueue = mWriteQueue; + byte[] buffer = mBuffer; + OutputStream termOut = mTermOut; + + int bytesAvailable = writeQueue.getBytesAvailable(); + int bytesToWrite = Math.min(bytesAvailable, buffer.length); + + if (bytesToWrite == 0) { + return; + } + + try { + writeQueue.read(buffer, 0, bytesToWrite); + termOut.write(buffer, 0, bytesToWrite); + termOut.flush(); + } catch (IOException e) { + // Ignore exception + // We don't really care if the receiver isn't listening. + // We just make a best effort to answer the query. + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + mWriterThread.setName("TermSession output writer"); + } + + protected void onProcessExit() { + finish(); + } + + /** + * Set the terminal emulator's window size and start terminal emulation. + * + * @param columns The number of columns in the terminal window. + * @param rows The number of rows in the terminal window. + */ + public void initializeEmulator(int columns, int rows) { + mTranscriptScreen = new TranscriptScreen(columns, TRANSCRIPT_ROWS, rows, mColorScheme); + mEmulator = new TerminalEmulator(this, mTranscriptScreen, columns, rows, mColorScheme); + mEmulator.setDefaultUTF8Mode(mDefaultUTF8Mode); + mEmulator.setKeyListener(mKeyListener); + + mIsRunning = true; + mReaderThread.start(); + mWriterThread.start(); + } + + /** + * Write data to the terminal output. The written data will be consumed by + * the emulation client as input. + *

+ * write itself runs on the main thread. The default + * implementation writes the data into a circular buffer and signals the + * writer thread to copy it from there to the {@link OutputStream}. + *

+ * Subclasses may override this method to modify the output before writing + * it to the stream, but implementations in derived classes should call + * through to this method to do the actual writing. + * + * @param data An array of bytes to write to the terminal. + * @param offset The offset into the array at which the data starts. + * @param count The number of bytes to be written. + */ + public void write(byte[] data, int offset, int count) { + try { + while (count > 0) { + int written = mWriteQueue.write(data, offset, count); + offset += written; + count -= written; + notifyNewOutput(); + } + } catch (InterruptedException e) { + } + } + + /** + * Write the UTF-8 representation of a String to the terminal output. The + * written data will be consumed by the emulation client as input. + *

+ * This implementation encodes the String and then calls + * {@link #write(byte[], int, int)} to do the actual writing. It should + * therefore usually be unnecessary to override this method; override + * {@link #write(byte[], int, int)} instead. + * + * @param data The String to write to the terminal. + */ + public void write(String data) { + try { + byte[] bytes = data.getBytes("UTF-8"); + write(bytes, 0, bytes.length); + } catch (UnsupportedEncodingException e) { + } + } + + /** + * Write the UTF-8 representation of a single Unicode code point to the + * terminal output. The written data will be consumed by the emulation + * client as input. + *

+ * This implementation encodes the code point and then calls + * {@link #write(byte[], int, int)} to do the actual writing. It should + * therefore usually be unnecessary to override this method; override + * {@link #write(byte[], int, int)} instead. + * + * @param codePoint The Unicode code point to write to the terminal. + */ + public void write(int codePoint) { + ByteBuffer byteBuf = mWriteByteBuffer; + if (codePoint < 128) { + // Fast path for ASCII characters + byte[] buf = byteBuf.array(); + buf[0] = (byte) codePoint; + write(buf, 0, 1); + return; + } + + CharBuffer charBuf = mWriteCharBuffer; + CharsetEncoder encoder = mUTF8Encoder; + + charBuf.clear(); + byteBuf.clear(); + Character.toChars(codePoint, charBuf.array(), 0); + encoder.reset(); + encoder.encode(charBuf, byteBuf, true); + encoder.flush(byteBuf); + write(byteBuf.array(), 0, byteBuf.position()-1); + } + + /* Notify the writer thread that there's new output waiting */ + private void notifyNewOutput() { + Handler writerHandler = mWriterHandler; + if (writerHandler == null) { + /* Writer thread isn't started -- will pick up data once it does */ + return; + } + writerHandler.sendEmptyMessage(NEW_OUTPUT); + } + + /** + * Get the {@link OutputStream} associated with this session. + * + * @return This session's {@link OutputStream}. + */ + public OutputStream getTermOut() { + return mTermOut; + } + + /** + * Set the {@link OutputStream} associated with this session. + * + * @param termOut This session's {@link OutputStream}. + */ + public void setTermOut(OutputStream termOut) { + mTermOut = termOut; + } + + /** + * Get the {@link InputStream} associated with this session. + * + * @return This session's {@link InputStream}. + */ + public InputStream getTermIn() { + return mTermIn; + } + + /** + * Set the {@link InputStream} associated with this session. + * + * @param termIn This session's {@link InputStream}. + */ + public void setTermIn(InputStream termIn) { + mTermIn = termIn; + } + + /** + * @return Whether the terminal emulation is currently running. + */ + public boolean isRunning() { + return mIsRunning; + } + + TranscriptScreen getTranscriptScreen() { + return mTranscriptScreen; + } + + TerminalEmulator getEmulator() { + return mEmulator; + } + + /** + * Set an {@link UpdateCallback} to be invoked when the terminal emulator's + * screen is changed. + * + * @param notify The {@link UpdateCallback} to be invoked on changes. + */ + public void setUpdateCallback(UpdateCallback notify) { + mNotify = notify; + } + + /** + * Notify the {@link UpdateCallback} registered by {@link + * #setUpdateCallback setUpdateCallback} that the screen has changed. + */ + protected void notifyUpdate() { + if (mNotify != null) { + mNotify.onUpdate(); + } + } + + /** + * Get the terminal session's title (may be null). + */ + public String getTitle() { + return mTitle; + } + + /** + * Change the terminal session's title. + */ + public void setTitle(String title) { + mTitle = title; + notifyTitleChanged(); + } + + /** + * Set an {@link UpdateCallback} to be invoked when the terminal emulator's + * title is changed. + * + * @param listener The {@link UpdateCallback} to be invoked on changes. + */ + public void setTitleChangedListener(UpdateCallback listener) { + mTitleChangedListener = listener; + } + + /** + * Notify the UpdateCallback registered for title changes, if any, that the + * terminal session's title has changed. + */ + protected void notifyTitleChanged() { + UpdateCallback listener = mTitleChangedListener; + if (listener != null) { + listener.onUpdate(); + } + } + + /** + * Change the terminal's window size. Will call {@link #initializeEmulator} + * if the emulator is not yet running. + *

+ * You should override this method if your application needs to be notified + * when the screen size changes (for example, if you need to issue + * TIOCSWINSZ to a tty to adjust the window size). If you + * do override this method, you must call through to the superclass + * implementation. + * + * @param columns The number of columns in the terminal window. + * @param rows The number of rows in the terminal window. + */ + public void updateSize(int columns, int rows) { + if (mEmulator == null) { + initializeEmulator(columns, rows); + } else { + mEmulator.updateSize(columns, rows); + } + } + + /** + * Retrieve the terminal's screen and scrollback buffer. + * + * @return A {@link String} containing the contents of the screen and + * scrollback buffer. + */ + public String getTranscriptText() { + return mTranscriptScreen.getTranscriptText(); + } + + /** + * Look for new input from the ptty, send it to the terminal emulator. + */ + private void readFromProcess() { + int bytesAvailable = mByteQueue.getBytesAvailable(); + int bytesToRead = Math.min(bytesAvailable, mReceiveBuffer.length); + int bytesRead = 0; + try { + bytesRead = mByteQueue.read(mReceiveBuffer, 0, bytesToRead); + } catch (InterruptedException e) { + return; + } + + // Give subclasses a chance to process the read data + processInput(mReceiveBuffer, 0, bytesRead); + notifyUpdate(); + } + + /** + * Process input and send it to the terminal emulator. This method is + * invoked on the main thread whenever new data is read from the + * InputStream. + *

+ * The default implementation sends the data straight to the terminal + * emulator without modifying it in any way. Subclasses can override it to + * modify the data before giving it to the terminal. + * + * @param data A byte array containing the data read. + * @param offset The offset into the buffer where the read data begins. + * @param count The number of bytes read. + */ + protected void processInput(byte[] data, int offset, int count) { + mEmulator.append(data, offset, count); + } + + /** + * Write something directly to the terminal emulator input, bypassing the + * emulation client, the session's {@link InputStream}, and any processing + * being done by {@link #processInput processInput}. + * + * @param data The data to be written to the terminal. + * @param offset The starting offset into the buffer of the data. + * @param count The length of the data to be written. + */ + protected final void appendToEmulator(byte[] data, int offset, int count) { + mEmulator.append(data, offset, count); + } + + /** + * Set the terminal emulator's color scheme (default colors). + * + * @param scheme The {@link ColorScheme} to be used (use null for the + * default scheme). + */ + public void setColorScheme(ColorScheme scheme) { + if (scheme == null) { + scheme = BaseTextRenderer.defaultColorScheme; + } + mColorScheme = scheme; + if (mEmulator == null) { + return; + } + mEmulator.setColorScheme(scheme); + } + + /** + * Set whether the terminal emulator should be in UTF-8 mode by default. + *

+ * In UTF-8 mode, the terminal will handle UTF-8 sequences, allowing the + * display of text in most of the world's languages, but applications must + * encode C1 control characters and graphics drawing characters as the + * corresponding UTF-8 sequences. + * + * @param utf8ByDefault Whether the terminal emulator should be in UTF-8 + * mode by default. + */ + public void setDefaultUTF8Mode(boolean utf8ByDefault) { + mDefaultUTF8Mode = utf8ByDefault; + if (mEmulator == null) { + return; + } + mEmulator.setDefaultUTF8Mode(utf8ByDefault); + } + + /** + * Get whether the terminal emulator is currently in UTF-8 mode. + * + * @return Whether the emulator is currently in UTF-8 mode. + */ + public boolean getUTF8Mode() { + if (mEmulator == null) { + return mDefaultUTF8Mode; + } else { + return mEmulator.getUTF8Mode(); + } + } + + /** + * Set an {@link UpdateCallback} to be invoked when the terminal emulator + * goes into or out of UTF-8 mode. + * + * @param utf8ModeNotify The {@link UpdateCallback} to be invoked. + */ + public void setUTF8ModeUpdateCallback(UpdateCallback utf8ModeNotify) { + if (mEmulator != null) { + mEmulator.setUTF8ModeUpdateCallback(utf8ModeNotify); + } + } + + /** + * Reset the terminal emulator's state. + */ + public void reset() { + mEmulator.reset(); + notifyUpdate(); + } + + /** + * Set a {@link FinishCallback} to be invoked once this terminal session is + * finished. + * + * @param callback The {@link FinishCallback} to be invoked on finish. + */ + public void setFinishCallback(FinishCallback callback) { + mFinishCallback = callback; + } + + /** + * Finish this terminal session. Frees resources used by the terminal + * emulator and closes the attached InputStream and + * OutputStream. + */ + public void finish() { + mIsRunning = false; + mEmulator.finish(); + if (mTranscriptScreen != null) { + mTranscriptScreen.finish(); + } + + // Stop the reader and writer threads, and close the I/O streams + if (mWriterHandler != null) { + mWriterHandler.sendEmptyMessage(FINISH); + } + try { + mTermIn.close(); + mTermOut.close(); + } catch (IOException e) { + // We don't care if this fails + } catch (NullPointerException e) { + } + + if (mFinishCallback != null) { + mFinishCallback.onSessionFinish(this); + } + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TerminalEmulator.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TerminalEmulator.java new file mode 100644 index 0000000..9c830b4 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TerminalEmulator.java @@ -0,0 +1,2011 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.util.Locale; + +import android.util.Log; + +/** + * Renders text into a screen. Contains all the terminal-specific knowledge and + * state. Emulates a subset of the X Window System xterm terminal, which in turn + * is an emulator for a subset of the Digital Equipment Corporation vt100 + * terminal. Missing functionality: text attributes (bold, underline, reverse + * video, color) alternate screen cursor key and keypad escape sequences. + */ +class TerminalEmulator { + public void setKeyListener(TermKeyListener l) { + mKeyListener = l; + } + private TermKeyListener mKeyListener; + /** + * The cursor row. Numbered 0..mRows-1. + */ + private int mCursorRow; + + /** + * The cursor column. Numbered 0..mColumns-1. + */ + private int mCursorCol; + + /** + * The number of character rows in the terminal screen. + */ + private int mRows; + + /** + * The number of character columns in the terminal screen. + */ + private int mColumns; + + /** + * Stores the characters that appear on the screen of the emulated terminal. + */ + private TranscriptScreen mMainBuffer; + private TranscriptScreen mAltBuffer; + private TranscriptScreen mScreen; + + /** + * The terminal session this emulator is bound to. + */ + private TermSession mSession; + + /** + * Keeps track of the current argument of the current escape sequence. + * Ranges from 0 to MAX_ESCAPE_PARAMETERS-1. (Typically just 0 or 1.) + */ + private int mArgIndex; + + /** + * The number of parameter arguments. This name comes from the ANSI standard + * for terminal escape codes. + */ + private static final int MAX_ESCAPE_PARAMETERS = 16; + + /** + * Holds the arguments of the current escape sequence. + */ + private int[] mArgs = new int[MAX_ESCAPE_PARAMETERS]; + + /** + * Holds OSC arguments, which can be strings. + */ + private byte[] mOSCArg = new byte[MAX_OSC_STRING_LENGTH]; + + private int mOSCArgLength; + + private int mOSCArgTokenizerIndex; + + /** + * Don't know what the actual limit is, this seems OK for now. + */ + private static final int MAX_OSC_STRING_LENGTH = 512; + + // Escape processing states: + + /** + * Escape processing state: Not currently in an escape sequence. + */ + private static final int ESC_NONE = 0; + + /** + * Escape processing state: Have seen an ESC character + */ + private static final int ESC = 1; + + /** + * Escape processing state: Have seen ESC POUND + */ + private static final int ESC_POUND = 2; + + /** + * Escape processing state: Have seen ESC and a character-set-select char + */ + private static final int ESC_SELECT_LEFT_PAREN = 3; + + /** + * Escape processing state: Have seen ESC and a character-set-select char + */ + private static final int ESC_SELECT_RIGHT_PAREN = 4; + + /** + * Escape processing state: ESC [ + */ + private static final int ESC_LEFT_SQUARE_BRACKET = 5; + + /** + * Escape processing state: ESC [ ? + */ + private static final int ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK = 6; + + /** + * Escape processing state: ESC % + */ + private static final int ESC_PERCENT = 7; + + /** + * Escape processing state: ESC ] (AKA OSC - Operating System Controls) + */ + private static final int ESC_RIGHT_SQUARE_BRACKET = 8; + + /** + * Escape processing state: ESC ] (AKA OSC - Operating System Controls) + */ + private static final int ESC_RIGHT_SQUARE_BRACKET_ESC = 9; + + /** + * True if the current escape sequence should continue, false if the current + * escape sequence should be terminated. Used when parsing a single + * character. + */ + private boolean mContinueSequence; + + /** + * The current state of the escape sequence state machine. + */ + private int mEscapeState; + + /** + * Saved state of the cursor row, Used to implement the save/restore cursor + * position escape sequences. + */ + private int mSavedCursorRow; + + /** + * Saved state of the cursor column, Used to implement the save/restore + * cursor position escape sequences. + */ + private int mSavedCursorCol; + + private int mSavedEffect; + + private int mSavedDecFlags_DECSC_DECRC; + + + // DecSet booleans + + /** + * This mask indicates 132-column mode is set. (As opposed to 80-column + * mode.) + */ + private static final int K_132_COLUMN_MODE_MASK = 1 << 3; + + /** + * DECSCNM - set means reverse video (light background.) + */ + private static final int K_REVERSE_VIDEO_MASK = 1 << 5; + + /** + * This mask indicates that origin mode is set. (Cursor addressing is + * relative to the absolute screen size, rather than the currently set top + * and bottom margins.) + */ + private static final int K_ORIGIN_MODE_MASK = 1 << 6; + + /** + * This mask indicates that wraparound mode is set. (As opposed to + * stop-at-right-column mode.) + */ + private static final int K_WRAPAROUND_MODE_MASK = 1 << 7; + + /** + * This mask indicates that the cursor should be shown. DECTCEM + */ + + private static final int K_SHOW_CURSOR_MASK = 1 << 25; + + /** This mask is the subset of DecSet bits that are saved / restored by + * the DECSC / DECRC commands + */ + private static final int K_DECSC_DECRC_MASK = + K_ORIGIN_MODE_MASK | K_WRAPAROUND_MODE_MASK; + + /** + * Holds multiple DECSET flags. The data is stored this way, rather than in + * separate booleans, to make it easier to implement the save-and-restore + * semantics. The various k*ModeMask masks can be used to extract and modify + * the individual flags current states. + */ + private int mDecFlags; + + /** + * Saves away a snapshot of the DECSET flags. Used to implement save and + * restore escape sequences. + */ + private int mSavedDecFlags; + + /** + * The current DECSET mouse tracking mode, zero for no mouse tracking. + */ + private int mMouseTrackingMode; + + // Modes set with Set Mode / Reset Mode + + /** + * True if insert mode (as opposed to replace mode) is active. In insert + * mode new characters are inserted, pushing existing text to the right. + */ + private boolean mInsertMode; + + /** + * An array of tab stops. mTabStop[i] is true if there is a tab stop set for + * column i. + */ + private boolean[] mTabStop; + + // The margins allow portions of the screen to be locked. + + /** + * The top margin of the screen, for scrolling purposes. Ranges from 0 to + * mRows-2. + */ + private int mTopMargin; + + /** + * The bottom margin of the screen, for scrolling purposes. Ranges from + * mTopMargin + 2 to mRows. (Defines the first row after the scrolling + * region. + */ + private int mBottomMargin; + + /** + * True if the next character to be emitted will be automatically wrapped to + * the next line. Used to disambiguate the case where the cursor is + * positioned on column mColumns-1. + */ + private boolean mAboutToAutoWrap; + + /** + * The width of the last emitted spacing character. Used to place + * combining characters into the correct column. + */ + private int mLastEmittedCharWidth = 0; + + /** + * True if we just auto-wrapped and no character has been emitted on this + * line yet. Used to ensure combining characters following a character + * at the edge of the screen are stored in the proper place. + */ + private boolean mJustWrapped = false; + + /** + * Used for debugging, counts how many chars have been processed. + */ + private int mProcessedCharCount; + + /** + * Foreground color, 0..255 + */ + private int mForeColor; + private int mDefaultForeColor; + + /** + * Background color, 0..255 + */ + private int mBackColor; + private int mDefaultBackColor; + + /** + * Current TextStyle effect + */ + private int mEffect; + + private boolean mbKeypadApplicationMode; + + /** false == G0, true == G1 */ + private boolean mAlternateCharSet; + + private final static int CHAR_SET_UK = 0; + private final static int CHAR_SET_ASCII = 1; + private final static int CHAR_SET_SPECIAL_GRAPHICS = 2; + private final static int CHAR_SET_ALT_STANDARD = 3; + private final static int CHAR_SET_ALT_SPECIAL_GRAPICS = 4; + + /** What is the current graphics character set. [0] == G0, [1] == G1 */ + private int[] mCharSet = new int[2]; + + /** Derived from mAlternateCharSet and mCharSet. + * True if we're supposed to be drawing the special graphics. + */ + private boolean mUseAlternateCharSet; + + /** + * Special graphics character set + */ + private static final char[] mSpecialGraphicsCharMap = new char[128]; + static { + for (char i = 0; i < 128; ++i) { + mSpecialGraphicsCharMap[i] = i; + } + mSpecialGraphicsCharMap['_'] = ' '; // Blank + mSpecialGraphicsCharMap['b'] = 0x2409; // Tab + mSpecialGraphicsCharMap['c'] = 0x240C; // Form feed + mSpecialGraphicsCharMap['d'] = 0x240D; // Carriage return + mSpecialGraphicsCharMap['e'] = 0x240A; // Line feed + mSpecialGraphicsCharMap['h'] = 0x2424; // New line + mSpecialGraphicsCharMap['i'] = 0x240B; // Vertical tab/"lantern" + mSpecialGraphicsCharMap['}'] = 0x00A3; // Pound sterling symbol + mSpecialGraphicsCharMap['f'] = 0x00B0; // Degree symbol + mSpecialGraphicsCharMap['`'] = 0x2B25; // Diamond + mSpecialGraphicsCharMap['~'] = 0x2022; // Bullet point + mSpecialGraphicsCharMap['y'] = 0x2264; // Less-than-or-equals sign (<=) + mSpecialGraphicsCharMap['|'] = 0x2260; // Not equals sign (!=) + mSpecialGraphicsCharMap['z'] = 0x2265; // Greater-than-or-equals sign (>=) + mSpecialGraphicsCharMap['g'] = 0x00B1; // Plus-or-minus sign (+/-) + mSpecialGraphicsCharMap['{'] = 0x03C0; // Lowercase Greek letter pi + mSpecialGraphicsCharMap['.'] = 0x25BC; // Down arrow + mSpecialGraphicsCharMap[','] = 0x25C0; // Left arrow + mSpecialGraphicsCharMap['+'] = 0x25B6; // Right arrow + mSpecialGraphicsCharMap['-'] = 0x25B2; // Up arrow + mSpecialGraphicsCharMap['h'] = '#'; // Board of squares + mSpecialGraphicsCharMap['a'] = 0x2592; // Checkerboard + mSpecialGraphicsCharMap['0'] = 0x2588; // Solid block + mSpecialGraphicsCharMap['q'] = 0x2500; // Horizontal line (box drawing) + mSpecialGraphicsCharMap['x'] = 0x2502; // Vertical line (box drawing) + mSpecialGraphicsCharMap['m'] = 0x2514; // Lower left hand corner (box drawing) + mSpecialGraphicsCharMap['j'] = 0x2518; // Lower right hand corner (box drawing) + mSpecialGraphicsCharMap['l'] = 0x250C; // Upper left hand corner (box drawing) + mSpecialGraphicsCharMap['k'] = 0x2510; // Upper right hand corner (box drawing) + mSpecialGraphicsCharMap['w'] = 0x252C; // T pointing downwards (box drawing) + mSpecialGraphicsCharMap['u'] = 0x2524; // T pointing leftwards (box drawing) + mSpecialGraphicsCharMap['t'] = 0x251C; // T pointing rightwards (box drawing) + mSpecialGraphicsCharMap['v'] = 0x2534; // T pointing upwards (box drawing) + mSpecialGraphicsCharMap['n'] = 0x253C; // Large plus/lines crossing (box drawing) + mSpecialGraphicsCharMap['o'] = 0x23BA; // Horizontal scanline 1 + mSpecialGraphicsCharMap['p'] = 0x23BB; // Horizontal scanline 3 + mSpecialGraphicsCharMap['r'] = 0x23BC; // Horizontal scanline 7 + mSpecialGraphicsCharMap['s'] = 0x23BD; // Horizontal scanline 9 + } + + /** + * Used for moving selection up along with the scrolling text + */ + private int mScrollCounter = 0; + + /** + * UTF-8 support + */ + private static final int UNICODE_REPLACEMENT_CHAR = 0xfffd; + private boolean mDefaultUTF8Mode = false; + private boolean mUTF8Mode = false; + private boolean mUTF8EscapeUsed = false; + private int mUTF8ToFollow = 0; + private ByteBuffer mUTF8ByteBuffer; + private CharBuffer mInputCharBuffer; + private CharsetDecoder mUTF8Decoder; + private UpdateCallback mUTF8ModeNotify; + + /** This is not accurate, but it makes the terminal more useful on + * small screens. + */ + private final static boolean DEFAULT_TO_AUTOWRAP_ENABLED = true; + + /** + * Construct a terminal emulator that uses the supplied screen + * + * @param session the terminal session the emulator is attached to + * @param screen the screen to render characters into. + * @param columns the number of columns to emulate + * @param rows the number of rows to emulate + * @param scheme the default color scheme of this emulator + */ + public TerminalEmulator(TermSession session, TranscriptScreen screen, int columns, int rows, ColorScheme scheme) { + mSession = session; + mMainBuffer = screen; + mScreen = mMainBuffer; + mAltBuffer = new TranscriptScreen(columns, rows, rows, scheme); + mRows = rows; + mColumns = columns; + mTabStop = new boolean[mColumns]; + + setColorScheme(scheme); + + mUTF8ByteBuffer = ByteBuffer.allocate(4); + mInputCharBuffer = CharBuffer.allocate(2); + mUTF8Decoder = Charset.forName("UTF-8").newDecoder(); + mUTF8Decoder.onMalformedInput(CodingErrorAction.REPLACE); + mUTF8Decoder.onUnmappableCharacter(CodingErrorAction.REPLACE); + + reset(); + } + + public TranscriptScreen getScreen() { + return mScreen; + } + + public void updateSize(int columns, int rows) { + if (mRows == rows && mColumns == columns) { + return; + } + if (columns <= 0) { + throw new IllegalArgumentException("rows:" + columns); + } + + if (rows <= 0) { + throw new IllegalArgumentException("rows:" + rows); + } + + TranscriptScreen screen = mScreen; + TranscriptScreen altScreen; + if (screen != mMainBuffer) { + altScreen = mMainBuffer; + } else { + altScreen = mAltBuffer; + } + + // Try to resize the screen without getting the transcript + int[] cursor = { mCursorCol, mCursorRow }; + boolean fastResize = screen.fastResize(columns, rows, cursor); + + GrowableIntArray cursorColor = null; + String charAtCursor = null; + GrowableIntArray colors = null; + String transcriptText = null; + if (!fastResize) { + /* Save the character at the cursor (if one exists) and store an + * ASCII ESC character at the cursor's location + * This is an epic hack that lets us restore the cursor later... + */ + cursorColor = new GrowableIntArray(1); + charAtCursor = screen.getSelectedText(cursorColor, mCursorCol, mCursorRow, mCursorCol, mCursorRow); + screen.set(mCursorCol, mCursorRow, 27, 0); + + colors = new GrowableIntArray(1024); + transcriptText = screen.getTranscriptText(colors); + screen.resize(columns, rows, getStyle()); + } + + boolean altFastResize = true; + GrowableIntArray altColors = null; + String altTranscriptText = null; + if (altScreen != null) { + altFastResize = altScreen.fastResize(columns, rows, null); + + if (!altFastResize) { + altColors = new GrowableIntArray(1024); + altTranscriptText = altScreen.getTranscriptText(altColors); + altScreen.resize(columns, rows, getStyle()); + } + } + + if (mRows != rows) { + mRows = rows; + mTopMargin = 0; + mBottomMargin = mRows; + } + if (mColumns != columns) { + int oldColumns = mColumns; + mColumns = columns; + boolean[] oldTabStop = mTabStop; + mTabStop = new boolean[mColumns]; + int toTransfer = Math.min(oldColumns, columns); + System.arraycopy(oldTabStop, 0, mTabStop, 0, toTransfer); + } + + if (!altFastResize) { + boolean wasAboutToAutoWrap = mAboutToAutoWrap; + + // Restore the contents of the inactive screen's buffer + mScreen = altScreen; + mCursorRow = 0; + mCursorCol = 0; + mAboutToAutoWrap = false; + + int end = altTranscriptText.length()-1; + /* Unlike for the main transcript below, don't trim off trailing + * newlines -- the alternate transcript lacks a cursor marking, so + * we might introduce an unwanted vertical shift in the screen + * contents this way */ + char c, cLow; + int colorOffset = 0; + for (int i = 0; i <= end; i++) { + c = altTranscriptText.charAt(i); + int style = altColors.at(i-colorOffset); + if (Character.isHighSurrogate(c)) { + cLow = altTranscriptText.charAt(++i); + emit(Character.toCodePoint(c, cLow), style); + ++colorOffset; + } else if (c == '\n') { + setCursorCol(0); + doLinefeed(); + } else { + emit(c, style); + } + } + + mScreen = screen; + mAboutToAutoWrap = wasAboutToAutoWrap; + } + + if (fastResize) { + // Only need to make sure the cursor is in the right spot + if (cursor[0] >= 0 && cursor[1] >= 0) { + mCursorCol = cursor[0]; + mCursorRow = cursor[1]; + } else { + // Cursor scrolled off screen, reset the cursor to top left + mCursorCol = 0; + mCursorRow = 0; + } + + return; + } + + mCursorRow = 0; + mCursorCol = 0; + mAboutToAutoWrap = false; + + int newCursorRow = -1; + int newCursorCol = -1; + int newCursorTranscriptPos = -1; + int end = transcriptText.length()-1; + while ((end >= 0) && transcriptText.charAt(end) == '\n') { + end--; + } + char c, cLow; + int colorOffset = 0; + for(int i = 0; i <= end; i++) { + c = transcriptText.charAt(i); + int style = colors.at(i-colorOffset); + if (Character.isHighSurrogate(c)) { + cLow = transcriptText.charAt(++i); + emit(Character.toCodePoint(c, cLow), style); + ++colorOffset; + } else if (c == '\n') { + setCursorCol(0); + doLinefeed(); + } else if (c == 27) { + /* We marked the cursor location with ESC earlier, so this + is the place to restore the cursor to */ + newCursorRow = mCursorRow; + newCursorCol = mCursorCol; + newCursorTranscriptPos = screen.getActiveRows(); + if (charAtCursor != null && charAtCursor.length() > 0) { + // Emit the real character that was in this spot + int encodedCursorColor = cursorColor.at(0); + emit(charAtCursor.toCharArray(), 0, charAtCursor.length(), encodedCursorColor); + } + } else { + emit(c, style); + } + } + + // If we marked a cursor location, move the cursor there now + if (newCursorRow != -1 && newCursorCol != -1) { + mCursorRow = newCursorRow; + mCursorCol = newCursorCol; + + /* Adjust for any scrolling between the time we marked the cursor + location and now */ + int scrollCount = screen.getActiveRows() - newCursorTranscriptPos; + if (scrollCount > 0 && scrollCount <= newCursorRow) { + mCursorRow -= scrollCount; + } else if (scrollCount > newCursorRow) { + // Cursor scrolled off screen -- reset to top left corner + mCursorRow = 0; + mCursorCol = 0; + } + } + } + + /** + * Get the cursor's current row. + * + * @return the cursor's current row. + */ + public final int getCursorRow() { + return mCursorRow; + } + + /** + * Get the cursor's current column. + * + * @return the cursor's current column. + */ + public final int getCursorCol() { + return mCursorCol; + } + + public final boolean getReverseVideo() { + return (mDecFlags & K_REVERSE_VIDEO_MASK) != 0; + } + + public final boolean getShowCursor() { + return (mDecFlags & K_SHOW_CURSOR_MASK) != 0; + } + + public final boolean getKeypadApplicationMode() { + return mbKeypadApplicationMode; + } + + /** + * Get the current DECSET mouse tracking mode, zero for no mouse tracking. + * + * @return the current DECSET mouse tracking mode. + */ + public final int getMouseTrackingMode() { + return mMouseTrackingMode; + } + + private void setDefaultTabStops() { + for (int i = 0; i < mColumns; i++) { + mTabStop[i] = (i & 7) == 0 && i != 0; + } + } + + /** + * Accept bytes (typically from the pseudo-teletype) and process them. + * + * @param buffer a byte array containing the bytes to be processed + * @param base the first index of the array to process + * @param length the number of bytes in the array to process + */ + public void append(byte[] buffer, int base, int length) { + if (EmulatorDebug.LOG_CHARACTERS_FLAG) { + Log.d(EmulatorDebug.LOG_TAG, "In: '" + EmulatorDebug.bytesToString(buffer, base, length) + "'"); + } + for (int i = 0; i < length; i++) { + byte b = buffer[base + i]; + try { + process(b); + mProcessedCharCount++; + } catch (Exception e) { + Log.e(EmulatorDebug.LOG_TAG, "Exception while processing character " + + Integer.toString(mProcessedCharCount) + " code " + + Integer.toString(b), e); + } + } + } + + private void process(byte b) { + process(b, true); + } + + private void process(byte b, boolean doUTF8) { + // Let the UTF-8 decoder try to handle it if we're in UTF-8 mode + if (doUTF8 && mUTF8Mode && handleUTF8Sequence(b)) { + return; + } + + // Handle C1 control characters + if ((b & 0x80) == 0x80 && (b & 0x7f) <= 0x1f) { + /* ESC ((code & 0x7f) + 0x40) is the two-byte escape sequence + corresponding to a particular C1 code */ + process((byte) 27, false); + process((byte) ((b & 0x7f) + 0x40), false); + return; + } + + switch (b) { + case 0: // NUL + // Do nothing + break; + + case 7: // BEL + /* If in an OSC sequence, BEL may terminate a string; otherwise do + * nothing */ + if (mEscapeState == ESC_RIGHT_SQUARE_BRACKET) { + doEscRightSquareBracket(b); + } + break; + + case 8: // BS + setCursorCol(Math.max(0, mCursorCol - 1)); + break; + + case 9: // HT + // Move to next tab stop, but not past edge of screen + setCursorCol(nextTabStop(mCursorCol)); + break; + + case 13: + setCursorCol(0); + break; + + case 10: // CR + case 11: // VT + case 12: // LF + doLinefeed(); + break; + + case 14: // SO: + setAltCharSet(true); + break; + + case 15: // SI: + setAltCharSet(false); + break; + + + case 24: // CAN + case 26: // SUB + if (mEscapeState != ESC_NONE) { + mEscapeState = ESC_NONE; + emit((byte) 127); + } + break; + + case 27: // ESC + // Starts an escape sequence unless we're parsing a string + if (mEscapeState != ESC_RIGHT_SQUARE_BRACKET) { + startEscapeSequence(ESC); + } else { + doEscRightSquareBracket(b); + } + break; + + default: + mContinueSequence = false; + switch (mEscapeState) { + case ESC_NONE: + if (b >= 32) { + emit(b); + } + break; + + case ESC: + doEsc(b); + break; + + case ESC_POUND: + doEscPound(b); + break; + + case ESC_SELECT_LEFT_PAREN: + doEscSelectLeftParen(b); + break; + + case ESC_SELECT_RIGHT_PAREN: + doEscSelectRightParen(b); + break; + + case ESC_LEFT_SQUARE_BRACKET: + doEscLeftSquareBracket(b); // CSI + break; + + case ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK: + doEscLSBQuest(b); // CSI ? + break; + + case ESC_PERCENT: + doEscPercent(b); + break; + + case ESC_RIGHT_SQUARE_BRACKET: + doEscRightSquareBracket(b); + break; + + case ESC_RIGHT_SQUARE_BRACKET_ESC: + doEscRightSquareBracketEsc(b); + break; + + default: + unknownSequence(b); + break; + } + if (!mContinueSequence) { + mEscapeState = ESC_NONE; + } + break; + } + } + + private boolean handleUTF8Sequence(byte b) { + if (mUTF8ToFollow == 0 && (b & 0x80) == 0) { + // ASCII character -- we don't need to handle this + return false; + } + + if (mUTF8ToFollow > 0) { + if ((b & 0xc0) != 0x80) { + /* Not a UTF-8 continuation byte (doesn't begin with 0b10) + Replace the entire sequence with the replacement char */ + mUTF8ToFollow = 0; + mUTF8ByteBuffer.clear(); + emit(UNICODE_REPLACEMENT_CHAR); + + /* The Unicode standard (section 3.9, definition D93) requires + * that we now attempt to process this byte as though it were + * the beginning of another possibly-valid sequence */ + return handleUTF8Sequence(b); + } + + mUTF8ByteBuffer.put(b); + if (--mUTF8ToFollow == 0) { + // Sequence complete -- decode and emit it + ByteBuffer byteBuf = mUTF8ByteBuffer; + CharBuffer charBuf = mInputCharBuffer; + CharsetDecoder decoder = mUTF8Decoder; + + byteBuf.rewind(); + decoder.reset(); + decoder.decode(byteBuf, charBuf, true); + decoder.flush(charBuf); + + char[] chars = charBuf.array(); + if (chars[0] >= 0x80 && chars[0] <= 0x9f) { + /* Sequence decoded to a C1 control character which needs + to be sent through process() again */ + process((byte) chars[0], false); + } else { + emit(chars); + } + + byteBuf.clear(); + charBuf.clear(); + } + } else { + if ((b & 0xe0) == 0xc0) { // 0b110 -- two-byte sequence + mUTF8ToFollow = 1; + } else if ((b & 0xf0) == 0xe0) { // 0b1110 -- three-byte sequence + mUTF8ToFollow = 2; + } else if ((b & 0xf8) == 0xf0) { // 0b11110 -- four-byte sequence + mUTF8ToFollow = 3; + } else { + // Not a valid UTF-8 sequence start -- replace this char + emit(UNICODE_REPLACEMENT_CHAR); + return true; + } + + mUTF8ByteBuffer.put(b); + } + + return true; + } + + private void setAltCharSet(boolean alternateCharSet) { + mAlternateCharSet = alternateCharSet; + computeEffectiveCharSet(); + } + + private void computeEffectiveCharSet() { + int charSet = mCharSet[mAlternateCharSet ? 1 : 0]; + mUseAlternateCharSet = charSet == CHAR_SET_SPECIAL_GRAPHICS; + } + + private int nextTabStop(int cursorCol) { + for (int i = cursorCol + 1; i < mColumns; i++) { + if (mTabStop[i]) { + return i; + } + } + return mColumns - 1; + } + + private int prevTabStop(int cursorCol) { + for (int i = cursorCol - 1; i >= 0; i--) { + if (mTabStop[i]) { + return i; + } + } + return 0; + } + + private void doEscPercent(byte b) { + switch (b) { + case '@': // Esc % @ -- return to ISO 2022 mode + setUTF8Mode(false); + mUTF8EscapeUsed = true; + break; + case 'G': // Esc % G -- UTF-8 mode + setUTF8Mode(true); + mUTF8EscapeUsed = true; + break; + default: // unimplemented character set + break; + } + } + + private void doEscLSBQuest(byte b) { + int arg = getArg0(0); + int mask = getDecFlagsMask(arg); + int oldFlags = mDecFlags; + switch (b) { + case 'h': // Esc [ ? Pn h - DECSET + mDecFlags |= mask; + switch (arg) { + case 1: + mKeyListener.setCursorKeysApplicationMode(true); + break; + case 47: + case 1047: + case 1049: + if (mAltBuffer != null) { + mScreen = mAltBuffer; + } + break; + } + if (arg >= 1000 && arg <= 1003) { + mMouseTrackingMode = arg; + } + break; + + case 'l': // Esc [ ? Pn l - DECRST + mDecFlags &= ~mask; + switch (arg) { + case 1: + mKeyListener.setCursorKeysApplicationMode(false); + break; + case 47: + case 1047: + case 1049: + mScreen = mMainBuffer; + break; + } + if (arg >= 1000 && arg <= 1003) { + mMouseTrackingMode = 0; + } + break; + + case 'r': // Esc [ ? Pn r - restore + mDecFlags = (mDecFlags & ~mask) | (mSavedDecFlags & mask); + break; + + case 's': // Esc [ ? Pn s - save + mSavedDecFlags = (mSavedDecFlags & ~mask) | (mDecFlags & mask); + break; + + default: + parseArg(b); + break; + } + + int newlySetFlags = (~oldFlags) & mDecFlags; + int changedFlags = oldFlags ^ mDecFlags; + + // 132 column mode + if ((changedFlags & K_132_COLUMN_MODE_MASK) != 0) { + // We don't actually set/reset 132 cols, but we do want the + // side effect of clearing the screen and homing the cursor. + blockClear(0, 0, mColumns, mRows); + setCursorRowCol(0, 0); + } + + // origin mode + if ((newlySetFlags & K_ORIGIN_MODE_MASK) != 0) { + // Home the cursor. + setCursorPosition(0, 0); + } + } + + private int getDecFlagsMask(int argument) { + if (argument >= 1 && argument <= 32) { + return (1 << argument); + } + + return 0; + } + + private void startEscapeSequence(int escapeState) { + mEscapeState = escapeState; + mArgIndex = 0; + for (int j = 0; j < MAX_ESCAPE_PARAMETERS; j++) { + mArgs[j] = -1; + } + } + + private void doLinefeed() { + int newCursorRow = mCursorRow + 1; + if (newCursorRow >= mBottomMargin) { + scroll(); + newCursorRow = mBottomMargin - 1; + } + setCursorRow(newCursorRow); + } + + private void continueSequence() { + mContinueSequence = true; + } + + private void continueSequence(int state) { + mEscapeState = state; + mContinueSequence = true; + } + + private void doEscSelectLeftParen(byte b) { + doSelectCharSet(0, b); + } + + private void doEscSelectRightParen(byte b) { + doSelectCharSet(1, b); + } + + private void doSelectCharSet(int charSetIndex, byte b) { + int charSet; + switch (b) { + case 'A': // United Kingdom character set + charSet = CHAR_SET_UK; + break; + case 'B': // ASCII set + charSet = CHAR_SET_ASCII; + break; + case '0': // Special Graphics + charSet = CHAR_SET_SPECIAL_GRAPHICS; + break; + case '1': // Alternate character set + charSet = CHAR_SET_ALT_STANDARD; + break; + case '2': + charSet = CHAR_SET_ALT_SPECIAL_GRAPICS; + break; + default: + unknownSequence(b); + return; + } + mCharSet[charSetIndex] = charSet; + computeEffectiveCharSet(); + } + + private void doEscPound(byte b) { + switch (b) { + case '8': // Esc # 8 - DECALN alignment test + mScreen.blockSet(0, 0, mColumns, mRows, 'E', + getStyle()); + break; + + default: + unknownSequence(b); + break; + } + } + + private void doEsc(byte b) { + switch (b) { + case '#': + continueSequence(ESC_POUND); + break; + + case '(': + continueSequence(ESC_SELECT_LEFT_PAREN); + break; + + case ')': + continueSequence(ESC_SELECT_RIGHT_PAREN); + break; + + case '7': // DECSC save cursor + mSavedCursorRow = mCursorRow; + mSavedCursorCol = mCursorCol; + mSavedEffect = mEffect; + mSavedDecFlags_DECSC_DECRC = mDecFlags & K_DECSC_DECRC_MASK; + break; + + case '8': // DECRC restore cursor + setCursorRowCol(mSavedCursorRow, mSavedCursorCol); + mEffect = mSavedEffect; + mDecFlags = (mDecFlags & ~ K_DECSC_DECRC_MASK) + | mSavedDecFlags_DECSC_DECRC; + break; + + case 'D': // INDEX + doLinefeed(); + break; + + case 'E': // NEL + setCursorCol(0); + doLinefeed(); + break; + + case 'F': // Cursor to lower-left corner of screen + setCursorRowCol(0, mBottomMargin - 1); + break; + + case 'H': // Tab set + mTabStop[mCursorCol] = true; + break; + + case 'M': // Reverse index + if (mCursorRow <= mTopMargin) { + mScreen.blockCopy(0, mTopMargin, mColumns, mBottomMargin + - (mTopMargin + 1), 0, mTopMargin + 1); + blockClear(0, mTopMargin, mColumns); + } else { + mCursorRow--; + } + + break; + + case 'N': // SS2 + unimplementedSequence(b); + break; + + case '0': // SS3 + unimplementedSequence(b); + break; + + case 'P': // Device control string + unimplementedSequence(b); + break; + + case 'Z': // return terminal ID + sendDeviceAttributes(); + break; + + case '[': + continueSequence(ESC_LEFT_SQUARE_BRACKET); + break; + + case '=': // DECKPAM + mbKeypadApplicationMode = true; + break; + + case ']': // OSC + startCollectingOSCArgs(); + continueSequence(ESC_RIGHT_SQUARE_BRACKET); + break; + + case '>' : // DECKPNM + mbKeypadApplicationMode = false; + break; + + default: + unknownSequence(b); + break; + } + } + + private void doEscLeftSquareBracket(byte b) { + // CSI + switch (b) { + case '@': // ESC [ Pn @ - ICH Insert Characters + { + int charsAfterCursor = mColumns - mCursorCol; + int charsToInsert = Math.min(getArg0(1), charsAfterCursor); + int charsToMove = charsAfterCursor - charsToInsert; + mScreen.blockCopy(mCursorCol, mCursorRow, charsToMove, 1, + mCursorCol + charsToInsert, mCursorRow); + blockClear(mCursorCol, mCursorRow, charsToInsert); + } + break; + + case 'A': // ESC [ Pn A - Cursor Up + setCursorRow(Math.max(mTopMargin, mCursorRow - getArg0(1))); + break; + + case 'B': // ESC [ Pn B - Cursor Down + setCursorRow(Math.min(mBottomMargin - 1, mCursorRow + getArg0(1))); + break; + + case 'C': // ESC [ Pn C - Cursor Right + setCursorCol(Math.min(mColumns - 1, mCursorCol + getArg0(1))); + break; + + case 'D': // ESC [ Pn D - Cursor Left + setCursorCol(Math.max(0, mCursorCol - getArg0(1))); + break; + + case 'G': // ESC [ Pn G - Cursor Horizontal Absolute + setCursorCol(Math.min(Math.max(1, getArg0(1)), mColumns) - 1); + break; + + case 'H': // ESC [ Pn ; H - Cursor Position + setHorizontalVerticalPosition(); + break; + + case 'J': // ESC [ Pn J - ED - Erase in Display + // ED ignores the scrolling margins. + switch (getArg0(0)) { + case 0: // Clear below + blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); + blockClear(0, mCursorRow + 1, mColumns, + mRows - (mCursorRow + 1)); + break; + + case 1: // Erase from the start of the screen to the cursor. + blockClear(0, 0, mColumns, mCursorRow); + blockClear(0, mCursorRow, mCursorCol + 1); + break; + + case 2: // Clear all + blockClear(0, 0, mColumns, mRows); + break; + + default: + unknownSequence(b); + break; + } + break; + + case 'K': // ESC [ Pn K - Erase in Line + switch (getArg0(0)) { + case 0: // Clear to right + blockClear(mCursorCol, mCursorRow, mColumns - mCursorCol); + break; + + case 1: // Erase start of line to cursor (including cursor) + blockClear(0, mCursorRow, mCursorCol + 1); + break; + + case 2: // Clear whole line + blockClear(0, mCursorRow, mColumns); + break; + + default: + unknownSequence(b); + break; + } + break; + + case 'L': // Insert Lines + { + int linesAfterCursor = mBottomMargin - mCursorRow; + int linesToInsert = Math.min(getArg0(1), linesAfterCursor); + int linesToMove = linesAfterCursor - linesToInsert; + mScreen.blockCopy(0, mCursorRow, mColumns, linesToMove, 0, + mCursorRow + linesToInsert); + blockClear(0, mCursorRow, mColumns, linesToInsert); + } + break; + + case 'M': // Delete Lines + { + int linesAfterCursor = mBottomMargin - mCursorRow; + int linesToDelete = Math.min(getArg0(1), linesAfterCursor); + int linesToMove = linesAfterCursor - linesToDelete; + mScreen.blockCopy(0, mCursorRow + linesToDelete, mColumns, + linesToMove, 0, mCursorRow); + blockClear(0, mCursorRow + linesToMove, mColumns, linesToDelete); + } + break; + + case 'P': // Delete Characters + { + int charsAfterCursor = mColumns - mCursorCol; + int charsToDelete = Math.min(getArg0(1), charsAfterCursor); + int charsToMove = charsAfterCursor - charsToDelete; + mScreen.blockCopy(mCursorCol + charsToDelete, mCursorRow, + charsToMove, 1, mCursorCol, mCursorRow); + blockClear(mCursorCol + charsToMove, mCursorRow, charsToDelete); + } + break; + + case 'T': // Mouse tracking + unimplementedSequence(b); + break; + + case 'X': // Erase characters + blockClear(mCursorCol, mCursorRow, getArg0(0)); + break; + + case 'Z': // Back tab + setCursorCol(prevTabStop(mCursorCol)); + break; + + case '?': // Esc [ ? -- start of a private mode set + continueSequence(ESC_LEFT_SQUARE_BRACKET_QUESTION_MARK); + break; + + case 'c': // Send device attributes + sendDeviceAttributes(); + break; + + case 'd': // ESC [ Pn d - Vert Position Absolute + setCursorRow(Math.min(Math.max(1, getArg0(1)), mRows) - 1); + break; + + case 'f': // Horizontal and Vertical Position + setHorizontalVerticalPosition(); + break; + + case 'g': // Clear tab stop + switch (getArg0(0)) { + case 0: + mTabStop[mCursorCol] = false; + break; + + case 3: + for (int i = 0; i < mColumns; i++) { + mTabStop[i] = false; + } + break; + + default: + // Specified to have no effect. + break; + } + break; + + case 'h': // Set Mode + doSetMode(true); + break; + + case 'l': // Reset Mode + doSetMode(false); + break; + + case 'm': // Esc [ Pn m - character attributes. + // (can have up to 16 numerical arguments) + selectGraphicRendition(); + break; + + case 'n': // Esc [ Pn n - ECMA-48 Status Report Commands + //sendDeviceAttributes() + switch (getArg0(0)) { + case 5: // Device status report (DSR): + // Answer is ESC [ 0 n (Terminal OK). + byte[] dsr = { (byte) 27, (byte) '[', (byte) '0', (byte) 'n' }; + mSession.write(dsr, 0, dsr.length); + break; + + case 6: // Cursor position report (CPR): + // Answer is ESC [ y ; x R, where x,y is + // the cursor location. + byte[] cpr = String.format(Locale.US, "\033[%d;%dR", + mCursorRow + 1, mCursorCol + 1).getBytes(); + mSession.write(cpr, 0, cpr.length); + break; + + default: + break; + } + break; + + case 'r': // Esc [ Pn ; Pn r - set top and bottom margins + { + // The top margin defaults to 1, the bottom margin + // (unusually for arguments) defaults to mRows. + // + // The escape sequence numbers top 1..23, but we + // number top 0..22. + // The escape sequence numbers bottom 2..24, and + // so do we (because we use a zero based numbering + // scheme, but we store the first line below the + // bottom-most scrolling line. + // As a result, we adjust the top line by -1, but + // we leave the bottom line alone. + // + // Also require that top + 2 <= bottom + + int top = Math.max(0, Math.min(getArg0(1) - 1, mRows - 2)); + int bottom = Math.max(top + 2, Math.min(getArg1(mRows), mRows)); + mTopMargin = top; + mBottomMargin = bottom; + + // The cursor is placed in the home position + setCursorRowCol(mTopMargin, 0); + } + break; + + default: + parseArg(b); + break; + } + } + + private void selectGraphicRendition() { + // SGR + for (int i = 0; i <= mArgIndex; i++) { + int code = mArgs[i]; + if ( code < 0) { + if (mArgIndex > 0) { + continue; + } else { + code = 0; + } + } + + // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics + + if (code == 0) { // reset + mForeColor = mDefaultForeColor; + mBackColor = mDefaultBackColor; + mEffect = TextStyle.fxNormal; + } else if (code == 1) { // bold + mEffect |= TextStyle.fxBold; + } else if (code == 3) { // italics, but rarely used as such; "standout" (inverse colors) with TERM=screen + mEffect |= TextStyle.fxItalic; + } else if (code == 4) { // underscore + mEffect |= TextStyle.fxUnderline; + } else if (code == 5) { // blink + mEffect |= TextStyle.fxBlink; + } else if (code == 7) { // inverse + mEffect |= TextStyle.fxInverse; + } else if (code == 8) { // invisible + mEffect |= TextStyle.fxInvisible; + } else if (code == 10) { // exit alt charset (TERM=linux) + setAltCharSet(false); + } else if (code == 11) { // enter alt charset (TERM=linux) + setAltCharSet(true); + } else if (code == 22) { // Normal color or intensity, neither bright, bold nor faint + //mEffect &= ~(TextStyle.fxBold | TextStyle.fxFaint); + mEffect &= ~TextStyle.fxBold; + } else if (code == 23) { // not italic, but rarely used as such; clears standout with TERM=screen + mEffect &= ~TextStyle.fxItalic; + } else if (code == 24) { // underline: none + mEffect &= ~TextStyle.fxUnderline; + } else if (code == 25) { // blink: none + mEffect &= ~TextStyle.fxBlink; + } else if (code == 27) { // image: positive + mEffect &= ~TextStyle.fxInverse; + } else if (code == 28) { // invisible + mEffect &= ~TextStyle.fxInvisible; + } else if (code >= 30 && code <= 37) { // foreground color + mForeColor = code - 30; + } else if (code == 38 && i+2 <= mArgIndex && mArgs[i+1] == 5) { // foreground 256 color + int color = mArgs[i+2]; + if (checkColor(color)) { + mForeColor = color; + } + i += 2; + } else if (code == 39) { // set default text color + mForeColor = mDefaultForeColor; + } else if (code >= 40 && code <= 47) { // background color + mBackColor = code - 40; + } else if (code == 48 && i+2 <= mArgIndex && mArgs[i+1] == 5) { // background 256 color + mBackColor = mArgs[i+2]; + int color = mArgs[i+2]; + if (checkColor(color)) { + mBackColor = color; + } + i += 2; + } else if (code == 49) { // set default background color + mBackColor = mDefaultBackColor; + } else if (code >= 90 && code <= 97) { // bright foreground color + mForeColor = code - 90 + 8; + } else if (code >= 100 && code <= 107) { // bright background color + mBackColor = code - 100 + 8; + } else { + if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) { + Log.w(EmulatorDebug.LOG_TAG, String.format("SGR unknown code %d", code)); + } + } + } + } + + private boolean checkColor(int color) { + boolean result = isValidColor(color); + if (!result) { + if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) { + Log.w(EmulatorDebug.LOG_TAG, + String.format("Invalid color %d", color)); + } + } + return result; + } + + private boolean isValidColor(int color) { + return color >= 0 && color < TextStyle.ciColorLength; + } + + private void doEscRightSquareBracket(byte b) { + switch (b) { + case 0x7: + doOSC(); + break; + case 0x1b: // Esc, probably start of Esc \ sequence + continueSequence(ESC_RIGHT_SQUARE_BRACKET_ESC); + break; + default: + collectOSCArgs(b); + break; + } + } + + private void doEscRightSquareBracketEsc(byte b) { + switch (b) { + case '\\': + doOSC(); + break; + + default: + // The ESC character was not followed by a \, so insert the ESC and + // the current character in arg buffer. + collectOSCArgs((byte) 0x1b); + collectOSCArgs(b); + continueSequence(ESC_RIGHT_SQUARE_BRACKET); + break; + } + } + + private void doOSC() { // Operating System Controls + startTokenizingOSC(); + int ps = nextOSCInt(';'); + switch (ps) { + case 0: // Change icon name and window title to T + case 1: // Change icon name to T + case 2: // Change window title to T + changeTitle(ps, nextOSCString(-1)); + break; + default: + unknownParameter(ps); + break; + } + finishSequence(); + } + + private void changeTitle(int parameter, String title) { + if (parameter == 0 || parameter == 2) { + mSession.setTitle(title); + } + } + + private void blockClear(int sx, int sy, int w) { + blockClear(sx, sy, w, 1); + } + + private void blockClear(int sx, int sy, int w, int h) { + mScreen.blockSet(sx, sy, w, h, ' ', getStyle()); + } + + private int getForeColor() { + return mForeColor; + } + + private int getBackColor() { + return mBackColor; + } + + private int getEffect() { + return mEffect; + } + + private int getStyle() { + return TextStyle.encode(getForeColor(), getBackColor(), getEffect()); + } + + private void doSetMode(boolean newValue) { + int modeBit = getArg0(0); + switch (modeBit) { + case 4: + mInsertMode = newValue; + break; + + default: + unknownParameter(modeBit); + break; + } + } + + private void setHorizontalVerticalPosition() { + + // Parameters are Row ; Column + + setCursorPosition(getArg1(1) - 1, getArg0(1) - 1); + } + + private void setCursorPosition(int x, int y) { + int effectiveTopMargin = 0; + int effectiveBottomMargin = mRows; + if ((mDecFlags & K_ORIGIN_MODE_MASK) != 0) { + effectiveTopMargin = mTopMargin; + effectiveBottomMargin = mBottomMargin; + } + int newRow = + Math.max(effectiveTopMargin, Math.min(effectiveTopMargin + y, + effectiveBottomMargin - 1)); + int newCol = Math.max(0, Math.min(x, mColumns - 1)); + setCursorRowCol(newRow, newCol); + } + + private void sendDeviceAttributes() { + // This identifies us as a DEC vt100 with advanced + // video options. This is what the xterm terminal + // emulator sends. + byte[] attributes = + { + /* VT100 */ + (byte) 27, (byte) '[', (byte) '?', (byte) '1', + (byte) ';', (byte) '2', (byte) 'c' + + /* VT220 + (byte) 27, (byte) '[', (byte) '?', (byte) '6', + (byte) '0', (byte) ';', + (byte) '1', (byte) ';', + (byte) '2', (byte) ';', + (byte) '6', (byte) ';', + (byte) '8', (byte) ';', + (byte) '9', (byte) ';', + (byte) '1', (byte) '5', (byte) ';', + (byte) 'c' + */ + }; + + mSession.write(attributes, 0, attributes.length); + } + + private void scroll() { + //System.out.println("Scroll(): mTopMargin " + mTopMargin + " mBottomMargin " + mBottomMargin); + mScrollCounter ++; + mScreen.scroll(mTopMargin, mBottomMargin, getStyle()); + } + + /** + * Process the next ASCII character of a parameter. + * + * @param b The next ASCII character of the paramater sequence. + */ + private void parseArg(byte b) { + if (b >= '0' && b <= '9') { + if (mArgIndex < mArgs.length) { + int oldValue = mArgs[mArgIndex]; + int thisDigit = b - '0'; + int value; + if (oldValue >= 0) { + value = oldValue * 10 + thisDigit; + } else { + value = thisDigit; + } + mArgs[mArgIndex] = value; + } + continueSequence(); + } else if (b == ';') { + if (mArgIndex < mArgs.length) { + mArgIndex++; + } + continueSequence(); + } else { + unknownSequence(b); + } + } + + private int getArg0(int defaultValue) { + return getArg(0, defaultValue, true); + } + + private int getArg1(int defaultValue) { + return getArg(1, defaultValue, true); + } + + private int getArg(int index, int defaultValue, + boolean treatZeroAsDefault) { + int result = mArgs[index]; + if (result < 0 || (result == 0 && treatZeroAsDefault)) { + result = defaultValue; + } + return result; + } + + private void startCollectingOSCArgs() { + mOSCArgLength = 0; + } + + private void collectOSCArgs(byte b) { + if (mOSCArgLength < MAX_OSC_STRING_LENGTH) { + mOSCArg[mOSCArgLength++] = b; + continueSequence(); + } else { + unknownSequence(b); + } + } + + private void startTokenizingOSC() { + mOSCArgTokenizerIndex = 0; + } + + private String nextOSCString(int delimiter) { + int start = mOSCArgTokenizerIndex; + int end = start; + while (mOSCArgTokenizerIndex < mOSCArgLength) { + byte b = mOSCArg[mOSCArgTokenizerIndex++]; + if ((int) b == delimiter) { + break; + } + end++; + } + if (start == end) { + return ""; + } + try { + return new String(mOSCArg, start, end-start, "UTF-8"); + } catch (UnsupportedEncodingException e) { + return new String(mOSCArg, start, end-start); + } + } + + private int nextOSCInt(int delimiter) { + int value = -1; + while (mOSCArgTokenizerIndex < mOSCArgLength) { + byte b = mOSCArg[mOSCArgTokenizerIndex++]; + if ((int) b == delimiter) { + break; + } else if (b >= '0' && b <= '9') { + if (value < 0) { + value = 0; + } + value = value * 10 + b - '0'; + } else { + unknownSequence(b); + } + } + return value; + } + + private void unimplementedSequence(byte b) { + if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) { + logError("unimplemented", b); + } + finishSequence(); + } + + private void unknownSequence(byte b) { + if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) { + logError("unknown", b); + } + finishSequence(); + } + + private void unknownParameter(int parameter) { + if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) { + StringBuilder buf = new StringBuilder(); + buf.append("Unknown parameter"); + buf.append(parameter); + logError(buf.toString()); + } + } + + private void logError(String errorType, byte b) { + if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) { + StringBuilder buf = new StringBuilder(); + buf.append(errorType); + buf.append(" sequence "); + buf.append(" EscapeState: "); + buf.append(mEscapeState); + buf.append(" char: '"); + buf.append((char) b); + buf.append("' ("); + buf.append(b); + buf.append(")"); + boolean firstArg = true; + for (int i = 0; i <= mArgIndex; i++) { + int value = mArgs[i]; + if (value >= 0) { + if (firstArg) { + firstArg = false; + buf.append("args = "); + } + buf.append(String.format("%d; ", value)); + } + } + logError(buf.toString()); + } + } + + private void logError(String error) { + if (EmulatorDebug.LOG_UNKNOWN_ESCAPE_SEQUENCES) { + Log.e(EmulatorDebug.LOG_TAG, error); + } + finishSequence(); + } + + private void finishSequence() { + mEscapeState = ESC_NONE; + } + + private boolean autoWrapEnabled() { + return (mDecFlags & K_WRAPAROUND_MODE_MASK) != 0; + } + + /** + * Send a Unicode code point to the screen. + * + * @param c The code point of the character to display + * @param foreColor The foreground color of the character + * @param backColor The background color of the character + */ + private void emit(int c, int style) { + boolean autoWrap = autoWrapEnabled(); + int width = UnicodeTranscript.charWidth(c); + + if (autoWrap) { + if (mCursorCol == mColumns - 1 && (mAboutToAutoWrap || width == 2)) { + mScreen.setLineWrap(mCursorRow); + mCursorCol = 0; + mJustWrapped = true; + if (mCursorRow + 1 < mBottomMargin) { + mCursorRow++; + } else { + scroll(); + } + } + } + + if (mInsertMode & width != 0) { // Move character to right one space + int destCol = mCursorCol + width; + if (destCol < mColumns) { + mScreen.blockCopy(mCursorCol, mCursorRow, mColumns - destCol, + 1, destCol, mCursorRow); + } + } + + if (width == 0) { + // Combining character -- store along with character it modifies + if (mJustWrapped) { + mScreen.set(mColumns - mLastEmittedCharWidth, mCursorRow - 1, c, style); + } else { + mScreen.set(mCursorCol - mLastEmittedCharWidth, mCursorRow, c, style); + } + } else { + mScreen.set(mCursorCol, mCursorRow, c, style); + mJustWrapped = false; + } + + if (autoWrap) { + mAboutToAutoWrap = (mCursorCol == mColumns - 1); + + //Force line-wrap flag to trigger even for lines being typed + if(mAboutToAutoWrap) + mScreen.setLineWrap(mCursorRow); + } + + mCursorCol = Math.min(mCursorCol + width, mColumns - 1); + if (width > 0) { + mLastEmittedCharWidth = width; + } + } + + private void emit(int c) { + emit(c, getStyle()); + } + + private void emit(byte b) { + if (mUseAlternateCharSet && b < 128) { + emit((int) mSpecialGraphicsCharMap[b]); + } else { + emit((int) b); + } + } + + /** + * Send a UTF-16 char or surrogate pair to the screen. + * + * @param c A char[2] containing either a single UTF-16 char or a surrogate pair to be sent to the screen. + */ + private void emit(char[] c) { + if (Character.isHighSurrogate(c[0])) { + emit(Character.toCodePoint(c[0], c[1])); + } else { + emit((int) c[0]); + } + } + + /** + * Send an array of UTF-16 chars to the screen. + * + * @param c A char[] array whose contents are to be sent to the screen. + */ + private void emit(char[] c, int offset, int length, int style) { + for (int i = offset; i < length; ++i) { + if (c[i] == 0) { + break; + } + if (Character.isHighSurrogate(c[i])) { + emit(Character.toCodePoint(c[i], c[i+1]), style); + ++i; + } else { + emit((int) c[i], style); + } + } + } + + private void setCursorRow(int row) { + mCursorRow = row; + mAboutToAutoWrap = false; + } + + private void setCursorCol(int col) { + mCursorCol = col; + mAboutToAutoWrap = false; + } + + private void setCursorRowCol(int row, int col) { + mCursorRow = Math.min(row, mRows-1); + mCursorCol = Math.min(col, mColumns-1); + mAboutToAutoWrap = false; + } + + public int getScrollCounter() { + return mScrollCounter; + } + + public void clearScrollCounter() { + mScrollCounter = 0; + } + + /** + * Reset the terminal emulator to its initial state. + */ + public void reset() { + mCursorRow = 0; + mCursorCol = 0; + mArgIndex = 0; + mContinueSequence = false; + mEscapeState = ESC_NONE; + mSavedCursorRow = 0; + mSavedCursorCol = 0; + mSavedEffect = 0; + mSavedDecFlags_DECSC_DECRC = 0; + mDecFlags = 0; + if (DEFAULT_TO_AUTOWRAP_ENABLED) { + mDecFlags |= K_WRAPAROUND_MODE_MASK; + } + mDecFlags |= K_SHOW_CURSOR_MASK; + mSavedDecFlags = 0; + mInsertMode = false; + mTopMargin = 0; + mBottomMargin = mRows; + mAboutToAutoWrap = false; + mForeColor = mDefaultForeColor; + mBackColor = mDefaultBackColor; + mbKeypadApplicationMode = false; + mAlternateCharSet = false; + mCharSet[0] = CHAR_SET_ASCII; + mCharSet[1] = CHAR_SET_SPECIAL_GRAPHICS; + computeEffectiveCharSet(); + // mProcessedCharCount is preserved unchanged. + setDefaultTabStops(); + blockClear(0, 0, mColumns, mRows); + + setUTF8Mode(mDefaultUTF8Mode); + mUTF8EscapeUsed = false; + mUTF8ToFollow = 0; + mUTF8ByteBuffer.clear(); + mInputCharBuffer.clear(); + } + + public void setDefaultUTF8Mode(boolean defaultToUTF8Mode) { + mDefaultUTF8Mode = defaultToUTF8Mode; + if (!mUTF8EscapeUsed) { + setUTF8Mode(defaultToUTF8Mode); + } + } + + public void setUTF8Mode(boolean utf8Mode) { + if (utf8Mode && !mUTF8Mode) { + mUTF8ToFollow = 0; + mUTF8ByteBuffer.clear(); + mInputCharBuffer.clear(); + } + mUTF8Mode = utf8Mode; + if (mUTF8ModeNotify != null) { + mUTF8ModeNotify.onUpdate(); + } + } + + public boolean getUTF8Mode() { + return mUTF8Mode; + } + + public void setUTF8ModeUpdateCallback(UpdateCallback utf8ModeNotify) { + mUTF8ModeNotify = utf8ModeNotify; + } + + public void setColorScheme(ColorScheme scheme) { + mDefaultForeColor = TextStyle.ciForeground; + mDefaultBackColor = TextStyle.ciBackground; + mMainBuffer.setColorScheme(scheme); + if (mAltBuffer != null) { + mAltBuffer.setColorScheme(scheme); + } + } + + public String getSelectedText(int x1, int y1, int x2, int y2) { + return mScreen.getSelectedText(x1, y1, x2, y2); + } + + public void finish() { + if (mAltBuffer != null) { + mAltBuffer.finish(); + mAltBuffer = null; + } + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TextRenderer.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TextRenderer.java new file mode 100644 index 0000000..39c615d --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TextRenderer.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +import android.graphics.Canvas; + +/** + * Text renderer interface + */ + +interface TextRenderer { + public static final int MODE_OFF = 0; + public static final int MODE_ON = 1; + public static final int MODE_LOCKED = 2; + public static final int MODE_MASK = 3; + + public static final int MODE_SHIFT_SHIFT = 0; + public static final int MODE_ALT_SHIFT = 2; + public static final int MODE_CTRL_SHIFT = 4; + public static final int MODE_FN_SHIFT = 6; + + void setReverseVideo(boolean reverseVideo); + float getCharacterWidth(); + int getCharacterHeight(); + /** @return pixels above top row of text to avoid looking cramped. */ + int getTopMargin(); + /** + * Draw a run of text + * @param canvas The canvas to draw into. + * @param x Canvas coordinate of the left edge of the whole line. + * @param y Canvas coordinate of the bottom edge of the whole line. + * @param lineOffset The screen character offset of this text run (0..length of line) + * @param runWidth + * @param text + * @param index + * @param count + * @param selectionStyle True to draw the text using the "selected" style (for clipboard copy) + * @param textStyle + * @param cursorOffset The screen character offset of the cursor (or -1 if not on this line.) + * @param cursorIndex The index of the cursor in text chars. + * @param cursorIncr The width of the cursor in text chars. (1 or 2) + * @param cursorWidth The width of the cursor in screen columns (1 or 2) + * @param cursorMode The cursor mode (used to show state of shift/control/alt/fn locks. + */ + void drawTextRun(Canvas canvas, float x, float y, + int lineOffset, int runWidth, char[] text, + int index, int count, boolean selectionStyle, int textStyle, + int cursorOffset, int cursorIndex, int cursorIncr, int cursorWidth, int cursorMode); +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TextStyle.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TextStyle.java new file mode 100644 index 0000000..bf16e43 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TextStyle.java @@ -0,0 +1,44 @@ +package jackpal.androidterm.emulatorview; + +final class TextStyle { + // Effect bitmasks: + final static int fxNormal = 0; + final static int fxBold = 1; // Originally Bright + //final static int fxFaint = 2; + final static int fxItalic = 1 << 1; + final static int fxUnderline = 1 << 2; + final static int fxBlink = 1 << 3; + final static int fxInverse = 1 << 4; + final static int fxInvisible = 1 << 5; + + // Special color indices + final static int ciForeground = 256; // VT100 text foreground color + final static int ciBackground = 257; // VT100 text background color + final static int ciCursorForeground = 258; // VT100 text cursor foreground color + final static int ciCursorBackground = 259; // VT100 text cursor background color + + final static int ciColorLength = ciCursorBackground + 1; + + final static int kNormalTextStyle = encode(ciForeground, ciBackground, fxNormal); + + static int encode(int foreColor, int backColor, int effect) { + return ((effect & 0x3f) << 18) | ((foreColor & 0x1ff) << 9) | (backColor & 0x1ff); + } + + static int decodeForeColor(int encodedColor) { + return (encodedColor >> 9) & 0x1ff; + } + + static int decodeBackColor(int encodedColor) { + return encodedColor & 0x1ff; + } + + static int decodeEffect(int encodedColor) { + return (encodedColor >> 18) & 0x3f; + } + + private TextStyle() { + // Prevent instantiation + throw new UnsupportedOperationException(); + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TranscriptScreen.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TranscriptScreen.java new file mode 100644 index 0000000..e1dc3c0 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/TranscriptScreen.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +import java.util.Arrays; +import android.graphics.Canvas; + +/** + * A TranscriptScreen is a screen that remembers data that's been scrolled. The + * old data is stored in a ring buffer to minimize the amount of copying that + * needs to be done. The transcript does its own drawing, to avoid having to + * expose its internal data structures. + */ +class TranscriptScreen implements Screen { + /** + * The width of the transcript, in characters. Fixed at initialization. + */ + private int mColumns; + + /** + * The total number of rows in the transcript and the screen. Fixed at + * initialization. + */ + private int mTotalRows; + + /** + * The number of rows in the screen. + */ + private int mScreenRows; + + private UnicodeTranscript mData; + + /** + * Create a transcript screen. + * + * @param columns the width of the screen in characters. + * @param totalRows the height of the entire text area, in rows of text. + * @param screenRows the height of just the screen, not including the + * transcript that holds lines that have scrolled off the top of the + * screen. + */ + public TranscriptScreen(int columns, int totalRows, int screenRows, + ColorScheme scheme) { + init(columns, totalRows, screenRows, TextStyle.kNormalTextStyle); + } + + private void init(int columns, int totalRows, int screenRows, int style) { + mColumns = columns; + mTotalRows = totalRows; + mScreenRows = screenRows; + + mData = new UnicodeTranscript(columns, totalRows, screenRows, style); + mData.blockSet(0, 0, mColumns, mScreenRows, ' ', style); + } + + public void setColorScheme(ColorScheme scheme) { + mData.setDefaultStyle(TextStyle.kNormalTextStyle); + } + + public void finish() { + /* + * The Android InputMethodService will sometimes hold a reference to + * us for a while after the activity closes, which is expensive because + * it means holding on to the now-useless mData array. Explicitly + * get rid of our references to this data to help keep the amount of + * memory being leaked down. + */ + mData = null; + } + + public void setLineWrap(int row) { + mData.setLineWrap(row); + } + + /** + * Store a Unicode code point into the screen at location (x, y) + * + * @param x X coordinate (also known as column) + * @param y Y coordinate (also known as row) + * @param codePoint Unicode codepoint to store + * @param foreColor the foreground color + * @param backColor the background color + */ + public void set(int x, int y, int codePoint, int style) { + mData.setChar(x, y, codePoint, style); + } + + public void set(int x, int y, byte b, int style) { + mData.setChar(x, y, b, style); + } + + /** + * Scroll the screen down one line. To scroll the whole screen of a 24 line + * screen, the arguments would be (0, 24). + * + * @param topMargin First line that is scrolled. + * @param bottomMargin One line after the last line that is scrolled. + * @param style the style for the newly exposed line. + */ + public void scroll(int topMargin, int bottomMargin, int style) { + mData.scroll(topMargin, bottomMargin, style); + } + + /** + * Block copy characters from one position in the screen to another. The two + * positions can overlap. All characters of the source and destination must + * be within the bounds of the screen, or else an InvalidParemeterException + * will be thrown. + * + * @param sx source X coordinate + * @param sy source Y coordinate + * @param w width + * @param h height + * @param dx destination X coordinate + * @param dy destination Y coordinate + */ + public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) { + mData.blockCopy(sx, sy, w, h, dx, dy); + } + + /** + * Block set characters. All characters must be within the bounds of the + * screen, or else and InvalidParemeterException will be thrown. Typically + * this is called with a "val" argument of 32 to clear a block of + * characters. + * + * @param sx source X + * @param sy source Y + * @param w width + * @param h height + * @param val value to set. + */ + public void blockSet(int sx, int sy, int w, int h, int val, + int style) { + mData.blockSet(sx, sy, w, h, val, style); + } + + /** + * Draw a row of text. Out-of-bounds rows are blank, not errors. + * + * @param row The row of text to draw. + * @param canvas The canvas to draw to. + * @param x The x coordinate origin of the drawing + * @param y The y coordinate origin of the drawing + * @param renderer The renderer to use to draw the text + * @param cx the cursor X coordinate, -1 means don't draw it + * @param selx1 the text selection start X coordinate + * @param selx2 the text selection end X coordinate, if equals to selx1 don't draw selection + * @param imeText current IME text, to be rendered at cursor + * @param cursorMode the cursor mode. See TextRenderer. + */ + public final void drawText(int row, Canvas canvas, float x, float y, + TextRenderer renderer, int cx, int selx1, int selx2, String imeText, int cursorMode) { + char[] line; + StyleRow color; + int cursorWidth = 1; + try { + line = mData.getLine(row); + color = mData.getLineColor(row); + } catch (IllegalArgumentException e) { + // Out-of-bounds rows are blank. + return; + } catch (NullPointerException e) { + // Attempt to draw on a finished transcript + // XXX Figure out why this happens on Honeycomb + return; + } + int defaultStyle = mData.getDefaultStyle(); + + if (line == null) { + // Line is blank. + if (selx1 != selx2) { + // We need to draw a selection + char[] blank = new char[selx2-selx1]; + Arrays.fill(blank, ' '); + renderer.drawTextRun(canvas, x, y, selx1, selx2-selx1, + blank, 0, 1, true, defaultStyle, + cx, 0, 1, 1, cursorMode); + } + if (cx != -1) { + char[] blank = new char[1]; + Arrays.fill(blank, ' '); + // We need to draw the cursor + renderer.drawTextRun(canvas, x, y, cx, 1, + blank, 0, 1, true, defaultStyle, + cx, 0, 1, 1, cursorMode); + } + + return; + } + + int columns = mColumns; + int lineLen = line.length; + int lastStyle = 0; + boolean lastSelectionStyle = false; + int runWidth = 0; + int lastRunStart = -1; + int lastRunStartIndex = -1; + boolean forceFlushRun = false; + int column = 0; + int nextColumn = 0; + int displayCharWidth = 0; + int index = 0; + int cursorIndex = 0; + int cursorIncr = 0; + while (column < columns && index < lineLen && line[index] != '\0') { + int incr = 1; + int width; + if (Character.isHighSurrogate(line[index])) { + width = UnicodeTranscript.charWidth(line, index); + incr++; + } else { + width = UnicodeTranscript.charWidth(line[index]); + } + if (width > 0) { + // We've moved on to the next column + column = nextColumn; + displayCharWidth = width; + } + int style = color.get(column); + boolean selectionStyle = false; + if ((column >= selx1 || (displayCharWidth == 2 && column == selx1 - 1)) && + column <= selx2) { + // Draw selection: + selectionStyle = true; + } + if (style != lastStyle + || selectionStyle != lastSelectionStyle + || (width > 0 && forceFlushRun)) { + if (lastRunStart >= 0) { + renderer.drawTextRun(canvas, x, y, lastRunStart, runWidth, + line, + lastRunStartIndex, index - lastRunStartIndex, + lastSelectionStyle, lastStyle, + cx, cursorIndex, cursorIncr, cursorWidth, cursorMode); + } + lastStyle = style; + lastSelectionStyle = selectionStyle; + runWidth = 0; + lastRunStart = column; + lastRunStartIndex = index; + forceFlushRun = false; + } + if (cx == column) { + if (width > 0) { + cursorIndex = index; + cursorIncr = incr; + cursorWidth = width; + } else { + // Combining char attaching to the char under the cursor + cursorIncr += incr; + } + } + runWidth += width; + nextColumn += width; + index += incr; + if (width > 1) { + /* We cannot draw two or more East Asian wide characters in the + same run, because we need to make each wide character take + up two columns, which may not match the font's idea of the + character width */ + forceFlushRun = true; + } + } + if (lastRunStart >= 0) { + renderer.drawTextRun(canvas, x, y, lastRunStart, runWidth, + line, + lastRunStartIndex, index - lastRunStartIndex, + lastSelectionStyle, lastStyle, + cx, cursorIndex, cursorIncr, cursorWidth, cursorMode); + } + + if (cx >= 0 && imeText.length() > 0) { + int imeLength = Math.min(columns, imeText.length()); + int imeOffset = imeText.length() - imeLength; + int imePosition = Math.min(cx, columns - imeLength); + renderer.drawTextRun(canvas, x, y, imePosition, imeLength, imeText.toCharArray(), + imeOffset, imeLength, true, TextStyle.encode(0x0f, 0x00, TextStyle.fxNormal), + -1, 0, 0, 0, 0); + } + } + + /** + * Get the count of active rows. + * + * @return the count of active rows. + */ + public int getActiveRows() { + return mData.getActiveRows(); + } + + /** + * Get the count of active transcript rows. + * + * @return the count of active transcript rows. + */ + public int getActiveTranscriptRows() { + return mData.getActiveTranscriptRows(); + } + + public String getTranscriptText() { + return internalGetTranscriptText(null, 0, -mData.getActiveTranscriptRows(), mColumns, mScreenRows); + } + + public String getTranscriptText(GrowableIntArray colors) { + return internalGetTranscriptText(colors, 0, -mData.getActiveTranscriptRows(), mColumns, mScreenRows); + } + + public String getSelectedText(int selX1, int selY1, int selX2, int selY2) { + return internalGetTranscriptText(null, selX1, selY1, selX2, selY2); + } + + public String getSelectedText(GrowableIntArray colors, int selX1, int selY1, int selX2, int selY2) { + return internalGetTranscriptText(colors, selX1, selY1, selX2, selY2); + } + + private String internalGetTranscriptText(GrowableIntArray colors, int selX1, int selY1, int selX2, int selY2) { + StringBuilder builder = new StringBuilder(); + UnicodeTranscript data = mData; + int columns = mColumns; + char[] line; + StyleRow rowColorBuffer = null; + if (selY1 < -data.getActiveTranscriptRows()) { + selY1 = -data.getActiveTranscriptRows(); + } + if (selY2 >= mScreenRows) { + selY2 = mScreenRows - 1; + } + for (int row = selY1; row <= selY2; row++) { + int x1 = 0; + int x2; + if ( row == selY1 ) { + x1 = selX1; + } + if ( row == selY2 ) { + x2 = selX2 + 1; + if (x2 > columns) { + x2 = columns; + } + } else { + x2 = columns; + } + line = data.getLine(row, x1, x2); + if (colors != null) { + rowColorBuffer = data.getLineColor(row, x1, x2); + } + if (line == null) { + if (!data.getLineWrap(row) && row < selY2 && row < mScreenRows - 1) { + builder.append('\n'); + if (colors != null) { + colors.append(0); + } + } + continue; + } + int defaultColor = mData.getDefaultStyle(); + int lastPrintingChar = -1; + int lineLen = line.length; + int i; + int column = 0; + for (i = 0; i < lineLen; ++i) { + char c = line[i]; + if (c == 0) { + break; + } + + int style = defaultColor; + try { + if (rowColorBuffer != null) { + style = rowColorBuffer.get(column); + } + } catch (ArrayIndexOutOfBoundsException e) { + // XXX This probably shouldn't happen ... + style = defaultColor; + } + + if (c != ' ' || style != defaultColor) { + lastPrintingChar = i; + } + if (!Character.isLowSurrogate(c)) { + column += UnicodeTranscript.charWidth(line, i); + } + } + if (data.getLineWrap(row) && lastPrintingChar > -1 && x2 == columns) { + // If the line was wrapped, we shouldn't lose trailing space + lastPrintingChar = i - 1; + } + builder.append(line, 0, lastPrintingChar + 1); + if (colors != null) { + if (rowColorBuffer != null) { + column = 0; + for (int j = 0; j <= lastPrintingChar; ++j) { + colors.append(rowColorBuffer.get(column)); + column += UnicodeTranscript.charWidth(line, j); + if (Character.isHighSurrogate(line[j])) { + ++j; + } + } + } else { + for (int j = 0; j <= lastPrintingChar; ++j) { + colors.append(defaultColor); + char c = line[j]; + if (Character.isHighSurrogate(c)) { + ++j; + } + } + } + } + if (!data.getLineWrap(row) && row < selY2 && row < mScreenRows - 1) { + builder.append('\n'); + if (colors != null) { + colors.append((char) 0); + } + } + } + return builder.toString(); + } + + public boolean fastResize(int columns, int rows, int[] cursor) { + if (mData == null) { + // XXX Trying to resize a finished TranscriptScreen? + return true; + } + if (mData.resize(columns, rows, cursor)) { + mColumns = columns; + mScreenRows = rows; + return true; + } else { + return false; + } + } + + public void resize(int columns, int rows, int style) { + // Ensure backing store will be large enough to hold the whole screen + if (rows > mTotalRows) { + mTotalRows = rows; + } + init(columns, mTotalRows, rows, style); + } + + /** + * + * Return the UnicodeTranscript line at this row index. + * @param row The row index to be queried + * @return The line of text at this row index + */ + char[] getScriptLine(int row) + { + try + { + return mData.getLine(row); + } + catch (IllegalArgumentException e) + { + return null; + } + catch (NullPointerException e) + { + return null; + } + } + + /** + * Get the line wrap status of the row provided. + * @param row The row to check for line-wrap status + * @return The line wrap status of the row provided + */ + boolean getScriptLineWrap(int row) + { + return mData.getLineWrap(row); + } + + /** + * Get whether the line at this index is "basic" (contains only BMP + * characters of width 1). + */ + boolean isBasicLine(int row) { + if (mData != null) { + return mData.isBasicLine(row); + } else { + return true; + } + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/UnicodeTranscript.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/UnicodeTranscript.java new file mode 100644 index 0000000..2b8fdfc --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/UnicodeTranscript.java @@ -0,0 +1,1141 @@ +/* + * Copyright (C) 2011 Steven Luo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +import android.util.Log; + +import jackpal.androidterm.emulatorview.compat.AndroidCharacterCompat; +import jackpal.androidterm.emulatorview.compat.AndroidCompat; + +/** + * A backing store for a TranscriptScreen. + * + * The text is stored as a circular buffer of rows. There are two types of + * row: + * - "basic", which is a char[] array used to store lines which consist + * entirely of regular-width characters (no combining characters, zero-width + * characters, East Asian double-width characters, etc.) in the BMP; and + * - "full", which is a char[] array with extra trappings which can be used to + * store a line containing any valid Unicode sequence. An array of short[] + * is used to store the "offset" at which each column starts; for example, + * if column 20 starts at index 23 in the array, then mOffset[20] = 3. + * + * Style information is stored in a separate circular buffer of StyleRows. + * + * Rows are allocated on demand, when a character is first stored into them. + * A "basic" row is allocated unless the store which triggers the allocation + * requires a "full" row. "Basic" rows are converted to "full" rows when + * needed. There is no conversion in the other direction -- a "full" row + * stays that way even if it contains only regular-width BMP characters. + */ +class UnicodeTranscript { + private static final String TAG = "UnicodeTranscript"; + + private Object[] mLines; + private StyleRow[] mColor; + private boolean[] mLineWrap; + private int mTotalRows; + private int mScreenRows; + private int mColumns; + private int mActiveTranscriptRows = 0; + private int mDefaultStyle = 0; + + private int mScreenFirstRow = 0; + + private char[] tmpLine; + private StyleRow tmpColor; + + public UnicodeTranscript(int columns, int totalRows, int screenRows, int defaultStyle) { + mColumns = columns; + mTotalRows = totalRows; + mScreenRows = screenRows; + mLines = new Object[totalRows]; + mColor = new StyleRow[totalRows]; + mLineWrap = new boolean[totalRows]; + tmpColor = new StyleRow(defaultStyle, mColumns); + + mDefaultStyle = defaultStyle; + } + + public void setDefaultStyle(int defaultStyle) { + mDefaultStyle = defaultStyle; + } + + public int getDefaultStyle() { + return mDefaultStyle; + } + + public int getActiveTranscriptRows() { + return mActiveTranscriptRows; + } + + public int getActiveRows() { + return mActiveTranscriptRows + mScreenRows; + } + + /** + * Convert a row value from the public external coordinate system to our + * internal private coordinate system. + * External coordinate system: + * -mActiveTranscriptRows to mScreenRows-1, with the screen being + * 0..mScreenRows-1 + * Internal coordinate system: the mScreenRows lines starting at + * mScreenFirstRow comprise the screen, while the mActiveTranscriptRows + * lines ending at mScreenRows-1 form the transcript (as a circular + * buffer). + * + * @param extRow a row in the external coordinate system. + * @return The row corresponding to the input argument in the private + * coordinate system. + */ + private int externalToInternalRow(int extRow) { + if (extRow < -mActiveTranscriptRows || extRow > mScreenRows) { + String errorMessage = "externalToInternalRow "+ extRow + + " " + mScreenRows + " " + mActiveTranscriptRows; + Log.e(TAG, errorMessage); + throw new IllegalArgumentException(errorMessage); + } + + if (extRow >= 0) { + return (mScreenFirstRow + extRow) % mTotalRows; + } else { + if (-extRow > mScreenFirstRow) { + return mTotalRows + mScreenFirstRow + extRow; + } else { + return mScreenFirstRow + extRow; + } + } + } + + public void setLineWrap(int row) { + mLineWrap[externalToInternalRow(row)] = true; + } + + public boolean getLineWrap(int row) { + return mLineWrap[externalToInternalRow(row)]; + } + + /** + * Resize the screen which this transcript backs. Currently, this + * only works if the number of columns does not change. + * + * @param newColumns The number of columns the screen should have. + * @param newRows The number of rows the screen should have. + * @param cursor An int[2] containing the current cursor location; if the + * resize succeeds, this will be updated with the new cursor + * location. If null, don't do cursor-position-dependent tasks such + * as trimming blank lines during the resize. + * @return Whether or not the resize succeeded. If the resize failed, + * the caller may "resize" the screen by copying out all the data + * and placing it into a new transcript of the correct size. + */ + public boolean resize(int newColumns, int newRows, int[] cursor) { + if (newColumns != mColumns || newRows > mTotalRows) { + return false; + } + + int screenRows = mScreenRows; + int activeTranscriptRows = mActiveTranscriptRows; + int shift = screenRows - newRows; + if (shift < -activeTranscriptRows) { + // We want to add blank lines at the bottom instead of at the top + Object[] lines = mLines; + Object[] color = mColor; + boolean[] lineWrap = mLineWrap; + int screenFirstRow = mScreenFirstRow; + int totalRows = mTotalRows; + for (int i = 0; i < activeTranscriptRows - shift; ++i) { + int index = (screenFirstRow + screenRows + i) % totalRows; + lines[index] = null; + color[index] = null; + lineWrap[index] = false; + } + shift = -activeTranscriptRows; + } else if (shift > 0 && cursor != null && cursor[1] != screenRows - 1) { + /* When shrinking the screen, we want to hide blank lines at the + bottom in preference to lines at the top of the screen */ + Object[] lines = mLines; + for (int i = screenRows - 1; i > cursor[1]; --i) { + int index = externalToInternalRow(i); + if (lines[index] == null) { + // Line is blank + --shift; + if (shift == 0) { + break; + } else { + continue; + } + } + + char[] line; + if (lines[index] instanceof char[]) { + line = (char[]) lines[index]; + } else { + line = ((FullUnicodeLine) lines[index]).getLine(); + } + + int len = line.length; + int j; + for (j = 0; j < len; ++j) { + if (line[j] == 0) { + // We've reached the end of the line + j = len; + break; + } else if (line[j] != ' ') { + // Line is not blank + break; + } + } + + if (j == len) { + // Line is blank + --shift; + if (shift == 0) { + break; + } else { + continue; + } + } else { + // Line not blank -- we keep it and everything above + break; + } + } + } + + if (shift > 0 || (shift < 0 && mScreenFirstRow >= -shift)) { + // All we're doing is moving the top of the screen. + mScreenFirstRow = (mScreenFirstRow + shift) % mTotalRows; + } else if (shift < 0) { + // The new top of the screen wraps around the top of the array. + mScreenFirstRow = mTotalRows + mScreenFirstRow + shift; + } + + if (mActiveTranscriptRows + shift < 0) { + mActiveTranscriptRows = 0; + } else { + mActiveTranscriptRows += shift; + } + if (cursor != null) { + cursor[1] -= shift; + } + mScreenRows = newRows; + + return true; + } + + /** + * Block copy lines and associated metadata from one location to another + * in the circular buffer, taking wraparound into account. + * + * @param src The first line to be copied. + * @param len The number of lines to be copied. + * @param shift The offset of the destination from the source. + */ + private void blockCopyLines(int src, int len, int shift) { + int totalRows = mTotalRows; + + int dst; + if (src + shift >= 0) { + dst = (src + shift) % totalRows; + } else { + dst = totalRows + src + shift; + } + + if (src + len <= totalRows && dst + len <= totalRows) { + // Fast path -- no wraparound + System.arraycopy(mLines, src, mLines, dst, len); + System.arraycopy(mColor, src, mColor, dst, len); + System.arraycopy(mLineWrap, src, mLineWrap, dst, len); + return; + } + + if (shift < 0) { + // Do the copy from top to bottom + for (int i = 0; i < len; ++i) { + mLines[(dst + i) % totalRows] = mLines[(src + i) % totalRows]; + mColor[(dst + i) % totalRows] = mColor[(src + i) % totalRows]; + mLineWrap[(dst + i) % totalRows] = mLineWrap[(src + i) % totalRows]; + } + } else { + // Do the copy from bottom to top + for (int i = len - 1; i >= 0; --i) { + mLines[(dst + i) % totalRows] = mLines[(src + i) % totalRows]; + mColor[(dst + i) % totalRows] = mColor[(src + i) % totalRows]; + mLineWrap[(dst + i) % totalRows] = mLineWrap[(src + i) % totalRows]; + } + } + } + + /** + * Scroll the screen down one line. To scroll the whole screen of a 24 line + * screen, the arguments would be (0, 24). + * + * @param topMargin First line that is scrolled. + * @param bottomMargin One line after the last line that is scrolled. + * @param style the style for the newly exposed line. + */ + public void scroll(int topMargin, int bottomMargin, int style) { + // Separate out reasons so that stack crawls help us + // figure out which condition was violated. + if (topMargin > bottomMargin - 1) { + throw new IllegalArgumentException(); + } + + if (topMargin < 0) { + throw new IllegalArgumentException(); + } + + if (bottomMargin > mScreenRows) { + throw new IllegalArgumentException(); + } + + int screenRows = mScreenRows; + int totalRows = mTotalRows; + + if (topMargin == 0 && bottomMargin == screenRows) { + // Fast path -- scroll the entire screen + mScreenFirstRow = (mScreenFirstRow + 1) % totalRows; + if (mActiveTranscriptRows < totalRows - screenRows) { + ++mActiveTranscriptRows; + } + + // Blank the bottom margin + int blankRow = externalToInternalRow(bottomMargin - 1); + mLines[blankRow] = null; + mColor[blankRow] = new StyleRow(style, mColumns); + mLineWrap[blankRow] = false; + + return; + } + + int screenFirstRow = mScreenFirstRow; + int topMarginInt = externalToInternalRow(topMargin); + int bottomMarginInt = externalToInternalRow(bottomMargin); + + /* Save the scrolled line, move the lines above it on the screen down + one line, move the lines on screen below the bottom margin down + one line, then insert the scrolled line into the transcript */ + Object[] lines = mLines; + StyleRow[] color = mColor; + boolean[] lineWrap = mLineWrap; + Object scrollLine = lines[topMarginInt]; + StyleRow scrollColor = color[topMarginInt]; + boolean scrollLineWrap = lineWrap[topMarginInt]; + blockCopyLines(screenFirstRow, topMargin, 1); + blockCopyLines(bottomMarginInt, screenRows - bottomMargin, 1); + lines[screenFirstRow] = scrollLine; + color[screenFirstRow] = scrollColor; + lineWrap[screenFirstRow] = scrollLineWrap; + + // Update the screen location + mScreenFirstRow = (screenFirstRow + 1) % totalRows; + if (mActiveTranscriptRows < totalRows - screenRows) { + ++mActiveTranscriptRows; + } + + // Blank the bottom margin + int blankRow = externalToInternalRow(bottomMargin - 1); + lines[blankRow] = null; + color[blankRow] = new StyleRow(style, mColumns); + lineWrap[blankRow] = false; + + return; + } + + /** + * Block copy characters from one position in the screen to another. The two + * positions can overlap. All characters of the source and destination must + * be within the bounds of the screen, or else an InvalidParameterException + * will be thrown. + * + * @param sx source X coordinate + * @param sy source Y coordinate + * @param w width + * @param h height + * @param dx destination X coordinate + * @param dy destination Y coordinate + */ + public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) { + if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows + || dx < 0 || dx + w > mColumns || dy < 0 + || dy + h > mScreenRows) { + throw new IllegalArgumentException(); + } + Object[] lines = mLines; + StyleRow[] color = mColor; + if (sy > dy) { + // Move in increasing order + for (int y = 0; y < h; y++) { + int srcRow = externalToInternalRow(sy + y); + int dstRow = externalToInternalRow(dy + y); + if (lines[srcRow] instanceof char[] && lines[dstRow] instanceof char[]) { + System.arraycopy(lines[srcRow], sx, lines[dstRow], dx, w); + } else { + // XXX There has to be a faster way to do this ... + int extDstRow = dy + y; + char[] tmp = getLine(sy + y, sx, sx + w, true); + if (tmp == null) { + // Source line was blank + blockSet(dx, extDstRow, w, 1, ' ', mDefaultStyle); + continue; + } + char cHigh = 0; + int x = 0; + int columns = mColumns; + for (int i = 0; i < tmp.length; ++i) { + if (tmp[i] == 0 || dx + x >= columns) { + break; + } + if (Character.isHighSurrogate(tmp[i])) { + cHigh = tmp[i]; + continue; + } else if (Character.isLowSurrogate(tmp[i])) { + int codePoint = Character.toCodePoint(cHigh, tmp[i]); + setChar(dx + x, extDstRow, codePoint); + x += charWidth(codePoint); + } else { + setChar(dx + x, extDstRow, tmp[i]); + x += charWidth(tmp[i]); + } + } + } + color[srcRow].copy(sx, color[dstRow], dx, w); + } + } else { + // Move in decreasing order + for (int y = 0; y < h; y++) { + int y2 = h - (y + 1); + int srcRow = externalToInternalRow(sy + y2); + int dstRow = externalToInternalRow(dy + y2); + if (lines[srcRow] instanceof char[] && lines[dstRow] instanceof char[]) { + System.arraycopy(lines[srcRow], sx, lines[dstRow], dx, w); + } else { + int extDstRow = dy + y2; + char[] tmp = getLine(sy + y2, sx, sx + w, true); + if (tmp == null) { + // Source line was blank + blockSet(dx, extDstRow, w, 1, ' ', mDefaultStyle); + continue; + } + char cHigh = 0; + int x = 0; + int columns = mColumns; + for (int i = 0; i < tmp.length; ++i) { + if (tmp[i] == 0 || dx + x >= columns) { + break; + } + if (Character.isHighSurrogate(tmp[i])) { + cHigh = tmp[i]; + continue; + } else if (Character.isLowSurrogate(tmp[i])) { + int codePoint = Character.toCodePoint(cHigh, tmp[i]); + setChar(dx + x, extDstRow, codePoint); + x += charWidth(codePoint); + } else { + setChar(dx + x, extDstRow, tmp[i]); + x += charWidth(tmp[i]); + } + } + } + color[srcRow].copy(sx, color[dstRow], dx, w); + } + } + } + + /** + * Block set characters. All characters must be within the bounds of the + * screen, or else and InvalidParemeterException will be thrown. Typically + * this is called with a "val" argument of 32 to clear a block of + * characters. + * + * @param sx source X + * @param sy source Y + * @param w width + * @param h height + * @param val value to set. + */ + public void blockSet(int sx, int sy, int w, int h, int val, int style) { + if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) { + Log.e(TAG, "illegal arguments! " + sx + " " + sy + " " + w + " " + h + " " + val + " " + mColumns + " " + mScreenRows); + throw new IllegalArgumentException(); + } + + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + setChar(sx + x, sy + y, val, style); + } + } + } + + /** + * Minimum API version for which we're willing to let Android try + * rendering conjoining Hangul jamo as composed syllable blocks. + * + * This appears to work on Android 4.1.2, 4.3, and 4.4 (real devices only; + * the emulator's broken for some reason), but not on 4.0.4 -- hence the + * choice of API 16 as the minimum. + */ + static final int HANGUL_CONJOINING_MIN_SDK = 16; + + /** + * Gives the display width of the code point in a monospace font. + * + * Nonspacing combining marks, format characters, and control characters + * have display width zero. East Asian fullwidth and wide characters + * have display width two. All other characters have display width one. + * + * Known issues: + * - Proper support for East Asian wide characters requires API >= 8. + * - Assigning all East Asian "ambiguous" characters a width of 1 may not + * be correct if Android renders those characters as wide in East Asian + * context (as the Unicode standard permits). + * - Isolated Hangul conjoining medial vowels and final consonants are + * treated as combining characters (they should only be combining when + * part of a Korean syllable block). + * + * @param codePoint A Unicode code point. + * @return The display width of the Unicode code point. + */ + public static int charWidth(int codePoint) { + // Early out for ASCII printable characters + if (codePoint > 31 && codePoint < 127) { + return 1; + } + + /* HACK: We're using ASCII ESC to save the location of the cursor + across screen resizes, so we need to pretend that it has width 1 */ + if (codePoint == 27) { + return 1; + } + + switch (Character.getType(codePoint)) { + case Character.CONTROL: + case Character.FORMAT: + case Character.NON_SPACING_MARK: + case Character.ENCLOSING_MARK: + return 0; + } + + if ((codePoint >= 0x1160 && codePoint <= 0x11FF) || + (codePoint >= 0xD7B0 && codePoint <= 0xD7FF)) { + if (AndroidCompat.SDK >= HANGUL_CONJOINING_MIN_SDK) { + /* Treat Hangul jamo medial vowels and final consonants as + * combining characters with width 0 to make jamo composition + * work correctly. + * + * XXX: This is wrong for medials/finals outside a Korean + * syllable block, but there's no easy solution to that + * problem, and we may as well at least get the common case + * right. */ + return 0; + } else { + /* Older versions of Android didn't compose Hangul jamo, but + * instead rendered them as individual East Asian wide + * characters (despite Unicode defining medial vowels and final + * consonants as East Asian neutral/narrow). Treat them as + * width 2 characters to match the rendering. */ + return 2; + } + } + if (Character.charCount(codePoint) == 1) { + // Android's getEastAsianWidth() only works for BMP characters + switch (AndroidCharacterCompat.getEastAsianWidth((char) codePoint)) { + case AndroidCharacterCompat.EAST_ASIAN_WIDTH_FULL_WIDTH: + case AndroidCharacterCompat.EAST_ASIAN_WIDTH_WIDE: + return 2; + } + } else { + // Outside the BMP, only the ideographic planes contain wide chars + switch ((codePoint >> 16) & 0xf) { + case 2: // Supplementary Ideographic Plane + case 3: // Tertiary Ideographic Plane + return 2; + } + } + + return 1; + } + + public static int charWidth(char cHigh, char cLow) { + return charWidth(Character.toCodePoint(cHigh, cLow)); + } + + /** + * Gives the display width of a code point in a char array + * in a monospace font. + * + * @param chars The array containing the code point in question. + * @param index The index into the array at which the code point starts. + * @return The display width of the Unicode code point. + */ + public static int charWidth(char[] chars, int index) { + char c = chars[index]; + if (Character.isHighSurrogate(c)) { + return charWidth(c, chars[index+1]); + } else { + return charWidth(c); + } + } + + /** + * Get the contents of a line (or part of a line) of the transcript. + * + * The char[] array returned may be part of the internal representation + * of the line -- make a copy first if you want to modify it. The returned + * array may be longer than the requested portion of the transcript; in + * this case, the last character requested will be followed by a NUL, and + * the contents of the rest of the array could potentially be garbage. + * + * @param row The row number to get (-mActiveTranscriptRows..mScreenRows-1) + * @param x1 The first screen position that's wanted + * @param x2 One after the last screen position that's wanted + * @return A char[] array containing the requested contents + */ + public char[] getLine(int row, int x1, int x2) { + return getLine(row, x1, x2, false); + } + + /** + * Get the whole contents of a line of the transcript. + */ + public char[] getLine(int row) { + return getLine(row, 0, mColumns, true); + } + + private char[] getLine(int row, int x1, int x2, boolean strictBounds) { + if (row < -mActiveTranscriptRows || row > mScreenRows-1) { + throw new IllegalArgumentException(); + } + + int columns = mColumns; + row = externalToInternalRow(row); + if (mLines[row] == null) { + // Line is blank + return null; + } + if (mLines[row] instanceof char[]) { + // Line contains only regular-width BMP characters + if (x1 == 0 && x2 == columns) { + // Want the whole row? Easy. + return (char[]) mLines[row]; + } else { + if (tmpLine == null || tmpLine.length < columns + 1) { + tmpLine = new char[columns+1]; + } + int length = x2 - x1; + System.arraycopy(mLines[row], x1, tmpLine, 0, length); + tmpLine[length] = 0; + return tmpLine; + } + } + + // Figure out how long the array needs to be + FullUnicodeLine line = (FullUnicodeLine) mLines[row]; + char[] rawLine = line.getLine(); + + if (x1 == 0 && x2 == columns) { + /* We can return the raw line after ensuring it's NUL-terminated at + * the appropriate place */ + int spaceUsed = line.getSpaceUsed(); + if (spaceUsed < rawLine.length) { + rawLine[spaceUsed] = 0; + } + return rawLine; + } + + x1 = line.findStartOfColumn(x1); + if (x2 < columns) { + int endCol = x2; + x2 = line.findStartOfColumn(endCol); + if (!strictBounds && endCol > 0 && endCol < columns - 1) { + /* If the end column is the middle of an East Asian wide + * character, include that character in the bounds */ + if (x2 == line.findStartOfColumn(endCol - 1)) { + x2 = line.findStartOfColumn(endCol + 1); + } + } + } else { + x2 = line.getSpaceUsed(); + } + int length = x2 - x1; + + if (tmpLine == null || tmpLine.length < length + 1) { + tmpLine = new char[length+1]; + } + System.arraycopy(rawLine, x1, tmpLine, 0, length); + tmpLine[length] = 0; + return tmpLine; + } + + /** + * Get color/formatting information for a particular line. + * The returned object may be a pointer to a temporary buffer, only good + * until the next call to getLineColor. + */ + public StyleRow getLineColor(int row, int x1, int x2) { + return getLineColor(row, x1, x2, false); + } + + public StyleRow getLineColor(int row) { + return getLineColor(row, 0, mColumns, true); + } + + private StyleRow getLineColor(int row, int x1, int x2, boolean strictBounds) { + if (row < -mActiveTranscriptRows || row > mScreenRows-1) { + throw new IllegalArgumentException(); + } + + row = externalToInternalRow(row); + StyleRow color = mColor[row]; + StyleRow tmp = tmpColor; + if (color != null) { + int columns = mColumns; + if (!strictBounds && mLines[row] != null && + mLines[row] instanceof FullUnicodeLine) { + FullUnicodeLine line = (FullUnicodeLine) mLines[row]; + /* If either the start or the end column is in the middle of + * an East Asian wide character, include the appropriate column + * of style information */ + if (x1 > 0 && line.findStartOfColumn(x1-1) == line.findStartOfColumn(x1)) { + --x1; + } + if (x2 < columns - 1 && line.findStartOfColumn(x2+1) == line.findStartOfColumn(x2)) { + ++x2; + } + } + if (x1 == 0 && x2 == columns) { + return color; + } + color.copy(x1, tmp, 0, x2-x1); + return tmp; + } else { + return null; + } + } + + boolean isBasicLine(int row) { + if (row < -mActiveTranscriptRows || row > mScreenRows-1) { + throw new IllegalArgumentException(); + } + + return (mLines[externalToInternalRow(row)] instanceof char[]); + } + + public boolean getChar(int row, int column) { + return getChar(row, column, 0); + } + + public boolean getChar(int row, int column, int charIndex) { + return getChar(row, column, charIndex, new char[1], 0); + } + + /** + * Get a character at a specific position in the transcript. + * + * @param row The row of the character to get. + * @param column The column of the character to get. + * @param charIndex The index of the character in the column to get + * (0 for the first character, 1 for the next, etc.) + * @param out The char[] array into which the character will be placed. + * @param offset The offset in the array at which the character will be placed. + * @return Whether or not there are characters following this one in the column. + */ + public boolean getChar(int row, int column, int charIndex, char[] out, int offset) { + if (row < -mActiveTranscriptRows || row > mScreenRows-1) { + throw new IllegalArgumentException(); + } + row = externalToInternalRow(row); + + if (mLines[row] instanceof char[]) { + // Fast path: all regular-width BMP chars in the row + char[] line = (char[]) mLines[row]; + out[offset] = line[column]; + return false; + } + + FullUnicodeLine line = (FullUnicodeLine) mLines[row]; + return line.getChar(column, charIndex, out, offset); + } + + private boolean isBasicChar(int codePoint) { + return !(charWidth(codePoint) != 1 || Character.charCount(codePoint) != 1); + } + + private char[] allocateBasicLine(int row, int columns) { + char[] line = new char[columns]; + + // Fill the line with blanks + for (int i = 0; i < columns; ++i) { + line[i] = ' '; + } + + mLines[row] = line; + if (mColor[row] == null) { + mColor[row] = new StyleRow(0, columns); + } + return line; + } + + private FullUnicodeLine allocateFullLine(int row, int columns) { + FullUnicodeLine line = new FullUnicodeLine(columns); + + mLines[row] = line; + if (mColor[row] == null) { + mColor[row] = new StyleRow(0, columns); + } + return line; + } + + public boolean setChar(int column, int row, int codePoint, int style) { + if (!setChar(column, row, codePoint)) { + return false; + } + + row = externalToInternalRow(row); + mColor[row].set(column, style); + + return true; + } + + public boolean setChar(int column, int row, int codePoint) { + if (row >= mScreenRows || column >= mColumns) { + Log.e(TAG, "illegal arguments! " + row + " " + column + " " + mScreenRows + " " + mColumns); + throw new IllegalArgumentException(); + } + row = externalToInternalRow(row); + + /* + * Whether data contains non-BMP or characters with charWidth != 1 + * 0 - false; 1 - true; -1 - undetermined + */ + int basicMode = -1; + + // Allocate a row on demand + if (mLines[row] == null) { + if (isBasicChar(codePoint)) { + allocateBasicLine(row, mColumns); + basicMode = 1; + } else { + allocateFullLine(row, mColumns); + basicMode = 0; + } + } + + if (mLines[row] instanceof char[]) { + char[] line = (char[]) mLines[row]; + + if (basicMode == -1) { + if (isBasicChar(codePoint)) { + basicMode = 1; + } else { + basicMode = 0; + } + } + + if (basicMode == 1) { + // Fast path -- just put the char in the array + line[column] = (char) codePoint; + return true; + } + + // Need to switch to the full-featured mode + mLines[row] = new FullUnicodeLine(line); + } + + FullUnicodeLine line = (FullUnicodeLine) mLines[row]; + line.setChar(column, codePoint); + return true; + } +} + +/* + * A representation of a line that's capable of handling non-BMP characters, + * East Asian wide characters, and combining characters. + * + * The text of the line is stored in an array of char[], allowing easy + * conversion to a String and/or reuse by other string-handling functions. + * An array of short[] is used to keep track of the difference between a column + * and the starting index corresponding to its contents in the char[] array (so + * if column 42 starts at index 45 in the char[] array, the offset stored is 3). + * Column 0 always starts at index 0 in the char[] array, so we use that + * element of the array to keep track of how much of the char[] array we're + * using at the moment. + */ +class FullUnicodeLine { + private static final float SPARE_CAPACITY_FACTOR = 1.5f; + + private char[] mText; + private short[] mOffset; + private int mColumns; + + public FullUnicodeLine(int columns) { + commonConstructor(columns); + char[] text = mText; + // Fill in the line with blanks + for (int i = 0; i < columns; ++i) { + text[i] = ' '; + } + // Store the space used + mOffset[0] = (short) columns; + } + + public FullUnicodeLine(char[] basicLine) { + commonConstructor(basicLine.length); + System.arraycopy(basicLine, 0, mText, 0, mColumns); + // Store the space used + mOffset[0] = (short) basicLine.length; + } + + private void commonConstructor(int columns) { + mColumns = columns; + mOffset = new short[columns]; + mText = new char[(int)(SPARE_CAPACITY_FACTOR*columns)]; + } + + public int getSpaceUsed() { + return mOffset[0]; + } + + public char[] getLine() { + return mText; + } + + public int findStartOfColumn(int column) { + if (column == 0) { + return 0; + } else { + return column + mOffset[column]; + } + } + + public boolean getChar(int column, int charIndex, char[] out, int offset) { + int pos = findStartOfColumn(column); + int length; + if (column + 1 < mColumns) { + length = findStartOfColumn(column + 1) - pos; + } else { + length = getSpaceUsed() - pos; + } + if (charIndex >= length) { + throw new IllegalArgumentException(); + } + out[offset] = mText[pos + charIndex]; + return (charIndex + 1 < length); + } + + public void setChar(int column, int codePoint) { + int columns = mColumns; + if (column < 0 || column >= columns) { + throw new IllegalArgumentException(); + } + + char[] text = mText; + short[] offset = mOffset; + int spaceUsed = offset[0]; + + int pos = findStartOfColumn(column); + + int charWidth = UnicodeTranscript.charWidth(codePoint); + int oldCharWidth = UnicodeTranscript.charWidth(text, pos); + + if (charWidth == 2 && column == columns - 1) { + // A width 2 character doesn't fit in the last column. + codePoint = ' '; + charWidth = 1; + } + + boolean wasExtraColForWideChar = false; + if (oldCharWidth == 2 && column > 0) { + /* If the previous screen column starts at the same offset in the + * array as this one, this column must be the second column used + * by an East Asian wide character */ + wasExtraColForWideChar = (findStartOfColumn(column - 1) == pos); + } + + // Get the number of elements in the mText array this column uses now + int oldLen; + if (wasExtraColForWideChar && column + 1 < columns) { + oldLen = findStartOfColumn(column + 1) - pos; + } else if (column + oldCharWidth < columns) { + oldLen = findStartOfColumn(column+oldCharWidth) - pos; + } else { + oldLen = spaceUsed - pos; + } + + // Find how much space this column will need + int newLen = Character.charCount(codePoint); + if (charWidth == 0) { + /* Combining characters are added to the contents of the column + instead of overwriting them, so that they modify the existing + contents */ + newLen += oldLen; + } + int shift = newLen - oldLen; + + // Shift the rest of the line right to make room if necessary + if (shift > 0) { + if (spaceUsed + shift > text.length) { + // We need to grow the array + char[] newText = new char[text.length + columns]; + System.arraycopy(text, 0, newText, 0, pos); + System.arraycopy(text, pos + oldLen, newText, pos + newLen, spaceUsed - pos - oldLen); + mText = text = newText; + } else { + System.arraycopy(text, pos + oldLen, text, pos + newLen, spaceUsed - pos - oldLen); + } + } + + // Store the character + if (charWidth > 0) { + Character.toChars(codePoint, text, pos); + } else { + /* Store a combining character at the end of the existing contents, + so that it modifies them */ + Character.toChars(codePoint, text, pos + oldLen); + } + + // Shift the rest of the line left to eliminate gaps if necessary + if (shift < 0) { + System.arraycopy(text, pos + oldLen, text, pos + newLen, spaceUsed - pos - oldLen); + } + + // Update space used + if (shift != 0) { + spaceUsed += shift; + offset[0] = (short) spaceUsed; + } + + /* + * Handle cases where we need to pad with spaces to preserve column + * alignment + * + * width 2 -> width 1: pad with a space before or after the new + * character, depending on which of the two previously-occupied columns + * we wrote into + * + * inserting width 2 character into the second column of an existing + * width 2 character: pad with a space before the new character + */ + if (oldCharWidth == 2 && charWidth == 1 || wasExtraColForWideChar && charWidth == 2) { + int nextPos = pos + newLen; + char[] newText = text; + if (spaceUsed + 1 > text.length) { + // Array needs growing + newText = new char[text.length + columns]; + System.arraycopy(text, 0, newText, 0, wasExtraColForWideChar ? pos : nextPos); + } + + if (wasExtraColForWideChar) { + // Padding goes before the new character + System.arraycopy(text, pos, newText, pos + 1, spaceUsed - pos); + newText[pos] = ' '; + } else { + // Padding goes after the new character + System.arraycopy(text, nextPos, newText, nextPos + 1, spaceUsed - nextPos); + newText[nextPos] = ' '; + } + + if (newText != text) { + // Update mText to point to the newly grown array + mText = text = newText; + } + + // Update space used + spaceUsed = ++offset[0]; + + // Correct the offset for the just-modified column to reflect + // width change + if (wasExtraColForWideChar) { + ++offset[column]; + ++pos; + } else { + if (column == 0) { + offset[1] = (short) (newLen - 1); + } else if (column + 1 < columns) { + offset[column + 1] = (short) (offset[column] + newLen - 1); + } + ++column; + } + + ++shift; + } + + /* + * Handle cases where we need to clobber the contents of the next + * column in order to preserve column alignment + * + * width 1 -> width 2: should clobber the contents of the next + * column (if next column contains wide char, need to pad with a space) + * + * inserting width 2 character into the second column of an existing + * width 2 character: same + */ + if (oldCharWidth == 1 && charWidth == 2 || wasExtraColForWideChar && charWidth == 2) { + if (column == columns - 2) { + // Correct offset for the next column to reflect width change + offset[column + 1] = (short) (offset[column] - 1); + + // Truncate the line after this character. + offset[0] = (short) (pos + newLen); + shift = 0; + } else { + // Overwrite the contents of the next column. + int nextPos = pos + newLen; + int nextWidth = UnicodeTranscript.charWidth(text, nextPos); + int nextLen; + if (column + nextWidth + 1 < columns) { + nextLen = findStartOfColumn(column + nextWidth + 1) + shift - nextPos; + } else { + nextLen = spaceUsed - nextPos; + } + + if (nextWidth == 2) { + text[nextPos] = ' '; + // Shift the array to match + if (nextLen > 1) { + System.arraycopy(text, nextPos + nextLen, text, nextPos + 1, spaceUsed - nextPos - nextLen); + shift -= nextLen - 1; + offset[0] -= nextLen - 1; + } + } else { + // Shift the array leftwards + System.arraycopy(text, nextPos + nextLen, text, nextPos, spaceUsed - nextPos - nextLen); + shift -= nextLen; + + // Truncate the line + offset[0] -= nextLen; + } + + // Correct the offset for the next column to reflect width change + if (column == 0) { + offset[1] = -1; + } else { + offset[column + 1] = (short) (offset[column] - 1); + } + ++column; + } + } + + // Update offset table + if (shift != 0) { + for (int i = column + 1; i < columns; ++i) { + offset[i] += shift; + } + } + } +} + diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/UpdateCallback.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/UpdateCallback.java new file mode 100644 index 0000000..b2dbe21 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/UpdateCallback.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview; + +/** + * Generic callback to be invoked to notify of updates. + */ +public interface UpdateCallback { + /** + * Callback function to be invoked when an update happens. + */ + void onUpdate(); +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/AndroidCharacterCompat.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/AndroidCharacterCompat.java new file mode 100644 index 0000000..91762a6 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/AndroidCharacterCompat.java @@ -0,0 +1,29 @@ +package jackpal.androidterm.emulatorview.compat; + +import android.text.AndroidCharacter; + +/** + * Definitions related to android.text.AndroidCharacter + */ +public class AndroidCharacterCompat { + public static final int EAST_ASIAN_WIDTH_NEUTRAL = 0; + public static final int EAST_ASIAN_WIDTH_AMBIGUOUS = 1; + public static final int EAST_ASIAN_WIDTH_HALF_WIDTH = 2; + public static final int EAST_ASIAN_WIDTH_FULL_WIDTH = 3; + public static final int EAST_ASIAN_WIDTH_NARROW = 4; + public static final int EAST_ASIAN_WIDTH_WIDE = 5; + + private static class Api8OrLater { + public static int getEastAsianWidth(char c) { + return AndroidCharacter.getEastAsianWidth(c); + } + } + + public static int getEastAsianWidth(char c) { + if (AndroidCompat.SDK >= 8) { + return Api8OrLater.getEastAsianWidth(c); + } else { + return EAST_ASIAN_WIDTH_NARROW; + } + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/AndroidCompat.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/AndroidCompat.java new file mode 100644 index 0000000..9f05558 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/AndroidCompat.java @@ -0,0 +1,40 @@ +package jackpal.androidterm.emulatorview.compat; + +/** + * The classes in this package take advantage of the fact that the VM does + * not attempt to load a class until it's accessed, and the verifier + * does not run until a class is loaded. By keeping the methods which + * are unavailable on older platforms in subclasses which are only ever + * accessed on platforms where they are available, we can preserve + * compatibility with older platforms without resorting to reflection. + * + * See http://developer.android.com/resources/articles/backward-compatibility.html + * and http://android-developers.blogspot.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html + * for further discussion of this technique. + */ + +public class AndroidCompat { + public final static int SDK = getSDK(); + + private final static int getSDK() { + int result; + try { + result = AndroidLevel4PlusCompat.getSDKInt(); + } catch (VerifyError e) { + // We must be at an SDK level less than 4. + try { + result = Integer.valueOf(android.os.Build.VERSION.SDK); + } catch (NumberFormatException e2) { + // Couldn't parse string, assume the worst. + result = 1; + } + } + return result; + } +} + +class AndroidLevel4PlusCompat { + static int getSDKInt() { + return android.os.Build.VERSION.SDK_INT; + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompat.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompat.java new file mode 100644 index 0000000..d38cfd0 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompat.java @@ -0,0 +1,9 @@ +package jackpal.androidterm.emulatorview.compat; + +public interface ClipboardManagerCompat { + CharSequence getText(); + + boolean hasText(); + + void setText(CharSequence text); +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatFactory.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatFactory.java new file mode 100644 index 0000000..5eaf2a2 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatFactory.java @@ -0,0 +1,18 @@ +package jackpal.androidterm.emulatorview.compat; + +import android.content.Context; + +public class ClipboardManagerCompatFactory { + + private ClipboardManagerCompatFactory() { + /* singleton */ + } + + public static ClipboardManagerCompat getManager(Context context) { + if (AndroidCompat.SDK < 11) { + return new ClipboardManagerCompatV1(context); + } else { + return new ClipboardManagerCompatV11(context); + } + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatV1.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatV1.java new file mode 100644 index 0000000..672e5c2 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatV1.java @@ -0,0 +1,29 @@ +package jackpal.androidterm.emulatorview.compat; + +import android.content.Context; +import android.text.ClipboardManager; + +@SuppressWarnings("deprecation") +public class ClipboardManagerCompatV1 implements ClipboardManagerCompat { + private final ClipboardManager clip; + + public ClipboardManagerCompatV1(Context context) { + clip = (ClipboardManager) context.getApplicationContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + } + + @Override + public CharSequence getText() { + return clip.getText(); + } + + @Override + public boolean hasText() { + return clip.hasText(); + } + + @Override + public void setText(CharSequence text) { + clip.setText(text); + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatV11.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatV11.java new file mode 100644 index 0000000..dfdbd78 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/ClipboardManagerCompatV11.java @@ -0,0 +1,35 @@ +package jackpal.androidterm.emulatorview.compat; + +import android.annotation.SuppressLint; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; +import android.content.ClipboardManager; + +@SuppressLint("NewApi") +public class ClipboardManagerCompatV11 implements ClipboardManagerCompat { + private final ClipboardManager clip; + + public ClipboardManagerCompatV11(Context context) { + clip = (ClipboardManager) context.getApplicationContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + } + + @Override + public CharSequence getText() { + ClipData.Item item = clip.getPrimaryClip().getItemAt(0); + return item.getText(); + } + + @Override + public boolean hasText() { + return (clip.hasPrimaryClip() && clip.getPrimaryClipDescription() + .hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)); + } + + @Override + public void setText(CharSequence text) { + ClipData clipData = ClipData.newPlainText("simple text", text); + clip.setPrimaryClip(clipData); + } +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/KeyCharacterMapCompat.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/KeyCharacterMapCompat.java new file mode 100644 index 0000000..242f088 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/KeyCharacterMapCompat.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2011 Jack Palevich + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview.compat; + +import android.view.KeyCharacterMap; + +public abstract class KeyCharacterMapCompat { + public static final int MODIFIER_BEHAVIOR_CHORDED = 0; + public static final int MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED = 1; + + public static KeyCharacterMapCompat wrap(Object map) { + if (map != null) { + if (AndroidCompat.SDK >= 11) { + return new KeyCharacterMapApi11OrLater(map); + } + } + return null; + } + + private static class KeyCharacterMapApi11OrLater + extends KeyCharacterMapCompat { + private KeyCharacterMap mMap; + public KeyCharacterMapApi11OrLater(Object map) { + mMap = (KeyCharacterMap) map; + } + public int getModifierBehaviour() { + return mMap.getModifierBehavior(); + } + } + + public abstract int getModifierBehaviour(); +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/KeycodeConstants.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/KeycodeConstants.java new file mode 100644 index 0000000..d025561 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/KeycodeConstants.java @@ -0,0 +1,490 @@ +package jackpal.androidterm.emulatorview.compat; + +/** + * Keycode constants and modifier masks for use with keyboard event listeners. + * + * The Meta masks (ctrl, alt, shift, and meta) are used as follows: + * KeyEvent keyEvent = ...; + * boolean isCtrlPressed = (keyEvent.getMetaState() & META_CTRL_ON) != 0 + * + * Contains the complete set of Android key codes that were defined as of the 2.3 API. + * We could pull in the constants from the 2.3 API, but then we would need to raise the + * SDK minVersion in the manifest. We want to keep compatibility with Android 1.6, + * and raising this level could result in the accidental use of a newer API. + */ +public class KeycodeConstants { + + /** Key code constant: Unknown key code. */ + public static final int KEYCODE_UNKNOWN = 0; + /** Key code constant: Soft Left key. + * Usually situated below the display on phones and used as a multi-function + * feature key for selecting a software defined function shown on the bottom left + * of the display. */ + public static final int KEYCODE_SOFT_LEFT = 1; + /** Key code constant: Soft Right key. + * Usually situated below the display on phones and used as a multi-function + * feature key for selecting a software defined function shown on the bottom right + * of the display. */ + public static final int KEYCODE_SOFT_RIGHT = 2; + /** Key code constant: Home key. + * This key is handled by the framework and is never delivered to applications. */ + public static final int KEYCODE_HOME = 3; + /** Key code constant: Back key. */ + public static final int KEYCODE_BACK = 4; + /** Key code constant: Call key. */ + public static final int KEYCODE_CALL = 5; + /** Key code constant: End Call key. */ + public static final int KEYCODE_ENDCALL = 6; + /** Key code constant: '0' key. */ + public static final int KEYCODE_0 = 7; + /** Key code constant: '1' key. */ + public static final int KEYCODE_1 = 8; + /** Key code constant: '2' key. */ + public static final int KEYCODE_2 = 9; + /** Key code constant: '3' key. */ + public static final int KEYCODE_3 = 10; + /** Key code constant: '4' key. */ + public static final int KEYCODE_4 = 11; + /** Key code constant: '5' key. */ + public static final int KEYCODE_5 = 12; + /** Key code constant: '6' key. */ + public static final int KEYCODE_6 = 13; + /** Key code constant: '7' key. */ + public static final int KEYCODE_7 = 14; + /** Key code constant: '8' key. */ + public static final int KEYCODE_8 = 15; + /** Key code constant: '9' key. */ + public static final int KEYCODE_9 = 16; + /** Key code constant: '*' key. */ + public static final int KEYCODE_STAR = 17; + /** Key code constant: '#' key. */ + public static final int KEYCODE_POUND = 18; + /** Key code constant: Directional Pad Up key. + * May also be synthesized from trackball motions. */ + public static final int KEYCODE_DPAD_UP = 19; + /** Key code constant: Directional Pad Down key. + * May also be synthesized from trackball motions. */ + public static final int KEYCODE_DPAD_DOWN = 20; + /** Key code constant: Directional Pad Left key. + * May also be synthesized from trackball motions. */ + public static final int KEYCODE_DPAD_LEFT = 21; + /** Key code constant: Directional Pad Right key. + * May also be synthesized from trackball motions. */ + public static final int KEYCODE_DPAD_RIGHT = 22; + /** Key code constant: Directional Pad Center key. + * May also be synthesized from trackball motions. */ + public static final int KEYCODE_DPAD_CENTER = 23; + /** Key code constant: Volume Up key. + * Adjusts the speaker volume up. */ + public static final int KEYCODE_VOLUME_UP = 24; + /** Key code constant: Volume Down key. + * Adjusts the speaker volume down. */ + public static final int KEYCODE_VOLUME_DOWN = 25; + /** Key code constant: Power key. */ + public static final int KEYCODE_POWER = 26; + /** Key code constant: Camera key. + * Used to launch a camera application or take pictures. */ + public static final int KEYCODE_CAMERA = 27; + /** Key code constant: Clear key. */ + public static final int KEYCODE_CLEAR = 28; + /** Key code constant: 'A' key. */ + public static final int KEYCODE_A = 29; + /** Key code constant: 'B' key. */ + public static final int KEYCODE_B = 30; + /** Key code constant: 'C' key. */ + public static final int KEYCODE_C = 31; + /** Key code constant: 'D' key. */ + public static final int KEYCODE_D = 32; + /** Key code constant: 'E' key. */ + public static final int KEYCODE_E = 33; + /** Key code constant: 'F' key. */ + public static final int KEYCODE_F = 34; + /** Key code constant: 'G' key. */ + public static final int KEYCODE_G = 35; + /** Key code constant: 'H' key. */ + public static final int KEYCODE_H = 36; + /** Key code constant: 'I' key. */ + public static final int KEYCODE_I = 37; + /** Key code constant: 'J' key. */ + public static final int KEYCODE_J = 38; + /** Key code constant: 'K' key. */ + public static final int KEYCODE_K = 39; + /** Key code constant: 'L' key. */ + public static final int KEYCODE_L = 40; + /** Key code constant: 'M' key. */ + public static final int KEYCODE_M = 41; + /** Key code constant: 'N' key. */ + public static final int KEYCODE_N = 42; + /** Key code constant: 'O' key. */ + public static final int KEYCODE_O = 43; + /** Key code constant: 'P' key. */ + public static final int KEYCODE_P = 44; + /** Key code constant: 'Q' key. */ + public static final int KEYCODE_Q = 45; + /** Key code constant: 'R' key. */ + public static final int KEYCODE_R = 46; + /** Key code constant: 'S' key. */ + public static final int KEYCODE_S = 47; + /** Key code constant: 'T' key. */ + public static final int KEYCODE_T = 48; + /** Key code constant: 'U' key. */ + public static final int KEYCODE_U = 49; + /** Key code constant: 'V' key. */ + public static final int KEYCODE_V = 50; + /** Key code constant: 'W' key. */ + public static final int KEYCODE_W = 51; + /** Key code constant: 'X' key. */ + public static final int KEYCODE_X = 52; + /** Key code constant: 'Y' key. */ + public static final int KEYCODE_Y = 53; + /** Key code constant: 'Z' key. */ + public static final int KEYCODE_Z = 54; + /** Key code constant: ',' key. */ + public static final int KEYCODE_COMMA = 55; + /** Key code constant: '.' key. */ + public static final int KEYCODE_PERIOD = 56; + /** Key code constant: Left Alt modifier key. */ + public static final int KEYCODE_ALT_LEFT = 57; + /** Key code constant: Right Alt modifier key. */ + public static final int KEYCODE_ALT_RIGHT = 58; + /** Key code constant: Left Shift modifier key. */ + public static final int KEYCODE_SHIFT_LEFT = 59; + /** Key code constant: Right Shift modifier key. */ + public static final int KEYCODE_SHIFT_RIGHT = 60; + /** Key code constant: Tab key. */ + public static final int KEYCODE_TAB = 61; + /** Key code constant: Space key. */ + public static final int KEYCODE_SPACE = 62; + /** Key code constant: Symbol modifier key. + * Used to enter alternate symbols. */ + public static final int KEYCODE_SYM = 63; + /** Key code constant: Explorer special function key. + * Used to launch a browser application. */ + public static final int KEYCODE_EXPLORER = 64; + /** Key code constant: Envelope special function key. + * Used to launch a mail application. */ + public static final int KEYCODE_ENVELOPE = 65; + /** Key code constant: Enter key. */ + public static final int KEYCODE_ENTER = 66; + /** Key code constant: Backspace key. + * Deletes characters before the insertion point, unlike {@link #KEYCODE_FORWARD_DEL}. */ + public static final int KEYCODE_DEL = 67; + /** Key code constant: '`' (backtick) key. */ + public static final int KEYCODE_GRAVE = 68; + /** Key code constant: '-'. */ + public static final int KEYCODE_MINUS = 69; + /** Key code constant: '=' key. */ + public static final int KEYCODE_EQUALS = 70; + /** Key code constant: '[' key. */ + public static final int KEYCODE_LEFT_BRACKET = 71; + /** Key code constant: ']' key. */ + public static final int KEYCODE_RIGHT_BRACKET = 72; + /** Key code constant: '\' key. */ + public static final int KEYCODE_BACKSLASH = 73; + /** Key code constant: ';' key. */ + public static final int KEYCODE_SEMICOLON = 74; + /** Key code constant: ''' (apostrophe) key. */ + public static final int KEYCODE_APOSTROPHE = 75; + /** Key code constant: '/' key. */ + public static final int KEYCODE_SLASH = 76; + /** Key code constant: '@' key. */ + public static final int KEYCODE_AT = 77; + /** Key code constant: Number modifier key. + * Used to enter numeric symbols. + * This key is not Num Lock; it is more like {@link #KEYCODE_ALT_LEFT} and is + * interpreted as an ALT key by {@link android.text.method.MetaKeyKeyListener}. */ + public static final int KEYCODE_NUM = 78; + /** Key code constant: Headset Hook key. + * Used to hang up calls and stop media. */ + public static final int KEYCODE_HEADSETHOOK = 79; + /** Key code constant: Camera Focus key. + * Used to focus the camera. */ + public static final int KEYCODE_FOCUS = 80; // *Camera* focus + /** Key code constant: '+' key. */ + public static final int KEYCODE_PLUS = 81; + /** Key code constant: Menu key. */ + public static final int KEYCODE_MENU = 82; + /** Key code constant: Notification key. */ + public static final int KEYCODE_NOTIFICATION = 83; + /** Key code constant: Search key. */ + public static final int KEYCODE_SEARCH = 84; + /** Key code constant: Play/Pause media key. */ + public static final int KEYCODE_MEDIA_PLAY_PAUSE= 85; + /** Key code constant: Stop media key. */ + public static final int KEYCODE_MEDIA_STOP = 86; + /** Key code constant: Play Next media key. */ + public static final int KEYCODE_MEDIA_NEXT = 87; + /** Key code constant: Play Previous media key. */ + public static final int KEYCODE_MEDIA_PREVIOUS = 88; + /** Key code constant: Rewind media key. */ + public static final int KEYCODE_MEDIA_REWIND = 89; + /** Key code constant: Fast Forward media key. */ + public static final int KEYCODE_MEDIA_FAST_FORWARD = 90; + /** Key code constant: Mute key. + * Mutes the microphone, unlike {@link #KEYCODE_VOLUME_MUTE}. */ + public static final int KEYCODE_MUTE = 91; + /** Key code constant: Page Up key. */ + public static final int KEYCODE_PAGE_UP = 92; + /** Key code constant: Page Down key. */ + public static final int KEYCODE_PAGE_DOWN = 93; + /** Key code constant: Picture Symbols modifier key. + * Used to switch symbol sets (Emoji, Kao-moji). */ + public static final int KEYCODE_PICTSYMBOLS = 94; // switch symbol-sets (Emoji,Kao-moji) + /** Key code constant: Switch Charset modifier key. + * Used to switch character sets (Kanji, Katakana). */ + public static final int KEYCODE_SWITCH_CHARSET = 95; // switch char-sets (Kanji,Katakana) + /** Key code constant: A Button key. + * On a game controller, the A button should be either the button labeled A + * or the first button on the upper row of controller buttons. */ + public static final int KEYCODE_BUTTON_A = 96; + /** Key code constant: B Button key. + * On a game controller, the B button should be either the button labeled B + * or the second button on the upper row of controller buttons. */ + public static final int KEYCODE_BUTTON_B = 97; + /** Key code constant: C Button key. + * On a game controller, the C button should be either the button labeled C + * or the third button on the upper row of controller buttons. */ + public static final int KEYCODE_BUTTON_C = 98; + /** Key code constant: X Button key. + * On a game controller, the X button should be either the button labeled X + * or the first button on the lower row of controller buttons. */ + public static final int KEYCODE_BUTTON_X = 99; + /** Key code constant: Y Button key. + * On a game controller, the Y button should be either the button labeled Y + * or the second button on the lower row of controller buttons. */ + public static final int KEYCODE_BUTTON_Y = 100; + /** Key code constant: Z Button key. + * On a game controller, the Z button should be either the button labeled Z + * or the third button on the lower row of controller buttons. */ + public static final int KEYCODE_BUTTON_Z = 101; + /** Key code constant: L1 Button key. + * On a game controller, the L1 button should be either the button labeled L1 (or L) + * or the top left trigger button. */ + public static final int KEYCODE_BUTTON_L1 = 102; + /** Key code constant: R1 Button key. + * On a game controller, the R1 button should be either the button labeled R1 (or R) + * or the top right trigger button. */ + public static final int KEYCODE_BUTTON_R1 = 103; + /** Key code constant: L2 Button key. + * On a game controller, the L2 button should be either the button labeled L2 + * or the bottom left trigger button. */ + public static final int KEYCODE_BUTTON_L2 = 104; + /** Key code constant: R2 Button key. + * On a game controller, the R2 button should be either the button labeled R2 + * or the bottom right trigger button. */ + public static final int KEYCODE_BUTTON_R2 = 105; + /** Key code constant: Left Thumb Button key. + * On a game controller, the left thumb button indicates that the left (or only) + * joystick is pressed. */ + public static final int KEYCODE_BUTTON_THUMBL = 106; + /** Key code constant: Right Thumb Button key. + * On a game controller, the right thumb button indicates that the right + * joystick is pressed. */ + public static final int KEYCODE_BUTTON_THUMBR = 107; + /** Key code constant: Start Button key. + * On a game controller, the button labeled Start. */ + public static final int KEYCODE_BUTTON_START = 108; + /** Key code constant: Select Button key. + * On a game controller, the button labeled Select. */ + public static final int KEYCODE_BUTTON_SELECT = 109; + /** Key code constant: Mode Button key. + * On a game controller, the button labeled Mode. */ + public static final int KEYCODE_BUTTON_MODE = 110; + /** Key code constant: Escape key. */ + public static final int KEYCODE_ESCAPE = 111; + /** Key code constant: Forward Delete key. + * Deletes characters ahead of the insertion point, unlike {@link #KEYCODE_DEL}. */ + public static final int KEYCODE_FORWARD_DEL = 112; + /** Key code constant: Left Control modifier key. */ + public static final int KEYCODE_CTRL_LEFT = 113; + /** Key code constant: Right Control modifier key. */ + public static final int KEYCODE_CTRL_RIGHT = 114; + /** Key code constant: Caps Lock modifier key. */ + public static final int KEYCODE_CAPS_LOCK = 115; + /** Key code constant: Scroll Lock key. */ + public static final int KEYCODE_SCROLL_LOCK = 116; + /** Key code constant: Left Meta modifier key. */ + public static final int KEYCODE_META_LEFT = 117; + /** Key code constant: Right Meta modifier key. */ + public static final int KEYCODE_META_RIGHT = 118; + /** Key code constant: Function modifier key. */ + public static final int KEYCODE_FUNCTION = 119; + /** Key code constant: System Request / Print Screen key. */ + public static final int KEYCODE_SYSRQ = 120; + /** Key code constant: Break / Pause key. */ + public static final int KEYCODE_BREAK = 121; + /** Key code constant: Home Movement key. + * Used for scrolling or moving the cursor around to the start of a line + * or to the top of a list. */ + public static final int KEYCODE_MOVE_HOME = 122; + /** Key code constant: End Movement key. + * Used for scrolling or moving the cursor around to the end of a line + * or to the bottom of a list. */ + public static final int KEYCODE_MOVE_END = 123; + /** Key code constant: Insert key. + * Toggles insert / overwrite edit mode. */ + public static final int KEYCODE_INSERT = 124; + /** Key code constant: Forward key. + * Navigates forward in the history stack. Complement of {@link #KEYCODE_BACK}. */ + public static final int KEYCODE_FORWARD = 125; + /** Key code constant: Play media key. */ + public static final int KEYCODE_MEDIA_PLAY = 126; + /** Key code constant: Pause media key. */ + public static final int KEYCODE_MEDIA_PAUSE = 127; + /** Key code constant: Close media key. + * May be used to close a CD tray, for example. */ + public static final int KEYCODE_MEDIA_CLOSE = 128; + /** Key code constant: Eject media key. + * May be used to eject a CD tray, for example. */ + public static final int KEYCODE_MEDIA_EJECT = 129; + /** Key code constant: Record media key. */ + public static final int KEYCODE_MEDIA_RECORD = 130; + /** Key code constant: F1 key. */ + public static final int KEYCODE_F1 = 131; + /** Key code constant: F2 key. */ + public static final int KEYCODE_F2 = 132; + /** Key code constant: F3 key. */ + public static final int KEYCODE_F3 = 133; + /** Key code constant: F4 key. */ + public static final int KEYCODE_F4 = 134; + /** Key code constant: F5 key. */ + public static final int KEYCODE_F5 = 135; + /** Key code constant: F6 key. */ + public static final int KEYCODE_F6 = 136; + /** Key code constant: F7 key. */ + public static final int KEYCODE_F7 = 137; + /** Key code constant: F8 key. */ + public static final int KEYCODE_F8 = 138; + /** Key code constant: F9 key. */ + public static final int KEYCODE_F9 = 139; + /** Key code constant: F10 key. */ + public static final int KEYCODE_F10 = 140; + /** Key code constant: F11 key. */ + public static final int KEYCODE_F11 = 141; + /** Key code constant: F12 key. */ + public static final int KEYCODE_F12 = 142; + /** Key code constant: Num Lock modifier key. + * This is the Num Lock key; it is different from {@link #KEYCODE_NUM}. + * This key generally modifies the behavior of other keys on the numeric keypad. */ + public static final int KEYCODE_NUM_LOCK = 143; + /** Key code constant: Numeric keypad '0' key. */ + public static final int KEYCODE_NUMPAD_0 = 144; + /** Key code constant: Numeric keypad '1' key. */ + public static final int KEYCODE_NUMPAD_1 = 145; + /** Key code constant: Numeric keypad '2' key. */ + public static final int KEYCODE_NUMPAD_2 = 146; + /** Key code constant: Numeric keypad '3' key. */ + public static final int KEYCODE_NUMPAD_3 = 147; + /** Key code constant: Numeric keypad '4' key. */ + public static final int KEYCODE_NUMPAD_4 = 148; + /** Key code constant: Numeric keypad '5' key. */ + public static final int KEYCODE_NUMPAD_5 = 149; + /** Key code constant: Numeric keypad '6' key. */ + public static final int KEYCODE_NUMPAD_6 = 150; + /** Key code constant: Numeric keypad '7' key. */ + public static final int KEYCODE_NUMPAD_7 = 151; + /** Key code constant: Numeric keypad '8' key. */ + public static final int KEYCODE_NUMPAD_8 = 152; + /** Key code constant: Numeric keypad '9' key. */ + public static final int KEYCODE_NUMPAD_9 = 153; + /** Key code constant: Numeric keypad '/' key (for division). */ + public static final int KEYCODE_NUMPAD_DIVIDE = 154; + /** Key code constant: Numeric keypad '*' key (for multiplication). */ + public static final int KEYCODE_NUMPAD_MULTIPLY = 155; + /** Key code constant: Numeric keypad '-' key (for subtraction). */ + public static final int KEYCODE_NUMPAD_SUBTRACT = 156; + /** Key code constant: Numeric keypad '+' key (for addition). */ + public static final int KEYCODE_NUMPAD_ADD = 157; + /** Key code constant: Numeric keypad '.' key (for decimals or digit grouping). */ + public static final int KEYCODE_NUMPAD_DOT = 158; + /** Key code constant: Numeric keypad ',' key (for decimals or digit grouping). */ + public static final int KEYCODE_NUMPAD_COMMA = 159; + /** Key code constant: Numeric keypad Enter key. */ + public static final int KEYCODE_NUMPAD_ENTER = 160; + /** Key code constant: Numeric keypad '=' key. */ + public static final int KEYCODE_NUMPAD_EQUALS = 161; + /** Key code constant: Numeric keypad '(' key. */ + public static final int KEYCODE_NUMPAD_LEFT_PAREN = 162; + /** Key code constant: Numeric keypad ')' key. */ + public static final int KEYCODE_NUMPAD_RIGHT_PAREN = 163; + /** Key code constant: Volume Mute key. + * Mutes the speaker, unlike {@link #KEYCODE_MUTE}. + * This key should normally be implemented as a toggle such that the first press + * mutes the speaker and the second press restores the original volume. */ + public static final int KEYCODE_VOLUME_MUTE = 164; + /** Key code constant: Info key. + * Common on TV remotes to show additional information related to what is + * currently being viewed. */ + public static final int KEYCODE_INFO = 165; + /** Key code constant: Channel up key. + * On TV remotes, increments the television channel. */ + public static final int KEYCODE_CHANNEL_UP = 166; + /** Key code constant: Channel down key. + * On TV remotes, decrements the television channel. */ + public static final int KEYCODE_CHANNEL_DOWN = 167; + /** Key code constant: Zoom in key. */ + public static final int KEYCODE_ZOOM_IN = 168; + /** Key code constant: Zoom out key. */ + public static final int KEYCODE_ZOOM_OUT = 169; + /** Key code constant: TV key. + * On TV remotes, switches to viewing live TV. */ + public static final int KEYCODE_TV = 170; + /** Key code constant: Window key. + * On TV remotes, toggles picture-in-picture mode or other windowing functions. */ + public static final int KEYCODE_WINDOW = 171; + /** Key code constant: Guide key. + * On TV remotes, shows a programming guide. */ + public static final int KEYCODE_GUIDE = 172; + /** Key code constant: DVR key. + * On some TV remotes, switches to a DVR mode for recorded shows. */ + public static final int KEYCODE_DVR = 173; + /** Key code constant: Bookmark key. + * On some TV remotes, bookmarks content or web pages. */ + public static final int KEYCODE_BOOKMARK = 174; + /** Key code constant: Toggle captions key. + * Switches the mode for closed-captioning text, for example during television shows. */ + public static final int KEYCODE_CAPTIONS = 175; + /** Key code constant: Settings key. + * Starts the system settings activity. */ + public static final int KEYCODE_SETTINGS = 176; + /** Key code constant: TV power key. + * On TV remotes, toggles the power on a television screen. */ + public static final int KEYCODE_TV_POWER = 177; + /** Key code constant: TV input key. + * On TV remotes, switches the input on a television screen. */ + public static final int KEYCODE_TV_INPUT = 178; + /** Key code constant: Set-top-box power key. + * On TV remotes, toggles the power on an external Set-top-box. */ + public static final int KEYCODE_STB_POWER = 179; + /** Key code constant: Set-top-box input key. + * On TV remotes, switches the input mode on an external Set-top-box. */ + public static final int KEYCODE_STB_INPUT = 180; + /** Key code constant: A/V Receiver power key. + * On TV remotes, toggles the power on an external A/V Receiver. */ + public static final int KEYCODE_AVR_POWER = 181; + /** Key code constant: A/V Receiver input key. + * On TV remotes, switches the input mode on an external A/V Receiver. */ + public static final int KEYCODE_AVR_INPUT = 182; + /** Key code constant: Red "programmable" key. + * On TV remotes, acts as a contextual/programmable key. */ + public static final int KEYCODE_PROG_RED = 183; + /** Key code constant: Green "programmable" key. + * On TV remotes, actsas a contextual/programmable key. */ + public static final int KEYCODE_PROG_GREEN = 184; + /** Key code constant: Yellow "programmable" key. + * On TV remotes, acts as a contextual/programmable key. */ + public static final int KEYCODE_PROG_YELLOW = 185; + /** Key code constant: Blue "programmable" key. + * On TV remotes, acts as a contextual/programmable key. */ + public static final int KEYCODE_PROG_BLUE = 186; + + public static final int LAST_KEYCODE = KEYCODE_PROG_BLUE; + + public static final int META_ALT_ON = 2; + public static final int META_CAPS_LOCK_ON = 0x00100000; + public static final int META_CTRL_ON = 0x1000; + public static final int META_SHIFT_ON = 1; + public static final int META_CTRL_MASK = 0x7000; + public static final int META_META_ON = 0x00010000; + public static final int META_META_MASK = 0x00070000; +} diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/Patterns.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/Patterns.java new file mode 100644 index 0000000..f485839 --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/compat/Patterns.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 Jack Palevich + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package jackpal.androidterm.emulatorview.compat; + +/** + * Based upon + * + * https://android.googlesource.com/platform/frameworks/base/+/android-4.4.2_r2/core/java/android/util/Patterns.java + * + */ + +import java.util.regex.Pattern; + +public class Patterns { + /** + * Regular expression to match all IANA top-level domains for WEB_URL. + * List accurate as of 2011/07/18. List taken from: + * http://data.iana.org/TLD/tlds-alpha-by-domain.txt + * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py + */ + public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = + "(?:" + + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])" + + "|(?:biz|b[abdefghijmnorstvwyz])" + + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])" + + "|d[ejkmoz]" + + "|(?:edu|e[cegrstu])" + + "|f[ijkmor]" + + "|(?:gov|g[abdefghilmnpqrstuwy])" + + "|h[kmnrtu]" + + "|(?:info|int|i[delmnoqrst])" + + "|(?:jobs|j[emop])" + + "|k[eghimnprwyz]" + + "|l[abcikrstuvy]" + + "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])" + + "|(?:name|net|n[acefgilopruz])" + + "|(?:org|om)" + + "|(?:pro|p[aefghklmnrstwy])" + + "|qa" + + "|r[eosuw]" + + "|s[abcdeghijklmnortuvyz]" + + "|(?:tel|travel|t[cdfghjklmnoprtvwz])" + + "|u[agksyz]" + + "|v[aceginu]" + + "|w[fs]" + + "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)" + + "|y[et]" + + "|z[amw]))"; + /** + * Good characters for Internationalized Resource Identifiers (IRI). + * This comprises most common used Unicode characters allowed in IRI + * as detailed in RFC 3987. + * Specifically, those two byte Unicode characters are not included. + */ + public static final String GOOD_IRI_CHAR = + "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; + /** + * Regular expression pattern to match most part of RFC 3987 + * Internationalized URLs, aka IRIs. Commonly used Unicode characters are + * added. + */ + public static final Pattern WEB_URL = Pattern.compile( + "((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" + + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" + + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" + + "((?:(?:[" + GOOD_IRI_CHAR + "][" + GOOD_IRI_CHAR + "\\-]{0,64}\\.)+" // named host + + TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL + + "|(?:(?:25[0-5]|2[0-4]" // or ip address + + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]" + + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]" + + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + + "|[1-9][0-9]|[0-9])))" + + "(?:\\:\\d{1,5})?)" // plus option port number + + "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params + + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" + + "(?:\\b|$)"); // and finally, a word boundary or end of + // input. This is to stop foo.sure from + // matching as foo.su +} \ No newline at end of file diff --git a/emulatorview/src/main/java/jackpal/androidterm/emulatorview/package.html b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/package.html new file mode 100644 index 0000000..559ee7c --- /dev/null +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/package.html @@ -0,0 +1,12 @@ + + +

This package provides a fairly complete VT100 terminal emulator {@link +jackpal.androidterm.emulatorview.TermSession TermSession} and a corresponding +Android view {@link jackpal.androidterm.emulatorview.EmulatorView EmulatorView}. + +

Most users will create a TermSession, connect it to an {@link +java.io.InputStream InputStream} and {@link java.io.OutputStream OutputStream} +from the emulation client, then instantiate the EmulatorView and +add it to an activity's layout. + + diff --git a/emulatorview/src/main/res/drawable-nodpi/atari_small_nodpi.png b/emulatorview/src/main/res/drawable-nodpi/atari_small_nodpi.png new file mode 100644 index 0000000000000000000000000000000000000000..8bdd6244568e8216d722632efd861c82e91c1fd0 GIT binary patch literal 974 zcmV;<12O!GP)ToIUF5Ia9x9TuWugbzE(iwXRbkY4MEOXJwBYjMv_}oa#p`r z1+UNX{f9BVVaheXI%eEC&GC$EA47!QV?NmPME`PhT;y>P2~U@jb&K70$PZ;nIQ_NQ zMdPrI6S)5T`b14(+@}Ii1)T?uYISx&tp-yI?u7zq&nO-SbymR!)j0BF>A2)KKcA7c z(r3pj)r+N`3V%mLT09IoFRW0QDSA1|JG432KUAEZy0eYZKVj_R2Qw`iINGS&_o*2z zLN4@xR`mQ&M^|bN7cVH{4uJe?$B6I%f+vhE70gPLTbl0b09*t>#OAoWD^{ZUMZ6@K z9~Y#J&`atWE~9Mm7|RcZa1mS!Ob1*Gt>sH$>2 zq(NDL@wDtufp~^0fidS=Ku^8LohU{#M`sj;k3tYu!op*ag9sgW-O=ujNwrca6^kjV zMB0MZ{+?1>HvYA^}K^w;sSuKNwH8g)8#X&2^K%28bb6c$N>nP zo6ftO<}fvF3U$N1Nd9#`v36PadxU2+f25#ewEEq;A>|Id1vV;J`!e=jDf&dB7D+p; ztNZ~(h%J?wdO)%3yQ{XS^rkM;7jXHOUXW1MUM3bXxb}1mLEG6AF4a1)A?|f6u1EhH zxjbGDr(rE}N>}kV(%M<{sZi94raX7v0bfckNsuFNE!CB?LJfNnMFH5%Dqv zn{GvAMTSXXu`alfXZO6t38FUV!S1O9Ko}K)D_uQNVbUA4qxO=|7L0(GA$79dn?4a0 z=-tLx>_k$c7b=%6g_t1Hr5|0vw(ca_g&2J^?)dr+DEfXS){B(8-dZU3OSaSj&TDCZ wtYy%*)B({;qTMyu;KzM83cGl=ToIUF5Ia9x9TuWugbzE(iwXRbkY4MEOXJwBYjMv_}oa#p`r z1+UNX{f9BVVaheXI%eEC&GC$EA47!QV?NmPME`PhT;y>P2~U@jb&K70$PZ;nIQ_NQ zMdPrI6S)5T`b14(+@}Ii1)T?uYISx&tp-yI?u7zq&nO-SbymR!)j0BF>A2)KKcA7c z(r3pj)r+N`3V%mLT09IoFRW0QDSA1|JG432KUAEZy0eYZKVj_R2Qw`iINGS&_o*2z zLN4@xR`mQ&M^|bN7cVH{4uJe?$B6I%f+vhE70gPLTbl0b09*t>#OAoWD^{ZUMZ6@K z9~Y#J&`atWE~9Mm7|RcZa1mS!Ob1*Gt>sH$>2 zq(NDL@wDtufp~^0fidS=Ku^8LohU{#M`sj;k3tYu!op*ag9sgW-O=ujNwrca6^kjV zMB0MZ{+?1>HvYA^}K^w;sSuKNwH8g)8#X&2^K%28bb6c$N>nP zo6ftO<}fvF3U$N1Nd9#`v36PadxU2+f25#ewEEq;A>|Id1vV;J`!e=jDf&dB7D+p; ztNZ~(h%J?wdO)%3yQ{XS^rkM;7jXHOUXW1MUM3bXxb}1mLEG6AF4a1)A?|f6u1EhH zxjbGDr(rE}N>}kV(%M<{sZi94raX7v0bfckNsuFNE!CB?LJfNnMFH5%Dqv zn{GvAMTSXXu`alfXZO6t38FUV!S1O9Ko}K)D_uQNVbUA4qxO=|7L0(GA$79dn?4a0 z=-tLx>_k$c7b=%6g_t1Hr5|0vw(ca_g&2J^?)dr+DEfXS){B(8-dZU3OSaSj&TDCZ wtYy%*)B({;qTMyu;KzM83cGl=1o_ul)D>ebz~ zs=Mmxr&>W81QY-S1PKWQ%N-;H^tS;2*XwVA`dej1RRn1z<;3VgfE4~kaG`A%QSPsR z#ovnZe+tS9%1MfeDyz`RirvdjPRK~p(#^q2(^5@O&NM19EHdvN-A&StN>0g6QA^VN z0Gx%Gq#PD$QMRFzmK+utjS^Y1F0e8&u&^=w5K<;4Rz|i3A=o|IKLY+g`iK6vfr9?+ z-`>gmU&i?FGSL5&F?TXFu`&Js6h;15QFkXp2M1H9|Eq~bpov-GU(uz%mH0n55wUl- zv#~ccAz`F5wlQ>e_KlJS3@{)B?^v*EQM=IxLa&76^y51a((wq|2-`qON>+4dLc{Oo z51}}o^Zen(oAjxDK7b++9_Yg`67p$bPo3~BCpGM7uAWmvIhWc5Gi+gQZ|Pwa-Gll@<1xmcPy z|NZmu6m)g5Ftu~BG&Xdxclw7Cij{xbBMBn-LMII#Slp`AElb&2^Hw+w>(3crLH!;I zN+Vk$D+wP1#^!MDCiad@vM>H#6+`Ct#~6VHL4lzmy;lSdk>`z6)=>Wh15Q2)dQtGqvn0vJU@+(B5{MUc*qs4!T+V=q=wy)<6$~ z!G>e_4dN@lGeF_$q9`Ju6Ncb*x?O7=l{anm7Eahuj_6lA{*#Gv*TaJclevPVbbVYu z(NY?5q+xxbO6%g1xF0r@Ix8fJ~u)VRUp`S%&rN$&e!Od`~s+64J z5*)*WSi*i{k%JjMSIN#X;jC{HG$-^iX+5f5BGOIHWAl*%15Z#!xntpk($-EGKCzKa zT7{siZ9;4TICsWQ$pu&wKZQTCvpI$Xvzwxoi+XkkpeE&&kFb!B?h2hi%^YlXt|-@5 zHJ~%AN!g_^tmn1?HSm^|gCE#!GRtK2(L{9pL#hp0xh zME}|DB>(5)`iE7CM)&_+S}-Bslc#@B5W4_+k4Cp$l>iVyg$KP>CN?SVGZ(&02>iZK zB<^HP$g$Lq*L$BWd?2(F?-MUbNWTJVQdW7$#8a|k_30#vHAD1Z{c#p;bETk0VnU5A zBgLe2HFJ3032$G<`m*OB!KM$*sdM20jm)It5OSru@tXpK5LT>#8)N!*skNu1$TpIw zufjjdp#lyH5bZ%|Iuo|iu9vG1HrIVWLH>278xo>aVBkPN3V$~!=KnlXQ4eDqS7%E% zQ!z^$Q$b^6Q)g#cLpwur(|<0gWHo6A6jc;n`t(V9T;LzTAU{IAu*uEQ%Ort1k+Kn+f_N`9|bxYC+~Z1 zCC1UCWv*Orx$_@ydv9mIe(liLfOr7mhbV@tKw{6)q^1DH1nmvZ0cj215R<~&I<4S| zgnr;9Cdjqpz#o8i0CQjtl`}{c*P)aSdH|abxGdrR)-3z+02-eX(k*B)Uqv6~^nh** z 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/libtermexec/.gitignore b/libtermexec/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/libtermexec/.gitignore @@ -0,0 +1 @@ +/build diff --git a/libtermexec/build.gradle b/libtermexec/build.gradle new file mode 100644 index 0000000..55d9f65 --- /dev/null +++ b/libtermexec/build.gradle @@ -0,0 +1,59 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0' + } +} +apply plugin: 'com.android.library' + +repositories { + jcenter() +} + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + minSdkVersion 4 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + + ndk { + moduleName 'libjackpal-termexec2' + abiFilters 'all' + ldLibs 'log', 'c' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +// by default recent plugin version does not copy any AIDL files "to avoid publishing too much" +android.libraryVariants.all { variant -> + Sync packageAidl = project.tasks.create("addPublic${variant.name.capitalize()}Aidl", Sync) { sync -> + from "$project.projectDir/src/main/aidl/" + into "$buildDir/intermediates/bundles/${variant.dirName}/aidl/" + } + + variant.javaCompile.dependsOn packageAidl +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + + compile 'com.android.support:support-annotations:21.0.0' +} diff --git a/libtermexec/proguard-rules.pro b/libtermexec/proguard-rules.pro new file mode 100644 index 0000000..cfa640e --- /dev/null +++ b/libtermexec/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/uniqa/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/libtermexec/src/androidTest/java/jackpal/androidterm/libtermexec/ApplicationTest.java b/libtermexec/src/androidTest/java/jackpal/androidterm/libtermexec/ApplicationTest.java new file mode 100644 index 0000000..ee475d0 --- /dev/null +++ b/libtermexec/src/androidTest/java/jackpal/androidterm/libtermexec/ApplicationTest.java @@ -0,0 +1,13 @@ +package jackpal.androidterm.libtermexec; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} diff --git a/libtermexec/src/main/AndroidManifest.xml b/libtermexec/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f106d2e --- /dev/null +++ b/libtermexec/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/libtermexec/src/main/aidl/jackpal/androidterm/libtermexec/v1/ITerminal.aidl b/libtermexec/src/main/aidl/jackpal/androidterm/libtermexec/v1/ITerminal.aidl new file mode 100644 index 0000000..092738b --- /dev/null +++ b/libtermexec/src/main/aidl/jackpal/androidterm/libtermexec/v1/ITerminal.aidl @@ -0,0 +1,39 @@ +package jackpal.androidterm.libtermexec.v1; + +import android.content.IntentSender; +import android.os.ParcelFileDescriptor; +import android.os.ResultReceiver; + +// see also: +// the (clumsy) way to handle object inheritance with Binder: +// https://kevinhartman.github.io/blog/2012/07/23/inheritance-through-ipc-using-aidl-in-android/ +// some (possibly outdated) notes on preserving backward compatibility: +// https://stackoverflow.com/questions/18197783/android-aidl-interface-parcelables-and-backwards-compatibility +/** + * An interface for interacting with Terminal implementation. + * + * The version of the interface is encoded in Intent action and the AIDL package name. New versions + * of this interface may be implemented in future. Those versions will be made available + * in separate packages and older versions will continue to work. + */ +interface ITerminal { + /** + * Start a new Terminal session. A session will remain hosted by service, that provides binding, + * but no gurantees of process pesistence as well as stability of connection are made. You + * should keep your ParcelFileDescriptor around and allow ServiceConnection to call this method + * again, when reconnection happens, in case service hosting the session is killed by system. + * + * Allows caller to be notified of terminal session events. Multiple calls can happen on each, + * and new call types can be introduced, so prepare to ignore unknown event codes. + * + * So far only notifications about session end (code 0) are supported. This notification is + * issued after abovementioned file descriptor is closed and the session is ended from + * Terminal's standpoint. + * + * @param pseudoTerminalMultiplexerFd file descriptor, obtained by opening /dev/ptmx. + * @param a callback + * + * @return IntentSender, that can be used to start corresponding Terminal Activity. + */ + IntentSender startSession(in ParcelFileDescriptor pseudoTerminalMultiplexerFd, in ResultReceiver callback); +} diff --git a/libtermexec/src/main/java/jackpal/androidterm/TermExec.java b/libtermexec/src/main/java/jackpal/androidterm/TermExec.java new file mode 100644 index 0000000..a3e9072 --- /dev/null +++ b/libtermexec/src/main/java/jackpal/androidterm/TermExec.java @@ -0,0 +1,132 @@ +package jackpal.androidterm; + +import android.annotation.TargetApi; +import android.os.*; +import android.support.annotation.NonNull; +import java.io.FileDescriptor; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.*; + +/** + * Utility methods for creating and managing a subprocess. This class differs from + * {@link java.lang.ProcessBuilder} in that a pty is used to communicate with the subprocess. + *

+ * Pseudo-terminals are a powerful Unix feature, that allows programs to interact with other programs + * they start in slightly more human-like way. For example, a pty owner can send ^C (aka SIGINT) + * to attached shell, even if said shell runs under a different user ID. + */ +public class TermExec { + // Warning: bump the library revision, when an incompatible change happens + static { + System.loadLibrary("jackpal-termexec2"); + } + + public static final String SERVICE_ACTION_V1 = "jackpal.androidterm.action.START_TERM.v1"; + + private static Field descriptorField; + + private final List command; + private final Map environment; + + public TermExec(@NonNull String... command) { + this(new ArrayList<>(Arrays.asList(command))); + } + + public TermExec(@NonNull List command) { + this.command = command; + this.environment = new Hashtable<>(System.getenv()); + } + + public @NonNull List command() { + return command; + } + + public @NonNull Map environment() { + return environment; + } + + public @NonNull TermExec command(@NonNull String... command) { + return command(new ArrayList<>(Arrays.asList(command))); + } + + public @NonNull TermExec command(List command) { + command.clear(); + command.addAll(command); + return this; + } + + /** + * Start the process and attach it to the pty, corresponding to given file descriptor. + * You have to obtain this file descriptor yourself by calling + * {@link android.os.ParcelFileDescriptor#open} on special terminal multiplexer + * device (located at /dev/ptmx). + *

+ * Callers are responsible for closing the descriptor. + * + * @return the PID of the started process. + */ + public int start(@NonNull ParcelFileDescriptor ptmxFd) throws IOException { + if (Looper.getMainLooper() == Looper.myLooper()) + throw new IllegalStateException("This method must not be called from the main thread!"); + + if (command.size() == 0) + throw new IllegalStateException("Empty command!"); + + final String cmd = command.remove(0); + final String[] cmdArray = command.toArray(new String[command.size()]); + final String[] envArray = new String[environment.size()]; + int i = 0; + for (Map.Entry entry : environment.entrySet()) { + envArray[i++] = entry.getKey() + "=" + entry.getValue(); + } + + return createSubprocess(ptmxFd, cmd, cmdArray, envArray); + } + + /** + * Causes the calling thread to wait for the process associated with the + * receiver to finish executing. + * + * @return The exit value of the Process being waited on + */ + public static native int waitFor(int processId); + + /** + * Send signal via the "kill" system call. Android {@link android.os.Process#sendSignal} does not + * allow negative numbers (denoting process groups) to be used. + */ + public static native void sendSignal(int processId, int signal); + + static int createSubprocess(ParcelFileDescriptor masterFd, String cmd, String[] args, String[] envVars) throws IOException + { + final int integerFd; + + if (Build.VERSION.SDK_INT >= 12) + integerFd = FdHelperHoneycomb.getFd(masterFd); + else { + try { + if (descriptorField == null) { + descriptorField = FileDescriptor.class.getDeclaredField("descriptor"); + descriptorField.setAccessible(true); + } + + integerFd = descriptorField.getInt(masterFd.getFileDescriptor()); + } catch (Exception e) { + throw new IOException("Unable to obtain file descriptor on this OS version: " + e.getMessage()); + } + } + + return createSubprocessInternal(cmd, args, envVars, integerFd); + } + + private static native int createSubprocessInternal(String cmd, String[] args, String[] envVars, int masterFd); +} + +// prevents runtime errors on old API versions with ruthless verifier +@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) +class FdHelperHoneycomb { + static int getFd(ParcelFileDescriptor descriptor) { + return descriptor.getFd(); + } +} diff --git a/libtermexec/src/main/jni/process.cpp b/libtermexec/src/main/jni/process.cpp new file mode 100644 index 0000000..91459a5 --- /dev/null +++ b/libtermexec/src/main/jni/process.cpp @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2012 Steven Luo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "process.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef unsigned short char16_t; + +class String8 { +public: + String8() { + mString = 0; + } + + ~String8() { + if (mString) { + free(mString); + } + } + + void set(const char16_t* o, size_t numChars) { + if (mString) { + free(mString); + } + mString = (char*) malloc(numChars + 1); + if (!mString) { + return; + } + for (size_t i = 0; i < numChars; i++) { + mString[i] = (char) o[i]; + } + mString[numChars] = '\0'; + } + + const char* string() { + return mString; + } +private: + char* mString; +}; + +static int throwOutOfMemoryError(JNIEnv *env, const char *message) +{ + jclass exClass; + const char *className = "java/lang/OutOfMemoryError"; + + exClass = env->FindClass(className); + return env->ThrowNew(exClass, message); +} + +static int throwIOException(JNIEnv *env, int errnum, const char *message) +{ + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "%s errno %s(%d)", + message, strerror(errno), errno); + + if (errnum != 0) { + const char *s = strerror(errnum); + if (strcmp(s, "Unknown error") != 0) + message = s; + } + + jclass exClass; + const char *className = "java/io/IOException"; + + exClass = env->FindClass(className); + return env->ThrowNew(exClass, message); +} + +static void closeNonstandardFileDescriptors() { + // Android uses shared memory to communicate between processes. The file descriptor is passed + // to child processes using the environment variable ANDROID_PROPERTY_WORKSPACE, which is of + // the form "properties_fd,sizeOfSharedMemory" + int properties_fd = -1; + char* properties_fd_string = getenv("ANDROID_PROPERTY_WORKSPACE"); + if (properties_fd_string != NULL) { + properties_fd = atoi(properties_fd_string); + } + DIR *dir = opendir("/proc/self/fd"); + if(dir != NULL) { + int dir_fd = dirfd(dir); + + while(true) { + struct dirent *entry = readdir(dir); + if(entry == NULL) { + break; + } + + int fd = atoi(entry->d_name); + if(fd > STDERR_FILENO && fd != dir_fd && fd != properties_fd) { + close(fd); + } + } + + closedir(dir); + } +} + +static int create_subprocess(JNIEnv *env, const char *cmd, char *const argv[], char *const envp[], int masterFd) +{ + // same size as Android 1.6 libc/unistd/ptsname_r.c + char devname[64]; + pid_t pid; + + fcntl(masterFd, F_SETFD, FD_CLOEXEC); + + // grantpt is unnecessary, because we already assume devpts by using /dev/ptmx + if(unlockpt(masterFd)){ + throwIOException(env, errno, "trouble with /dev/ptmx"); + return -1; + } + memset(devname, 0, sizeof(devname)); + // Early (Android 1.6) bionic versions of ptsname_r had a bug where they returned the buffer + // instead of 0 on success. A compatible way of telling whether ptsname_r + // succeeded is to zero out errno and check it after the call + errno = 0; + int ptsResult = ptsname_r(masterFd, devname, sizeof(devname)); + if (ptsResult && errno) { + throwIOException(env, errno, "ptsname_r returned error"); + return -1; + } + + pid = fork(); + if(pid < 0) { + throwIOException(env, errno, "fork failed"); + return -1; + } + + if(pid == 0){ + int pts; + + setsid(); + + pts = open(devname, O_RDWR); + if(pts < 0) exit(-1); + + ioctl(pts, TIOCSCTTY, 0); + + dup2(pts, 0); + dup2(pts, 1); + dup2(pts, 2); + + closeNonstandardFileDescriptors(); + + if (envp) { + for (; *envp; ++envp) { + putenv(*envp); + } + } + + execv(cmd, argv); + exit(-1); + } else { + return (int) pid; + } +} + +extern "C" { + +JNIEXPORT void JNICALL Java_jackpal_androidterm_TermExec_sendSignal(JNIEnv *env, jobject clazz, + jint procId, jint signal) +{ + kill(procId, signal); +} + +JNIEXPORT jint JNICALL Java_jackpal_androidterm_TermExec_waitFor(JNIEnv *env, jclass clazz, jint procId) { + int status; + waitpid(procId, &status, 0); + int result = 0; + if (WIFEXITED(status)) { + result = WEXITSTATUS(status); + } + return result; +} + +JNIEXPORT jint JNICALL Java_jackpal_androidterm_TermExec_createSubprocessInternal(JNIEnv *env, jclass clazz, + jstring cmd, jobjectArray args, jobjectArray envVars, jint masterFd) +{ + const jchar* str = cmd ? env->GetStringCritical(cmd, 0) : 0; + String8 cmd_8; + if (str) { + cmd_8.set(str, env->GetStringLength(cmd)); + env->ReleaseStringCritical(cmd, str); + } + + jsize size = args ? env->GetArrayLength(args) : 0; + char **argv = NULL; + String8 tmp_8; + if (size > 0) { + argv = (char **)malloc((size+1)*sizeof(char *)); + if (!argv) { + throwOutOfMemoryError(env, "Couldn't allocate argv array"); + return 0; + } + for (int i = 0; i < size; ++i) { + jstring arg = reinterpret_cast(env->GetObjectArrayElement(args, i)); + str = env->GetStringCritical(arg, 0); + if (!str) { + throwOutOfMemoryError(env, "Couldn't get argument from array"); + return 0; + } + tmp_8.set(str, env->GetStringLength(arg)); + env->ReleaseStringCritical(arg, str); + argv[i] = strdup(tmp_8.string()); + } + argv[size] = NULL; + } + + size = envVars ? env->GetArrayLength(envVars) : 0; + char **envp = NULL; + if (size > 0) { + envp = (char **)malloc((size+1)*sizeof(char *)); + if (!envp) { + throwOutOfMemoryError(env, "Couldn't allocate envp array"); + return 0; + } + for (int i = 0; i < size; ++i) { + jstring var = reinterpret_cast(env->GetObjectArrayElement(envVars, i)); + str = env->GetStringCritical(var, 0); + if (!str) { + throwOutOfMemoryError(env, "Couldn't get env var from array"); + return 0; + } + tmp_8.set(str, env->GetStringLength(var)); + env->ReleaseStringCritical(var, str); + envp[i] = strdup(tmp_8.string()); + } + envp[size] = NULL; + } + + int ptm = create_subprocess(env, cmd_8.string(), argv, envp, masterFd); + + if (argv) { + for (char **tmp = argv; *tmp; ++tmp) { + free(*tmp); + } + free(argv); + } + if (envp) { + for (char **tmp = envp; *tmp; ++tmp) { + free(*tmp); + } + free(envp); + } + + return ptm; +} + +} diff --git a/libtermexec/src/main/jni/process.h b/libtermexec/src/main/jni/process.h new file mode 100644 index 0000000..60349f6 --- /dev/null +++ b/libtermexec/src/main/jni/process.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012 Steven Luo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _JACKPAL_PROCESS_H +#define _JACKPAL_PROCESS_H 1 + +#include +#include "jni.h" +#include + +#define LOG_TAG "jackpal-termexec" + +extern "C" { +JNIEXPORT jint JNICALL Java_jackpal_androidterm_TermExec_createSubprocessInternal + (JNIEnv *, jclass, jstring, jobjectArray, jobjectArray, jint); + + JNIEXPORT jint JNICALL Java_jackpal_androidterm_TermExec_waitFor + (JNIEnv *, jclass, jint); +} + +#endif /* !defined(_JACKPAL_PROCESS_H) */ diff --git a/samples/intents/.gitignore b/samples/intents/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/samples/intents/.gitignore @@ -0,0 +1 @@ +/build diff --git a/samples/intents/build.gradle b/samples/intents/build.gradle new file mode 100644 index 0000000..0ab284c --- /dev/null +++ b/samples/intents/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + applicationId "jackpal.androidterm.sample.intents" + minSdkVersion 4 + targetSdkVersion 22 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} diff --git a/samples/intents/lint.xml b/samples/intents/lint.xml new file mode 100644 index 0000000..45998cd --- /dev/null +++ b/samples/intents/lint.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/intents/src/main/AndroidManifest.xml b/samples/intents/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1645991 --- /dev/null +++ b/samples/intents/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/samples/intents/src/main/java/jackpal/androidterm/sample/intents/IntentSampleActivity.java b/samples/intents/src/main/java/jackpal/androidterm/sample/intents/IntentSampleActivity.java new file mode 100644 index 0000000..ed0a173 --- /dev/null +++ b/samples/intents/src/main/java/jackpal/androidterm/sample/intents/IntentSampleActivity.java @@ -0,0 +1,84 @@ +package jackpal.androidterm.sample.intents; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; + +public class IntentSampleActivity extends Activity +{ + private String mHandle; + private static final int REQUEST_WINDOW_HANDLE = 1; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + addClickListener(R.id.openNewWindow, new OnClickListener() { + public void onClick(View v) { + // Intent for opening a new window without providing script + Intent intent = + new Intent("jackpal.androidterm.OPEN_NEW_WINDOW"); + intent.addCategory(Intent.CATEGORY_DEFAULT); + startActivity(intent); + }}); + + final EditText script = (EditText) findViewById(R.id.script); + script.setText(getString(R.string.default_script)); + addClickListener(R.id.runScript, new OnClickListener() { + public void onClick(View v) { + /* Intent for opening a new window and running the provided + script -- you must declare the permission + jackpal.androidterm.permission.RUN_SCRIPT in your manifest + to use */ + Intent intent = + new Intent("jackpal.androidterm.RUN_SCRIPT"); + intent.addCategory(Intent.CATEGORY_DEFAULT); + String command = script.getText().toString(); + intent.putExtra("jackpal.androidterm.iInitialCommand", command); + startActivity(intent); + }}); + addClickListener(R.id.runScriptSaveWindow, new OnClickListener() { + public void onClick(View v) { + /* Intent for running a script in a previously opened window, + if it still exists + This will open another window if it doesn't find a match */ + Intent intent = + new Intent("jackpal.androidterm.RUN_SCRIPT"); + intent.addCategory(Intent.CATEGORY_DEFAULT); + String command = script.getText().toString(); + intent.putExtra("jackpal.androidterm.iInitialCommand", command); + if (mHandle != null) { + // Identify the targeted window by its handle + intent.putExtra("jackpal.androidterm.window_handle", + mHandle); + } + /* The handle for the targeted window -- whether newly opened + or reused -- is returned to us via onActivityResult() + You can compare it against an existing saved handle to + determine whether or not a new window was opened */ + startActivityForResult(intent, REQUEST_WINDOW_HANDLE); + }}); + } + + private void addClickListener(int buttonId, OnClickListener onClickListener) { + ((Button) findViewById(buttonId)).setOnClickListener(onClickListener); + } + + protected void onActivityResult(int request, int result, Intent data) { + if (result != RESULT_OK) { + return; + } + + if (request == REQUEST_WINDOW_HANDLE && data != null) { + mHandle = data.getStringExtra("jackpal.androidterm.window_handle"); + ((Button) findViewById(R.id.runScriptSaveWindow)).setText( + R.string.run_script_existing_window); + } + } +} diff --git a/samples/intents/src/main/res/layout/main.xml b/samples/intents/src/main/res/layout/main.xml new file mode 100644 index 0000000..96ea176 --- /dev/null +++ b/samples/intents/src/main/res/layout/main.xml @@ -0,0 +1,42 @@ + + + + + +