Feature: NeoTerm Runtime

This commit is contained in:
zt515
2017-06-13 18:07:14 +08:00
parent 085c0b95f1
commit 4a44777c92
7 changed files with 308 additions and 25 deletions

View File

@ -18,6 +18,11 @@ android {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
} }
} }
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
} }
buildTypes { buildTypes {
release { release {

View File

@ -3,6 +3,9 @@
package="io.neoterm"> package="io.neoterm">
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@ -0,0 +1,182 @@
package io.neoterm.installer;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Build;
import android.system.Os;
import android.util.Log;
import android.util.Pair;
import android.view.WindowManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import io.neoterm.backend.EmulatorDebug;
import io.neoterm.preference.NeoTermPreference;
public final class BaseFileInstaller {
public static void installBaseFiles(final Activity activity, final Runnable whenDone) {
final File PREFIX_FILE = new File(NeoTermPreference.USR_PATH);
if (PREFIX_FILE.isDirectory()) {
whenDone.run();
return;
}
final ProgressDialog progress = ProgressDialog.show(activity, null, "Installing", true, false);
new Thread() {
@Override
public void run() {
try {
final String STAGING_PREFIX_PATH = NeoTermPreference.ROOT_PATH + "/usr-staging";
final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
if (STAGING_PREFIX_FILE.exists()) {
deleteFolder(STAGING_PREFIX_FILE);
}
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
final URL zipUrl = determineZipUrl();
try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName().equals("SYMLINKS.txt")) {
BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
String line;
while ((line = symlinksReader.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
String[] parts = line.split("");
if (parts.length != 2)
throw new RuntimeException("Malformed symlink line: " + line);
String oldPath = parts[0];
String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
if (zipEntry.isDirectory()) {
if (!targetFile.mkdirs())
throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
} else {
try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
int readBytes;
while ((readBytes = zipInput.read(buffer)) != -1)
outStream.write(buffer, 0, readBytes);
}
if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
//noinspection OctalInteger
Os.chmod(targetFile.getAbsolutePath(), 0700);
}
}
}
}
}
if (symlinks.isEmpty())
throw new RuntimeException("No SYMLINKS.txt encountered");
for (Pair<String, String> symlink : symlinks) {
Os.symlink(symlink.first, symlink.second);
}
if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
throw new RuntimeException("Unable to rename staging folder");
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
whenDone.run();
}
});
} catch (final Exception e) {
Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
new AlertDialog.Builder(activity).setTitle("ERROR").setMessage(e.toString())
.setNegativeButton("Abort", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
activity.finish();
}
}).setPositiveButton("Retry", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
BaseFileInstaller.installBaseFiles(activity, whenDone);
}
}).show();
} catch (WindowManager.BadTokenException e) {
// Activity already dismissed - ignore.
}
}
});
} finally {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
progress.dismiss();
} catch (RuntimeException e) {
// Activity already dismissed - ignore.
}
}
});
}
}
}.start();
}
private static URL determineZipUrl() throws MalformedURLException {
String archName = determineArchName();
return new URL("https://kernel19.cc/neoterm/boot/" + archName + ".zip");
}
private static String determineArchName() {
for (String androidArch : Build.SUPPORTED_ABIS) {
switch (androidArch) {
case "arm64-v8a":
return "aarch64";
case "armeabi-v7a":
return "arm";
case "x86_64":
return "x86_64";
case "x86":
return "i686";
}
}
throw new RuntimeException("Unable to determine arch from Build.SUPPORTED_ABIS = " +
Arrays.toString(Build.SUPPORTED_ABIS));
}
private static void deleteFolder(File fileOrDirectory) {
File[] children = fileOrDirectory.listFiles();
if (children != null) {
for (File child : children) {
deleteFolder(child);
}
}
if (!fileOrDirectory.delete()) {
throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
}
}
}

View File

@ -0,0 +1,46 @@
package io.neoterm.preference
import android.Manifest
import android.app.Activity
import android.app.AlertDialog
import android.content.DialogInterface
import android.content.pm.PackageManager
import android.os.Build
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
/**
* @author kiva
*/
object NeoPermission {
const val REQUEST_APP_PERMISSION = 10086
fun initAppPermission(context: Activity, requestCode: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return
}
if (ContextCompat.checkSelfPermission(context,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(context,
Manifest.permission.READ_CONTACTS)) {
AlertDialog.Builder(context).setMessage("需要存储权限来访问存储设备上的文件")
.setPositiveButton(android.R.string.ok, { _: DialogInterface, _: Int ->
ActivityCompat.requestPermissions(context,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.RECEIVE_BOOT_COMPLETED),
requestCode)
})
.show()
} else {
ActivityCompat.requestPermissions(context,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.RECEIVE_BOOT_COMPLETED),
requestCode)
}
}
}
}

View File

@ -5,12 +5,18 @@ import android.content.SharedPreferences
import android.preference.PreferenceManager import android.preference.PreferenceManager
import io.neoterm.backend.TerminalSession import io.neoterm.backend.TerminalSession
import io.neoterm.services.NeoTermService import io.neoterm.services.NeoTermService
import java.io.File
/** /**
* @author kiva * @author kiva
*/ */
object NeoTermPreference { object NeoTermPreference {
const val ROOT_PATH = "/data/data/io.neoterm/files"
const val USR_PATH = ROOT_PATH + "/usr"
const val HOME_PATH = ROOT_PATH + "/home"
const val KEY_FONT_SIZE = "neoterm_general_font_size" const val KEY_FONT_SIZE = "neoterm_general_font_size"
const val KEY_CURRENT_SESSION = "neoterm_service_current_session" const val KEY_CURRENT_SESSION = "neoterm_service_current_session"
@ -80,4 +86,27 @@ object NeoTermPreference {
} }
return null return null
} }
fun buildEnvironment(cwd: String?): Array<String> {
var cwd = cwd
File(HOME_PATH).mkdirs()
if (cwd == null) cwd = HOME_PATH
val termEnv = "TERM=xterm-256color"
val homeEnv = "HOME=" + HOME_PATH
val androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT")
val androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA")
val externalStorageEnv = "EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE")
val ps1Env = "PS1=$ "
val ldEnv = "LD_LIBRARY_PATH=$USR_PATH/lib"
val langEnv = "LANG=en_US.UTF-8"
val pathEnv = "PATH=$USR_PATH/bin:$USR_PATH/bin/applets"
val pwdEnv = "PWD=" + cwd
val tmpdirEnv = "TMPDIR=$USR_PATH/tmp"
return arrayOf(termEnv, homeEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv, externalStorageEnv, tmpdirEnv)
}
} }

View File

@ -10,13 +10,12 @@ import android.os.Binder
import android.os.IBinder import android.os.IBinder
import android.support.v4.content.WakefulBroadcastReceiver import android.support.v4.content.WakefulBroadcastReceiver
import android.util.Log import android.util.Log
import java.util.ArrayList
import io.neoterm.ui.NeoTermActivity
import io.neoterm.R import io.neoterm.R
import io.neoterm.backend.EmulatorDebug import io.neoterm.backend.EmulatorDebug
import io.neoterm.backend.TerminalSession import io.neoterm.backend.TerminalSession
import io.neoterm.preference.NeoTermPreference
import io.neoterm.ui.NeoTermActivity
import java.util.*
/** /**
* @author kiva * @author kiva
@ -71,25 +70,21 @@ class NeoTermService : Service() {
fun createTermSession(executablePath: String?, arguments: Array<String>?, cwd: String?, env: Array<String>?, sessionCallback: TerminalSession.SessionChangedCallback?): TerminalSession { fun createTermSession(executablePath: String?, arguments: Array<String>?, cwd: String?, env: Array<String>?, sessionCallback: TerminalSession.SessionChangedCallback?): TerminalSession {
var executablePath = executablePath var executablePath = executablePath
var arguments = arguments var arguments = arguments
var cwd = cwd
if (cwd == null) cwd = filesDir.absolutePath
var isLoginShell = false var cwd = cwd
if (cwd == null) {
cwd = NeoTermPreference.HOME_PATH
}
if (executablePath == null) { if (executablePath == null) {
// Fall back to system shell as last resort: executablePath = NeoTermPreference.USR_PATH + "/bin/bash"
executablePath = "/system/bin/sh"
isLoginShell = true
} }
if (arguments == null) { if (arguments == null) {
arguments = arrayOf<String>(executablePath) arguments = arrayOf<String>(executablePath)
} }
val lastSlashIndex = executablePath.lastIndexOf('/') val session = TerminalSession(executablePath, cwd, arguments, env ?: NeoTermPreference.buildEnvironment(cwd), sessionCallback)
val processName = (if (isLoginShell) "-" else "") + if (lastSlashIndex == -1) executablePath else executablePath.substring(lastSlashIndex + 1)
val session = TerminalSession(executablePath, cwd, arguments, env, sessionCallback)
mTerminalSessions.add(session) mTerminalSessions.add(session)
updateNotification() updateNotification()
return session return session
@ -126,9 +121,7 @@ class NeoTermService : Service() {
} }
companion object { companion object {
val ACTION_SERVICE_STOP = "neoterm.action.service.stop" val ACTION_SERVICE_STOP = "neoterm.action.service.stop"
private val NOTIFICATION_ID = 52019 private val NOTIFICATION_ID = 52019
} }
} }

View File

@ -1,8 +1,11 @@
package io.neoterm.ui package io.neoterm.ui
import android.app.AlertDialog
import android.content.ComponentName import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
@ -18,6 +21,8 @@ import de.mrapp.android.tabswitcher.*
import de.mrapp.android.tabswitcher.view.TabSwitcherButton import de.mrapp.android.tabswitcher.view.TabSwitcherButton
import io.neoterm.R import io.neoterm.R
import io.neoterm.backend.TerminalSession import io.neoterm.backend.TerminalSession
import io.neoterm.installer.BaseFileInstaller
import io.neoterm.preference.NeoPermission
import io.neoterm.preference.NeoTermPreference import io.neoterm.preference.NeoTermPreference
import io.neoterm.services.NeoTermService import io.neoterm.services.NeoTermService
import io.neoterm.ui.settings.SettingActivity import io.neoterm.ui.settings.SettingActivity
@ -41,19 +46,23 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection {
return return
} }
if (!termService!!.sessions.isEmpty()) { BaseFileInstaller.installBaseFiles(this, {
for (session in termService!!.sessions) { if (!termService!!.sessions.isEmpty()) {
addNewSession(session) for (session in termService!!.sessions) {
addNewSession(session)
}
switchToSession(getStoredCurrentSessionOrLast())
} else {
tabSwitcher.showSwitcher()
addNewSession("NeoTerm #0", createRevealAnimation())
} }
switchToSession(getStoredCurrentSessionOrLast()) })
} else {
tabSwitcher.showSwitcher()
addNewSession("NeoTerm #0", createRevealAnimation())
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
NeoPermission.initAppPermission(this, NeoPermission.REQUEST_APP_PERMISSION)
NeoTermPreference.init(this) NeoTermPreference.init(this)
if (NeoTermPreference.loadBoolean(R.string.key_ui_fullscreen, false)) { if (NeoTermPreference.loadBoolean(R.string.key_ui_fullscreen, false)) {
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
@ -150,6 +159,22 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection {
return super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
when (requestCode) {
NeoPermission.REQUEST_APP_PERMISSION -> {
if (grantResults.isEmpty()
|| grantResults[0] != PackageManager.PERMISSION_GRANTED) {
AlertDialog.Builder(this).setMessage("应用无法取得必须的权限,正在退出")
.setPositiveButton(android.R.string.ok, { _: DialogInterface, _: Int ->
finish()
})
.show()
}
return
}
}
}
private fun addNewSession(session: TerminalSession?) { private fun addNewSession(session: TerminalSession?) {
if (session == null) { if (session == null) {
return return
@ -168,7 +193,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection {
val tab = createTab(sessionName) as TermTab val tab = createTab(sessionName) as TermTab
tab.sessionCallback = TermSessionChangedCallback() tab.sessionCallback = TermSessionChangedCallback()
tab.viewClient = TermViewClient(this) tab.viewClient = TermViewClient(this)
tab.termSession = termService!!.createTermSession(null, null, "/", null, tab.sessionCallback) tab.termSession = termService!!.createTermSession(null, null, null, null, tab.sessionCallback)
if (sessionName != null) { if (sessionName != null) {
tab.termSession!!.mSessionName = sessionName tab.termSession!!.mSessionName = sessionName