Feature: NeoTerm Runtime
This commit is contained in:
@ -18,6 +18,11 @@ android {
|
||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
@ -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"
|
||||
|
182
app/src/main/java/io/neoterm/installer/BaseFileInstaller.java
Normal file
182
app/src/main/java/io/neoterm/installer/BaseFileInstaller.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
46
app/src/main/java/io/neoterm/preference/NeoPermission.kt
Normal file
46
app/src/main/java/io/neoterm/preference/NeoPermission.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user