OSDN Git Service

Support multiple sessions per terminal activity.
authorSteven Luo <steven+android@steven676.net>
Sat, 27 Aug 2011 20:23:52 +0000 (13:23 -0700)
committerJack Palevich <jack.palevich@gmail.com>
Sat, 27 Aug 2011 20:23:52 +0000 (13:23 -0700)
This patch does the following:

* Replace the EmulatorView widget in the activity layout with a
  TermViewFlipper, which is a ViewFlipper with a couple of added
  conveniences for dealing with EmulatorViews (e.g. pausing/resuming
  them when changing the view).
* Add an ArrayList in the service to hold TermSessions, and make the
  activity bind to the service so that it can retrieve the list.  This isn't
  strictly necessary for multisession support, but allows us to decouple
  the activity from the sessions, which will (in the future) allow us to
  restart or even close the activity without losing running sessions.
* Now that EmulatorViews aren't being created automatically when the
  layout is inflated, but are instead created by hand, move a bunch of
  initialization stuff to the EmulatorView constructor.
* Make the input reader threads for each session exit on EOF.  These
  threads hold a reference to their TermSession objects, which would
  leak otherwise.

For now, one EmulatorView and TermSession are created as soon as the
activity binds to the service, if none exist; no UI to create additional
sessions is included.

res/layout/term_activity.xml
src/jackpal/androidterm/EmulatorView.java
src/jackpal/androidterm/Term.java
src/jackpal/androidterm/TermService.java
src/jackpal/androidterm/TermViewFlipper.java [new file with mode: 0644]
src/jackpal/androidterm/session/TermSession.java
src/jackpal/androidterm/session/TerminalEmulator.java

index 4c3da6d..b84a51a 100644 (file)
 */
 -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<jackpal.androidterm.TermViewFlipper xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/view_flipper"
     android:layout_width="fill_parent" 
     android:layout_height="fill_parent"
-    android:orientation="vertical">
-
-    <jackpal.androidterm.EmulatorView android:id="@+id/emulatorView"
-             android:layout_width="wrap_content"
-             android:layout_height="wrap_content"
-             android:layout_alignParentLeft="true"
-             />
-   
-</LinearLayout>
+    android:background="@android:color/black"
+    />
index 0ac912a..e02e52d 100644 (file)
@@ -167,6 +167,8 @@ public class EmulatorView extends View implements GestureDetector.OnGestureListe
     private boolean mIsSelectingText = false;
 
 
+    private float mDensity;
+
     private float mScaledDensity;
     private static final int SELECT_TEXT_OFFSET_Y = -40;
     private int mSelXAnchor = -1;
@@ -233,13 +235,15 @@ public class EmulatorView extends View implements GestureDetector.OnGestureListe
         return mUpdateNotify;
     }
 
-    public EmulatorView(Context context) {
+    public EmulatorView(Context context, TermSession session, TermViewFlipper viewFlipper, DisplayMetrics metrics) {
         super(context);
-        commonConstructor();
+        commonConstructor(session, viewFlipper);
+        setDensity(metrics);
     }
 
-    public void setScaledDensity(float scaledDensity) {
-        mScaledDensity = scaledDensity;
+    public void setDensity(DisplayMetrics metrics) {
+        mDensity = metrics.density;
+        mScaledDensity = metrics.scaledDensity;
     }
 
     public void onResume() {
@@ -257,9 +261,9 @@ public class EmulatorView extends View implements GestureDetector.OnGestureListe
         }
     }
 
-    public void updatePrefs(TermSettings settings, DisplayMetrics metrics) {
+    public void updatePrefs(TermSettings settings) {
         mSettings = settings;
-        setTextSize((int) (mSettings.getFontSize() * metrics.density));
+        setTextSize((int) (mSettings.getFontSize() * mDensity));
         setCursorStyle(mSettings.getCursorStyle(), mSettings.getCursorBlink());
         setUseCookedIME(mSettings.useCookedIME());
         setColors();
@@ -272,10 +276,6 @@ public class EmulatorView extends View implements GestureDetector.OnGestureListe
         updateText();
     }
 
-    public String getTranscriptText() {
-        return mEmulator.getTranscriptText();
-    }
-
     public void resetTerminal() {
         mEmulator.reset();
         invalidate();
@@ -570,21 +570,7 @@ public class EmulatorView extends View implements GestureDetector.OnGestureListe
         return mEmulator.getKeypadApplicationMode();
     }
 
-    public EmulatorView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public EmulatorView(Context context, AttributeSet attrs,
-            int defStyle) {
-        super(context, attrs, defStyle);
-        // TypedArray a =
-        //        context.obtainStyledAttributes(android.R.styleable.View);
-        // initializeScrollbars(a);
-        // a.recycle();
-        commonConstructor();
-    }
-
-    private void commonConstructor() {
+    private void commonConstructor(TermSession session, TermViewFlipper viewFlipper) {
         mTextRenderer = null;
         mCursorPaint = new Paint();
         mCursorPaint.setARGB(255,128,128,128);
@@ -594,6 +580,10 @@ public class EmulatorView extends View implements GestureDetector.OnGestureListe
         mGestureDetector = new GestureDetector(this);
         // mGestureDetector.setIsLongpressEnabled(false);
         setVerticalScrollBarEnabled(true);
+        setFocusable(true);
+        setFocusableInTouchMode(true);
+
+        initialize(session, viewFlipper);
     }
 
     @Override
@@ -616,7 +606,7 @@ public class EmulatorView extends View implements GestureDetector.OnGestureListe
      *
      * @param session The terminal session this view will be displaying
      */
-    public void initialize(TermSession session) {
+    private void initialize(TermSession session, TermViewFlipper viewFlipper) {
         mTermSession = session;
         mTranscriptScreen = session.getTranscriptScreen();
         mEmulator = session.getEmulator();
@@ -627,6 +617,8 @@ public class EmulatorView extends View implements GestureDetector.OnGestureListe
         mForeground = TermSettings.WHITE;
         mBackground = TermSettings.BLACK;
         updateText();
+
+        requestFocus();
     }
 
     /**
index 0e4a274..f912dca 100644 (file)
 package jackpal.androidterm;
 
 import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
 
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.ServiceConnection;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.net.Uri;
 import android.net.wifi.WifiManager;
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.PowerManager;
 import android.preference.PreferenceManager;
 import android.text.ClipboardManager;
@@ -35,6 +39,7 @@ import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
@@ -42,6 +47,7 @@ import android.view.View;
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.inputmethod.InputMethodManager;
+import android.widget.FrameLayout;
 
 import jackpal.androidterm.session.TermSession;
 import jackpal.androidterm.util.TermSettings;
@@ -52,18 +58,16 @@ import jackpal.androidterm.util.TermSettings;
 
 public class Term extends Activity {
     /**
-     * Our main view. Displays the emulated terminal screen.
+     * The ViewFlipper which holds the collection of EmulatorView widgets.
      */
-    private EmulatorView mEmulatorView;
+    private TermViewFlipper mViewFlipper;
 
     /**
-     * The name of our emulator view in the view resource.
+     * The name of the ViewFlipper in the resources.
      */
-    private static final int EMULATOR_VIEW = R.id.emulatorView;
+    private static final int VIEW_FLIPPER = R.id.view_flipper;
 
-    private TermSession mTermSession;
-
-    private String mInitialCommand;
+    private ArrayList<TermSession> mTermSessions;
 
     private SharedPreferences mPrefs;
     private TermSettings mSettings;
@@ -74,12 +78,27 @@ public class Term extends Activity {
 
     private boolean mAlreadyStarted = false;
 
-    public TermService mTermService;
     private Intent TSIntent;
 
     private PowerManager.WakeLock mWakeLock;
     private WifiManager.WifiLock mWifiLock;
 
+    private TermService mTermService;
+    private ServiceConnection mTSConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            Log.i(TermDebug.LOG_TAG, "Bound to TermService");
+            TermService.TSBinder binder = (TermService.TSBinder) service;
+            mTermService = binder.getService();
+            populateViewFlipper();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName arg0) {
+            mTermService = null;
+        }
+    };
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -90,35 +109,12 @@ public class Term extends Activity {
         TSIntent = new Intent(this, TermService.class);
         startService(TSIntent);
 
-        setContentView(R.layout.term_activity);
-
-        mEmulatorView = (EmulatorView) findViewById(EMULATOR_VIEW);
-
-        /* Check whether we've received an initial command from the
-         * launching application
-         */
-        mInitialCommand = mSettings.getInitialCommand();
-        String iInitialCommand = getIntent().getStringExtra("jackpal.androidterm.iInitialCommand");
-        if (iInitialCommand != null) {
-            if (mInitialCommand != null) {
-                mInitialCommand += "\r" + iInitialCommand;
-            } else {
-                mInitialCommand = iInitialCommand;
-            }
+        if (!bindService(TSIntent, mTSConnection, BIND_AUTO_CREATE)) {
+            Log.w(TermDebug.LOG_TAG, "bind to service failed!");
         }
-        mTermSession = new TermSession(mSettings, mEmulatorView.getUpdateCallback(), mInitialCommand);
 
-        mEmulatorView.initialize(mTermSession);
-
-        DisplayMetrics metrics = new DisplayMetrics();
-        getWindowManager().getDefaultDisplay().getMetrics(metrics);
-        mEmulatorView.setScaledDensity(metrics.scaledDensity);
-
-        mEmulatorView.setFocusable(true);
-        mEmulatorView.setFocusableInTouchMode(true);
-        mEmulatorView.requestFocus();
-
-        registerForContextMenu(mEmulatorView);
+        setContentView(R.layout.term_activity);
+        mViewFlipper = (TermViewFlipper) findViewById(VIEW_FLIPPER);
 
         PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
         mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TermDebug.LOG_TAG);
@@ -129,17 +125,38 @@ public class Term extends Activity {
         mAlreadyStarted = true;
     }
 
+    private void populateViewFlipper() {
+        if (mTermService != null) {
+            mTermSessions = mTermService.getSessions();
+
+            if (mTermSessions.size() == 0) {
+                mTermSessions.add(createTermSession());
+            }
+
+            for (TermSession session : mTermSessions) {
+                EmulatorView view = createEmulatorView(session);
+                mViewFlipper.addView(view);
+                mViewFlipper.showNext();
+            }
+
+            updatePrefs();
+        }
+    }
+
     @Override
     public void onDestroy() {
         super.onDestroy();
-        mTermSession.finish();
+        mViewFlipper.removeAllViews();
+        unbindService(mTSConnection);
+        stopService(TSIntent);
+        mTermService = null;
+        mTSConnection = null;
         if (mWakeLock.isHeld()) {
             mWakeLock.release();
         }
         if (mWifiLock.isHeld()) {
             mWifiLock.release();
         }
-        stopService(TSIntent);
     }
 
     private void restart() {
@@ -147,10 +164,57 @@ public class Term extends Activity {
         finish();
     }
 
+    private TermSession createTermSession() {
+        /* Check whether we've received an initial command from the
+         * launching application
+         */
+        String initialCommand = mSettings.getInitialCommand();
+        String iInitialCommand = getIntent().getStringExtra("jackpal.androidterm.iInitialCommand");
+        if (iInitialCommand != null) {
+            if (initialCommand != null) {
+                initialCommand += "\r" + iInitialCommand;
+            } else {
+                initialCommand = iInitialCommand;
+            }
+        }
+
+        return new TermSession(mSettings, null, initialCommand);
+    }
+
+    private EmulatorView createEmulatorView(TermSession session) {
+        DisplayMetrics metrics = new DisplayMetrics();
+        getWindowManager().getDefaultDisplay().getMetrics(metrics);
+        EmulatorView emulatorView = new EmulatorView(this, session, mViewFlipper, metrics);
+
+        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
+            FrameLayout.LayoutParams.WRAP_CONTENT,
+            FrameLayout.LayoutParams.WRAP_CONTENT,
+            Gravity.LEFT
+        );
+        emulatorView.setLayoutParams(params);
+
+        session.setUpdateCallback(emulatorView.getUpdateCallback());
+
+        registerForContextMenu(emulatorView);
+
+        return emulatorView;
+    }
+
+    private TermSession getCurrentTermSession() {
+        return mTermSessions.get(mViewFlipper.getDisplayedChild());
+    }
+
+    private EmulatorView getCurrentEmulatorView() {
+        return (EmulatorView) mViewFlipper.getCurrentView();
+    }
+
     private void updatePrefs() {
         DisplayMetrics metrics = new DisplayMetrics();
         getWindowManager().getDefaultDisplay().getMetrics(metrics);
-        mEmulatorView.updatePrefs(mSettings, metrics);
+
+        for (View v : mViewFlipper) {
+            ((EmulatorView) v).updatePrefs(mSettings, metrics);
+        }
         {
             Window win = getWindow();
             WindowManager.LayoutParams params = win.getAttributes();
@@ -174,20 +238,25 @@ public class Term extends Activity {
         mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
         mSettings.readPrefs(mPrefs);
         updatePrefs();
-        mEmulatorView.onResume();
+
+        mViewFlipper.resumeCurrentView();
     }
 
     @Override
     public void onPause() {
         super.onPause();
-        mEmulatorView.onPause();
+
+        mViewFlipper.pauseCurrentView();
     }
 
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
 
-        mEmulatorView.updateSize(true);
+        EmulatorView v = (EmulatorView) mViewFlipper.getCurrentView();
+        if (v != null) {
+            v.updateSize(true);
+        }
     }
 
     @Override
@@ -251,7 +320,7 @@ public class Term extends Activity {
     public boolean onContextItemSelected(MenuItem item) {
           switch (item.getItemId()) {
           case SELECT_TEXT_ID:
-            mEmulatorView.toggleSelectingText();
+            getCurrentEmulatorView().toggleSelectingText();
             return true;
           case COPY_ALL_ID:
             doCopyAll();
@@ -289,14 +358,14 @@ public class Term extends Activity {
                 new Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"
                         + addr));
 
-        intent.putExtra("body", mEmulatorView.getTranscriptText().trim());
+        intent.putExtra("body", getCurrentTermSession().getTranscriptText().trim());
         startActivity(intent);
     }
 
     private void doCopyAll() {
         ClipboardManager clip = (ClipboardManager)
              getSystemService(Context.CLIPBOARD_SERVICE);
-        clip.setText(mEmulatorView.getTranscriptText().trim());
+        clip.setText(getCurrentTermSession().getTranscriptText().trim());
     }
 
     private void doPaste() {
@@ -310,7 +379,7 @@ public class Term extends Activity {
             Log.e(TermDebug.LOG_TAG, "UTF-8 encoding not found.");
             return;
         }
-        mTermSession.write(paste.toString());
+        getCurrentTermSession().write(paste.toString());
     }
 
     private void doDocumentKeys() {
index fbf2811..d98aab2 100644 (file)
 
 package jackpal.androidterm;
 
+import java.util.ArrayList;
+
 import android.app.Service;
+import android.os.Binder;
 import android.os.IBinder;
 import android.content.Intent;
 import android.util.Log;
@@ -24,6 +27,7 @@ import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 
+import jackpal.androidterm.session.TermSession;
 import jackpal.androidterm.util.ServiceForegroundCompat;
 
 public class TermService extends Service
@@ -34,6 +38,16 @@ public class TermService extends Service
     private static final int RUNNING_NOTIFICATION = 1;
     private ServiceForegroundCompat compat;
 
+    private ArrayList<TermSession> mTermSessions;
+
+    public class TSBinder extends Binder {
+        TermService getService() {
+            Log.i("TermService", "Activity binding to service");
+            return TermService.this;
+        }
+    }
+    private final IBinder mTSBinder = new TSBinder();
+
     @Override
     public void onStart(Intent intent, int flags) {
     }
@@ -45,12 +59,14 @@ public class TermService extends Service
 
     @Override
     public IBinder onBind(Intent intent) {
-        return null;
+        Log.i("TermService", "Activity called onBind()");
+        return mTSBinder;
     }
 
     @Override
     public void onCreate() {
         compat = new ServiceForegroundCompat(this);
+        mTermSessions = new ArrayList<TermSession>();
 
         /* Put the service in the foreground. */
         Notification notification = new Notification(R.drawable.app_terminal, getText(R.string.service_notify_text), System.currentTimeMillis());
@@ -68,6 +84,14 @@ public class TermService extends Service
     @Override
     public void onDestroy() {
         compat.stopForeground(true);
+        for (TermSession session : mTermSessions) {
+            session.finish();
+        }
+        mTermSessions.clear();
         return;
     }
+
+    public ArrayList<TermSession> getSessions() {
+        return mTermSessions;
+    }
 }
diff --git a/src/jackpal/androidterm/TermViewFlipper.java b/src/jackpal/androidterm/TermViewFlipper.java
new file mode 100644 (file)
index 0000000..3a1b64e
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * 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;
+
+import java.util.Iterator;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Toast;
+import android.widget.ViewFlipper;
+
+public class TermViewFlipper extends ViewFlipper implements Iterable<View> {
+    private Context context;
+    private Toast mToast;
+
+    class ViewFlipperIterator implements Iterator<View> {
+        int pos = 0;
+
+        @Override
+        public boolean hasNext() {
+            return (pos < getChildCount());
+        }
+
+        @Override
+        public View next() {
+            return getChildAt(pos++);
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    public TermViewFlipper(Context context) {
+        super(context);
+        this.context = context;
+    }
+
+    public TermViewFlipper(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        this.context = context;
+    }
+
+    @Override
+    public Iterator<View> iterator() {
+        return new ViewFlipperIterator();
+    }
+
+    public void pauseCurrentView() {
+        EmulatorView view = (EmulatorView) getCurrentView();
+        if (view == null) {
+            return;
+        }
+        view.onPause();
+    }
+
+    public void resumeCurrentView() {
+        EmulatorView view = (EmulatorView) getCurrentView();
+        if (view == null) {
+            return;
+        }
+        view.onResume();
+    }
+
+    private void showTitle() {
+        if (getChildCount() == 0) {
+            return;
+        }
+        String title = "Window " + (getDisplayedChild()+1);
+        if (mToast == null) {
+            mToast = Toast.makeText(context, title, Toast.LENGTH_SHORT);
+        } else {
+            mToast.setText(title);
+        }
+        mToast.show();
+    }
+
+    @Override
+    public void showPrevious() {
+        pauseCurrentView();
+        super.showPrevious();
+        showTitle();
+        resumeCurrentView();
+    }
+
+    @Override
+    public void showNext() {
+        pauseCurrentView();
+        super.showNext();
+        showTitle();
+        resumeCurrentView();
+    }
+
+    @Override
+    public void setDisplayedChild(int position) {
+        pauseCurrentView();
+        super.setDisplayedChild(position);
+        showTitle();
+        resumeCurrentView();
+    }
+}
index b5d567c..c529fb1 100644 (file)
@@ -116,6 +116,10 @@ public class TermSession {
                 try {
                     while(true) {
                         int read = mTermIn.read(mBuffer);
+                        if (read == -1) {
+                            // EOF -- process exited
+                            return;
+                        }
                         mByteQueue.write(mBuffer, 0, read);
                         mMsgHandler.sendMessage(
                                 mMsgHandler.obtainMessage(NEW_INPUT));
@@ -239,6 +243,10 @@ public class TermSession {
         mEmulator.updateSize(columns, rows);
     }
 
+    public String getTranscriptText() {
+        return mTranscriptScreen.getTranscriptText();
+    }
+
     /**
      * Look for new input from the ptty, send it to the terminal emulator.
      */
index 6ad27f5..6e60106 100644 (file)
@@ -1227,9 +1227,6 @@ public class TerminalEmulator {
         blockClear(0, 0, mColumns, mRows);
     }
 
-    public String getTranscriptText() {
-        return mScreen.getTranscriptText();
-    }
     public String getSelectedText(int x1, int y1, int x2, int y2) {
         return mScreen.getSelectedText(x1, y1, x2, y2);
     }