summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.mk2
-rw-r--r--AndroidManifest.xml9
-rw-r--r--jni/com_android_terminal_Terminal.cpp48
-rw-r--r--res/layout/activity.xml30
-rw-r--r--res/menu/activity.xml28
-rw-r--r--res/values/strings.xml4
-rw-r--r--src/com/android/terminal/Terminal.java17
-rw-r--r--src/com/android/terminal/TerminalActivity.java141
-rw-r--r--src/com/android/terminal/TerminalService.java73
-rw-r--r--src/com/android/terminal/TerminalView.java41
10 files changed, 372 insertions, 21 deletions
diff --git a/Android.mk b/Android.mk
index 5e545b6..198c11e 100644
--- a/Android.mk
+++ b/Android.mk
@@ -5,6 +5,8 @@ LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
+
LOCAL_JNI_SHARED_LIBRARIES := libjni_terminal
LOCAL_PROGUARD_FLAG_FILES := proguard.flags
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 397c79f..078ace7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,14 +16,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.terminal">
- <application android:label="@string/app_label">
+ <application
+ android:label="@string/app_label"
+ android:persistent="true">
+
<activity android:name=".TerminalActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
- </activity>
+ </activity>
+
+ <service android:name=".TerminalService" />
</application>
</manifest>
diff --git a/jni/com_android_terminal_Terminal.cpp b/jni/com_android_terminal_Terminal.cpp
index 2439346..c84f323 100644
--- a/jni/com_android_terminal_Terminal.cpp
+++ b/jni/com_android_terminal_Terminal.cpp
@@ -34,7 +34,9 @@
#include <string.h>
-#define USE_TEST_SHELL true
+#define USE_TEST_SHELL 1
+#define DEBUG_CALLBACKS 0
+#define DEBUG_IO 0
namespace android {
@@ -81,6 +83,7 @@ public:
~Terminal();
int run();
+ int stop();
size_t write(const char *bytes, size_t len);
@@ -102,6 +105,7 @@ private:
jobject mCallbacks;
short unsigned int mRows;
short unsigned int mCols;
+ bool mStopped;
};
static JNIEnv* getEnv() {
@@ -120,7 +124,9 @@ static JNIEnv* getEnv() {
static int term_damage(VTermRect rect, void *user) {
Terminal* term = reinterpret_cast<Terminal*>(user);
+#if DEBUG_CALLBACKS
ALOGW("term_damage");
+#endif
JNIEnv* env = getEnv();
if (env == NULL) {
@@ -134,7 +140,9 @@ static int term_damage(VTermRect rect, void *user) {
static int term_prescroll(VTermRect rect, void *user) {
Terminal* term = reinterpret_cast<Terminal*>(user);
+#if DEBUG_CALLBACKS
ALOGW("term_prescroll");
+#endif
JNIEnv* env = getEnv();
if (env == NULL) {
@@ -148,7 +156,9 @@ static int term_prescroll(VTermRect rect, void *user) {
static int term_moverect(VTermRect dest, VTermRect src, void *user) {
Terminal* term = reinterpret_cast<Terminal*>(user);
+#if DEBUG_CALLBACKS
ALOGW("term_moverect");
+#endif
JNIEnv* env = getEnv();
if (env == NULL) {
@@ -163,7 +173,9 @@ static int term_moverect(VTermRect dest, VTermRect src, void *user) {
static int term_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) {
Terminal* term = reinterpret_cast<Terminal*>(user);
+#if DEBUG_CALLBACKS
ALOGW("term_movecursor");
+#endif
JNIEnv* env = getEnv();
if (env == NULL) {
@@ -177,7 +189,9 @@ static int term_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *use
static int term_settermprop(VTermProp prop, VTermValue *val, void *user) {
Terminal* term = reinterpret_cast<Terminal*>(user);
+#if DEBUG_CALLBACKS
ALOGW("term_settermprop");
+#endif
JNIEnv* env = getEnv();
if (env == NULL) {
@@ -205,13 +219,17 @@ static int term_settermprop(VTermProp prop, VTermValue *val, void *user) {
static int term_setmousefunc(VTermMouseFunc func, void *data, void *user) {
Terminal* term = reinterpret_cast<Terminal*>(user);
+#if DEBUG_CALLBACKS
ALOGW("term_setmousefunc");
+#endif
return 1;
}
static int term_bell(void *user) {
Terminal* term = reinterpret_cast<Terminal*>(user);
+#if DEBUG_CALLBACKS
ALOGW("term_bell");
+#endif
JNIEnv* env = getEnv();
if (env == NULL) {
@@ -224,7 +242,9 @@ static int term_bell(void *user) {
static int term_resize(int rows, int cols, void *user) {
Terminal* term = reinterpret_cast<Terminal*>(user);
+#if DEBUG_CALLBACKS
ALOGW("term_resize");
+#endif
JNIEnv* env = getEnv();
if (env == NULL) {
@@ -247,7 +267,7 @@ static VTermScreenCallbacks cb = {
};
Terminal::Terminal(jobject callbacks, int rows, int cols) :
- mCallbacks(callbacks), mRows(rows), mCols(cols) {
+ mCallbacks(callbacks), mRows(rows), mCols(cols), mStopped(false) {
/* Create VTerm */
mVt = vterm_new(rows, cols);
vterm_parser_set_utf8(mVt, 1);
@@ -315,7 +335,7 @@ int Terminal::run() {
}
char *shell = "/system/bin/sh"; //getenv("SHELL");
-#ifdef USE_TEST_SHELL
+#if USE_TEST_SHELL
char *args[4] = {shell, "-c", "x=1; c=0; while true; do echo -e \"stop \e[00;3${c}mechoing\e[00m yourself! ($x)\"; x=$(( $x + 1 )); c=$((($c+1)%7)); sleep 0.5; done", NULL};
#else
char *args[2] = {shell, NULL};
@@ -330,8 +350,14 @@ int Terminal::run() {
while (1) {
char buffer[4096];
ssize_t bytes = ::read(mMasterFd, buffer, sizeof buffer);
- ALOGD("Read %d bytes:", bytes);
+#if DEBUG_IO
+ ALOGD("read() returned %d bytes", bytes);
+#endif
+ if (mStopped) {
+ ALOGD("stop() requested");
+ break;
+ }
if (bytes == 0) {
ALOGD("read() found EOF");
break;
@@ -346,7 +372,13 @@ int Terminal::run() {
vterm_screen_flush_damage(mVts);
}
- return 1;
+ return 0;
+}
+
+int Terminal::stop() {
+ // TODO: explicitly kill forked child process
+ mStopped = true;
+ return 0;
}
size_t Terminal::write(const char *bytes, size_t len) {
@@ -403,6 +435,11 @@ static jint com_android_terminal_Terminal_nativeRun(JNIEnv* env, jclass clazz, j
return term->run();
}
+static jint com_android_terminal_Terminal_nativeStop(JNIEnv* env, jclass clazz, jint ptr) {
+ Terminal* term = reinterpret_cast<Terminal*>(ptr);
+ return term->stop();
+}
+
static jint com_android_terminal_Terminal_nativeFlushDamage(JNIEnv* env, jclass clazz, jint ptr) {
Terminal* term = reinterpret_cast<Terminal*>(ptr);
return term->flushDamage();
@@ -502,6 +539,7 @@ static jint com_android_terminal_Terminal_nativeGetCols(JNIEnv* env, jclass claz
static JNINativeMethod gMethods[] = {
{ "nativeInit", "(Lcom/android/terminal/TerminalCallbacks;II)I", (void*)com_android_terminal_Terminal_nativeInit },
{ "nativeRun", "(I)I", (void*)com_android_terminal_Terminal_nativeRun },
+ { "nativeStop", "(I)I", (void*)com_android_terminal_Terminal_nativeStop },
{ "nativeFlushDamage", "(I)I", (void*)com_android_terminal_Terminal_nativeFlushDamage },
{ "nativeResize", "(III)I", (void*)com_android_terminal_Terminal_nativeResize },
{ "nativeGetCellRun", "(IIILcom/android/terminal/Terminal$CellRun;)I", (void*)com_android_terminal_Terminal_nativeGetCellRun },
diff --git a/res/layout/activity.xml b/res/layout/activity.xml
new file mode 100644
index 0000000..1ec603d
--- /dev/null
+++ b/res/layout/activity.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<android.support.v4.view.ViewPager
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v4.view.PagerTitleStrip
+ android:id="@+id/titles"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top"
+ />
+
+</android.support.v4.view.ViewPager>
diff --git a/res/menu/activity.xml b/res/menu/activity.xml
new file mode 100644
index 0000000..d1be0ae
--- /dev/null
+++ b/res/menu/activity.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2013 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_new_tab"
+ android:title="@string/menu_new_tab"
+ android:icon="@android:drawable/ic_menu_add"
+ android:showAsAction="always" />
+ <item
+ android:id="@+id/menu_close_tab"
+ android:title="@string/menu_close_tab"
+ android:icon="@android:drawable/ic_menu_close_clear_cancel"
+ android:showAsAction="ifRoom" />
+</menu>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1aca939..cbcaef3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -17,4 +17,8 @@
<resources>
<!-- Title of the Terminal activity. -->
<string name="app_label">Terminal</string>
+
+ <string name="menu_new_tab">New tab</string>
+ <string name="menu_close_tab">Close tab</string>
+
</resources>
diff --git a/src/com/android/terminal/Terminal.java b/src/com/android/terminal/Terminal.java
index d37e6a8..c9e45e7 100644
--- a/src/com/android/terminal/Terminal.java
+++ b/src/com/android/terminal/Terminal.java
@@ -24,6 +24,8 @@ import android.graphics.Color;
public class Terminal {
private static final String TAG = "Terminal";
+ private static int sNumber = 0;
+
static {
System.loadLibrary("jni_terminal");
}
@@ -58,6 +60,8 @@ public class Terminal {
private final int mNativePtr;
private final Thread mThread;
+ private String mTitle;
+
private TerminalClient mClient;
private final TerminalCallbacks mCallbacks = new TerminalCallbacks() {
@@ -90,6 +94,7 @@ public class Terminal {
public Terminal() {
mNativePtr = nativeInit(mCallbacks, 25, 80);
+ mTitle = TAG + " " + sNumber++;
mThread = new Thread(TAG) {
@Override
public void run() {
@@ -105,6 +110,12 @@ public class Terminal {
mThread.start();
}
+ public void stop() {
+ if (nativeStop(mNativePtr) != 0) {
+ throw new IllegalStateException("stop failed");
+ }
+ }
+
public void setClient(TerminalClient client) {
mClient = client;
}
@@ -135,8 +146,14 @@ public class Terminal {
}
}
+ public String getTitle() {
+ // TODO: hook up to title passed through termprop
+ return mTitle;
+ }
+
private static native int nativeInit(TerminalCallbacks callbacks, int rows, int cols);
private static native int nativeRun(int ptr);
+ private static native int nativeStop(int ptr);
private static native int nativeFlushDamage(int ptr);
private static native int nativeResize(int ptr, int rows, int cols);
diff --git a/src/com/android/terminal/TerminalActivity.java b/src/com/android/terminal/TerminalActivity.java
index bef1859..99d2be6 100644
--- a/src/com/android/terminal/TerminalActivity.java
+++ b/src/com/android/terminal/TerminalActivity.java
@@ -17,23 +17,154 @@
package com.android.terminal;
import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.PagerTitleStrip;
+import android.support.v4.view.ViewPager;
import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+/**
+ * Activity that displays all {@link Terminal} instances running in a bound
+ * {@link TerminalService}.
+ */
public class TerminalActivity extends Activity {
private static final String TAG = "Terminal";
+ private TerminalService mService;
+
+ private ViewPager mPager;
+ private PagerTitleStrip mTitles;
+
+ private final ServiceConnection mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = ((TerminalService.ServiceBinder) service).getService();
+
+ final int size = mService.getTerminals().size();
+ Log.d(TAG, "Bound to service with " + size + " active terminals");
+
+ // Give ourselves at least one terminal session
+ if (size == 0) {
+ mService.createTerminal();
+ }
+
+ // Bind UI to known terminals
+ mTermAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mService = null;
+ throw new RuntimeException("Service in same process disconnected?");
+ }
+ };
+
+ private final PagerAdapter mTermAdapter = new PagerAdapter() {
+ @Override
+ public int getCount() {
+ if (mService != null) {
+ return mService.getTerminals().size();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ final Terminal term = mService.getTerminals().get(position);
+ final TerminalView view = new TerminalView(container.getContext());
+ view.setTerminal(term);
+ container.addView(view);
+ return view;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ final TerminalView view = (TerminalView) object;
+ view.setTerminal(null);
+ container.removeView(view);
+ }
+
+ @Override
+ public int getItemPosition(Object object) {
+ final int index = mService.getTerminals().indexOf(object);
+ if (index == -1) {
+ return POSITION_NONE;
+ } else {
+ return index;
+ }
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return view == object;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return mService.getTerminals().get(position).getTitle();
+ }
+ };
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- final Terminal term = new Terminal();
- term.start();
+ setContentView(R.layout.activity);
+
+ mPager = (ViewPager) findViewById(R.id.pager);
+ mTitles = (PagerTitleStrip) findViewById(R.id.titles);
+
+ mPager.setAdapter(mTermAdapter);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ bindService(
+ new Intent(this, TerminalService.class), mServiceConn, Context.BIND_AUTO_CREATE);
+ }
- final TerminalView view = new TerminalView(this, term);
+ @Override
+ protected void onStop() {
+ super.onStop();
+ unbindService(mServiceConn);
+ }
- setContentView(view);
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.activity, menu);
+ return true;
+ }
- Log.d(TAG, "Rows: " + term.getRows());
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_new_tab: {
+ mService.createTerminal();
+ mTermAdapter.notifyDataSetChanged();
+ final int index = mService.getTerminals().size() - 1;
+ mPager.setCurrentItem(index, true);
+ return true;
+ }
+ case R.id.menu_close_tab: {
+ final int index = mPager.getCurrentItem();
+ final Terminal term = mService.getTerminals().get(index);
+ mService.destroyTerminal(term);
+ // TODO: ask adamp about buggy zero item behavior
+ mTermAdapter.notifyDataSetChanged();
+ return true;
+ }
+ }
+ return false;
}
}
diff --git a/src/com/android/terminal/TerminalService.java b/src/com/android/terminal/TerminalService.java
new file mode 100644
index 0000000..ebbdcce
--- /dev/null
+++ b/src/com/android/terminal/TerminalService.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2013 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 com.android.terminal;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Background service that keeps {@link Terminal} instances running and warm
+ * when UI isn't present.
+ */
+public class TerminalService extends Service {
+ private static final String TAG = "Terminal";
+
+ private final ArrayList<Terminal> mTerminals = new ArrayList<Terminal>();
+
+ public class ServiceBinder extends Binder {
+ public TerminalService getService() {
+ return TerminalService.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return new ServiceBinder();
+ }
+
+ public List<Terminal> getTerminals() {
+ return Collections.unmodifiableList(mTerminals);
+ }
+
+ public Terminal createTerminal() {
+ // If our first terminal, start ourselves as long-lived service
+ if (mTerminals.isEmpty()) {
+ startService(new Intent(this, TerminalService.class));
+ }
+
+ final Terminal term = new Terminal();
+ term.start();
+ mTerminals.add(term);
+ return term;
+ }
+
+ public void destroyTerminal(Terminal term) {
+ term.stop();
+ mTerminals.remove(term);
+
+ // If our last terminal, tear down long-lived service
+ if (mTerminals.isEmpty()) {
+ stopService(new Intent(this, TerminalService.class));
+ }
+ }
+}
diff --git a/src/com/android/terminal/TerminalView.java b/src/com/android/terminal/TerminalView.java
index 35192e9..64aeabd 100644
--- a/src/com/android/terminal/TerminalView.java
+++ b/src/com/android/terminal/TerminalView.java
@@ -35,11 +35,11 @@ import com.android.terminal.Terminal.TerminalClient;
*/
public class TerminalView extends View {
private static final String TAG = "Terminal";
+ private static final boolean LOGD = true;
private static final int MAX_RUN_LENGTH = 128;
private final Context mContext;
- private final Terminal mTerm;
private final Paint mBgPaint = new Paint();
private final Paint mTextPaint = new Paint();
@@ -49,6 +49,8 @@ public class TerminalView extends View {
/** Screen coordinates to draw chars into */
private final float[] mPos;
+ private Terminal mTerm;
+
private int mCharTop;
private int mCharWidth;
private int mCharHeight;
@@ -59,7 +61,7 @@ public class TerminalView extends View {
private TerminalClient mClient = new TerminalClient() {
@Override
public void damage(int startRow, int endRow, int startCol, int endCol) {
- Log.d(TAG, "damage(" + startRow + ", " + endRow + ", " + startCol + ", " + endCol + ")");
+ if (LOGD) Log.d(TAG, "damage(" + startRow + ", " + endRow + ", " + startCol + ", " + endCol + ")");
// Invalidate region on screen
final int top = startRow * mCharHeight;
@@ -86,10 +88,9 @@ public class TerminalView extends View {
}
};
- public TerminalView(Context context, Terminal term) {
+ public TerminalView(Context context) {
super(context);
mContext = context;
- mTerm = term;
mRun = new Terminal.CellRun();
mRun.data = new char[MAX_RUN_LENGTH];
@@ -110,20 +111,37 @@ public class TerminalView extends View {
});
}
+ public void setTerminal(Terminal term) {
+ final Terminal orig = mTerm;
+ if (orig != null) {
+ orig.setClient(null);
+ }
+ mTerm = term;
+ if (term != null) {
+ term.setClient(mClient);
+ }
+ updateTerminalSize();
+ }
+
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- mTerm.setClient(mClient);
+ if (mTerm != null) {
+ mTerm.setClient(mClient);
+ }
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- mTerm.setClient(null);
+ if (mTerm != null) {
+ mTerm.setClient(null);
+ }
}
public void setTextSize(float textSize) {
mTextPaint.setTypeface(Typeface.MONOSPACE);
+ mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(textSize);
// Read metrics to get exact pixel dimensions
@@ -149,11 +167,10 @@ public class TerminalView extends View {
* and request that {@link Terminal} change to that size.
*/
public void updateTerminalSize() {
- if (getWidth() > 0 && getHeight() > 0) {
+ if (getWidth() > 0 && getHeight() > 0 && mTerm != null) {
final int rows = getHeight() / mCharHeight;
final int cols = getWidth() / mCharWidth;
mTerm.resize(rows, cols);
- mTerm.flushDamage();
}
}
@@ -169,6 +186,12 @@ public class TerminalView extends View {
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
+ if (mTerm == null) {
+ Log.w(TAG, "onDraw() without a terminal");
+ canvas.drawColor(Color.MAGENTA);
+ return;
+ }
+
final long start = SystemClock.elapsedRealtime();
// Only draw dirty region of console
@@ -210,6 +233,6 @@ public class TerminalView extends View {
}
final long delta = SystemClock.elapsedRealtime() - start;
- Log.d(TAG, "onDraw() took " + delta + "ms");
+ if (LOGD) Log.d(TAG, "onDraw() took " + delta + "ms");
}
}