package io.neoterm.installer; import android.app.Activity; import android.app.ProgressDialog; import android.os.Build; import android.system.Os; import android.util.Log; import android.util.Pair; 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.customize.NeoTermPath; import io.neoterm.preference.NeoTermPreference; public final class BaseFileInstaller { public interface ResultListener { void onResult(Exception error); } public static void installBaseFiles(final Activity activity, final ResultListener resultListener) { final File PREFIX_FILE = new File(NeoTermPath.USR_PATH); if (PREFIX_FILE.isDirectory()) { resultListener.onResult(null); return; } final ProgressDialog progress = ProgressDialog.show(activity, null, "Installing", true, false); new Thread() { @Override public void run() { try { final String STAGING_PREFIX_PATH = NeoTermPath.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> 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() { resultListener.onResult(null); } }); } catch (final Exception e) { Log.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 { 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"); } 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"; 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()); } } }