Feature: NeoTermService holds TermSessions

This commit is contained in:
zt515 2017-06-12 05:19:14 +08:00
parent cad1304d3d
commit a6bf16bc83
11 changed files with 475 additions and 156 deletions

View File

@ -11,7 +11,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity <activity
android:name=".MainActivity" android:name=".NeoTermActivity"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBar" android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible"> android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
@ -21,6 +21,10 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".NeoTermService"
android:enabled="true" />
</application> </application>
</manifest> </manifest>

View File

@ -1,138 +0,0 @@
package io.neoterm
import android.os.Bundle
import android.support.v4.content.ContextCompat
import android.support.v4.view.OnApplyWindowInsetsListener
import android.support.v4.view.ViewCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.View
import android.widget.ImageButton
import de.mrapp.android.tabswitcher.*
import io.neoterm.tab.TermTab
import io.neoterm.tab.TermTabDecorator
class MainActivity : AppCompatActivity() {
lateinit var tabSwitcher: TabSwitcher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.tab_main)
tabSwitcher = findViewById(R.id.tab_switcher) as TabSwitcher
tabSwitcher.decorator = TermTabDecorator(this)
ViewCompat.setOnApplyWindowInsetsListener(tabSwitcher, createWindowInsetsListener())
tabSwitcher.showToolbars(true)
tabSwitcher
.setToolbarNavigationIcon(R.drawable.ic_add_box_white_24dp, createAddTabListener())
tabSwitcher.inflateToolbarMenu(R.menu.tab_switcher, createToolbarMenuListener())
tabSwitcher.addListener(object : TabSwitcherListener {
override fun onSwitcherShown(tabSwitcher: TabSwitcher) {
}
override fun onSwitcherHidden(tabSwitcher: TabSwitcher) {
}
override fun onSelectionChanged(tabSwitcher: TabSwitcher, selectedTabIndex: Int, selectedTab: Tab?) {
}
override fun onTabAdded(tabSwitcher: TabSwitcher, index: Int, tab: Tab, animation: Animation) {
}
override fun onTabRemoved(tabSwitcher: TabSwitcher, index: Int, tab: Tab, animation: Animation) {
if (tab is TermTab) {
tab.termSession?.finishIfRunning()
tab.viewClient?.termView = null
tab.viewClient?.extraKeysView = null
tab.sessionCallback?.termView = null
tab.termSession = null
}
}
override fun onAllTabsRemoved(tabSwitcher: TabSwitcher, tabs: Array<out Tab>, animation: Animation) {
}
})
}
fun createAddTabListener(): View.OnClickListener {
return View.OnClickListener {
val index = tabSwitcher.count
val animation = createRevealAnimation()
tabSwitcher.addTab(createTab(index), 0, animation)
}
}
fun createToolbarMenuListener(): Toolbar.OnMenuItemClickListener {
return Toolbar.OnMenuItemClickListener { item ->
when (item.itemId) {
R.id.add_tab_menu_item -> {
val index = tabSwitcher.count
val tab = createTab(index)
if (tabSwitcher.isSwitcherShown) {
tabSwitcher.addTab(tab, 0, createRevealAnimation())
} else {
tabSwitcher.addTab(tab, 0, createPeekAnimation())
}
true
}
else -> false
}
}
}
private fun createTab(index: Int): Tab {
val tab = TermTab("Neo Term #" + index)
tab.isCloseable = true
tab.parameters = Bundle()
tab.setBackgroundColor(ContextCompat.getColor(this, R.color.tab_background_color))
tab.setTitleTextColor(ContextCompat.getColor(this, R.color.tab_title_text_color))
return tab
}
private fun createRevealAnimation(): Animation {
var x = 0f
var y = 0f
val view = getNavigationMenuItem()
if (view != null) {
val location = IntArray(2)
view.getLocationInWindow(location)
x = location[0] + view.width / 2f
y = location[1] + view.height / 2f
}
return RevealAnimation.Builder().setX(x).setY(y).create()
}
private fun createPeekAnimation(): Animation {
return PeekAnimation.Builder().setX(tabSwitcher.width / 2f).create()
}
private fun getNavigationMenuItem(): View? {
val toolbars = tabSwitcher.toolbars
if (toolbars != null) {
val toolbar = if (toolbars.size > 1) toolbars[1] else toolbars[0]
val size = toolbar.childCount
(0..size - 1)
.map { toolbar.getChildAt(it) }
.filterIsInstance<ImageButton>()
.forEach { return it }
}
return null
}
private fun createWindowInsetsListener(): OnApplyWindowInsetsListener {
return OnApplyWindowInsetsListener { v, insets ->
tabSwitcher.setPadding(insets.systemWindowInsetLeft,
insets.systemWindowInsetTop, insets.systemWindowInsetRight,
insets.systemWindowInsetBottom)
insets
}
}
}

View File

@ -0,0 +1,260 @@
package io.neoterm
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.content.ContextCompat
import android.support.v4.view.OnApplyWindowInsetsListener
import android.support.v4.view.ViewCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.ImageButton
import de.mrapp.android.tabswitcher.*
import de.mrapp.android.tabswitcher.view.TabSwitcherButton
import io.neoterm.preference.NeoTermPreference
import io.neoterm.tab.TermSessionChangedCallback
import io.neoterm.tab.TermTab
import io.neoterm.tab.TermTabDecorator
import io.neoterm.tab.TermViewClient
import io.neoterm.terminal.TerminalSession
class NeoTermActivity : AppCompatActivity(), ServiceConnection {
lateinit var tabSwitcher: TabSwitcher
var termService: NeoTermService? = null
override fun onServiceDisconnected(name: ComponentName?) {
if (termService != null) {
finish()
}
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
termService = (service as NeoTermService.NeoTermBinder).service
if (termService == null) {
finish()
return
}
if (!termService!!.sessions.isEmpty()) {
for (session in termService!!.sessions) {
addNewSession(session)
}
switchToSession(getStoredCurrentSessionOrLast())
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.tab_main)
tabSwitcher = findViewById(R.id.tab_switcher) as TabSwitcher
tabSwitcher.decorator = TermTabDecorator(this)
ViewCompat.setOnApplyWindowInsetsListener(tabSwitcher, createWindowInsetsListener())
tabSwitcher.showToolbars(true)
tabSwitcher
.setToolbarNavigationIcon(R.drawable.ic_add_box_white_24dp, createAddSessionListener())
tabSwitcher.inflateToolbarMenu(R.menu.tab_switcher, createToolbarMenuListener())
tabSwitcher.addListener(object : TabSwitcherListener {
override fun onSwitcherShown(tabSwitcher: TabSwitcher) {
}
override fun onSwitcherHidden(tabSwitcher: TabSwitcher) {
}
override fun onSelectionChanged(tabSwitcher: TabSwitcher, selectedTabIndex: Int, selectedTab: Tab?) {
if (selectedTab is TermTab && selectedTab.termSession != null) {
NeoTermPreference.storeCurrentSession(this@NeoTermActivity, selectedTab.termSession!!)
}
}
override fun onTabAdded(tabSwitcher: TabSwitcher, index: Int, tab: Tab, animation: Animation) {
updateTabSwitcherButton()
}
override fun onTabRemoved(tabSwitcher: TabSwitcher, index: Int, tab: Tab, animation: Animation) {
if (tab is TermTab) {
tab.termSession?.finishIfRunning()
tab.viewClient?.termView = null
tab.viewClient?.extraKeysView = null
tab.sessionCallback?.termView = null
removeFinishedSession(tab.termSession)
tab.termSession = null
}
updateTabSwitcherButton()
}
override fun onAllTabsRemoved(tabSwitcher: TabSwitcher, tabs: Array<out Tab>, animation: Animation) {
}
})
val serviceIntent = Intent(this, NeoTermService::class.java)
startService(serviceIntent)
bindService(serviceIntent, this, 0)
}
override fun onDestroy() {
super.onDestroy()
if (termService != null) {
if (termService!!.sessions.isEmpty()) {
termService!!.stopSelf()
}
termService = null
}
unbindService(this)
}
private fun addNewSession(session: TerminalSession?) {
if (session == null) {
return
}
val tab = createTab(session.mSessionName) as TermTab
tab.sessionCallback = session.sessionChangedCallback as TermSessionChangedCallback
tab.viewClient = TermViewClient(this)
tab.termSession = session
addNewTab(tab, createRevealAnimation())
switchToSession(tab)
}
private fun addNewSession(sessionName: String?, animation: Animation) {
val tab = createTab(sessionName) as TermTab
tab.sessionCallback = TermSessionChangedCallback()
tab.viewClient = TermViewClient(this)
tab.termSession = termService!!.createTermSession(null, null, "/", null, tab.sessionCallback)
if (sessionName != null) {
tab.termSession!!.mSessionName = sessionName
}
addNewTab(tab, animation)
switchToSession(tab)
}
private fun switchToSession(session: TerminalSession?) {
if (session == null) {
return
}
for (i in 0..tabSwitcher.count - 1) {
val tab = tabSwitcher.getTab(i)
if (tab is TermTab && tab.termSession == session) {
switchToSession(tab)
break
}
}
}
private fun switchToSession(tab: Tab?) {
if (tab == null) {
return
}
tabSwitcher.selectTab(tab)
}
private fun addNewTab(tab: Tab, animation: Animation) {
tabSwitcher.addTab(tab, 0, animation)
}
private fun removeFinishedSession(finishedSession: TerminalSession?) {
if (termService == null || finishedSession == null) {
return
}
termService!!.removeTermSession(finishedSession)
}
private fun updateTabSwitcherButton() {
val menu = tabSwitcher.toolbarMenu
if (menu != null) {
val switcherButton = menu.findItem(R.id.toggle_tab_switcher_menu_item).actionView as TabSwitcherButton
switcherButton.setCount(tabSwitcher.count)
}
}
private fun getStoredCurrentSessionOrLast(): TerminalSession? {
val stored = NeoTermPreference.getCurrentSession(this)
if (stored != null) return stored
val numberOfSessions = termService!!.sessions.size
if (numberOfSessions == 0) return null
return termService!!.sessions[numberOfSessions - 1]
}
fun createAddSessionListener(): View.OnClickListener {
return View.OnClickListener {
val index = tabSwitcher.count
addNewSession("NeoTerm #" + index, createRevealAnimation())
}
}
fun createToolbarMenuListener(): Toolbar.OnMenuItemClickListener {
return Toolbar.OnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_item_settings -> {
true
}
else -> false
}
}
}
private fun createTab(tabTitle: String?): Tab {
val tab = TermTab(tabTitle ?: "NeoTerm")
tab.isCloseable = true
tab.parameters = Bundle()
tab.setBackgroundColor(ContextCompat.getColor(this, R.color.tab_background_color))
tab.setTitleTextColor(ContextCompat.getColor(this, R.color.tab_title_text_color))
return tab
}
private fun createRevealAnimation(): Animation {
var x = 0f
var y = 0f
val view = getNavigationMenuItem()
if (view != null) {
val location = IntArray(2)
view.getLocationInWindow(location)
x = location[0] + view.width / 2f
y = location[1] + view.height / 2f
}
return RevealAnimation.Builder().setX(x).setY(y).create()
}
private fun createPeekAnimation(): Animation {
return PeekAnimation.Builder().setX(tabSwitcher.width / 2f).create()
}
private fun getNavigationMenuItem(): View? {
val toolbars = tabSwitcher.toolbars
if (toolbars != null) {
val toolbar = if (toolbars.size > 1) toolbars[1] else toolbars[0]
val size = toolbar.childCount
(0..size - 1)
.map { toolbar.getChildAt(it) }
.filterIsInstance<ImageButton>()
.forEach { return it }
}
return null
}
private fun createWindowInsetsListener(): OnApplyWindowInsetsListener {
return OnApplyWindowInsetsListener { _, insets ->
tabSwitcher.setPadding(insets.systemWindowInsetLeft,
insets.systemWindowInsetTop, insets.systemWindowInsetRight,
insets.systemWindowInsetBottom)
insets
}
}
}

View File

@ -0,0 +1,136 @@
package io.neoterm;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import io.neoterm.tab.TermSessionChangedCallback;
import io.neoterm.terminal.EmulatorDebug;
import io.neoterm.terminal.TerminalSession;
/**
* @author kiva
*/
public class NeoTermService extends Service {
public class NeoTermBinder extends Binder {
public NeoTermService service = NeoTermService.this;
}
public static final String ACTION_SERVICE_STOP = "neoterm.action.service.stop";
private static final int NOTIFICATION_ID = 52019;
private final NeoTermBinder neoTermBinder = new NeoTermBinder();
private final List<TerminalSession> mTerminalSessions = new ArrayList<>();
@Override
public void onCreate() {
super.onCreate();
startForeground(NOTIFICATION_ID, createNotification());
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return neoTermBinder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
if (ACTION_SERVICE_STOP.equals(action)) {
for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning();
stopSelf();
} else if (action != null) {
Log.e(EmulatorDebug.LOG_TAG, "Unknown NeoTermService action: '" + action + "'");
}
if ((flags & START_FLAG_REDELIVERY) == 0) {
// Service is started by WBR, not restarted by system, so release the WakeLock from WBR.
WakefulBroadcastReceiver.completeWakefulIntent(intent);
}
return Service.START_NOT_STICKY;
}
@Override
public void onDestroy() {
stopForeground(true);
for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning();
mTerminalSessions.clear();
}
public List<TerminalSession> getSessions() {
return mTerminalSessions;
}
TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, String[] env, TermSessionChangedCallback sessionCallback) {
if (cwd == null) cwd = getFilesDir().getAbsolutePath();
boolean isLoginShell = false;
if (executablePath == null) {
// Fall back to system shell as last resort:
executablePath = "/system/bin/sh";
isLoginShell = true;
}
if (arguments == null) {
arguments = new String[]{executablePath};
}
int lastSlashIndex = executablePath.lastIndexOf('/');
String processName = (isLoginShell ? "-" : "") +
(lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1));
TerminalSession session = new TerminalSession(executablePath, cwd, arguments, env, sessionCallback);
mTerminalSessions.add(session);
updateNotification();
return session;
}
public int removeTermSession(TerminalSession sessionToRemove) {
int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove);
mTerminalSessions.remove(indexOfRemoved);
updateNotification();
return indexOfRemoved;
}
private void updateNotification() {
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, createNotification());
}
private Notification createNotification() {
Intent notifyIntent = new Intent(this, NeoTermActivity.class);
notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
int sessionCount = mTerminalSessions.size();
String contentText = sessionCount + " session" + (sessionCount == 1 ? "" : "s");
Notification.Builder builder = new Notification.Builder(this);
builder.setContentTitle(getText(R.string.app_name));
builder.setContentText(contentText);
builder.setSmallIcon(R.drawable.ic_service_notification);
builder.setContentIntent(pendingIntent);
builder.setOngoing(true);
builder.setShowWhen(false);
builder.setColor(0xFF000000);
return builder.build();
}
}

View File

@ -0,0 +1,31 @@
package io.neoterm.preference
import android.content.Context
import android.preference.PreferenceManager
import io.neoterm.NeoTermActivity
import io.neoterm.terminal.TerminalSession
/**
* @author kiva
*/
object NeoTermPreference {
var CURRENT_SESSION_KEY = "neoterm_current_session"
fun storeCurrentSession(context: Context, session: TerminalSession) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(NeoTermPreference.CURRENT_SESSION_KEY, session.mHandle).apply()
}
fun getCurrentSession(termActivity: NeoTermActivity): TerminalSession? {
val sessionHandle = PreferenceManager.getDefaultSharedPreferences(termActivity).getString(CURRENT_SESSION_KEY, "")
var i = 0
val len = termActivity.termService!!.sessions.size
while (i < len) {
val session = termActivity.termService!!.sessions[i]
if (session.mHandle == sessionHandle) return session
i++
}
return null
}
}

View File

@ -11,7 +11,7 @@ import android.view.ViewGroup
import de.mrapp.android.tabswitcher.Tab import de.mrapp.android.tabswitcher.Tab
import de.mrapp.android.tabswitcher.TabSwitcher import de.mrapp.android.tabswitcher.TabSwitcher
import de.mrapp.android.tabswitcher.TabSwitcherDecorator import de.mrapp.android.tabswitcher.TabSwitcherDecorator
import io.neoterm.MainActivity import io.neoterm.NeoTermActivity
import io.neoterm.R import io.neoterm.R
import io.neoterm.terminal.TerminalSession import io.neoterm.terminal.TerminalSession
import io.neoterm.view.ExtraKeysView import io.neoterm.view.ExtraKeysView
@ -20,7 +20,7 @@ import io.neoterm.view.TerminalView
/** /**
* @author kiva * @author kiva
*/ */
class TermTabDecorator(val context: MainActivity) : TabSwitcherDecorator() { class TermTabDecorator(val context: NeoTermActivity) : TabSwitcherDecorator() {
override fun onInflateView(inflater: LayoutInflater, parent: ViewGroup?, viewType: Int): View { override fun onInflateView(inflater: LayoutInflater, parent: ViewGroup?, viewType: Int): View {
val view = inflater.inflate(R.layout.term, parent, false) val view = inflater.inflate(R.layout.term, parent, false)
val toolbar = view.findViewById(R.id.terminal_toolbar) as Toolbar val toolbar = view.findViewById(R.id.terminal_toolbar) as Toolbar
@ -57,19 +57,9 @@ class TermTabDecorator(val context: MainActivity) : TabSwitcherDecorator() {
val termTab = tab as TermTab val termTab = tab as TermTab
// 复用前一次的 TermSession // 复用前一次的 TermSession
if (termTab.sessionCallback == null) {
termTab.sessionCallback = TermSessionChangedCallback()
}
termTab.sessionCallback?.termView = view termTab.sessionCallback?.termView = view
termTab.termSession = termTab.termSession ?: TerminalSession("/system/bin/sh", "/",
arrayOf("/system/bin/sh"),
arrayOf("TERM=screen", "HOME=" + context.filesDir), termTab.sessionCallback)
// 复用上一次的 TermViewClient // 复用上一次的 TermViewClient
if (termTab.viewClient == null) {
termTab.viewClient = TermViewClient(context)
}
termTab.viewClient?.termView = view termTab.viewClient?.termView = view
termTab.viewClient?.extraKeysView = extraKeysView termTab.viewClient?.extraKeysView = extraKeysView

View File

@ -5,6 +5,6 @@ import android.util.Log;
public final class EmulatorDebug { public final class EmulatorDebug {
/** The tag to use with {@link Log}. */ /** The tag to use with {@link Log}. */
public static final String LOG_TAG = "neoterm-termux"; public static final String LOG_TAG = "neoterm";
} }

View File

@ -85,6 +85,10 @@ public final class TerminalSession extends TerminalOutput {
/** Buffer to write translate code points into utf8 before writing to mTerminalToProcessIOQueue */ /** Buffer to write translate code points into utf8 before writing to mTerminalToProcessIOQueue */
private final byte[] mUtf8InputBuffer = new byte[5]; private final byte[] mUtf8InputBuffer = new byte[5];
public SessionChangedCallback getSessionChangedCallback() {
return mChangeCallback;
}
/** Callback which gets notified when a session finishes or changes title. */ /** Callback which gets notified when a session finishes or changes title. */
final SessionChangedCallback mChangeCallback; final SessionChangedCallback mChangeCallback;

View File

@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="48dp"
android:width="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<!--
https://material.google.com/style/icons.html
-->
<!-- Screen border. -->
<path android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeWidth="3"
android:pathData="M7,4
l34,0
q3 0,3 3
l0,34
q0 3, -3 3
l-34,0
q-3 0, -3-3
l0 -34
q0 -3, 3 -3"
/>
<!-- Block cursor. -->
<path android:fillColor="#000"
android:pathData="M14,14
l5,0
l0,10
l-5,0"
/>
</vector>

View File

@ -23,8 +23,8 @@ License.
app:showAsAction="ifRoom"/> app:showAsAction="ifRoom"/>
<item <item
android:id="@+id/add_tab_menu_item" android:id="@+id/menu_item_settings"
android:title="@string/add_tab_menu_item" android:title="@string/menu_settings"
app:showAsAction="never"/> app:showAsAction="never"/>
</menu> </menu>

View File

@ -5,6 +5,5 @@
<string name="text_selection_more">More</string> <string name="text_selection_more">More</string>
<string name="toggle_tab_switcher_menu_item">Toggle switcher</string> <string name="toggle_tab_switcher_menu_item">Toggle switcher</string>
<string name="add_tab_menu_item">New tab</string> <string name="menu_settings">Settings</string>
<string name="clear_tabs_menu_item">Clear all tabs</string>
</resources> </resources>