-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Fix Android SD Card permissions for API 30+ (Android 11+) #13456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
c86660c
59c182a
10d9216
34c8cf2
fd8bc16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,14 +4,22 @@ | |
import java.lang.reflect.Method; | ||
|
||
import android.content.Context; | ||
import android.content.Intent; | ||
import android.content.pm.PackageManager; | ||
import android.net.Uri; | ||
import android.os.Build; | ||
import android.os.Bundle; | ||
import android.os.Environment; | ||
import android.os.PowerManager; | ||
import android.net.wifi.WifiManager; | ||
import android.provider.Settings; | ||
import android.util.Log; | ||
import android.view.WindowManager; | ||
import android.app.Activity; | ||
import android.os.storage.StorageManager; | ||
import android.os.storage.StorageVolume; | ||
import androidx.core.app.ActivityCompat; | ||
import androidx.core.content.ContextCompat; | ||
|
||
import org.qtproject.qt.android.bindings.QtActivity; | ||
|
||
|
@@ -145,6 +153,67 @@ public static String getSDCardPath() { | |
return ""; | ||
} | ||
|
||
/** | ||
* Checks and requests storage permissions for SD card access. | ||
* For Android 11+ (API 30+), this requires MANAGE_EXTERNAL_STORAGE permission. | ||
* | ||
* @return true if permissions are granted, false otherwise | ||
*/ | ||
public static boolean checkStoragePermissions() { | ||
if (m_instance == null) { | ||
Log.e(TAG, "Activity instance is null"); | ||
return false; | ||
} | ||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { | ||
// Android 11+ (API 30+) requires MANAGE_EXTERNAL_STORAGE for full SD card access | ||
if (!Environment.isExternalStorageManager()) { | ||
Log.i(TAG, "MANAGE_EXTERNAL_STORAGE not granted, requesting..."); | ||
try { | ||
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); | ||
intent.setData(Uri.parse("package:" + m_instance.getPackageName())); | ||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||
m_instance.startActivity(intent); | ||
} catch (Exception e) { | ||
Log.e(TAG, "Failed to open storage permission settings", e); | ||
// Fallback to general settings | ||
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); | ||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | ||
m_instance.startActivity(intent); | ||
} | ||
return false; | ||
} | ||
Log.i(TAG, "MANAGE_EXTERNAL_STORAGE already granted"); | ||
return true; | ||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||
// Android 6.0+ (API 23+) requires runtime permissions | ||
String[] permissions = { | ||
android.Manifest.permission.READ_EXTERNAL_STORAGE, | ||
android.Manifest.permission.WRITE_EXTERNAL_STORAGE | ||
}; | ||
|
||
boolean allGranted = true; | ||
for (String permission : permissions) { | ||
if (ContextCompat.checkSelfPermission(m_instance, permission) != PackageManager.PERMISSION_GRANTED) { | ||
allGranted = false; | ||
break; | ||
} | ||
} | ||
|
||
if (!allGranted) { | ||
Log.i(TAG, "Storage permissions not granted, requesting..."); | ||
ActivityCompat.requestPermissions(m_instance, permissions, 1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The request code '1' is a magic number. Consider defining it as a named constant (e.g., STORAGE_PERMISSION_REQUEST_CODE = 1) to improve code maintainability and make the purpose clearer. Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
return false; | ||
} | ||
|
||
Log.i(TAG, "Storage permissions already granted"); | ||
return true; | ||
} else { | ||
// Below Android 6.0, permissions are granted at install time | ||
return true; | ||
} | ||
} | ||
|
||
// Native C++ functions | ||
public native boolean nativeInit(); | ||
public native void qgcLogDebug(final String message); | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -12,7 +12,6 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
#include <QtCore/QJniObject> | ||||||||||||||||||||||||||||||||||||||||||||||||
#include <QtCore/QJniEnvironment> | ||||||||||||||||||||||||||||||||||||||||||||||||
#include <QtCore/private/qandroidextras_p.h> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
QGC_LOGGING_CATEGORY(AndroidInterfaceLog, "qgc.android.src.androidinterface") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -107,25 +106,20 @@ void jniLogWarning(JNIEnv *envA, jobject thizA, jstring messageA) | |||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
bool checkStoragePermissions() | ||||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||||
const QString readPermission("android.permission.READ_EXTERNAL_STORAGE"); | ||||||||||||||||||||||||||||||||||||||||||||||||
const QString writePermission("android.permission.WRITE_EXTERNAL_STORAGE"); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
const QStringList permissions = { readPermission, writePermission }; | ||||||||||||||||||||||||||||||||||||||||||||||||
for (const auto& permission: permissions) { | ||||||||||||||||||||||||||||||||||||||||||||||||
QFuture<QtAndroidPrivate::PermissionResult> futurePermissionResult = QtAndroidPrivate::checkPermission(permission); | ||||||||||||||||||||||||||||||||||||||||||||||||
QtAndroidPrivate::PermissionResult permissionResult = futurePermissionResult.result(); | ||||||||||||||||||||||||||||||||||||||||||||||||
if (permissionResult == QtAndroidPrivate::PermissionResult::Denied) { | ||||||||||||||||||||||||||||||||||||||||||||||||
futurePermissionResult = QtAndroidPrivate::requestPermission(permission); | ||||||||||||||||||||||||||||||||||||||||||||||||
permissionResult = futurePermissionResult.result(); | ||||||||||||||||||||||||||||||||||||||||||||||||
if (permissionResult == QtAndroidPrivate::PermissionResult::Denied) { | ||||||||||||||||||||||||||||||||||||||||||||||||
qCWarning(AndroidInterfaceLog) << "Denied:" << permission; | ||||||||||||||||||||||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
// Call the Java method to check and request storage permissions | ||||||||||||||||||||||||||||||||||||||||||||||||
const bool hasPermission = QJniObject::callStaticMethod<jboolean>( | ||||||||||||||||||||||||||||||||||||||||||||||||
kJniQGCActivityClassName, | ||||||||||||||||||||||||||||||||||||||||||||||||
"checkStoragePermissions", | ||||||||||||||||||||||||||||||||||||||||||||||||
"()Z" | ||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
if (hasPermission) { | ||||||||||||||||||||||||||||||||||||||||||||||||
qCDebug(AndroidInterfaceLog) << "Storage permissions granted"; | ||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||
qCWarning(AndroidInterfaceLog) << "Storage permissions not granted"; | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
qCDebug(AndroidInterfaceLog) << "checkStoragePermissions Accepted"; | ||||||||||||||||||||||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+115
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The QJniObject::callStaticMethod call should include error handling. If the Java method fails or throws an exception, this could return an undefined value. Consider wrapping this in a try-catch block or checking if the QJniObject is valid.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||||||||||||||||||||||||||||||||
return hasPermission; | ||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
QString getSDCardPath() | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback intent creation should also be wrapped in a try-catch block since startActivity() can throw ActivityNotFoundException if no app can handle the intent. The current code could crash if both the specific and general settings intents fail.
Copilot uses AI. Check for mistakes.