Skip to content

Commit f68674f

Browse files
CopilotHTRamsey
andauthored
Fix Android SD Card permissions for API 30+ (Android 11+) (#13456)
* Initial plan * Fix Android SD card permissions for API 30+ - Add MANAGE_EXTERNAL_STORAGE permission to manifest for Android 11+ full SD card access - Replace deprecated QtAndroidPrivate permission APIs with Java-based implementation - Add proper permission handling for Android 11+ (API 30) and Android 6+ (API 23) - Fix permission check to open settings for MANAGE_EXTERNAL_STORAGE grant on Android 11+ Co-authored-by: HTRamsey <[email protected]> * Fix QJniObject::callStaticMethod usage for boolean return Co-authored-by: HTRamsey <[email protected]> * Fix SD card path detection for Android 11+ Use StorageVolume.getDirectory() API for Android 11+ instead of deprecated reflection-based getPath() method. This fixes the "/dev/null" path issue where the reflection method returns invalid paths on newer Android versions. Co-authored-by: HTRamsey <[email protected]> * Fix Android Activity Name --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: HTRamsey <[email protected]> Co-authored-by: Holden <[email protected]>
1 parent 9849776 commit f68674f

File tree

3 files changed

+111
-36
lines changed

3 files changed

+111
-36
lines changed

android/AndroidManifest.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
<!-- Needed for read/write to SD Card Path in AppSettings -->
1515
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
1616
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
17+
<!-- For Android 11+ (API 30+), MANAGE_EXTERNAL_STORAGE is needed for full SD card access -->
18+
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
1719

1820
<!-- %%INSERT_FEATURES -->
1921

@@ -45,7 +47,7 @@
4547
<!-- android:theme="@style/AppTheme" -->
4648

4749
<activity
48-
android:name="org.qtproject.qt.android.bindings.QtActivity"
50+
android:name="org.mavlink.qgroundcontrol.QGCActivity"
4951
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
5052
android:label="-- %%INSERT_APP_NAME%% --"
5153
android:launchMode="singleTop"

android/src/org/mavlink/qgroundcontrol/QGCActivity.java

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
package org.mavlink.qgroundcontrol;
22

3+
import java.io.File;
34
import java.util.List;
45
import java.lang.reflect.Method;
56

67
import android.content.Context;
8+
import android.content.Intent;
9+
import android.content.pm.PackageManager;
10+
import android.net.Uri;
11+
import android.os.Build;
712
import android.os.Bundle;
13+
import android.os.Environment;
814
import android.os.PowerManager;
915
import android.net.wifi.WifiManager;
16+
import android.provider.Settings;
1017
import android.util.Log;
1118
import android.view.WindowManager;
1219
import android.app.Activity;
1320
import android.os.storage.StorageManager;
1421
import android.os.storage.StorageVolume;
22+
import androidx.core.app.ActivityCompat;
23+
import androidx.core.content.ContextCompat;
1524

1625
import org.qtproject.qt.android.bindings.QtActivity;
1726

@@ -119,32 +128,102 @@ private void releaseMulticastLock() {
119128
public static String getSDCardPath() {
120129
StorageManager storageManager = (StorageManager)m_instance.getSystemService(Activity.STORAGE_SERVICE);
121130
List<StorageVolume> volumes = storageManager.getStorageVolumes();
122-
Method mMethodGetPath;
123-
String path = "";
131+
124132
for (StorageVolume vol : volumes) {
125-
try {
126-
mMethodGetPath = vol.getClass().getMethod("getPath");
127-
} catch (NoSuchMethodException e) {
128-
e.printStackTrace();
133+
if (!vol.isRemovable()) {
129134
continue;
130135
}
131-
try {
132-
path = (String) mMethodGetPath.invoke(vol);
133-
} catch (Exception e) {
134-
e.printStackTrace();
135-
continue;
136+
137+
String path = null;
138+
139+
// For Android 11+ (API 30+), use the proper getDirectory() method
140+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
141+
File directory = vol.getDirectory();
142+
if (directory != null) {
143+
path = directory.getAbsolutePath();
144+
}
145+
} else {
146+
// For older versions, use reflection to get the path
147+
try {
148+
Method mMethodGetPath = vol.getClass().getMethod("getPath");
149+
path = (String) mMethodGetPath.invoke(vol);
150+
} catch (Exception e) {
151+
Log.e(TAG, "Failed to get path via reflection", e);
152+
continue;
153+
}
136154
}
137-
138-
if (vol.isRemovable() == true) {
139-
Log.i(TAG, "removable sd card mounted " + path);
155+
156+
if (path != null && !path.isEmpty()) {
157+
Log.i(TAG, "removable sd card mounted at " + path);
140158
return path;
141-
} else {
142-
Log.i(TAG, "storage mounted " + path);
143159
}
144160
}
161+
162+
Log.w(TAG, "No removable SD card found");
145163
return "";
146164
}
147165

166+
/**
167+
* Checks and requests storage permissions for SD card access.
168+
* For Android 11+ (API 30+), this requires MANAGE_EXTERNAL_STORAGE permission.
169+
*
170+
* @return true if permissions are granted, false otherwise
171+
*/
172+
public static boolean checkStoragePermissions() {
173+
if (m_instance == null) {
174+
Log.e(TAG, "Activity instance is null");
175+
return false;
176+
}
177+
178+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
179+
// Android 11+ (API 30+) requires MANAGE_EXTERNAL_STORAGE for full SD card access
180+
if (!Environment.isExternalStorageManager()) {
181+
Log.i(TAG, "MANAGE_EXTERNAL_STORAGE not granted, requesting...");
182+
try {
183+
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
184+
intent.setData(Uri.parse("package:" + m_instance.getPackageName()));
185+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
186+
m_instance.startActivity(intent);
187+
} catch (Exception e) {
188+
Log.e(TAG, "Failed to open storage permission settings", e);
189+
// Fallback to general settings
190+
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
191+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
192+
m_instance.startActivity(intent);
193+
}
194+
return false;
195+
}
196+
Log.i(TAG, "MANAGE_EXTERNAL_STORAGE already granted");
197+
return true;
198+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
199+
// Android 6.0+ (API 23+) requires runtime permissions
200+
String[] permissions = {
201+
android.Manifest.permission.READ_EXTERNAL_STORAGE,
202+
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
203+
};
204+
205+
boolean allGranted = true;
206+
for (String permission : permissions) {
207+
if (ContextCompat.checkSelfPermission(m_instance, permission) != PackageManager.PERMISSION_GRANTED) {
208+
allGranted = false;
209+
break;
210+
}
211+
}
212+
213+
if (!allGranted) {
214+
Log.i(TAG, "Storage permissions not granted, requesting...");
215+
ActivityCompat.requestPermissions(m_instance, permissions, 1);
216+
return false;
217+
}
218+
219+
Log.i(TAG, "Storage permissions already granted");
220+
return true;
221+
} else {
222+
// Below Android 6.0, permissions are granted at install time
223+
return true;
224+
}
225+
}
226+
148227
// Native C++ functions
149228
public native boolean nativeInit();
150229
public native void qgcLogDebug(final String message);

src/Android/AndroidInterface.cc

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
#include <QtCore/QJniObject>
1414
#include <QtCore/QJniEnvironment>
15-
#include <QtCore/private/qandroidextras_p.h>
1615

1716
QGC_LOGGING_CATEGORY(AndroidInterfaceLog, "qgc.android.src.androidinterface")
1817

@@ -107,25 +106,20 @@ void jniLogWarning(JNIEnv *envA, jobject thizA, jstring messageA)
107106

108107
bool checkStoragePermissions()
109108
{
110-
const QString readPermission("android.permission.READ_EXTERNAL_STORAGE");
111-
const QString writePermission("android.permission.WRITE_EXTERNAL_STORAGE");
112-
113-
const QStringList permissions = { readPermission, writePermission };
114-
for (const auto& permission: permissions) {
115-
QFuture<QtAndroidPrivate::PermissionResult> futurePermissionResult = QtAndroidPrivate::checkPermission(permission);
116-
QtAndroidPrivate::PermissionResult permissionResult = futurePermissionResult.result();
117-
if (permissionResult == QtAndroidPrivate::PermissionResult::Denied) {
118-
futurePermissionResult = QtAndroidPrivate::requestPermission(permission);
119-
permissionResult = futurePermissionResult.result();
120-
if (permissionResult == QtAndroidPrivate::PermissionResult::Denied) {
121-
qCWarning(AndroidInterfaceLog) << "Denied:" << permission;
122-
return false;
123-
}
124-
}
109+
// Call the Java method to check and request storage permissions
110+
const bool hasPermission = QJniObject::callStaticMethod<jboolean>(
111+
kJniQGCActivityClassName,
112+
"checkStoragePermissions",
113+
"()Z"
114+
);
115+
116+
if (hasPermission) {
117+
qCDebug(AndroidInterfaceLog) << "Storage permissions granted";
118+
} else {
119+
qCWarning(AndroidInterfaceLog) << "Storage permissions not granted";
125120
}
126-
127-
qCDebug(AndroidInterfaceLog) << "checkStoragePermissions Accepted";
128-
return true;
121+
122+
return hasPermission;
129123
}
130124

131125
QString getSDCardPath()

0 commit comments

Comments
 (0)