2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package jackpal.androidterm;
19 import java.io.FileDescriptor;
20 import java.io.FileOutputStream;
21 import java.io.IOException;
22 import java.io.UnsupportedEncodingException;
23 import java.util.ArrayList;
25 import android.app.Activity;
26 import android.app.AlertDialog;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.content.res.Configuration;
31 import android.content.res.Resources;
32 import android.net.Uri;
33 import android.net.wifi.WifiManager;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.Message;
37 import android.os.PowerManager;
38 import android.preference.PreferenceManager;
39 import android.text.ClipboardManager;
40 import android.util.DisplayMetrics;
41 import android.util.Log;
42 import android.view.ContextMenu;
43 import android.view.ContextMenu.ContextMenuInfo;
44 import android.view.KeyEvent;
45 import android.view.Menu;
46 import android.view.MenuItem;
47 import android.view.MotionEvent;
48 import android.view.View;
49 import android.view.Window;
50 import android.view.WindowManager;
51 import android.view.inputmethod.InputMethodManager;
54 * A terminal emulator activity.
57 public class Term extends Activity {
59 * Our main view. Displays the emulated terminal screen.
61 private EmulatorView mEmulatorView;
64 * The pseudo-teletype (pty) file descriptor that we use to communicate with
65 * another process, typically a shell.
67 private FileDescriptor mTermFd;
70 * Used to send data to the remote process.
72 private FileOutputStream mTermOut;
75 * The process ID of the remote process.
77 private int mProcId = 0;
80 * A key listener that tracks the modifier keys and allows the full ASCII
81 * character set to be entered.
83 private TermKeyListener mKeyListener;
86 * The name of our emulator view in the view resource.
88 private static final int EMULATOR_VIEW = R.id.emulatorView;
90 private int mStatusBar = 0;
91 private int mCursorStyle = 0;
92 private int mCursorBlink = 0;
93 private int mFontSize = 9;
94 private int mColorId = 2;
95 private int mControlKeyId = 5; // Default to Volume Down
96 private int mFnKeyId = 4; // Default to Volume Up
97 private int mUseCookedIME = 0;
99 private static final String STATUSBAR_KEY = "statusbar";
100 private static final String CURSORSTYLE_KEY = "cursorstyle";
101 private static final String CURSORBLINK_KEY = "cursorblink";
102 private static final String FONTSIZE_KEY = "fontsize";
103 private static final String COLOR_KEY = "color";
104 private static final String CONTROLKEY_KEY = "controlkey";
105 private static final String FNKEY_KEY = "fnkey";
106 private static final String IME_KEY = "ime";
107 private static final String SHELL_KEY = "shell";
108 private static final String INITIALCOMMAND_KEY = "initialcommand";
110 public static final int WHITE = 0xffffffff;
111 public static final int BLACK = 0xff000000;
112 public static final int BLUE = 0xff344ebd;
113 public static final int GREEN = 0xff00ff00;
114 public static final int AMBER = 0xffffb651;
115 public static final int RED = 0xffff0113;
117 private static final int[][] COLOR_SCHEMES = {
118 {BLACK, WHITE}, {WHITE, BLACK}, {WHITE, BLUE}, {GREEN, BLACK}, {AMBER, BLACK}, {RED, BLACK}};
120 private static final int CONTROL_KEY_ID_NONE = 7;
121 /** An integer not in the range of real key codes. */
122 private static final int KEYCODE_NONE = -1;
124 private static final int[] CONTROL_KEY_SCHEMES = {
125 KeyEvent.KEYCODE_DPAD_CENTER,
127 KeyEvent.KEYCODE_ALT_LEFT,
128 KeyEvent.KEYCODE_ALT_RIGHT,
129 KeyEvent.KEYCODE_VOLUME_UP,
130 KeyEvent.KEYCODE_VOLUME_DOWN,
131 KeyEvent.KEYCODE_CAMERA,
135 private static final int FN_KEY_ID_NONE = 7;
137 private static final int[] FN_KEY_SCHEMES = {
138 KeyEvent.KEYCODE_DPAD_CENTER,
140 KeyEvent.KEYCODE_ALT_LEFT,
141 KeyEvent.KEYCODE_ALT_RIGHT,
142 KeyEvent.KEYCODE_VOLUME_UP,
143 KeyEvent.KEYCODE_VOLUME_DOWN,
144 KeyEvent.KEYCODE_CAMERA,
148 private int mControlKeyCode;
149 private int mFnKeyCode;
151 private final static String DEFAULT_SHELL = "/system/bin/sh -";
152 private String mShell;
154 private final static String DEFAULT_INITIAL_COMMAND =
155 "export PATH=/data/local/bin:$PATH";
156 private String mInitialCommand;
158 private SharedPreferences mPrefs;
160 private final static int SELECT_TEXT_ID = 0;
161 private final static int COPY_ALL_ID = 1;
162 private final static int PASTE_ID = 2;
164 private boolean mAlreadyStarted = false;
166 public TermService mTermService;
167 private Intent TSIntent;
169 private PowerManager.WakeLock mWakeLock;
170 private WifiManager.WifiLock mWifiLock;
173 public void onCreate(Bundle icicle) {
174 super.onCreate(icicle);
175 Log.e(TermDebug.LOG_TAG, "onCreate");
176 mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
179 TSIntent = new Intent(this, TermService.class);
180 startService(TSIntent);
182 setContentView(R.layout.term_activity);
184 mEmulatorView = (EmulatorView) findViewById(EMULATOR_VIEW);
186 DisplayMetrics metrics = new DisplayMetrics();
187 getWindowManager().getDefaultDisplay().getMetrics(metrics);
188 mEmulatorView.setScaledDensity(metrics.scaledDensity);
192 mKeyListener = new TermKeyListener();
194 mEmulatorView.setFocusable(true);
195 mEmulatorView.setFocusableInTouchMode(true);
196 mEmulatorView.requestFocus();
197 mEmulatorView.register(this, mKeyListener);
199 registerForContextMenu(mEmulatorView);
201 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
202 mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermDebug.LOG_TAG);
203 WifiManager wm = (WifiManager)getSystemService(Context.WIFI_SERVICE);
204 mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL, TermDebug.LOG_TAG);
207 mAlreadyStarted = true;
211 public void onDestroy() {
214 Exec.hangupProcessGroup(mProcId);
217 if (mTermFd != null) {
221 if (mWakeLock.isHeld()) {
224 if (mWifiLock.isHeld()) {
227 stopService(TSIntent);
230 private void startListening() {
231 int[] processId = new int[1];
233 createSubprocess(processId);
234 mProcId = processId[0];
236 final Handler handler = new Handler() {
238 public void handleMessage(Message msg) {
242 Runnable watchForDeath = new Runnable() {
245 Log.i(TermDebug.LOG_TAG, "waiting for: " + mProcId);
246 int result = Exec.waitFor(mProcId);
247 Log.i(TermDebug.LOG_TAG, "Subprocess exited: " + result);
248 handler.sendEmptyMessage(result);
252 Thread watcher = new Thread(watchForDeath);
255 mTermOut = new FileOutputStream(mTermFd);
257 mEmulatorView.initialize(mTermFd, mTermOut);
259 /* Check whether we've received an initial command from the
260 * launching application
262 String iInitialCommand = getIntent().getStringExtra("jackpal.androidterm.iInitialCommand");
263 if (iInitialCommand != null) {
264 if (mInitialCommand != null) {
265 mInitialCommand += "\r" + iInitialCommand;
267 mInitialCommand = iInitialCommand;
271 sendInitialCommand();
274 private void sendInitialCommand() {
275 String initialCommand = mInitialCommand;
276 if (initialCommand == null || initialCommand.equals("")) {
277 initialCommand = DEFAULT_INITIAL_COMMAND;
279 if (initialCommand.length() > 0) {
280 write(initialCommand + '\r');
284 private void restart() {
285 startActivity(getIntent());
289 private void write(String data) {
291 mTermOut.write(data.getBytes());
293 } catch (IOException e) {
295 // We don't really care if the receiver isn't listening.
296 // We just make a best effort to answer the query.
300 private void createSubprocess(int[] processId) {
301 String shell = mShell;
302 if (shell == null || shell.equals("")) {
303 shell = DEFAULT_SHELL;
305 ArrayList<String> args = parse(shell);
306 String arg0 = args.get(0);
309 if (args.size() >= 2) {
312 if (args.size() >= 3) {
315 mTermFd = Exec.createSubprocess(arg0, arg1, arg2, processId);
318 private ArrayList<String> parse(String cmd) {
320 final int WHITESPACE = 1;
321 final int INQUOTE = 2;
322 int state = WHITESPACE;
323 ArrayList<String> result = new ArrayList<String>();
324 int cmdLen = cmd.length();
325 StringBuilder builder = new StringBuilder();
326 for (int i = 0; i < cmdLen; i++) {
327 char c = cmd.charAt(i);
328 if (state == PLAIN) {
329 if (Character.isWhitespace(c)) {
330 result.add(builder.toString());
331 builder.delete(0,builder.length());
333 } else if (c == '"') {
338 } else if (state == WHITESPACE) {
339 if (Character.isWhitespace(c)) {
341 } else if (c == '"') {
347 } else if (state == INQUOTE) {
349 if (i + 1 < cmdLen) {
351 builder.append(cmd.charAt(i));
353 } else if (c == '"') {
360 if (builder.length() > 0) {
361 result.add(builder.toString());
366 private void readPrefs() {
367 mStatusBar = readIntPref(STATUSBAR_KEY, mStatusBar, 1);
368 // mCursorStyle = readIntPref(CURSORSTYLE_KEY, mCursorStyle, 2);
369 // mCursorBlink = readIntPref(CURSORBLINK_KEY, mCursorBlink, 1);
370 mFontSize = readIntPref(FONTSIZE_KEY, mFontSize, 20);
371 mColorId = readIntPref(COLOR_KEY, mColorId, COLOR_SCHEMES.length - 1);
372 mControlKeyId = readIntPref(CONTROLKEY_KEY, mControlKeyId,
373 CONTROL_KEY_SCHEMES.length - 1);
374 mFnKeyId = readIntPref(FNKEY_KEY, mFnKeyId,
375 FN_KEY_SCHEMES.length - 1);
376 mUseCookedIME = readIntPref(IME_KEY, mUseCookedIME, 1);
378 String newShell = readStringPref(SHELL_KEY, mShell);
379 if ((newShell == null) || ! newShell.equals(mShell)) {
380 if (mShell != null) {
381 Log.i(TermDebug.LOG_TAG, "New shell set. Restarting.");
388 String newInitialCommand = readStringPref(INITIALCOMMAND_KEY,
390 if ((newInitialCommand == null)
391 || ! newInitialCommand.equals(mInitialCommand)) {
392 if (mInitialCommand != null) {
393 Log.i(TermDebug.LOG_TAG, "New initial command set. Restarting.");
396 mInitialCommand = newInitialCommand;
401 private void updatePrefs() {
402 DisplayMetrics metrics = new DisplayMetrics();
403 getWindowManager().getDefaultDisplay().getMetrics(metrics);
404 mEmulatorView.setTextSize((int) (mFontSize * metrics.density));
405 mEmulatorView.setCursorStyle(mCursorStyle, mCursorBlink);
406 mEmulatorView.setUseCookedIME(mUseCookedIME != 0);
408 mControlKeyCode = CONTROL_KEY_SCHEMES[mControlKeyId];
409 mFnKeyCode = FN_KEY_SCHEMES[mFnKeyId];
411 Window win = getWindow();
412 WindowManager.LayoutParams params = win.getAttributes();
413 final int FULLSCREEN = WindowManager.LayoutParams.FLAG_FULLSCREEN;
414 int desiredFlag = mStatusBar != 0 ? 0 : FULLSCREEN;
415 if (desiredFlag != (params.flags & FULLSCREEN)) {
416 if (mAlreadyStarted) {
417 // Can't switch to/from fullscreen after
418 // starting the activity.
421 win.setFlags(desiredFlag, FULLSCREEN);
427 private int readIntPref(String key, int defaultValue, int maxValue) {
430 val = Integer.parseInt(
431 mPrefs.getString(key, Integer.toString(defaultValue)));
432 } catch (NumberFormatException e) {
435 val = Math.max(0, Math.min(val, maxValue));
439 private String readStringPref(String key, String defaultValue) {
440 return mPrefs.getString(key, defaultValue);
443 public int getControlKeyCode() {
444 return mControlKeyCode;
447 public int getFnKeyCode() {
452 public void onResume() {
456 mEmulatorView.onResume();
460 public void onPause() {
462 mEmulatorView.onPause();
466 public void onConfigurationChanged(Configuration newConfig) {
467 super.onConfigurationChanged(newConfig);
469 mEmulatorView.updateSize(true);
473 public boolean onCreateOptionsMenu(Menu menu) {
474 getMenuInflater().inflate(R.menu.main, menu);
479 public boolean onOptionsItemSelected(MenuItem item) {
480 int id = item.getItemId();
481 if (id == R.id.menu_preferences) {
483 } else if (id == R.id.menu_reset) {
485 } else if (id == R.id.menu_send_email) {
487 } else if (id == R.id.menu_special_keys) {
489 } else if (id == R.id.menu_toggle_soft_keyboard) {
490 doToggleSoftKeyboard();
491 } else if (id == R.id.menu_toggle_wakelock) {
493 } else if (id == R.id.menu_toggle_wifilock) {
496 return super.onOptionsItemSelected(item);
500 public boolean onPrepareOptionsMenu(Menu menu) {
501 MenuItem wakeLockItem = menu.findItem(R.id.menu_toggle_wakelock);
502 MenuItem wifiLockItem = menu.findItem(R.id.menu_toggle_wifilock);
503 if (mWakeLock.isHeld()) {
504 wakeLockItem.setTitle(R.string.disable_wakelock);
506 wakeLockItem.setTitle(R.string.enable_wakelock);
508 if (mWifiLock.isHeld()) {
509 wifiLockItem.setTitle(R.string.disable_wifilock);
511 wifiLockItem.setTitle(R.string.enable_wifilock);
513 return super.onPrepareOptionsMenu(menu);
517 public void onCreateContextMenu(ContextMenu menu, View v,
518 ContextMenuInfo menuInfo) {
519 super.onCreateContextMenu(menu, v, menuInfo);
520 menu.setHeaderTitle(R.string.edit_text);
521 menu.add(0, SELECT_TEXT_ID, 0, R.string.select_text);
522 menu.add(0, COPY_ALL_ID, 0, R.string.copy_all);
523 menu.add(0, PASTE_ID, 0, R.string.paste);
525 menu.getItem(PASTE_ID).setEnabled(false);
530 public boolean onContextItemSelected(MenuItem item) {
531 switch (item.getItemId()) {
533 mEmulatorView.toggleSelectingText();
542 return super.onContextItemSelected(item);
546 private boolean canPaste() {
547 ClipboardManager clip = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
548 if (clip.hasText()) {
554 private void doPreferences() {
555 startActivity(new Intent(this, TermPreferences.class));
558 private void setColors() {
559 int[] scheme = COLOR_SCHEMES[mColorId];
560 mEmulatorView.setColors(scheme[0], scheme[1]);
563 private void doResetTerminal() {
567 private void doEmailTranscript() {
568 // Don't really want to supply an address, but
569 // currently it's required, otherwise we get an
571 String addr = "user@example.com";
573 new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"
576 intent.putExtra("body", mEmulatorView.getTranscriptText().trim());
577 startActivity(intent);
580 private void doCopyAll() {
581 ClipboardManager clip = (ClipboardManager)
582 getSystemService(Context.CLIPBOARD_SERVICE);
583 clip.setText(mEmulatorView.getTranscriptText().trim());
586 private void doPaste() {
587 ClipboardManager clip = (ClipboardManager)
588 getSystemService(Context.CLIPBOARD_SERVICE);
589 CharSequence paste = clip.getText();
592 utf8 = paste.toString().getBytes("UTF-8");
593 } catch (UnsupportedEncodingException e) {
594 Log.e(TermDebug.LOG_TAG, "UTF-8 encoding not found.");
598 mTermOut.write(utf8);
599 } catch (IOException e) {
600 Log.e(TermDebug.LOG_TAG, "could not write paste text to terminal.");
604 private void doDocumentKeys() {
605 AlertDialog.Builder dialog = new AlertDialog.Builder(this);
606 Resources r = getResources();
607 dialog.setTitle(r.getString(R.string.control_key_dialog_title));
609 formatMessage(mControlKeyId, CONTROL_KEY_ID_NONE,
610 r, R.array.control_keys_short_names,
611 R.string.control_key_dialog_control_text,
612 R.string.control_key_dialog_control_disabled_text, "CTRLKEY")
614 formatMessage(mFnKeyId, FN_KEY_ID_NONE,
615 r, R.array.fn_keys_short_names,
616 R.string.control_key_dialog_fn_text,
617 R.string.control_key_dialog_fn_disabled_text, "FNKEY"));
621 private String formatMessage(int keyId, int disabledKeyId,
622 Resources r, int arrayId,
624 int disabledId, String regex) {
625 if (keyId == disabledKeyId) {
626 return r.getString(disabledId);
628 String[] keyNames = r.getStringArray(arrayId);
629 String keyName = keyNames[keyId];
630 String template = r.getString(enabledId);
631 String result = template.replaceAll(regex, keyName);
635 private void doToggleSoftKeyboard() {
636 InputMethodManager imm = (InputMethodManager)
637 getSystemService(Context.INPUT_METHOD_SERVICE);
638 imm.toggleSoftInput(InputMethodManager.SHOW_FORCED,0);
642 private void doToggleWakeLock() {
643 if (mWakeLock.isHeld()) {
650 private void doToggleWifiLock() {
651 if (mWifiLock.isHeld()) {