Skip to content

Commit

Permalink
Added: Add support for MANAGE_EXTERNAL_STORAGE when targeting targetS…
Browse files Browse the repository at this point in the history
…dkVersion 30

Termux will now automatically request legacy `WRITE_EXTERNAL_STORAGE` or `MANAGE_EXTERNAL_STORAGE` permissions if targeting targetSdkVersion `30` (android `11`) and running on sdk `30` (android `11`) and higher when `termux-setup-storage` is run.

Functions have been added to `PermissionUtils` to automatically check and request either permission depending on app `targetSdkVersion` and android version. Functions have been added to `PackagUtils` to get `requestLegacyExternalStorage` value from app manifest if added. If legacy storage is possible, then it must be set to `true`. Check `PermissionUtils.checkAndRequestLegacyOrManageExternalStoragePermission()`, `PermissionUtils.isLegacyExternalStoragePossible()` and `PermissionUtils.checkIfHasRequestedLegacyExternalStorage()` for details.
  • Loading branch information
agnostic-apollo committed Jan 22, 2022
1 parent 32dd7ea commit 9eeb2ba
Show file tree
Hide file tree
Showing 5 changed files with 352 additions and 21 deletions.
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
Expand All @@ -41,6 +43,7 @@
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/Theme.Termux">
Expand Down
58 changes: 41 additions & 17 deletions app/src/main/java/com/termux/app/TermuxActivity.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.termux.app;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
Expand All @@ -11,7 +10,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
Expand All @@ -35,6 +33,7 @@
import com.termux.R;
import com.termux.app.terminal.TermuxActivityRootView;
import com.termux.shared.activities.ReportActivity;
import com.termux.shared.data.IntentUtils;
import com.termux.shared.packages.PermissionUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
Expand Down Expand Up @@ -63,6 +62,8 @@
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.viewpager.widget.ViewPager;

import java.util.Arrays;

/**
* A terminal emulator activity.
* <p/>
Expand Down Expand Up @@ -712,25 +713,49 @@ private void requestAutoFill() {


/**
* For processes to access shared internal storage (/sdcard) we need this permission.
* For processes to access primary external storage (/sdcard, /storage/emulated/0, ~/storage/shared),
* termux needs to be granted legacy WRITE_EXTERNAL_STORAGE or MANAGE_EXTERNAL_STORAGE permissions
* if targeting targetSdkVersion 30 (android 11) and running on sdk 30 (android 11) and higher.
*/
public boolean ensureStoragePermissionGranted() {
if (PermissionUtils.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
return true;
} else {
Logger.logInfo(LOG_TAG, "Storage permission not granted, requesting permission.");
PermissionUtils.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION);
return false;
public void requestStoragePermission(boolean isPermissionCallback) {
new Thread() {
@Override
public void run() {
// Do not ask for permission again
int requestCode = isPermissionCallback ? -1 : PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION;

// If permission is granted, then also setup storage symlinks.
if(PermissionUtils.checkAndRequestLegacyOrManageExternalStoragePermission(
TermuxActivity.this, requestCode, !isPermissionCallback)) {
if (isPermissionCallback)
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
getString(com.termux.shared.R.string.msg_storage_permission_granted_on_request));

TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
} else {
if (isPermissionCallback)
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
getString(com.termux.shared.R.string.msg_storage_permission_not_granted_on_request));
}
}
}.start();
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Logger.logVerbose(LOG_TAG, "onActivityResult: requestCode: " + requestCode + ", resultCode: " + resultCode + ", data: " + IntentUtils.getIntentString(data));
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
requestStoragePermission(true);
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
TermuxInstaller.setupStorageSymlinks(this);
} else {
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Logger.logVerbose(LOG_TAG, "onRequestPermissionsResult: requestCode: " + requestCode + ", permissions: " + Arrays.toString(permissions) + ", grantResults: " + Arrays.toString(grantResults));
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
requestStoragePermission(true);
}
}

Expand Down Expand Up @@ -862,8 +887,7 @@ public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
if (ensureStoragePermissionGranted())
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
requestStoragePermission(false);
return;
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
Logger.logDebug(LOG_TAG, "Received intent to reload styling");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
import com.termux.shared.data.DataUtils;
import com.termux.shared.interact.MessageDialogUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.reflection.ReflectionUtils;
import com.termux.shared.termux.TermuxConstants;

import java.lang.reflect.Field;
import java.security.MessageDigest;
import java.util.List;

Expand Down Expand Up @@ -158,6 +160,63 @@ public static ApplicationInfo getApplicationInfoForPackage(@NonNull final Contex
}
}

/**
* Get the {@code privateFlags} {@link Field} of the {@link ApplicationInfo} class.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns the private flags or {@code null} if an exception was raised.
*/
@Nullable
public static Integer getApplicationInfoPrivateFlagsForPackage(@NonNull final ApplicationInfo applicationInfo) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
try {
return (Integer) ReflectionUtils.invokeField(ApplicationInfo.class, "privateFlags", applicationInfo).value;
} catch (Exception e) {
// ClassCastException may be thrown
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get privateFlags field value for ApplicationInfo class", e);
return null;
}
}

/**
* Get the {@code privateFlags} {@link Field} of the {@link ApplicationInfo} class.
*
* @param fieldName The name of the field to get.
* @return Returns the field value or {@code null} if an exception was raised.
*/
@Nullable
public static Integer getApplicationInfoStaticIntFieldValue(@NonNull String fieldName) {
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
try {
return (Integer) ReflectionUtils.invokeField(ApplicationInfo.class, fieldName, null).value;
} catch (Exception e) {
// ClassCastException may be thrown
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field value for ApplicationInfo class", e);
return null;
}
}

/**
* Check if the app associated with the {@code applicationInfo} has a specific flag set.
*
* @param flagToCheckName The name of the field for the flag to check.
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns {@code true} if app has flag is set, otherwise {@code false}. This will be
* {@code null} if an exception is raised.
*/
@Nullable
public static Boolean isApplicationInfoPrivateFlagSetForPackage(@NonNull String flagToCheckName, @NonNull final ApplicationInfo applicationInfo) {
Integer privateFlags = getApplicationInfoPrivateFlagsForPackage(applicationInfo);
if (privateFlags == null) return null;

Integer flagToCheck = getApplicationInfoStaticIntFieldValue(flagToCheckName);
if (flagToCheck == null) return null;

return ( 0 != ( privateFlags & flagToCheck ) );
}





/**
Expand Down Expand Up @@ -297,6 +356,36 @@ public static boolean isAppInstalledOnExternalStorage(@NonNull final Application



/**
* Check if the app associated with the {@code context} has
* ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE (requestLegacyExternalStorage)
* set to {@code true} in app manifest.
*
* @param context The {@link Context} for the package.
* @return Returns {@code true} if app has requested legacy external storage, otherwise
* {@code false}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static Boolean hasRequestedLegacyExternalStorage(@NonNull final Context context) {
return hasRequestedLegacyExternalStorage(context.getApplicationInfo());
}

/**
* Check if the app associated with the {@code applicationInfo} has
* ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE (requestLegacyExternalStorage)
* set to {@code true} in app manifest.
*
* @param applicationInfo The {@link ApplicationInfo} for the package.
* @return Returns {@code true} if app has requested legacy external storage, otherwise
* {@code false}. This will be {@code null} if an exception is raised.
*/
@Nullable
public static Boolean hasRequestedLegacyExternalStorage(@NonNull final ApplicationInfo applicationInfo) {
return isApplicationInfoPrivateFlagSetForPackage("PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE", applicationInfo);
}



/**
* Get the {@code versionCode} for the package associated with the {@code context}.
*
Expand Down Expand Up @@ -545,8 +634,8 @@ public static String isAppInstalled(@NonNull final Context context, String appNa
*/
@Nullable
public static String setComponentState(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, boolean state, String toastString,
boolean showErrorMessage) {
@NonNull String className, boolean state, String toastString,
boolean showErrorMessage) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
Expand Down Expand Up @@ -579,7 +668,7 @@ public static String setComponentState(@NonNull final Context context, @NonNull
* get the state.
*/
public static Boolean isComponentDisabled(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, boolean logErrorMessage) {
@NonNull String className, boolean logErrorMessage) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
Expand Down Expand Up @@ -607,7 +696,7 @@ public static Boolean isComponentDisabled(@NonNull final Context context, @NonNu
* @return Returns {@code true} if it exists, otherwise {@code false}.
*/
public static boolean doesActivityComponentExist(@NonNull final Context context, @NonNull String packageName,
@NonNull String className, int flags) {
@NonNull String className, int flags) {
try {
PackageManager packageManager = context.getPackageManager();
if (packageManager != null) {
Expand Down
Loading

0 comments on commit 9eeb2ba

Please sign in to comment.