OSDN Git Service

99237052e382726f0d64cc1e784c8c7e38ba78e9
[android-x86/packages-apps-AndroidTerm.git] / src / jackpal / androidterm / Term.java
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package jackpal.androidterm;
18
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;
24
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;
52
53 /**
54  * A terminal emulator activity.
55  */
56
57 public class Term extends Activity {
58     /**
59      * Our main view. Displays the emulated terminal screen.
60      */
61     private EmulatorView mEmulatorView;
62
63     /**
64      * The pseudo-teletype (pty) file descriptor that we use to communicate with
65      * another process, typically a shell.
66      */
67     private FileDescriptor mTermFd;
68
69     /**
70      * Used to send data to the remote process.
71      */
72     private FileOutputStream mTermOut;
73
74     /**
75      * The process ID of the remote process.
76      */
77     private int mProcId = 0;
78
79     /**
80      * A key listener that tracks the modifier keys and allows the full ASCII
81      * character set to be entered.
82      */
83     private TermKeyListener mKeyListener;
84
85     /**
86      * The name of our emulator view in the view resource.
87      */
88     private static final int EMULATOR_VIEW = R.id.emulatorView;
89
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;
98
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";
109
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;
116
117     private static final int[][] COLOR_SCHEMES = {
118         {BLACK, WHITE}, {WHITE, BLACK}, {WHITE, BLUE}, {GREEN, BLACK}, {AMBER, BLACK}, {RED, BLACK}};
119
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;
123
124         private static final int[] CONTROL_KEY_SCHEMES = {
125         KeyEvent.KEYCODE_DPAD_CENTER,
126         KeyEvent.KEYCODE_AT,
127         KeyEvent.KEYCODE_ALT_LEFT,
128         KeyEvent.KEYCODE_ALT_RIGHT,
129         KeyEvent.KEYCODE_VOLUME_UP,
130         KeyEvent.KEYCODE_VOLUME_DOWN,
131         KeyEvent.KEYCODE_CAMERA,
132         KEYCODE_NONE
133     };
134
135     private static final int FN_KEY_ID_NONE = 7;
136
137     private static final int[] FN_KEY_SCHEMES = {
138         KeyEvent.KEYCODE_DPAD_CENTER,
139         KeyEvent.KEYCODE_AT,
140         KeyEvent.KEYCODE_ALT_LEFT,
141         KeyEvent.KEYCODE_ALT_RIGHT,
142         KeyEvent.KEYCODE_VOLUME_UP,
143         KeyEvent.KEYCODE_VOLUME_DOWN,
144         KeyEvent.KEYCODE_CAMERA,
145         KEYCODE_NONE
146     };
147
148     private int mControlKeyCode;
149     private int mFnKeyCode;
150
151     private final static String DEFAULT_SHELL = "/system/bin/sh -";
152     private String mShell;
153
154     private final static String DEFAULT_INITIAL_COMMAND =
155         "export PATH=/data/local/bin:$PATH";
156     private String mInitialCommand;
157
158     private SharedPreferences mPrefs;
159
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;
163
164     private boolean mAlreadyStarted = false;
165
166     public TermService mTermService;
167     private Intent TSIntent;
168
169     private PowerManager.WakeLock mWakeLock;
170     private WifiManager.WifiLock mWifiLock;
171
172     @Override
173     public void onCreate(Bundle icicle) {
174         super.onCreate(icicle);
175         Log.e(TermDebug.LOG_TAG, "onCreate");
176         mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
177         readPrefs();
178
179         TSIntent = new Intent(this, TermService.class);
180         startService(TSIntent);
181
182         setContentView(R.layout.term_activity);
183
184         mEmulatorView = (EmulatorView) findViewById(EMULATOR_VIEW);
185
186         DisplayMetrics metrics = new DisplayMetrics();
187         getWindowManager().getDefaultDisplay().getMetrics(metrics);
188         mEmulatorView.setScaledDensity(metrics.scaledDensity);
189
190         startListening();
191
192         mKeyListener = new TermKeyListener();
193
194         mEmulatorView.setFocusable(true);
195         mEmulatorView.setFocusableInTouchMode(true);
196         mEmulatorView.requestFocus();
197         mEmulatorView.register(this, mKeyListener);
198
199         registerForContextMenu(mEmulatorView);
200
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);
205
206         updatePrefs();
207         mAlreadyStarted = true;
208     }
209
210     @Override
211     public void onDestroy() {
212         super.onDestroy();
213         if (mProcId != 0) {
214             Exec.hangupProcessGroup(mProcId);
215             mProcId = 0;
216         }
217         if (mTermFd != null) {
218             Exec.close(mTermFd);
219             mTermFd = null;
220         }
221         if (mWakeLock.isHeld()) {
222             mWakeLock.release();
223         }
224         if (mWifiLock.isHeld()) {
225             mWifiLock.release();
226         }
227         stopService(TSIntent);
228     }
229
230     private void startListening() {
231         int[] processId = new int[1];
232
233         createSubprocess(processId);
234         mProcId = processId[0];
235
236         final Handler handler = new Handler() {
237             @Override
238             public void handleMessage(Message msg) {
239             }
240         };
241
242         Runnable watchForDeath = new Runnable() {
243
244             public void run() {
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);
249              }
250
251         };
252         Thread watcher = new Thread(watchForDeath);
253         watcher.start();
254
255         mTermOut = new FileOutputStream(mTermFd);
256
257         mEmulatorView.initialize(mTermFd, mTermOut);
258
259         /* Check whether we've received an initial command from the
260          * launching application
261          */
262         String iInitialCommand = getIntent().getStringExtra("jackpal.androidterm.iInitialCommand");
263         if (iInitialCommand != null) {
264             if (mInitialCommand != null) {
265                 mInitialCommand += "\r" + iInitialCommand;
266             } else {
267                 mInitialCommand = iInitialCommand;
268             }
269         }
270
271         sendInitialCommand();
272     }
273
274     private void sendInitialCommand() {
275         String initialCommand = mInitialCommand;
276         if (initialCommand == null || initialCommand.equals("")) {
277             initialCommand = DEFAULT_INITIAL_COMMAND;
278         }
279         if (initialCommand.length() > 0) {
280             write(initialCommand + '\r');
281         }
282     }
283
284     private void restart() {
285         startActivity(getIntent());
286         finish();
287     }
288
289     private void write(String data) {
290         try {
291             mTermOut.write(data.getBytes());
292             mTermOut.flush();
293         } catch (IOException e) {
294             // Ignore exception
295             // We don't really care if the receiver isn't listening.
296             // We just make a best effort to answer the query.
297         }
298     }
299
300     private void createSubprocess(int[] processId) {
301         String shell = mShell;
302         if (shell == null || shell.equals("")) {
303             shell = DEFAULT_SHELL;
304         }
305         ArrayList<String> args = parse(shell);
306         String arg0 = args.get(0);
307         String arg1 = null;
308         String arg2 = null;
309         if (args.size() >= 2) {
310             arg1 = args.get(1);
311         }
312         if (args.size() >= 3) {
313             arg2 = args.get(2);
314         }
315         mTermFd = Exec.createSubprocess(arg0, arg1, arg2, processId);
316     }
317
318     private ArrayList<String> parse(String cmd) {
319         final int PLAIN = 0;
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());
332                     state = WHITESPACE;
333                 } else if (c == '"') {
334                     state = INQUOTE;
335                 } else {
336                     builder.append(c);
337                 }
338             } else if (state == WHITESPACE) {
339                 if (Character.isWhitespace(c)) {
340                     // do nothing
341                 } else if (c == '"') {
342                     state = INQUOTE;
343                 } else {
344                     state = PLAIN;
345                     builder.append(c);
346                 }
347             } else if (state == INQUOTE) {
348                 if (c == '\\') {
349                     if (i + 1 < cmdLen) {
350                         i += 1;
351                         builder.append(cmd.charAt(i));
352                     }
353                 } else if (c == '"') {
354                     state = PLAIN;
355                 } else {
356                     builder.append(c);
357                 }
358             }
359         }
360         if (builder.length() > 0) {
361             result.add(builder.toString());
362         }
363         return result;
364     }
365
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);
377         {
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.");
382                     restart();
383                 }
384                 mShell = newShell;
385             }
386         }
387         {
388             String newInitialCommand = readStringPref(INITIALCOMMAND_KEY,
389                     mInitialCommand);
390             if ((newInitialCommand == null)
391                     || ! newInitialCommand.equals(mInitialCommand)) {
392                 if (mInitialCommand != null) {
393                     Log.i(TermDebug.LOG_TAG, "New initial command set. Restarting.");
394                     restart();
395                 }
396                 mInitialCommand = newInitialCommand;
397             }
398         }
399     }
400
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);
407         setColors();
408         mControlKeyCode = CONTROL_KEY_SCHEMES[mControlKeyId];
409         mFnKeyCode = FN_KEY_SCHEMES[mFnKeyId];
410         {
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.
419                     restart();
420                 } else {
421                     win.setFlags(desiredFlag, FULLSCREEN);
422                 }
423             }
424         }
425     }
426
427     private int readIntPref(String key, int defaultValue, int maxValue) {
428         int val;
429         try {
430             val = Integer.parseInt(
431                 mPrefs.getString(key, Integer.toString(defaultValue)));
432         } catch (NumberFormatException e) {
433             val = defaultValue;
434         }
435         val = Math.max(0, Math.min(val, maxValue));
436         return val;
437     }
438
439     private String readStringPref(String key, String defaultValue) {
440         return mPrefs.getString(key, defaultValue);
441     }
442
443     public int getControlKeyCode() {
444         return mControlKeyCode;
445     }
446
447     public int getFnKeyCode() {
448         return mFnKeyCode;
449     }
450
451     @Override
452     public void onResume() {
453         super.onResume();
454         readPrefs();
455         updatePrefs();
456         mEmulatorView.onResume();
457     }
458
459     @Override
460     public void onPause() {
461         super.onPause();
462         mEmulatorView.onPause();
463     }
464
465     @Override
466     public void onConfigurationChanged(Configuration newConfig) {
467         super.onConfigurationChanged(newConfig);
468
469         mEmulatorView.updateSize(true);
470     }
471
472     @Override
473     public boolean onCreateOptionsMenu(Menu menu) {
474         getMenuInflater().inflate(R.menu.main, menu);
475         return true;
476     }
477
478     @Override
479     public boolean onOptionsItemSelected(MenuItem item) {
480         int id = item.getItemId();
481         if (id == R.id.menu_preferences) {
482             doPreferences();
483         } else if (id == R.id.menu_reset) {
484             doResetTerminal();
485         } else if (id == R.id.menu_send_email) {
486             doEmailTranscript();
487         } else if (id == R.id.menu_special_keys) {
488             doDocumentKeys();
489         } else if (id == R.id.menu_toggle_soft_keyboard) {
490             doToggleSoftKeyboard();
491         } else if (id == R.id.menu_toggle_wakelock) {
492             doToggleWakeLock();
493         } else if (id == R.id.menu_toggle_wifilock) {
494             doToggleWifiLock();
495         }
496         return super.onOptionsItemSelected(item);
497     }
498
499     @Override
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);
505         } else {
506             wakeLockItem.setTitle(R.string.enable_wakelock);
507         }
508         if (mWifiLock.isHeld()) {
509             wifiLockItem.setTitle(R.string.disable_wifilock);
510         } else {
511             wifiLockItem.setTitle(R.string.enable_wifilock);
512         }
513         return super.onPrepareOptionsMenu(menu);
514     }
515
516     @Override
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);
524       if (!canPaste()) {
525           menu.getItem(PASTE_ID).setEnabled(false);
526       }
527     }
528
529     @Override
530     public boolean onContextItemSelected(MenuItem item) {
531           switch (item.getItemId()) {
532           case SELECT_TEXT_ID:
533             mEmulatorView.toggleSelectingText();
534             return true;
535           case COPY_ALL_ID:
536             doCopyAll();
537             return true;
538           case PASTE_ID:
539             doPaste();
540             return true;
541           default:
542             return super.onContextItemSelected(item);
543           }
544         }
545
546     private boolean canPaste() {
547         ClipboardManager clip = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
548         if (clip.hasText()) {
549             return true;
550         }
551         return false;
552     }
553
554     private void doPreferences() {
555         startActivity(new Intent(this, TermPreferences.class));
556     }
557
558     private void setColors() {
559         int[] scheme = COLOR_SCHEMES[mColorId];
560         mEmulatorView.setColors(scheme[0], scheme[1]);
561     }
562
563     private void doResetTerminal() {
564         restart();
565     }
566
567     private void doEmailTranscript() {
568         // Don't really want to supply an address, but
569         // currently it's required, otherwise we get an
570         // exception.
571         String addr = "user@example.com";
572         Intent intent =
573                 new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"
574                         + addr));
575
576         intent.putExtra("body", mEmulatorView.getTranscriptText().trim());
577         startActivity(intent);
578     }
579
580     private void doCopyAll() {
581         ClipboardManager clip = (ClipboardManager)
582              getSystemService(Context.CLIPBOARD_SERVICE);
583         clip.setText(mEmulatorView.getTranscriptText().trim());
584     }
585
586     private void doPaste() {
587         ClipboardManager clip = (ClipboardManager)
588          getSystemService(Context.CLIPBOARD_SERVICE);
589         CharSequence paste = clip.getText();
590         byte[] utf8;
591         try {
592             utf8 = paste.toString().getBytes("UTF-8");
593         } catch (UnsupportedEncodingException e) {
594             Log.e(TermDebug.LOG_TAG, "UTF-8 encoding not found.");
595             return;
596         }
597         try {
598             mTermOut.write(utf8);
599         } catch (IOException e) {
600             Log.e(TermDebug.LOG_TAG, "could not write paste text to terminal.");
601         }
602     }
603
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));
608         dialog.setMessage(
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")
613             + "\n\n" +
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"));
618          dialog.show();
619      }
620
621      private String formatMessage(int keyId, int disabledKeyId,
622          Resources r, int arrayId,
623          int enabledId,
624          int disabledId, String regex) {
625          if (keyId == disabledKeyId) {
626              return r.getString(disabledId);
627          }
628          String[] keyNames = r.getStringArray(arrayId);
629          String keyName = keyNames[keyId];
630          String template = r.getString(enabledId);
631          String result = template.replaceAll(regex, keyName);
632          return result;
633     }
634
635     private void doToggleSoftKeyboard() {
636         InputMethodManager imm = (InputMethodManager)
637             getSystemService(Context.INPUT_METHOD_SERVICE);
638         imm.toggleSoftInput(InputMethodManager.SHOW_FORCED,0);
639
640     }
641
642     private void doToggleWakeLock() {
643         if (mWakeLock.isHeld()) {
644             mWakeLock.release();
645         } else {
646             mWakeLock.acquire();
647         }
648     }
649
650     private void doToggleWifiLock() {
651         if (mWifiLock.isHeld()) {
652             mWifiLock.release();
653         } else {
654             mWifiLock.acquire();
655         }
656     }
657 }