From 6fa4b9b7cd256a7dec78fcb8ec274bca727e4c1b Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 10 Jul 2021 15:56:52 +0500 Subject: [PATCH] Ensure termux files directory is accessible before bootstrap installation 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//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 #2168 --- .../java/com/termux/app/TermuxInstaller.java | 29 ++++++++-- app/src/main/res/values/strings.xml | 2 +- .../termux/shared/file/TermuxFileUtils.java | 41 ++++++++++++++ .../termux/shared/packages/PackageUtils.java | 55 ++++++++++++++++++- .../termux/shared/termux/AndroidUtils.java | 19 +++++-- .../com/termux/shared/termux/TermuxUtils.java | 9 +++ 6 files changed, 144 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index 60c5dc00de..2789fbf43b 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.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 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 c5133bb2e0..2ec1304fa1 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 7748a6bc86..f2ac1dce76 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.util.regex.Pattern; public class TermuxFileUtils { + /** * Replace "$PREFIX/" or "~/" prefix with termux absolute paths. * @@ -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); + } + } 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 0ef2666409..dbd27155c7 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.termux.TermuxConstants; import java.security.MessageDigest; +import java.util.List; import javax.annotation.Nullable; @@ -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) { @@ -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 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 698d2c2867..418f7d85aa 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 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(); } @@ -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) { @@ -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, "-"); } 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 3eeb584eca..ba623bc279 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 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 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));