Setup: Support multiple setup source

This commit is contained in:
zt515 2017-12-16 16:59:59 +08:00
parent d18f0bf763
commit c354ee703e
10 changed files with 417 additions and 34 deletions

View File

@ -86,7 +86,7 @@ open class NeoColorScheme : CodeGenObject {
COLOR_FOREGROUND -> foregroundColor COLOR_FOREGROUND -> foregroundColor
COLOR_CURSOR -> cursorColor COLOR_CURSOR -> cursorColor
else -> { else -> {
if (type in (0..color.size - 1)) { if (type in (0 until color.size)) {
color[type] color[type]
} else { } else {
"" ""

View File

@ -0,0 +1,8 @@
package io.neoterm.component.setup;
/**
* @author kiva
*/
public interface ResultListener {
void onResult(Exception error);
}

View File

@ -0,0 +1,77 @@
package io.neoterm.component.setup;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Build;
import android.system.Os;
import android.util.Pair;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
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.R;
import io.neoterm.backend.EmulatorDebug;
import io.neoterm.frontend.logging.NLog;
import io.neoterm.frontend.config.NeoPreference;
import io.neoterm.frontend.config.NeoTermPath;
/**
* @author kiva
*/
public final class SetupHelper {
public static boolean needSetup() {
final File PREFIX_FILE = new File(NeoTermPath.USR_PATH);
return !PREFIX_FILE.isDirectory();
}
public static void setup(final Activity activity, final SourceConnection connection,
final ResultListener resultListener) {
if (!needSetup()) {
resultListener.onResult(null);
return;
}
final File prefixFile = new File(NeoTermPath.USR_PATH);
final ProgressDialog progress = makeProgressDialog(activity);
progress.setMax(100);
progress.show();
new SetupThread(activity, connection, prefixFile, resultListener, progress);
}
private static ProgressDialog makeProgressDialog(Context context) {
ProgressDialog dialog = new ProgressDialog(context);
dialog.setMessage(context.getString(R.string.installer_message));
dialog.setIndeterminate(false);
dialog.setCancelable(false);
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
return dialog;
}
public 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";
}
}
throw new RuntimeException("Unable to determine arch from Build.SUPPORTED_ABIS = " +
Arrays.toString(Build.SUPPORTED_ABIS));
}
}

View File

@ -0,0 +1,172 @@
package io.neoterm.component.setup;
import android.app.Activity;
import android.app.ProgressDialog;
import android.system.Os;
import android.util.Pair;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import io.neoterm.backend.EmulatorDebug;
import io.neoterm.frontend.config.NeoTermPath;
import io.neoterm.frontend.logging.NLog;
/**
* @author kiva
*/
final class SetupThread extends Thread {
private final SourceConnection sourceConnection;
private final File prefixPath;
private final Activity activity;
private final ResultListener resultListener;
private final ProgressDialog progressDialog;
public SetupThread(Activity activity, SourceConnection sourceConnection,
File prefixPath, ResultListener resultListener,
ProgressDialog progressDialog) {
this.activity = activity;
this.sourceConnection = sourceConnection;
this.prefixPath = prefixPath;
this.resultListener = resultListener;
this.progressDialog = progressDialog;
}
@Override
public void run() {
try {
final String stagingPrefixPath = NeoTermPath.ROOT_PATH + "/usr-staging";
final File stagingPrefixFile = new File(stagingPrefixPath);
if (stagingPrefixFile.exists()) {
deleteFolder(stagingPrefixFile);
}
int totalBytes = 0;
int totalReadBytes = 0;
final byte[] buffer = new byte[8096];
final List<Pair<String, String>> symlinks = new ArrayList<>(50);
totalBytes = sourceConnection.getSize();
try (ZipInputStream zipInput = new ZipInputStream(sourceConnection.getInputStream())) {
ZipEntry zipEntry;
while ((zipEntry = zipInput.getNextEntry()) != null) {
totalReadBytes += zipEntry.getCompressedSize();
final int totalReadBytesFinal = totalReadBytes;
final int totalBytesFinal = totalBytes;
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
double progressFloat = ((double) totalReadBytesFinal) / ((double) totalBytesFinal) * 100.0;
progressDialog.setProgress((int) progressFloat);
} catch (RuntimeException ignore) {
// activity dismissed
}
}
});
if (zipEntry.getName().contains("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 = stagingPrefixPath + "/" + parts[1];
symlinks.add(Pair.create(oldPath, newPath));
}
} else {
String zipEntryName = zipEntry.getName();
File targetFile = new File(stagingPrefixPath, 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);
}
}
}
}
}
sourceConnection.close();
if (symlinks.isEmpty())
throw new RuntimeException("No SYMLINKS.txt encountered");
for (Pair<String, String> symlink : symlinks) {
NLog.INSTANCE.e("Setup", "Linking " + symlink.first + " to " + symlink.second);
Os.symlink(symlink.first, symlink.second);
}
if (!stagingPrefixFile.renameTo(prefixPath)) {
throw new RuntimeException("Unable to rename staging folder");
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
resultListener.onResult(null);
}
});
} catch (final Exception e) {
NLog.INSTANCE.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
resultListener.onResult(e);
} catch (RuntimeException e) {
// Activity already dismissed - ignore.
}
}
});
} finally {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
progressDialog.dismiss();
} catch (RuntimeException e) {
// Activity already dismissed - ignore.
}
}
});
}
}
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,16 @@
package io.neoterm.component.setup;
import java.io.IOException;
import java.io.InputStream;
/**
* @author kiva
*/
public interface SourceConnection {
InputStream getInputStream() throws IOException;
int getSize();
void close();
}

View File

@ -0,0 +1,56 @@
package io.neoterm.component.setup.connection;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import io.neoterm.R;
import io.neoterm.component.setup.SetupHelper;
import io.neoterm.component.setup.SourceConnection;
import io.neoterm.frontend.config.NeoPreference;
import io.neoterm.frontend.config.NeoTermPath;
/**
* @author kiva
*/
public class NetworkConnection implements SourceConnection {
private HttpURLConnection connection = null;
@Override
public InputStream getInputStream() throws IOException {
if (connection == null) {
connection = openBaseFileConnection();
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
}
return connection.getInputStream();
}
@Override
public int getSize() {
if (connection != null) {
return connection.getContentLength();
}
return 0;
}
@Override
public void close() {
if (connection != null) {
connection.disconnect();
}
}
private static HttpURLConnection openBaseFileConnection() throws IOException {
String arch = SetupHelper.determineArchName();
String baseUrl = NeoTermPath.INSTANCE.getSERVER_BASE_URL();
// Use the same source
NeoPreference.INSTANCE.store(R.string.key_package_source, baseUrl);
return (HttpURLConnection) new URL(baseUrl + "/boot/" + arch + ".zip").openConnection();
}
}

View File

@ -0,0 +1,51 @@
package io.neoterm.component.setup.connection;
import java.io.IOException;
import java.io.InputStream;
import io.neoterm.App;
import io.neoterm.component.setup.SetupHelper;
import io.neoterm.component.setup.SourceConnection;
import io.neoterm.utils.AssetsUtils;
/**
* @author kiva
*/
public class OfflineConnection implements SourceConnection {
private InputStream inputStream;
@Override
public InputStream getInputStream() throws IOException {
if (inputStream == null) {
String arch = SetupHelper.determineArchName();
String fileName = "offline_setup/" + arch + ".zip";
inputStream = AssetsUtils.INSTANCE.openAssetsFile(App.Companion.get(), fileName);
}
return inputStream;
}
@Override
public int getSize() {
if (inputStream != null) {
try {
return inputStream.available();
} catch (IOException e) {
e.printStackTrace();
return 0;
}
}
return 0;
}
@Override
public void close() {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ignore) {
ignore.printStackTrace();
}
}
}
}

View File

@ -1,12 +1,8 @@
package io.neoterm.ui.setup package io.neoterm.ui.setup
import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import io.neoterm.App
import io.neoterm.R import io.neoterm.R
import io.neoterm.component.setup.BaseFileInstaller
import io.neoterm.utils.PackageUtils import io.neoterm.utils.PackageUtils
@ -19,35 +15,36 @@ class SetupActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.ui_setup) setContentView(R.layout.ui_setup)
installBaseFiles() setup()
} }
private fun installBaseFiles() { private fun setup() {
var resultListener: BaseFileInstaller.ResultListener? = null // TODO: Refactor
resultListener = BaseFileInstaller.ResultListener { error -> // var resultListener: SetupHelper.ResultListener? = null
if (error == null) { // resultListener = SetupHelper.ResultListener { error ->
setResult(Activity.RESULT_OK) // if (error == null) {
PackageUtils.syncSource() // setResult(Activity.RESULT_OK)
executeAptUpdate() // PackageUtils.syncSource()
} else { // executeAptUpdate()
AlertDialog.Builder(this@SetupActivity) // } else {
.setTitle(R.string.error) // AlertDialog.Builder(this@SetupActivity)
.setMessage(error.toString()) // .setTitle(R.string.error)
.setNegativeButton(R.string.use_system_shell, { _, _ -> // .setMessage(error.toString())
setResult(Activity.RESULT_CANCELED) // .setNegativeButton(R.string.use_system_shell, { _, _ ->
finish() // setResult(Activity.RESULT_CANCELED)
}) // finish()
.setPositiveButton(R.string.retry, { dialog, _ -> // })
dialog.dismiss() // .setPositiveButton(R.string.retry, { dialog, _ ->
BaseFileInstaller.installBaseFiles(this@SetupActivity, resultListener) // dialog.dismiss()
}) // SetupHelper.setup(this@SetupActivity, resultListener)
.setNeutralButton(R.string.show_help, { _, _ -> // })
App.get().openHelpLink() // .setNeutralButton(R.string.show_help, { _, _ ->
}) // App.get().openHelpLink()
.show() // })
} // .show()
} // }
BaseFileInstaller.installBaseFiles(this, resultListener) // }
// SetupHelper.setup(this, resultListener)
} }
private fun executeAptUpdate() { private fun executeAptUpdate() {

View File

@ -20,7 +20,7 @@ import de.mrapp.android.tabswitcher.*
import io.neoterm.App import io.neoterm.App
import io.neoterm.R import io.neoterm.R
import io.neoterm.backend.TerminalSession import io.neoterm.backend.TerminalSession
import io.neoterm.component.setup.BaseFileInstaller import io.neoterm.component.setup.SetupHelper
import io.neoterm.frontend.session.shell.client.TermSessionCallback import io.neoterm.frontend.session.shell.client.TermSessionCallback
import io.neoterm.frontend.session.shell.client.TermViewClient import io.neoterm.frontend.session.shell.client.TermViewClient
import io.neoterm.frontend.session.shell.client.event.* import io.neoterm.frontend.session.shell.client.event.*
@ -319,7 +319,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
} }
if (!isRecreating()) { if (!isRecreating()) {
if (BaseFileInstaller.needSetup()) { if (SetupHelper.needSetup()) {
val intent = Intent(this, SetupActivity::class.java) val intent = Intent(this, SetupActivity::class.java)
startActivityForResult(intent, REQUEST_SETUP) startActivityForResult(intent, REQUEST_SETUP)
return return

View File

@ -2,6 +2,7 @@ package io.neoterm.utils
import android.content.Context import android.content.Context
import java.io.File import java.io.File
import java.io.InputStream
/** /**
* @author kiva * @author kiva
@ -18,4 +19,9 @@ object AssetsUtils {
} }
} }
} }
fun openAssetsFile(context: Context, fileName: String) : InputStream {
val assets = context.assets
return assets.open(fileName)
}
} }