termux-app/app/src/main/java/com/termux/app/TermuxInstaller.java

249 lines
12 KiB
Java
Raw Normal View History

2015-10-25 14:27:32 +00:00
package com.termux.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Environment;
2016-07-31 20:28:17 +00:00
import android.os.UserManager;
2015-10-25 14:27:32 +00:00
import android.system.Os;
import android.util.Pair;
import android.view.WindowManager;
2015-10-25 14:27:32 +00:00
import com.termux.R;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants;
2015-10-25 14:27:32 +00:00
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;
2015-10-25 14:27:32 +00:00
/**
* Install the Termux bootstrap packages if necessary by following the below steps:
* <p/>
2015-10-25 14:27:32 +00:00
* (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.
* <p/>
2015-10-25 14:27:32 +00:00
* (2) A progress dialog is shown with "Installing..." message and a spinner.
* <p/>
* (3) A staging directory, $STAGING_PREFIX, is cleared if left over from broken installation below.
* <p/>
* (4) The zip file is loaded from a shared library.
* <p/>
2015-10-25 14:27:32 +00:00
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
2017-09-13 15:16:08 +00:00
* continuously encountering zip file entries:
* <p/>
2015-10-25 14:27:32 +00:00
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
* <p/>
2015-10-25 14:27:32 +00:00
* (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary.
*/
final class TermuxInstaller {
Implement GUI based Termux settings manager and a centralized logging framework The settings activity can be accessed by long pressing on terminal view and selecting "Settings" from the popup shown. It uses the Android's Preference framework. Currently only debugging preferences to set log level and enabling terminal view key logging are provided. The Preference framework by default uses the keys set in `app:key` attribute in the respective preferences XML file to store the values in the default `SharedPreferences` file of the app. However, since we rely on `TermuxPreferenceConstants` and `TermuxPropertyConstants` classes to define key names so that they can be easily shared between termux and its plugin apps, we provide our own `PreferenceDataStore` for storing key/value pairs. The key name in the XML file can optionally be the same. Check `DebuggingPreferencesFragment` class for a sample. Each new preference category fragment should be added to `app/settings/` with its data store. This commit may allow support to be added for modifying `termux.properties` file directly from the UI but that requires more work, since writing to property files with comments require in-place modification. The `Logger` class provides various static functions for logging that should be used from now on instead of directly calling android `Log.*` functions. The log level is automatically loaded from shared preferences at application startup via `TermuxApplication` and set in the static `Logger.CURRENT_LOG_LEVEL` variable. Changing the log level through the settings activity also changes the log level immediately. The 4 supported log levels are: - LOG_LEVEL_OFF which will log nothing. - LOG_LEVEL_NORMAL which will start logging error, warn and info messages and stacktraces. - LOG_LEVEL_DEBUG which will start logging debug messages. - LOG_LEVEL_VERBOSE which will start logging verbose messages. The default log level is `LOG_LEVEL_NORMAL` which will not log debug or verbose messages. Contributors can add useful log entries at those levels where ever they feel is appropriate so that it allows users and devs to more easily help solve issues or find bugs, specially without having to recompile termux after having to manually add general log entries to the source. DO NOT log data that may have private info of users like command arguments at log levels below debug, like `BackgroundJob` was doing previously. Logging to file support may be added later, will require log file rotation support and storage permissions.
2021-03-13 11:49:29 +00:00
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:
2016-07-31 20:28:17 +00:00
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)
2018-09-28 22:49:05 +00:00
.setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
return;
}
2015-10-25 14:27:32 +00:00
final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR;
if (PREFIX_FILE.isDirectory()) {
whenDone.run();
return;
}
2015-10-25 14:27:32 +00:00
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);
2015-10-25 14:27:32 +00:00
errmsg = FileUtils.clearDirectory(activity, "prefix staging directory", STAGING_PREFIX_PATH);
if (errmsg != null) {
throw new RuntimeException(errmsg);
}
2015-10-25 14:27:32 +00:00
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<Pair<String, String>> symlinks = new ArrayList<>(50);
2015-10-25 14:27:32 +00:00
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);
}
}
}
}
}
2015-10-25 14:27:32 +00:00
if (symlinks.isEmpty())
throw new RuntimeException("No SYMLINKS.txt encountered");
for (Pair<String, String> symlink : symlinks) {
Os.symlink(symlink.first, symlink.second);
}
2015-10-25 14:27:32 +00:00
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");
}
2015-10-25 14:27:32 +00:00
Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully.");
2018-09-28 22:49:05 +00:00
activity.runOnUiThread(whenDone);
} catch (final Exception e) {
Implement GUI based Termux settings manager and a centralized logging framework The settings activity can be accessed by long pressing on terminal view and selecting "Settings" from the popup shown. It uses the Android's Preference framework. Currently only debugging preferences to set log level and enabling terminal view key logging are provided. The Preference framework by default uses the keys set in `app:key` attribute in the respective preferences XML file to store the values in the default `SharedPreferences` file of the app. However, since we rely on `TermuxPreferenceConstants` and `TermuxPropertyConstants` classes to define key names so that they can be easily shared between termux and its plugin apps, we provide our own `PreferenceDataStore` for storing key/value pairs. The key name in the XML file can optionally be the same. Check `DebuggingPreferencesFragment` class for a sample. Each new preference category fragment should be added to `app/settings/` with its data store. This commit may allow support to be added for modifying `termux.properties` file directly from the UI but that requires more work, since writing to property files with comments require in-place modification. The `Logger` class provides various static functions for logging that should be used from now on instead of directly calling android `Log.*` functions. The log level is automatically loaded from shared preferences at application startup via `TermuxApplication` and set in the static `Logger.CURRENT_LOG_LEVEL` variable. Changing the log level through the settings activity also changes the log level immediately. The 4 supported log levels are: - LOG_LEVEL_OFF which will log nothing. - LOG_LEVEL_NORMAL which will start logging error, warn and info messages and stacktraces. - LOG_LEVEL_DEBUG which will start logging debug messages. - LOG_LEVEL_VERBOSE which will start logging verbose messages. The default log level is `LOG_LEVEL_NORMAL` which will not log debug or verbose messages. Contributors can add useful log entries at those levels where ever they feel is appropriate so that it allows users and devs to more easily help solve issues or find bugs, specially without having to recompile termux after having to manually add general log entries to the source. DO NOT log data that may have private info of users like command arguments at log levels below debug, like `BackgroundJob` was doing previously. Logging to file support may be added later, will require log file rotation support and storage permissions.
2021-03-13 11:49:29 +00:00
Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e);
2018-09-28 22:49:05 +00:00
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();
2018-09-28 22:49:05 +00:00
} catch (WindowManager.BadTokenException e1) {
// Activity already dismissed - ignore.
}
});
} finally {
2018-09-28 22:49:05 +00:00
activity.runOnUiThread(() -> {
try {
progress.dismiss();
} catch (RuntimeException e) {
// Activity already dismissed - ignore.
}
});
}
}
}.start();
}
2015-10-25 14:27:32 +00:00
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) {
Implement GUI based Termux settings manager and a centralized logging framework The settings activity can be accessed by long pressing on terminal view and selecting "Settings" from the popup shown. It uses the Android's Preference framework. Currently only debugging preferences to set log level and enabling terminal view key logging are provided. The Preference framework by default uses the keys set in `app:key` attribute in the respective preferences XML file to store the values in the default `SharedPreferences` file of the app. However, since we rely on `TermuxPreferenceConstants` and `TermuxPropertyConstants` classes to define key names so that they can be easily shared between termux and its plugin apps, we provide our own `PreferenceDataStore` for storing key/value pairs. The key name in the XML file can optionally be the same. Check `DebuggingPreferencesFragment` class for a sample. Each new preference category fragment should be added to `app/settings/` with its data store. This commit may allow support to be added for modifying `termux.properties` file directly from the UI but that requires more work, since writing to property files with comments require in-place modification. The `Logger` class provides various static functions for logging that should be used from now on instead of directly calling android `Log.*` functions. The log level is automatically loaded from shared preferences at application startup via `TermuxApplication` and set in the static `Logger.CURRENT_LOG_LEVEL` variable. Changing the log level through the settings activity also changes the log level immediately. The 4 supported log levels are: - LOG_LEVEL_OFF which will log nothing. - LOG_LEVEL_NORMAL which will start logging error, warn and info messages and stacktraces. - LOG_LEVEL_DEBUG which will start logging debug messages. - LOG_LEVEL_VERBOSE which will start logging verbose messages. The default log level is `LOG_LEVEL_NORMAL` which will not log debug or verbose messages. Contributors can add useful log entries at those levels where ever they feel is appropriate so that it allows users and devs to more easily help solve issues or find bugs, specially without having to recompile termux after having to manually add general log entries to the source. DO NOT log data that may have private info of users like command arguments at log levels below debug, like `BackgroundJob` was doing previously. Logging to file support may be added later, will require log file rotation support and storage permissions.
2021-03-13 11:49:29 +00:00
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();
2015-10-25 14:27:32 +00:00
}