diff options
-rw-r--r-- | Android.mk | 2 | ||||
-rw-r--r-- | AndroidManifest.xml | 9 | ||||
-rw-r--r-- | jni/com_android_terminal_Terminal.cpp | 48 | ||||
-rw-r--r-- | res/layout/activity.xml | 30 | ||||
-rw-r--r-- | res/menu/activity.xml | 28 | ||||
-rw-r--r-- | res/values/strings.xml | 4 | ||||
-rw-r--r-- | src/com/android/terminal/Terminal.java | 17 | ||||
-rw-r--r-- | src/com/android/terminal/TerminalActivity.java | 141 | ||||
-rw-r--r-- | src/com/android/terminal/TerminalService.java | 73 | ||||
-rw-r--r-- | src/com/android/terminal/TerminalView.java | 41 |
10 files changed, 372 insertions, 21 deletions
@@ -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"); } } |