diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java
index 60c5dc00..2789fbf4 100644
--- a/app/src/main/java/com/termux/app/TermuxInstaller.java
+++ b/app/src/main/java/com/termux/app/TermuxInstaller.java
@@ -5,7 +5,6 @@ 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;
@@ -13,9 +12,12 @@ 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;
@@ -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);
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c5133bb2..2ec1304f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -31,7 +31,7 @@
&TERMUX_APP_NAME; was unable to install the bootstrap packages.
Abort
Try again
- &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\".
+ &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.
diff --git a/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java b/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java
index 7748a6bc..f2ac1dce 100644
--- a/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/file/TermuxFileUtils.java
@@ -1,7 +1,10 @@
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;
@@ -9,6 +12,7 @@ import java.io.File;
import java.util.regex.Pattern;
public class TermuxFileUtils {
+
/**
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
*
@@ -120,4 +124,41 @@ public class TermuxFileUtils {
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);
+ }
+
}
diff --git a/termux-shared/src/main/java/com/termux/shared/packages/PackageUtils.java b/termux-shared/src/main/java/com/termux/shared/packages/PackageUtils.java
index 0ef26664..dbd27155 100644
--- a/termux-shared/src/main/java/com/termux/shared/packages/PackageUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/packages/PackageUtils.java
@@ -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;
@@ -14,6 +17,7 @@ import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxConstants;
import java.security.MessageDigest;
+import java.util.List;
import javax.annotation.Nullable;
@@ -163,7 +167,7 @@ public class PackageUtils {
* 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) {
@@ -184,4 +188,53 @@ public class PackageUtils {
}
}
+
+
+ /**
+ * 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 activeAdmins = devicePolicyManager.getActiveAdmins();
+ if (activeAdmins != null){
+ for (ComponentName admin:activeAdmins){
+ String packageName = admin.getPackageName();
+ if(devicePolicyManager.isProfileOwnerApp(packageName))
+ return packageName;
+ }
+ }
+ return null;
+ }
+
}
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/AndroidUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/AndroidUtils.java
index 698d2c28..418f7d85 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/AndroidUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/AndroidUtils.java
@@ -40,6 +40,17 @@ public class AndroidUtils {
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();
}
@@ -139,7 +150,7 @@ public class AndroidUtils {
return systemProperties;
}
- private static String getSystemPropertyWithAndroidAPI(@NonNull String property) {
+ public static String getSystemPropertyWithAndroidAPI(@NonNull String property) {
try {
return System.getProperty(property);
} catch (Exception e) {
@@ -148,17 +159,17 @@ public class AndroidUtils {
}
}
- 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, "-");
}
diff --git a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java
index 3eeb584e..ba623bc2 100644
--- a/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java
+++ b/termux-shared/src/main/java/com/termux/shared/termux/TermuxUtils.java
@@ -8,9 +8,11 @@ import android.content.pm.ResolveInfo;
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;
@@ -210,6 +212,13 @@ public class TermuxUtils {
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));