diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..421502f0b --- /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/.gitignore b/.gitignore index bcc5997c2..8c8ca10a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -bin/ -gen/ -libs/ -obj/ -.classpath -.project -.settings/ +.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 000000000..d6b233251 --- /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 000000000..217af471a --- /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 000000000..e7bedf337 --- /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 000000000..e206d70d8 --- /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 000000000..6c701da8d --- /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 000000000..59436c989 --- /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 000000000..a7403e3fe --- /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 000000000..922003b84 --- /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 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android.mk b/Android.mk deleted file mode 100644 index 6604ad755..000000000 --- a/Android.mk +++ /dev/null @@ -1,42 +0,0 @@ -# -# Copyright (C) 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. -# 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. -# - -# This makefile shows how to build a shared library and an activity that -# bundles the shared library and calls it using JNI. - -TOP_LOCAL_PATH:= $(call my-dir) - -# Build activity - -LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -LOCAL_MODULE_TAGS := optional - -LOCAL_SRC_FILES := $(call all-subdir-java-files) - -LOCAL_PACKAGE_NAME := AndroidTerm - -LOCAL_OVERRIDES_PACKAGES := Term - -LOCAL_REQUIRED_MODULES := libandroidterm2 - -include $(BUILD_PACKAGE) - -# ============================================================ - -# Also build all of the sub-targets under this one: the shared library. -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index fff51653d..000000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md index 3825795cf..b62014876 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,32 @@ -#Android Terminal Emulator +# Terminal Emulator for Android -Android Terminal Emulator is a terminal emulator for communicating with the built-in Android shell. Emulates a reasonably large subset of Digital Equipment Corporation VT-100 terminal codes, so that programs like "vi", "Emacs" and "NetHack" will display properly. +*Note:* Terminal Emulator for Android development is complete. I am not +accepting pull requests any more. -This code is based on the "Term" application which is included in the Android source code release. It's provided as a separate project for the convenience of developers who do not want to deal with installing and building the whole Android source tree. +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. -Although this program does not include a built-in ssh client, it can be used with command-line-based ssh tools such as dropbear. +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.) -Got questions? Please check out the [FAQ](http://github.com/jackpal/Android-Terminal-Emulator/wiki/Frequently-Asked-Questions) before emailing or adding an issue. Thanks! +This code is based on the "Term" application which is included in the Android +Open Source Project. (Which I also wrote. :-) ) -Please see the [Recent Updates](http://github.com/jackpal/Android-Terminal-Emulator/wiki/Recent-Updates) -page for recent updates. \ No newline at end of file +[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/README.txt b/README.txt deleted file mode 100644 index dd6403c39..000000000 --- a/README.txt +++ /dev/null @@ -1,35 +0,0 @@ -This is an Android Terminal emulator. It enables you to access your Android -device's built-in command-line shell. - -This terminal emulator emulates Digital Equipment Corporation VT-100 terminal -escape codes. It is designed to be used with command-line programs and -curses-based applications like vi, emacs, nethack, and dropbear ssh. - -Features: - -+ Connects to your Android device's built-in command-line shell. -+ Comprehensive VT100 terminal emulation. -+ Supports a range of font sizes -+ Supports several choices of text and background color. -+ 1000-line transcript buffer. -+ Can send transcript as an email message. - -FAQ: - -+ What can I do with this? - -A you can run and control command-line applications on your Android device. - -+ Why would I want to do that? - -For fun! There are a wealth of Linux utility programs that can be made to work -on Android, and this terminal emulator provides a good way of interacting with -them. - -+ Does this application give me "root" access? - -No, it doesn't. It runs as an ordinary application, with no special access -rights. - -+ How can I build this? -Build instructions can be found in the docs directory. diff --git a/artwork/Feature Graphic.xcf b/artwork/Feature Graphic.xcf new file mode 100644 index 000000000..38c8670ca Binary files /dev/null and b/artwork/Feature Graphic.xcf differ diff --git a/artwork/android-terminal-emulator-512.png b/artwork/android-terminal-emulator-512.png new file mode 100644 index 000000000..2b4ec5616 Binary files /dev/null and b/artwork/android-terminal-emulator-512.png differ diff --git a/artwork/android-terminal-emulator.svg b/artwork/android-terminal-emulator.svg new file mode 100644 index 000000000..4d1369673 --- /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 000000000..30baf09d1 --- /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/default.properties b/default.properties deleted file mode 100644 index 9d79b12c7..000000000 --- a/default.properties +++ /dev/null @@ -1,11 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system use, -# "build.properties", and override values to adapt the script to your -# project structure. - -# Project target. -target=android-4 diff --git a/docs/Building.md b/docs/Building.md new file mode 100644 index 000000000..3d04c6875 --- /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/Building.txt b/docs/Building.txt deleted file mode 100644 index d2afa34a7..000000000 --- a/docs/Building.txt +++ /dev/null @@ -1,68 +0,0 @@ -Obtain the Software Needed to Build Android Term ------------------------------------------------- - -Android is built using the Android SDK r08 or newer and the Android NDK r5 or newer. -You can downlaod them from: - -http://developer.android.com/sdk - -You will also need Eclipse 3.4 or newer, Java version, available from: - -http://eclipse.org - -After you install the SDK and the NDK, please build some of the samples that -come with the NDK in order to get a feel for using the SDK and the NDK. - -Building Android Term ---------------------- - -There are three parts to building Android Term: - - 1) Build the shared library - 2) Create the Eclipse Project for the Application - 3) Build the Java apk. - -Build the shared library: - - cd - /ndk-build - - (Be sure to use the actual pathnames from your system. For example, - on my system I actually type the following: - - cd ~/code/androidterm - ../android-ndk-r5/ndk-build - -This should chug away for a while and ultimately produce the file - -libs/armeabi/libandroidterm.so - -Create the Eclipse project: - - Start Eclipse - From the menu choose File : New Project - Choose Android:Android Project as a project type - Press Next - Set Project Name: AndroidTerm - Choose "Create project from existing source" - Browse to the location of the AndroidTerm source directory. - Make sure the "Android 1.6" Target Name is checked. - Make sure the Min SDK Version is 4 - Press Finish - - NOTE: When you Press Finish you may get the following error - - Invalid project description. - overlaps the location of another - project: '' - - This seems to be due to a bug in the Android SDK, where it - can't import projects that are located in the Eclipse "workspace" - directory. The work-around is to move the androidterm project - directory to a different location on your file system, so - that it is _not_ in the Eclipse "workspace" directory. - -Build the Java apk: - - This should happen automatically once you've created the Eclipse project. - diff --git a/docs/Market Icon.psd b/docs/Market Icon.psd deleted file mode 100644 index 83d61fce6..000000000 Binary files a/docs/Market Icon.psd and /dev/null differ diff --git a/docs/UTF-8-SMP-chars-demo.txt b/docs/UTF-8-SMP-chars-demo.txt new file mode 100644 index 000000000..3a2e48bd7 --- /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 000000000..4363f27bd --- /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/res/drawable/atari_small_notice.txt b/docs/atari_small_notice.txt similarity index 100% rename from res/drawable/atari_small_notice.txt rename to docs/atari_small_notice.txt diff --git a/docs/notification icon source.png b/docs/notification icon source.png new file mode 100644 index 000000000..03afa71fb Binary files /dev/null and b/docs/notification icon source.png differ diff --git a/docs/releaseChecklist.md b/docs/releaseChecklist.md new file mode 100644 index 000000000..a826e3f94 --- /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 000000000..796b96d1c --- /dev/null +++ b/emulatorview/.gitignore @@ -0,0 +1 @@ +/build diff --git a/emulatorview/build.gradle b/emulatorview/build.gradle new file mode 100644 index 000000000..c2f7adb99 --- /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 000000000..bd07ec0a0 --- /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 000000000..5542ec111 --- /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 000000000..f2282fd21 --- /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 000000000..fa0608ee0 --- /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 000000000..d81f1f1c2 --- /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 000000000..ff84c0c2a --- /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 000000000..ba192f90e --- /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 000000000..9576ae713 --- /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 000000000..4b2137a34 --- /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 000000000..84110beac --- /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 000000000..de9484a7f --- /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 000000000..eb2ae6099 --- /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 000000000..4d588e538 --- /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 000000000..9c830b429 --- /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 000000000..39c615da8 --- /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 000000000..bf16e4356 --- /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 000000000..e1dc3c02b --- /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 000000000..2b8fdfcbe --- /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/src/jackpal/androidterm/TermPreferences.java b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/UpdateCallback.java similarity index 61% rename from src/jackpal/androidterm/TermPreferences.java rename to emulatorview/src/main/java/jackpal/androidterm/emulatorview/UpdateCallback.java index 1bbf0e7ce..b2dbe2184 100644 --- a/src/jackpal/androidterm/TermPreferences.java +++ b/emulatorview/src/main/java/jackpal/androidterm/emulatorview/UpdateCallback.java @@ -14,19 +14,14 @@ * limitations under the License. */ -package jackpal.androidterm2; - -import android.os.Bundle; -import android.preference.PreferenceActivity; - -public class TermPreferences extends PreferenceActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Load the preferences from an XML resource - addPreferencesFromResource(R.xml.preferences); - } +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 000000000..91762a6a7 --- /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 000000000..9f0555869 --- /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 000000000..d38cfd032 --- /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 000000000..5eaf2a215 --- /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 000000000..672e5c2a8 --- /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 000000000..dfdbd7868 --- /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 000000000..242f08868 --- /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 000000000..d025561b2 --- /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 000000000..f48583967 --- /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 000000000..559ee7c78 --- /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/res/drawable-hdpi/atari_small.png b/emulatorview/src/main/res/drawable-nodpi/atari_small_nodpi.png old mode 100755 new mode 100644 similarity index 100% rename from res/drawable-hdpi/atari_small.png rename to emulatorview/src/main/res/drawable-nodpi/atari_small_nodpi.png diff --git a/emulatorview/src/main/res/drawable/atari_small.png b/emulatorview/src/main/res/drawable/atari_small.png new file mode 100644 index 000000000..8bdd62445 Binary files /dev/null and b/emulatorview/src/main/res/drawable/atari_small.png differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..8c0fb64a8 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..0c71e760d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 10 15:27:10 PDT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + 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 000000000..8a0b282aa --- /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/jni/Android.mk b/jni/Android.mk deleted file mode 100644 index b36394288..000000000 --- a/jni/Android.mk +++ /dev/null @@ -1,54 +0,0 @@ -# -# Copyright (C) 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. -# 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. -# - -# This makefile supplies the rules for building a library of JNI code for -# use by our example of how to bundle a shared library with an APK. - -LOCAL_PATH:= $(call my-dir) -include $(CLEAR_VARS) - -LOCAL_MODULE_TAGS := optional - -# This is the target being built. -LOCAL_MODULE:= libandroidterm2 - - -# All of the source files that we will compile. -LOCAL_SRC_FILES:= \ - termExec.cpp - -# All of the shared libraries we link against. -LOCAL_SHARED_LIBRARIES := \ - libutils - -# No static libraries. -LOCAL_STATIC_LIBRARIES := - -# Also need the JNI headers. -LOCAL_C_INCLUDES += \ - $(JNI_H_INCLUDE) - -# No special compiler flags. -LOCAL_CFLAGS += - -# Don't prelink this library. For more efficient code, you may want -# to add this library to the prelink map and set this to true. However, -# it's difficult to do this for applications that are not supplied as -# part of a system image. - -LOCAL_PRELINK_MODULE := false - -include $(BUILD_SHARED_LIBRARY) diff --git a/jni/termExec.cpp b/jni/termExec.cpp deleted file mode 100644 index 52f0ac555..000000000 --- a/jni/termExec.cpp +++ /dev/null @@ -1,353 +0,0 @@ -/* - * Copyright (C) 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. - * 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. - */ - -/* - * 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. - */ - -#define LOG_TAG "Exec" - -#include "jni.h" -#include - -#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__); } while(0) -#define LOGW(...) do { __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__); } while(0) -#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__); } while(0) - -#include -#include -#include -#include -#include -#include -#include -#include - -static jclass class_fileDescriptor; -static jfieldID field_fileDescriptor_descriptor; -static jmethodID method_fileDescriptor_init; - -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); - 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 create_subprocess(const char *cmd, const char *arg0, const char *arg1, - int* pProcessId) -{ - char *devname; - int ptm; - pid_t pid; - - ptm = open("/dev/ptmx", O_RDWR); // | O_NOCTTY); - if(ptm < 0){ - LOGE("[ cannot open /dev/ptmx - %s ]\n",strerror(errno)); - return -1; - } - fcntl(ptm, F_SETFD, FD_CLOEXEC); - - if(grantpt(ptm) || unlockpt(ptm) || - ((devname = (char*) ptsname(ptm)) == 0)){ - LOGE("[ trouble with /dev/ptmx - %s ]\n", strerror(errno)); - return -1; - } - - pid = fork(); - if(pid < 0) { - LOGE("- fork failed: %s -\n", strerror(errno)); - return -1; - } - - if(pid == 0){ - close(ptm); - - int pts; - - setsid(); - - pts = open(devname, O_RDWR); - if(pts < 0) exit(-1); - - dup2(pts, 0); - dup2(pts, 1); - dup2(pts, 2); - - execl(cmd, cmd, arg0, arg1, NULL); - exit(-1); - } else { - *pProcessId = (int) pid; - return ptm; - } -} - - -static jobject android_os_Exec_createSubProcess(JNIEnv *env, jobject clazz, - jstring cmd, jstring arg0, jstring arg1, jintArray processIdArray) -{ - 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); - } - - str = arg0 ? env->GetStringCritical(arg0, 0) : 0; - const char* arg0Str = 0; - String8 arg0_8; - if (str) { - arg0_8.set(str, env->GetStringLength(arg0)); - env->ReleaseStringCritical(arg0, str); - arg0Str = arg0_8.string(); - } - - str = arg1 ? env->GetStringCritical(arg1, 0) : 0; - const char* arg1Str = 0; - String8 arg1_8; - if (str) { - arg1_8.set(str, env->GetStringLength(arg1)); - env->ReleaseStringCritical(arg1, str); - arg1Str = arg1_8.string(); - } - - int procId; - int ptm = create_subprocess(cmd_8.string(), arg0Str, arg1Str, &procId); - - if (processIdArray) { - int procIdLen = env->GetArrayLength(processIdArray); - if (procIdLen > 0) { - jboolean isCopy; - - int* pProcId = (int*) env->GetPrimitiveArrayCritical(processIdArray, &isCopy); - if (pProcId) { - *pProcId = procId; - env->ReleasePrimitiveArrayCritical(processIdArray, pProcId, 0); - } - } - } - - jobject result = env->NewObject(class_fileDescriptor, method_fileDescriptor_init); - - if (!result) { - LOGE("Couldn't create a FileDescriptor."); - } - else { - env->SetIntField(result, field_fileDescriptor_descriptor, ptm); - } - - return result; -} - - -static void android_os_Exec_setPtyWindowSize(JNIEnv *env, jobject clazz, - jobject fileDescriptor, jint row, jint col, jint xpixel, jint ypixel) -{ - int fd; - struct winsize sz; - - fd = env->GetIntField(fileDescriptor, field_fileDescriptor_descriptor); - - if (env->ExceptionOccurred() != NULL) { - return; - } - - sz.ws_row = row; - sz.ws_col = col; - sz.ws_xpixel = xpixel; - sz.ws_ypixel = ypixel; - - ioctl(fd, TIOCSWINSZ, &sz); -} - -static int android_os_Exec_waitFor(JNIEnv *env, jobject clazz, - jint procId) { - int status; - waitpid(procId, &status, 0); - int result = 0; - if (WIFEXITED(status)) { - result = WEXITSTATUS(status); - } - return result; -} - -static void android_os_Exec_close(JNIEnv *env, jobject clazz, jobject fileDescriptor) -{ - int fd; - struct winsize sz; - - fd = env->GetIntField(fileDescriptor, field_fileDescriptor_descriptor); - - if (env->ExceptionOccurred() != NULL) { - return; - } - - close(fd); -} - - -static int register_FileDescriptor(JNIEnv *env) -{ - class_fileDescriptor = env->FindClass("java/io/FileDescriptor"); - - if (class_fileDescriptor == NULL) { - LOGE("Can't find java/io/FileDescriptor"); - return -1; - } - - field_fileDescriptor_descriptor = env->GetFieldID(class_fileDescriptor, "descriptor", "I"); - - if (field_fileDescriptor_descriptor == NULL) { - LOGE("Can't find FileDescriptor.descriptor"); - return -1; - } - - method_fileDescriptor_init = env->GetMethodID(class_fileDescriptor, "", "()V"); - if (method_fileDescriptor_init == NULL) { - LOGE("Can't find FileDescriptor.init"); - return -1; - } - return 0; -} - - -static const char *classPathName = "jackpal/androidterm2/Exec"; - -static JNINativeMethod method_table[] = { - { "createSubprocess", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[I)Ljava/io/FileDescriptor;", - (void*) android_os_Exec_createSubProcess }, - { "setPtyWindowSize", "(Ljava/io/FileDescriptor;IIII)V", - (void*) android_os_Exec_setPtyWindowSize}, - { "waitFor", "(I)I", - (void*) android_os_Exec_waitFor}, - { "close", "(Ljava/io/FileDescriptor;)V", - (void*) android_os_Exec_close} -}; - -/* - * Register several native methods for one class. - */ -static int registerNativeMethods(JNIEnv* env, const char* className, - JNINativeMethod* gMethods, int numMethods) -{ - jclass clazz; - - clazz = env->FindClass(className); - if (clazz == NULL) { - LOGE("Native registration unable to find class '%s'", className); - return JNI_FALSE; - } - if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { - LOGE("RegisterNatives failed for '%s'", className); - return JNI_FALSE; - } - - return JNI_TRUE; -} - -/* - * Register native methods for all classes we know about. - * - * returns JNI_TRUE on success. - */ -static int registerNatives(JNIEnv* env) -{ - if (!registerNativeMethods(env, classPathName, method_table, - sizeof(method_table) / sizeof(method_table[0]))) { - return JNI_FALSE; - } - - return JNI_TRUE; -} - - -// ---------------------------------------------------------------------------- - -/* - * This is called by the VM when the shared library is first loaded. - */ - -typedef union { - JNIEnv* env; - void* venv; -} UnionJNIEnvToVoid; - -jint JNI_OnLoad(JavaVM* vm, void* reserved) { - UnionJNIEnvToVoid uenv; - uenv.venv = NULL; - jint result = -1; - JNIEnv* env = NULL; - - LOGI("JNI_OnLoad"); - - if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) { - LOGE("ERROR: GetEnv failed"); - goto bail; - } - env = uenv.env; - - if ((result = register_FileDescriptor(env)) < 0) { - LOGE("ERROR: registerFileDescriptor failed"); - goto bail; - } - - if (registerNatives(env) != JNI_TRUE) { - LOGE("ERROR: registerNatives failed"); - goto bail; - } - - result = JNI_VERSION_1_4; - -bail: - return result; -} diff --git a/libtermexec/.gitignore b/libtermexec/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/libtermexec/.gitignore @@ -0,0 +1 @@ +/build diff --git a/libtermexec/build.gradle b/libtermexec/build.gradle new file mode 100644 index 000000000..55d9f65f6 --- /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 000000000..cfa640e43 --- /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 000000000..ee475d052 --- /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 000000000..f106d2ed6 --- /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 000000000..092738b94 --- /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 000000000..a3e907294 --- /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 000000000..91459a532 --- /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 000000000..60349f6f4 --- /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/res/drawable-hdpi/app_terminal.png b/res/drawable-hdpi/app_terminal.png deleted file mode 100755 index 278b2a54f..000000000 Binary files a/res/drawable-hdpi/app_terminal.png and /dev/null differ diff --git a/res/drawable-mdpi/app_terminal.png b/res/drawable-mdpi/app_terminal.png deleted file mode 100644 index 1ec3b4b88..000000000 Binary files a/res/drawable-mdpi/app_terminal.png and /dev/null differ diff --git a/res/drawable-mdpi/atari_small.png b/res/drawable-mdpi/atari_small.png deleted file mode 100644 index 535e295c8..000000000 Binary files a/res/drawable-mdpi/atari_small.png and /dev/null differ diff --git a/res/values-de/arrays.xml b/res/values-de/arrays.xml deleted file mode 100644 index 1bd3287fd..000000000 --- a/res/values-de/arrays.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - Statuszeile zeigen - Statuszeile verbergen - - - - Permanenter Cursor - Blinkender Cursor - - - - Rechteck - Unterstrichen - Vertikaler Balken - - - - 4x8 Pixel - 6 Punkte - 7 Punkte - 8 Punkte - 9 Punkte - 10 Punkte - 12 Punkte - 14 Punkte - 16 Punkte - 20 Punkte - - - - Schwarzer Text auf weiss - Weisser Text auf schwarz - Weisser Text auf blau - Grüner Text auf schwarz - Oranger Text auf schwarz - Roter Text auf schwarz - - - - Trackball - \@ Taste - Linke Alt-Taste - Rechte Alt-Taste - Lauter - Leiser - - - - Zeichen-basiert - Wort-basiert - - diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml deleted file mode 100644 index 9451a3d01..000000000 --- a/res/values-de/strings.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - Terminal Emulator - Einstellungen - Zurücksetzen - Email an - Spezialtasten - Tastatur an/aus - - Text bearbeiten - Alles kopieren - Einfügen - - - Bildschirm - - Statuszeile - Die Statuszeile anzeigen/verbergen. - Statuszeile - - Aussehen Cursor - Das Aussehen des Cursors auswählen. - Aussehen Cursor - - Cursor-Blinken - Die Art auswählen, wie der Cursor blinken soll. - Cursor-Blinken - - Text - - Schriftgröße - Die Zeichengröße in Punkten auswählen. - Schriftgröße - - Farben - Die Textfarben auswählen. - Textfarbe - - Tastatur - - Steuerungstaste - Die Steuerungstaste auswählen. - Steuerungstaste - - Eingabemethode - Die Eingabemethode für die Soft-Tastatur auswählen. - Eingabemethode - - Shell - Shell - Die zu verwendene Shell auswählen. - Shell - - Startkommando - Kommando eingeben, dass beim Start an die Shell gesendet wird. - Startkommando - diff --git a/res/values-fr/arrays.xml b/res/values-fr/arrays.xml deleted file mode 100644 index a7c51b72c..000000000 --- a/res/values-fr/arrays.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - Afficher la barre de statut - Masquer la barre de statut - - - - Curseur non clignotant - Curseur clignotant - - - - Rectangle - Souligné - Barre verticale - - - - Texte noir sur blanc - Texte blanc sur noir - Texte blanc sur bleu - Texte vert sur noir - Texte orange sur noir - Texte rouge sur noir - - - - Trackball - Touche \@ - Touche Alt gauche - Touche Alt droite - Touche Vol Haut - Touche Vol Bas - - - - Par caractère - Par mot - - diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml deleted file mode 100644 index e133ae518..000000000 --- a/res/values-fr/strings.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - Terminal Emulator - Préférences - RàZ terminal - Envoyer par e-mail - Touches spéciales - Afficher/Masquer Clavier - - Modifier le texte - Tout copier - Coller - - - Écran - - Barre de statut - Afficher/Masquer la barre de statut - Barre de statut - - Style du curseur - Choisir le style du curseur - Style du curseur - - Clignotement curseur - Choisir si le curseur doit clignoter - Clignotement curseur - - Texte - - Taille police - Choisir la taille de la police de caractères en points - Taille de la police - - Couleurs - Choisir la couleur du texte - Couleur du texte - - Clavier - - Touche CTRL - Choisir quelle touche utiliser pour control (CTRL) - Touche CTRL - - Méthode d\'entrée - Choisir la méthode d\'entrée du clavier virtuel - Méthode d\'entrée - - Shell - Ligne de commande - Régler la ligne de commande du shell - Shell - - Commande initiale - Envoyée au shell au démarrage - Commande initiale - diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml deleted file mode 100644 index 20fbccc4b..000000000 --- a/res/values-it/strings.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - Emulatore Terminale - Preferenze - Resetta term - Invia Email - Combinazione tasti - Attiva/disattiva keyboard - - Modifica testo - Copia tutto - Incolla - - - Schermo - - Status bar - Mostra/nascondi status bar. - Status bar - - Stile cursore - Scegli lo stile del cursore. - Stile cursore - - Lampeggio cursore - Scegli lampeggio cursore. - Lampeggio cursore - - Testo - - Dimensione carattere - Scegli l\'altezza dei caratteri in punti. - Dimensione carattere - - Colori - Scegli il colore del testo. - Colore testo - - Tastierra - - Tasto control - Scegli tasto control. - Tasto control - - Metodo inserimento - Scegli il metodo di inserimento per la tastiera virtuale. - Metodo inserimento - - Shell - Linea di comando - Specifica la shell. - Shell - - Comando iniziale - Inviato alla shell al suo avvio. - Comando iniziale - - diff --git a/res/values-ja/arrays.xml b/res/values-ja/arrays.xml deleted file mode 100644 index 939a401fa..000000000 --- a/res/values-ja/arrays.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - ステータスバーを表示する - ステータスバーを隠す - - - - 点滅しないカーソル - 点滅するカーソル - - - - 四角 - 下線 - 縦線 - - - - 4 x 8ピクセル - 6 pt - 7 pt - 8 pt - 9 pt - 10 pt - 12 pt - 14 pt - 16 pt - 20 pt - - - - 白背景に黒字 - 黒背景に白字 - 青背景に白字 - 黒背景に緑字 - 黒背景にアンバー字 - 黒背景に赤字 - - - - トラックボール - \@キー - 左Altキー - 右Altキー - ボリュームアップ - ボリュームダウン - - - - 文字ベース - 単語ベース - - - diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml deleted file mode 100644 index 7c96949ae..000000000 --- a/res/values-ja/strings.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - 端末エミュレータ - 設定 - 端末をリセット - メール送信 - 特殊キー - ソフトキーボード - - テキスト編集 - すべてコピー - 貼付け - - - スクリーン - - ステータスバー - ステータスバーの表示/非表示 - ステータスバー - - カーソルのスタイル - カーソルスタイルの選択 - カーソルスタイル - - カーソルの点滅 - カーソルの点滅を選択 - カーソルの点滅 - - テキスト - - フォントサイズ - 文字の高さと大きさを選択 - Font size - - - 文字の色を選択 - 文字の色 - - キーボード - - コントロールキー - コントロールキーを選択 - コントロールキー - - インプットメソッド - ソフトキーボードのインプットメソッドを選択 - インプットメソッド - - シェル - コマンドライン - コマンドラインシェルを指定 - シェル - - 初期コマンド - 開始時、シェルにコマンドを送信する - 初期コマンド - - diff --git a/res/values-ru/arrays.xml b/res/values-ru/arrays.xml deleted file mode 100644 index 5be72f1e6..000000000 --- a/res/values-ru/arrays.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - Показывать - Скрывать - - - - 4 x 8 пикселей - 6 pt - 7 pt - 8 pt - 9 pt - 10 pt - 12 pt - 14 pt - 16 pt - 20 pt - - - - Чёрный на белом - Белый на чёрном - Белый на синем - Зелёный на чёрном - Жёлтый на чёрном - Красный на чёрном - - - - Jog ball - Клавиша \@ - Левый Alt - Правый Alt - Громкость вверх - Громкость вниз - - - - По знакам - По словам - - - diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml deleted file mode 100644 index ca1fcfb07..000000000 --- a/res/values-ru/strings.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - Эмулятор Терминала - Настройки - Сбросить терм. - Отправит Email - Специальные клавиши - Экранная клавиатура - Изменить - Копировать всё - Вставить - Экран - Статус бар - Показать/Скрыть статус бар. - Статус бар - Текст - Размер шрифта - Выберите размер шрифта. - Размер шрифта - Цвета - Выберите цвет текста. - Цвет текста - Клавиатура - Клавиша Control - Выберите что будет клавишей Control. - Клавиша Control - Способ ввода - Выберите способ ввода для экранной клавиатуры. - Способ ввода - Командная оболочка - Командная строка - Укажите строку обращения к командной оболочки. - Оболочка - Команды запуска - Передаются облочке при запуске терминала. - Команды запуска - diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml deleted file mode 100644 index 6f47b4f0b..000000000 --- a/res/values-zh-rCN/strings.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - 终端模拟器 - 首选项 - 重置终端 - 发送电子邮件到... - 特殊键 - 打开/关闭软键盘 - - 编辑文本 - 全部复制 - 粘贴 - - - 屏幕 - - 状态栏 - 显示/隐藏状态栏。 - 状态栏 - - 光标样式 - 选择光标样式 - 光标样式 - - 光标闪烁 - 选择光标闪烁模式 - 光标闪烁 - - 文本 - - 文本大小 - 选择文本大小 - 文本大小 - - 颜色 - 选择文本颜色 - 文本颜色 - - 键盘 - - Ctrl 键 - 设置 Ctrl 键 - Ctrl 键 - - 输入方式 - 选择输入方式或软键盘 - 输入方式 - - Shell - 命令行 - 指定命令行使用的 Shell - Shell - - 初始命令 - 启动 Shell 时自动执行的命令 - 初始命令 - - diff --git a/res/values/strings.xml b/res/values/strings.xml deleted file mode 100644 index a39e26f79..000000000 --- a/res/values/strings.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - Terminal Emulator - Preferences - Reset term - Email to - Special keys - Toggle soft keyboard - - Edit text - Select text - Copy all - Paste - - - Screen - - Status bar - Show/hide status bar. - Status bar - - Cursor style - Choose cursor style. - Cursor style - - Cursor blink - Choose cursor blink. - Cursor blink - - Text - - Font size - Choose character height in points. - Font size - - Colors - Choose text color. - Text color - - Keyboard - - Control key - Choose control key. - Control key - - Input method - Choose input method for soft keyboard. - Input method - - Shell - Command line - Specify the shell command line. - Shell - - Initial command - Sent to the shell when it starts. - Initial Command - - - 0 - 0 - 0 - 10 - 2 - 0 - 0 - /system/bin/sh - - export PATH=/data/local/bin:$PATH - diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml deleted file mode 100644 index df50b011a..000000000 --- a/res/xml/preferences.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/intents/.gitignore b/samples/intents/.gitignore new file mode 100644 index 000000000..796b96d1c --- /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 000000000..0ab284c86 --- /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 000000000..45998cd64 --- /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 000000000..1645991cc --- /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 000000000..ed0a173d3 --- /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 000000000..96ea176e4 --- /dev/null +++ b/samples/intents/src/main/res/layout/main.xml @@ -0,0 +1,42 @@ + + + + + +