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'
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
}
buildTypes {
release {

View File

@ -3,6 +3,9 @@
package="io.neoterm">
<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
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 io.neoterm.backend.TerminalSession
import io.neoterm.services.NeoTermService
import java.io.File
/**
* @author kiva
*/
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_CURRENT_SESSION = "neoterm_service_current_session"
@ -80,4 +86,27 @@ object NeoTermPreference {
}
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.support.v4.content.WakefulBroadcastReceiver
import android.util.Log
import java.util.ArrayList
import io.neoterm.ui.NeoTermActivity
import io.neoterm.R
import io.neoterm.backend.EmulatorDebug
import io.neoterm.backend.TerminalSession
import io.neoterm.preference.NeoTermPreference
import io.neoterm.ui.NeoTermActivity
import java.util.*
/**
* @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 {
var executablePath = executablePath
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) {
// Fall back to system shell as last resort:
executablePath = "/system/bin/sh"
isLoginShell = true
executablePath = NeoTermPreference.USR_PATH + "/bin/bash"
}
if (arguments == null) {
arguments = arrayOf<String>(executablePath)
}
val lastSlashIndex = executablePath.lastIndexOf('/')
val processName = (if (isLoginShell) "-" else "") + if (lastSlashIndex == -1) executablePath else executablePath.substring(lastSlashIndex + 1)
val session = TerminalSession(executablePath, cwd, arguments, env, sessionCallback)
val session = TerminalSession(executablePath, cwd, arguments, env ?: NeoTermPreference.buildEnvironment(cwd), sessionCallback)
mTerminalSessions.add(session)
updateNotification()
return session
@ -126,9 +121,7 @@ class NeoTermService : Service() {
}
companion object {
val ACTION_SERVICE_STOP = "neoterm.action.service.stop"
private val NOTIFICATION_ID = 52019
}
}

View File

@ -1,8 +1,11 @@
package io.neoterm.ui
import android.app.AlertDialog
import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.IBinder
import android.support.v4.content.ContextCompat
@ -18,6 +21,8 @@ import de.mrapp.android.tabswitcher.*
import de.mrapp.android.tabswitcher.view.TabSwitcherButton
import io.neoterm.R
import io.neoterm.backend.TerminalSession
import io.neoterm.installer.BaseFileInstaller
import io.neoterm.preference.NeoPermission
import io.neoterm.preference.NeoTermPreference
import io.neoterm.services.NeoTermService
import io.neoterm.ui.settings.SettingActivity
@ -41,19 +46,23 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection {
return
}
if (!termService!!.sessions.isEmpty()) {
for (session in termService!!.sessions) {
addNewSession(session)
BaseFileInstaller.installBaseFiles(this, {
if (!termService!!.sessions.isEmpty()) {
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?) {
super.onCreate(savedInstanceState)
NeoPermission.initAppPermission(this, NeoPermission.REQUEST_APP_PERMISSION)
NeoTermPreference.init(this)
if (NeoTermPreference.loadBoolean(R.string.key_ui_fullscreen, false)) {
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
@ -150,6 +159,22 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection {
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?) {
if (session == null) {
return
@ -168,7 +193,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection {
val tab = createTab(sessionName) as TermTab
tab.sessionCallback = TermSessionChangedCallback()
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) {
tab.termSession!!.mSessionName = sessionName