package com.termux.app; import android.app.Activity; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.os.Environment; import android.os.UserManager; import android.system.Os; import android.util.Pair; import android.view.WindowManager; import com.termux.R; import com.termux.shared.file.FileUtils; import com.termux.shared.logger.Logger; import com.termux.shared.termux.TermuxConstants; import java.io.BufferedReader; import java.io.ByteArrayInputStream; 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; /** * Install the Termux bootstrap packages if necessary by following the below steps: *

* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a * broken $PREFIX directory below. *

* (2) A progress dialog is shown with "Installing..." message and a spinner. *

* (3) A staging directory, $STAGING_PREFIX, is cleared if left over from broken installation below. *

* (4) The zip file is loaded from a shared library. *

* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream * continuously encountering zip file entries: *

* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup. *

* (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary. */ final class TermuxInstaller { private static final String LOG_TAG = "TermuxInstaller"; /** Performs bootstrap setup if necessary. */ static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) { // Termux can only be run as the primary user (device owner) since only that // account has the expected file system paths. Verify that: UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE); boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0; if (!isPrimaryUser) { String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH); Logger.logError(LOG_TAG, bootstrapErrorMessage); new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(bootstrapErrorMessage) .setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show(); return; } final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR; if (PREFIX_FILE.isDirectory()) { whenDone.run(); return; } final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false); new Thread() { @Override public void run() { try { Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages."); String errmsg; final String STAGING_PREFIX_PATH = TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH; final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH); errmsg = FileUtils.clearDirectory(activity, "prefix staging directory", STAGING_PREFIX_PATH); if (errmsg != null) { throw new RuntimeException(errmsg); } Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "\"."); final byte[] buffer = new byte[8096]; final List> symlinks = new ArrayList<>(50); final byte[] zipBytes = loadZipBytes(); try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { 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) { 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)); ensureDirectoryExists(activity, new File(newPath).getParentFile()); } } else { String zipEntryName = zipEntry.getName(); File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName); boolean isDirectory = zipEntry.isDirectory(); ensureDirectoryExists(activity, isDirectory ? targetFile : targetFile.getParentFile()); if (!isDirectory) { 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); } Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory."); if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) { throw new RuntimeException("Moving prefix staging to prefix directory failed"); } Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully."); activity.runOnUiThread(whenDone); } catch (final Exception e) { Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e); activity.runOnUiThread(() -> { try { new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body) .setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> { dialog.dismiss(); activity.finish(); }).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> { dialog.dismiss(); TermuxInstaller.setupBootstrapIfNeeded(activity, whenDone); }).show(); } catch (WindowManager.BadTokenException e1) { // Activity already dismissed - ignore. } }); } finally { activity.runOnUiThread(() -> { try { progress.dismiss(); } catch (RuntimeException e) { // Activity already dismissed - ignore. } }); } } }.start(); } static void setupStorageSymlinks(final Context context) { final String LOG_TAG = "termux-storage"; Logger.logInfo(LOG_TAG, "Setting up storage symlinks."); new Thread() { public void run() { try { String errmsg; File storageDir = TermuxConstants.TERMUX_STORAGE_HOME_DIR; errmsg = FileUtils.clearDirectory(context, "~/storage", storageDir.getAbsolutePath()); if (errmsg != null) { Logger.logErrorAndShowToast(context, LOG_TAG, errmsg); return; } Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\"."); File sharedDir = Environment.getExternalStorageDirectory(); Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath()); File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); Os.symlink(downloadsDir.getAbsolutePath(), new File(storageDir, "downloads").getAbsolutePath()); File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); Os.symlink(dcimDir.getAbsolutePath(), new File(storageDir, "dcim").getAbsolutePath()); File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); Os.symlink(picturesDir.getAbsolutePath(), new File(storageDir, "pictures").getAbsolutePath()); File musicDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); Os.symlink(musicDir.getAbsolutePath(), new File(storageDir, "music").getAbsolutePath()); File moviesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); Os.symlink(moviesDir.getAbsolutePath(), new File(storageDir, "movies").getAbsolutePath()); final File[] dirs = context.getExternalFilesDirs(null); if (dirs != null && dirs.length > 1) { for (int i = 1; i < dirs.length; i++) { File dir = dirs[i]; if (dir == null) continue; String symlinkName = "external-" + i; Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\"."); Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath()); } } Logger.logInfo(LOG_TAG, "Storage symlinks created successfully."); } catch (Exception e) { Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e); } } }.start(); } private static void ensureDirectoryExists(Context context, File directory) { String errmsg; errmsg = FileUtils.createDirectoryFile(context, directory.getAbsolutePath()); if (errmsg != null) { throw new RuntimeException(errmsg); } } public static byte[] loadZipBytes() { // Only load the shared library when necessary to save memory usage. System.loadLibrary("termux-bootstrap"); return getZip(); } public static native byte[] getZip(); }