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