diff --git a/app/build.gradle b/app/build.gradle
index 6aefc8c..392699a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -18,6 +18,11 @@ android {
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
}
+ sourceSets {
+ main {
+ jniLibs.srcDirs = ['src/main/jniLibs']
+ }
+ }
}
buildTypes {
release {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 848c04b..3d47e09 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,9 @@
package="io.neoterm">
+
+
+
> 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 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());
+ }
+ }
+}
diff --git a/app/src/main/java/io/neoterm/preference/NeoPermission.kt b/app/src/main/java/io/neoterm/preference/NeoPermission.kt
new file mode 100644
index 0000000..a9779bc
--- /dev/null
+++ b/app/src/main/java/io/neoterm/preference/NeoPermission.kt
@@ -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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/neoterm/preference/NeoTermPreference.kt b/app/src/main/java/io/neoterm/preference/NeoTermPreference.kt
index 9d2ea10..4ea2c70 100644
--- a/app/src/main/java/io/neoterm/preference/NeoTermPreference.kt
+++ b/app/src/main/java/io/neoterm/preference/NeoTermPreference.kt
@@ -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 {
+ 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)
+
+ }
}
diff --git a/app/src/main/java/io/neoterm/services/NeoTermService.kt b/app/src/main/java/io/neoterm/services/NeoTermService.kt
index 2138a95..431fe18 100644
--- a/app/src/main/java/io/neoterm/services/NeoTermService.kt
+++ b/app/src/main/java/io/neoterm/services/NeoTermService.kt
@@ -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?, cwd: String?, env: Array?, 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(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
}
}
diff --git a/app/src/main/java/io/neoterm/ui/NeoTermActivity.kt b/app/src/main/java/io/neoterm/ui/NeoTermActivity.kt
index c4635af..748a2c9 100644
--- a/app/src/main/java/io/neoterm/ui/NeoTermActivity.kt
+++ b/app/src/main/java/io/neoterm/ui/NeoTermActivity.kt
@@ -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, 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