mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-06 18:55:31 +08:00
Added: Add support for MANAGE_EXTERNAL_STORAGE when targeting targetSdkVersion 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.
This commit is contained in:
@@ -22,7 +22,9 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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.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.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@@ -41,6 +43,7 @@
|
|||||||
android:extractNativeLibs="true"
|
android:extractNativeLibs="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/application_name"
|
android:label="@string/application_name"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="false"
|
android:supportsRtl="false"
|
||||||
android:theme="@style/Theme.Termux">
|
android:theme="@style/Theme.Termux">
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
package com.termux.app;
|
package com.termux.app;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
@@ -11,7 +10,6 @@ import android.content.Context;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@@ -35,6 +33,7 @@ import android.widget.Toast;
|
|||||||
import com.termux.R;
|
import com.termux.R;
|
||||||
import com.termux.app.terminal.TermuxActivityRootView;
|
import com.termux.app.terminal.TermuxActivityRootView;
|
||||||
import com.termux.shared.activities.ReportActivity;
|
import com.termux.shared.activities.ReportActivity;
|
||||||
|
import com.termux.shared.data.IntentUtils;
|
||||||
import com.termux.shared.packages.PermissionUtils;
|
import com.termux.shared.packages.PermissionUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY;
|
||||||
@@ -63,6 +62,8 @@ import androidx.core.content.ContextCompat;
|
|||||||
import androidx.drawerlayout.widget.DrawerLayout;
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A terminal emulator activity.
|
* A terminal emulator activity.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -712,25 +713,49 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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() {
|
public void requestStoragePermission(boolean isPermissionCallback) {
|
||||||
if (PermissionUtils.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
new Thread() {
|
||||||
return true;
|
@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 {
|
} else {
|
||||||
Logger.logInfo(LOG_TAG, "Storage permission not granted, requesting permission.");
|
if (isPermissionCallback)
|
||||||
PermissionUtils.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION);
|
Logger.logInfoAndShowToast(TermuxActivity.this, LOG_TAG,
|
||||||
return false;
|
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
|
@Override
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
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) {
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
|
Logger.logVerbose(LOG_TAG, "onRequestPermissionsResult: requestCode: " + requestCode + ", permissions: " + Arrays.toString(permissions) + ", grantResults: " + Arrays.toString(grantResults));
|
||||||
TermuxInstaller.setupStorageSymlinks(this);
|
if (requestCode == PermissionUtils.REQUEST_GRANT_STORAGE_PERMISSION) {
|
||||||
} else {
|
requestStoragePermission(true);
|
||||||
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,8 +887,7 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||||||
switch (intent.getAction()) {
|
switch (intent.getAction()) {
|
||||||
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
|
case TERMUX_ACTIVITY.ACTION_REQUEST_PERMISSIONS:
|
||||||
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
|
Logger.logDebug(LOG_TAG, "Received intent to request storage permissions");
|
||||||
if (ensureStoragePermissionGranted())
|
requestStoragePermission(false);
|
||||||
TermuxInstaller.setupStorageSymlinks(TermuxActivity.this);
|
|
||||||
return;
|
return;
|
||||||
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
|
case TERMUX_ACTIVITY.ACTION_RELOAD_STYLE:
|
||||||
Logger.logDebug(LOG_TAG, "Received intent to reload styling");
|
Logger.logDebug(LOG_TAG, "Received intent to reload styling");
|
||||||
|
@@ -18,8 +18,10 @@ import com.termux.shared.R;
|
|||||||
import com.termux.shared.data.DataUtils;
|
import com.termux.shared.data.DataUtils;
|
||||||
import com.termux.shared.interact.MessageDialogUtils;
|
import com.termux.shared.interact.MessageDialogUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
|
import com.termux.shared.reflection.ReflectionUtils;
|
||||||
import com.termux.shared.termux.TermuxConstants;
|
import com.termux.shared.termux.TermuxConstants;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -158,6 +160,63 @@ public class PackageUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -297,6 +356,36 @@ public class PackageUtils {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}.
|
* Get the {@code versionCode} for the package associated with the {@code context}.
|
||||||
*
|
*
|
||||||
|
@@ -9,6 +9,7 @@ import android.content.pm.PackageInfo;
|
|||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ import androidx.core.content.ContextCompat;
|
|||||||
|
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.termux.shared.R;
|
import com.termux.shared.R;
|
||||||
|
import com.termux.shared.file.FileUtils;
|
||||||
import com.termux.shared.logger.Logger;
|
import com.termux.shared.logger.Logger;
|
||||||
import com.termux.shared.models.errors.Error;
|
import com.termux.shared.models.errors.Error;
|
||||||
import com.termux.shared.models.errors.FunctionErrno;
|
import com.termux.shared.models.errors.FunctionErrno;
|
||||||
@@ -199,6 +201,215 @@ public class PermissionUtils {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** If path is under primary external storage directory and storage permission is missing,
|
||||||
|
* then legacy or manage external storage permission will be requested from the user via a call
|
||||||
|
* to {@link #checkAndRequestLegacyOrManageExternalStoragePermission(Context, int, boolean)}.
|
||||||
|
*
|
||||||
|
* @param context The context for operations.
|
||||||
|
* @param filePath The path to check.
|
||||||
|
* @param requestCode The request code to use while asking for permission.
|
||||||
|
* @param showErrorMessage If an error message toast should be shown if permission is not granted.
|
||||||
|
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
@SuppressLint("SdCardPath")
|
||||||
|
public static boolean checkAndRequestLegacyOrManageExternalStoragePermissionIfPathOnPrimaryExternalStorage(
|
||||||
|
@NonNull Context context, String filePath, int requestCode, boolean showErrorMessage) {
|
||||||
|
// If path is under primary external storage directory, then check for missing permissions.
|
||||||
|
if (!FileUtils.isPathInDirPaths(filePath,
|
||||||
|
Arrays.asList(Environment.getExternalStorageDirectory().getAbsolutePath(), "/sdcard"), true))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return checkAndRequestLegacyOrManageExternalStoragePermission(context, requestCode, showErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if legacy or manage external storage permissions has been granted. If
|
||||||
|
* {@link #isLegacyExternalStoragePossible(Context)} returns {@code true}, them it will be
|
||||||
|
* checked if app has has been granted {@link Manifest.permission#READ_EXTERNAL_STORAGE} and
|
||||||
|
* {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions, otherwise it will be checked
|
||||||
|
* if app has been granted the {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} permission.
|
||||||
|
*
|
||||||
|
* If storage permission is missing, it will be requested from the user if {@code context} is an
|
||||||
|
* instance of {@link Activity} or {@link AppCompatActivity} and {@code requestCode}
|
||||||
|
* is `>=0` and the function will automatically return. The caller should register for
|
||||||
|
* Activity.onActivityResult() and Activity.onRequestPermissionsResult() and call this function
|
||||||
|
* again but set {@code requestCode} to `-1` to check if permission was granted or not.
|
||||||
|
*
|
||||||
|
* Caller must add following to AndroidManifest.xml of the app, otherwise errors will be thrown.
|
||||||
|
* {@code
|
||||||
|
* <manifest
|
||||||
|
* <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" />
|
||||||
|
*
|
||||||
|
* <application
|
||||||
|
* android:requestLegacyExternalStorage="true"
|
||||||
|
* ....
|
||||||
|
* </application>
|
||||||
|
* </manifest>
|
||||||
|
*}
|
||||||
|
* @param context The context for operations.
|
||||||
|
* @param requestCode The request code to use while asking for permission.
|
||||||
|
* @param showErrorMessage If an error message toast should be shown if permission is not granted.
|
||||||
|
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
public static boolean checkAndRequestLegacyOrManageExternalStoragePermission(@NonNull Context context,
|
||||||
|
int requestCode,
|
||||||
|
boolean showErrorMessage) {
|
||||||
|
String errmsg;
|
||||||
|
boolean requestLegacyStoragePermission = isLegacyExternalStoragePossible(context);
|
||||||
|
boolean checkIfHasRequestedLegacyExternalStorage = checkIfHasRequestedLegacyExternalStorage(context);
|
||||||
|
|
||||||
|
if (requestLegacyStoragePermission && checkIfHasRequestedLegacyExternalStorage) {
|
||||||
|
// Check if requestLegacyExternalStorage is set to true in app manifest
|
||||||
|
if (!hasRequestedLegacyExternalStorage(context, showErrorMessage))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkStoragePermission(context, requestLegacyStoragePermission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
errmsg = context.getString(R.string.msg_storage_permission_not_granted);
|
||||||
|
Logger.logError(LOG_TAG, errmsg);
|
||||||
|
if (showErrorMessage)
|
||||||
|
Logger.showToast(context, errmsg, false);
|
||||||
|
|
||||||
|
if (requestCode < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (requestLegacyStoragePermission) {
|
||||||
|
requestLegacyStorageExternalPermission(context, requestCode);
|
||||||
|
} else {
|
||||||
|
requestManageStorageExternalPermission(context, requestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if app has been granted storage permission.
|
||||||
|
*
|
||||||
|
* @param context The context for operations.
|
||||||
|
* @param checkLegacyStoragePermission If set to {@code true}, then it will be checked if app
|
||||||
|
* has been granted {@link Manifest.permission#READ_EXTERNAL_STORAGE}
|
||||||
|
* and {@link Manifest.permission#WRITE_EXTERNAL_STORAGE}
|
||||||
|
* permissions, otherwise it will be checked if app has been
|
||||||
|
* granted the {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE}
|
||||||
|
* permission.
|
||||||
|
* @return Returns {@code true} if permission is granted, otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
public static boolean checkStoragePermission(@NonNull Context context, boolean checkLegacyStoragePermission) {
|
||||||
|
if (checkLegacyStoragePermission) {
|
||||||
|
return checkPermissions(context,
|
||||||
|
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE});
|
||||||
|
} else {
|
||||||
|
return Environment.isExternalStorageManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request user to grant {@link Manifest.permission#READ_EXTERNAL_STORAGE} and
|
||||||
|
* {@link Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions to the app.
|
||||||
|
*
|
||||||
|
* @param context The context for operations. It must be an instance of {@link Activity} or
|
||||||
|
* {@link AppCompatActivity}.
|
||||||
|
* @param requestCode The request code to use while asking for permission. It must be `>=0` or
|
||||||
|
* will fail silently and will log an exception.
|
||||||
|
* @return Returns {@code true} if requesting the permission was successful, otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
public static boolean requestLegacyStorageExternalPermission(@NonNull Context context, int requestCode) {
|
||||||
|
Logger.logInfo(LOG_TAG, "Requesting legacy external storage permission");
|
||||||
|
return requestPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE, requestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request user to grant {@link Manifest.permission#MANAGE_EXTERNAL_STORAGE} permission to the app.
|
||||||
|
*
|
||||||
|
* @param context The context for operations. It must be an instance of {@link Activity} or
|
||||||
|
* {@link AppCompatActivity}.
|
||||||
|
* @param requestCode The request code to use while asking for permission. It must be `>=0` or
|
||||||
|
* will fail silently and will log an exception.
|
||||||
|
* @return Returns {@code true} if requesting the permission was successful, otherwise {@code false}.
|
||||||
|
*/
|
||||||
|
public static boolean requestManageStorageExternalPermission(@NonNull Context context, int requestCode) {
|
||||||
|
Logger.logInfo(LOG_TAG, "Requesting manage external storage permission");
|
||||||
|
|
||||||
|
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
||||||
|
intent.addCategory("android.intent.category.DEFAULT");
|
||||||
|
intent.setData(Uri.parse("package:" + context.getPackageName()));
|
||||||
|
boolean result = ActivityUtils.startActivityForResult(context, requestCode, intent);
|
||||||
|
|
||||||
|
// Use fallback if matching Activity did not exist for ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION.
|
||||||
|
if (!result) {
|
||||||
|
intent = new Intent();
|
||||||
|
intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
|
||||||
|
return ActivityUtils.startActivityForResult(context, requestCode, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If app is targeting targetSdkVersion 30 (android 11) and running on sdk 30 (android 11) or
|
||||||
|
* higher, then {@link android.R.attr#requestLegacyExternalStorage} attribute is ignored.
|
||||||
|
* https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
|
||||||
|
*/
|
||||||
|
public static boolean isLegacyExternalStoragePossible(@NonNull Context context) {
|
||||||
|
return !(PackageUtils.getTargetSDKForPackage(context) >= Build.VERSION_CODES.R &&
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether it should be checked if app has set
|
||||||
|
* {@link android.R.attr#requestLegacyExternalStorage} attribute to {@code true}, if storage
|
||||||
|
* permissions are to be requested based on if {@link #isLegacyExternalStoragePossible(Context)}
|
||||||
|
* return {@code true}.
|
||||||
|
*
|
||||||
|
* If app is targeting targetSdkVersion 30 (android 11), then legacy storage can only be
|
||||||
|
* requested if running on sdk 29 (android 10).
|
||||||
|
* If app is targeting targetSdkVersion 29 (android 10), then legacy storage can only be
|
||||||
|
* requested if running on sdk 29 (android 10) and higher.
|
||||||
|
*/
|
||||||
|
public static boolean checkIfHasRequestedLegacyExternalStorage(@NonNull Context context) {
|
||||||
|
int targetSdkVersion = PackageUtils.getTargetSDKForPackage(context);
|
||||||
|
|
||||||
|
if (targetSdkVersion >= Build.VERSION_CODES.R) {
|
||||||
|
return Build.VERSION.SDK_INT == Build.VERSION_CODES.Q;
|
||||||
|
} else if (targetSdkVersion == Build.VERSION_CODES.Q) {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call to {@link Environment#isExternalStorageLegacy()} will not return the actual value defined
|
||||||
|
* in app manifest for {@link android.R.attr#requestLegacyExternalStorage} attribute,
|
||||||
|
* since an app may inherit its legacy state based on when it was first installed, target sdk and
|
||||||
|
* other factors. To provide consistent experience for all users regardless of current legacy
|
||||||
|
* state on a specific device, we directly use the value defined in app` manifest.
|
||||||
|
*/
|
||||||
|
public static boolean hasRequestedLegacyExternalStorage(@NonNull Context context,
|
||||||
|
boolean showErrorMessage) {
|
||||||
|
String errmsg;
|
||||||
|
Boolean hasRequestedLegacyExternalStorage = PackageUtils.hasRequestedLegacyExternalStorage(context);
|
||||||
|
if (hasRequestedLegacyExternalStorage != null && !hasRequestedLegacyExternalStorage) {
|
||||||
|
errmsg = context.getString(R.string.error_has_not_requested_legacy_external_storage,
|
||||||
|
context.getPackageName(), PackageUtils.getTargetSDKForPackage(context), Build.VERSION.SDK_INT);
|
||||||
|
Logger.logError(LOG_TAG, errmsg);
|
||||||
|
if (showErrorMessage)
|
||||||
|
Logger.showToast(context, errmsg, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if {@link Manifest.permission#SYSTEM_ALERT_WINDOW} permission has been granted.
|
* Check if {@link Manifest.permission#SYSTEM_ALERT_WINDOW} permission has been granted.
|
||||||
|
@@ -41,6 +41,7 @@
|
|||||||
<string name="error_failed_to_request_permissions">Failed to request permissions with request code %1$d: %2$s</string>
|
<string name="error_failed_to_request_permissions">Failed to request permissions with request code %1$d: %2$s</string>
|
||||||
<string name="error_attempted_to_check_for_permissions_not_requested">Attempted to check for permissions that have not been requested in app manifest: %1$s</string>
|
<string name="error_attempted_to_check_for_permissions_not_requested">Attempted to check for permissions that have not been requested in app manifest: %1$s</string>
|
||||||
<string name="error_attempted_to_ask_for_permissions_not_requested">Attempted to ask for permissions that have not been requested in app manifest: %1$s</string>
|
<string name="error_attempted_to_ask_for_permissions_not_requested">Attempted to ask for permissions that have not been requested in app manifest: %1$s</string>
|
||||||
|
<string name="error_has_not_requested_legacy_external_storage">The \"%1$s\" package is targeting targetSdkVersion %2$d and is running on android sdk %3$d but has not set requestLegacyExternalStorage to true in app manifest"</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -101,6 +102,9 @@
|
|||||||
<string name="action_cancel">Cancel</string>
|
<string name="action_cancel">Cancel</string>
|
||||||
<string name="action_save_to_file">Save To File</string>
|
<string name="action_save_to_file">Save To File</string>
|
||||||
|
|
||||||
|
<string name="msg_storage_permission_granted_on_request">"The storage permission granted by user on request"</string>
|
||||||
|
<string name="msg_storage_permission_not_granted_on_request">"The storage permission not granted by user on request"</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Launcher Icons -->
|
<!-- Launcher Icons -->
|
||||||
|
Reference in New Issue
Block a user