Skip to content

Commit

Permalink
Ensure termux files directory is accessible before bootstrap installa…
Browse files Browse the repository at this point in the history
…tion and provide better info when running as secondary user/profile

Termux will check if termux files directory `/data/data/com.termux/files` has rwx permission access before installing bootstrap or starting terminal. Missing permission will automatically be set if possible. The `/data/data/com.termux` directory will also be created if it did not already exist, like if android did not already create it.

Users will now also be shown a crash notification if they attempt to start termux as a secondary user or in a work profile with info of the "alternate" termux files directory `/data/user/<id>/com.termux` set by android and the profile owner app if running under work profile (not secondary user). A notification will also be shown if the termux files directory (not "alternate") is not accessible.

Related termux#2168
  • Loading branch information
agnostic-apollo committed Jul 10, 2021
1 parent d1c7845 commit 4eeda58
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 11 deletions.
29 changes: 24 additions & 5 deletions app/src/main/java/com/termux/app/TermuxInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
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.app.utils.CrashUtils;
import com.termux.shared.file.FileUtils;
import com.termux.shared.file.TermuxFileUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.errors.Error;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.termux.TermuxConstants;

import java.io.BufferedReader;
Expand Down Expand Up @@ -53,13 +55,30 @@ final class TermuxInstaller {

/** Performs bootstrap setup if necessary. */
static void setupBootstrapIfNeeded(final Activity activity, final Runnable whenDone) {
String bootstrapErrorMessage;
Error filesDirectoryAccessibleError;

// This will also call Context.getFilesDir(), which should ensure that TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH
// is created if it does not already exist, like if it was not already created by android
filesDirectoryAccessibleError = TermuxFileUtils.isTermuxFilesDirectoryAccessible(activity, true, true);
boolean isFilesDirectoryAccessible = filesDirectoryAccessibleError == null;

// 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);
if (!PackageUtils.isCurrentUserThePrimaryUser(activity)) {
bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_PREFIX_DIR_PATH, false));
Logger.logError(LOG_TAG, bootstrapErrorMessage);
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + bootstrapErrorMessage, true, true);
MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
return;
}

if (!isFilesDirectoryAccessible) {
bootstrapErrorMessage = Error.getMinimalErrorString(filesDirectoryAccessibleError) + "\nTERMUX_FILES_DIR: " + MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_FILES_DIR_PATH, false);
Logger.logError(LOG_TAG, bootstrapErrorMessage);
CrashUtils.sendCrashReportNotification(activity, LOG_TAG, "## Bootstrap Error\n\n" + bootstrapErrorMessage, true, true);
MessageDialogUtils.exitAppWithErrorMessage(activity,
activity.getString(R.string.bootstrap_error_title),
bootstrapErrorMessage);
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<string name="bootstrap_error_body">&TERMUX_APP_NAME; was unable to install the bootstrap packages.</string>
<string name="bootstrap_error_abort">Abort</string>
<string name="bootstrap_error_try_again">Try again</string>
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed under any path other than \"%1$s\".</string>
<string name="bootstrap_error_not_primary_user_message">&TERMUX_APP_NAME; can only be run as the primary user.\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed under any path other than %1$s.</string>



Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.termux.shared.file;

import android.content.Context;
import android.os.Environment;

import androidx.annotation.NonNull;

import com.termux.shared.models.errors.Error;
import com.termux.shared.termux.TermuxConstants;

import java.io.File;
import java.util.regex.Pattern;

public class TermuxFileUtils {

/**
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
*
Expand Down Expand Up @@ -120,4 +124,41 @@ public static Error validateDirectoryFileExistenceAndPermissions(String label, f
ignoreErrorsIfPathIsInParentDirPath, ignoreIfNotExecutable);
}

/**
* Validate the existence and permissions of {@link TermuxConstants#TERMUX_FILES_DIR_PATH}.
*
* The directory will not be created manually but by calling {@link Context#getFilesDir()}
* so that android itself creates it. The `/data/data/[package_name]` directory cannot be
* created by an app itself. Note that the path returned by {@link Context#getFilesDir()} will
* be under `/data/user/[id]/[package_name]` instead of `/data/data/[package_name]`
* defined by default by {@link TermuxConstants#TERMUX_FILES_DIR_PATH}, where id will be 0 for
* primary user and a higher number for other users/profiles. If app is running under work profile
* or secondary user, then {@link TermuxConstants#TERMUX_FILES_DIR_PATH} will not be accessible
* and will not be automatically created, unless there is a bind mount from `/data/user/[id]`
* to `/data/data`, ideally in the right namespace.
*
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
*
* https://source.android.com/devices/tech/admin/multi-user
*
* @param context The {@link Context} for operations.
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
* should be created if its missing.
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
* automatically set.
* @return Returns the {@code error} if path is not a directory file, failed to create it,
* or validating permissions failed, otherwise {@code null}.
*/
public static Error isTermuxFilesDirectoryAccessible(@NonNull final Context context, boolean createDirectoryIfMissing, boolean setMissingPermissions) {
if (createDirectoryIfMissing)
context.getFilesDir();

if (setMissingPermissions)
FileUtils.setMissingFilePermissions("Termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS);

return FileUtils.checkMissingFilePermissions("Termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, false);
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.termux.shared.packages;

import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.UserManager;

import androidx.annotation.NonNull;

Expand All @@ -14,6 +17,7 @@
import com.termux.shared.termux.TermuxConstants;

import java.security.MessageDigest;
import java.util.List;

import javax.annotation.Nullable;

Expand Down Expand Up @@ -163,7 +167,7 @@ public static String getVersionNameForPackage(@NonNull final Context context) {
* Get the {@code SHA-256 digest} of signing certificate for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the{@code SHA-256 digest}. This will be {@code null} if an exception is raised.
* @return Returns the {@code SHA-256 digest}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context) {
Expand All @@ -184,4 +188,53 @@ public static String getSigningCertificateSHA256DigestForPackage(@NonNull final
}
}



/**
* Get the serial number for the current user.
*
* @param context The {@link Context} for operations.
* @return Returns the serial number. This will be {@code null} if failed to get it.
*/
@Nullable
public static Long getSerialNumberForCurrentUser(@NonNull Context context) {
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (userManager == null) return null;
return userManager.getSerialNumberForUser(android.os.Process.myUserHandle());
}

/**
* Check if the current user is the primary user. This is done by checking if the the serial
* number for the current user equals 0.
*
* @param context The {@link Context} for operations.
* @return Returns {@code true} if the current user is the primary user, otherwise [@code false}.
*/
public static boolean isCurrentUserThePrimaryUser(@NonNull Context context) {
Long userId = getSerialNumberForCurrentUser(context);
return userId != null && userId == 0;
}

/**
* Get the profile owner package name for the current user.
*
* @param context The {@link Context} for operations.
* @return Returns the profile owner package name. This will be {@code null} if failed to get it
* or no profile owner for the current user.
*/
@Nullable
public static String getProfileOwnerPackageNameForUser(@NonNull Context context) {
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
if (devicePolicyManager == null) return null;
List<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
if (activeAdmins != null){
for (ComponentName admin:activeAdmins){
String packageName = admin.getPackageName();
if(devicePolicyManager.isProfileOwnerApp(packageName))
return packageName;
}
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ public static String getAppInfoMarkdownString(@NonNull final Context context) {
AndroidUtils.appendPropertyToMarkdown(markdownString,"TARGET_SDK", PackageUtils.getTargetSDKForPackage(context));
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_DEBUG_BUILD", PackageUtils.isAppForPackageADebugBuild(context));

String filesDir = context.getFilesDir().getAbsolutePath();
if (!filesDir.equals("/data/user/0/" + context.getPackageName() + "/files") &&
!filesDir.equals("/data/data/" + context.getPackageName() + "/files"))
AndroidUtils.appendPropertyToMarkdown(markdownString,"FILES_DIR", filesDir);

Long userId = PackageUtils.getSerialNumberForCurrentUser(context);
if (userId == null || userId != 0)
AndroidUtils.appendPropertyToMarkdown(markdownString,"USER_ID", userId);

AndroidUtils.appendPropertyToMarkdownIfSet(markdownString,"PROFILE_OWNER", PackageUtils.getProfileOwnerPackageNameForUser(context));

return markdownString.toString();
}

Expand Down Expand Up @@ -139,7 +150,7 @@ public static Properties getSystemProperties() {
return systemProperties;
}

private static String getSystemPropertyWithAndroidAPI(@NonNull String property) {
public static String getSystemPropertyWithAndroidAPI(@NonNull String property) {
try {
return System.getProperty(property);
} catch (Exception e) {
Expand All @@ -148,17 +159,17 @@ private static String getSystemPropertyWithAndroidAPI(@NonNull String property)
}
}

private static void appendPropertyToMarkdownIfSet(StringBuilder markdownString, String label, Object value) {
public static void appendPropertyToMarkdownIfSet(StringBuilder markdownString, String label, Object value) {
if (value == null) return;
if (value instanceof String && (((String) value).isEmpty()) || "REL".equals(value)) return;
markdownString.append("\n").append(getPropertyMarkdown(label, value));
}

static void appendPropertyToMarkdown(StringBuilder markdownString, String label, Object value) {
public static void appendPropertyToMarkdown(StringBuilder markdownString, String label, Object value) {
markdownString.append("\n").append(getPropertyMarkdown(label, value));
}

private static String getPropertyMarkdown(String label, Object value) {
public static String getPropertyMarkdown(String label, Object value) {
return MarkdownUtils.getSingleLineMarkdownStringEntry(label, value, "-");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import androidx.annotation.NonNull;

import com.termux.shared.R;
import com.termux.shared.file.TermuxFileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.models.ExecutionCommand;
import com.termux.shared.models.errors.Error;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.shell.TermuxShellEnvironmentClient;
import com.termux.shared.shell.TermuxTask;
Expand Down Expand Up @@ -210,6 +212,13 @@ public static String getAppInfoMarkdownStringInner(@NonNull final Context contex

markdownString.append((AndroidUtils.getAppInfoMarkdownString(context)));

Error error;
error = TermuxFileUtils.isTermuxFilesDirectoryAccessible(context, true, true);
if (error != null) {
AndroidUtils.appendPropertyToMarkdown(markdownString, "TERMUX_FILES_DIR", TermuxConstants.TERMUX_FILES_DIR_PATH);
AndroidUtils.appendPropertyToMarkdown(markdownString, "IS_TERMUX_FILES_DIR_ACCESSIBLE", "false - " + Error.getMinimalErrorString(error));
}

String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
if (signingCertificateSHA256Digest != null) {
AndroidUtils.appendPropertyToMarkdown(markdownString,"APK_RELEASE", getAPKRelease(signingCertificateSHA256Digest));
Expand Down

0 comments on commit 4eeda58

Please sign in to comment.