Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<!-- Needed for read/write to SD Card Path in AppSettings -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- For Android 11+ (API 30+), MANAGE_EXTERNAL_STORAGE is needed for full SD card access -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

<!-- %%INSERT_FEATURES -->

Expand Down
69 changes: 69 additions & 0 deletions android/src/org/mavlink/qgroundcontrol/QGCActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Comment on lines +190 to +192
Copy link

Copilot AI Sep 30, 2025

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.

Suggested change
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
m_instance.startActivity(intent);
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
m_instance.startActivity(intent);
} catch (Exception ex) {
Log.e(TAG, "Failed to open general storage permission settings", ex);
}

Copilot uses AI. Check for mistakes.

}
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);
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The 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.

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);
Expand Down
32 changes: 13 additions & 19 deletions src/Android/AndroidInterface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

#include <QtCore/QJniObject>
#include <QtCore/QJniEnvironment>
#include <QtCore/private/qandroidextras_p.h>

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

Expand Down Expand Up @@ -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
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

The 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
if (hasPermission) {
qCDebug(AndroidInterfaceLog) << "Storage permissions granted";
} else {
qCWarning(AndroidInterfaceLog) << "Storage permissions not granted";
}
qCDebug(AndroidInterfaceLog) << "checkStoragePermissions Accepted";
return true;
// Check for Java exceptions after the JNI call
QJniEnvironment env;
if (env.checkAndClearExceptions()) {
qCWarning(AndroidInterfaceLog) << "Exception occurred while calling checkStoragePermissions";
return false;
}
if (hasPermission) {
qCDebug(AndroidInterfaceLog) << "Storage permissions granted";
} else {
qCWarning(AndroidInterfaceLog) << "Storage permissions not granted";
}

Copilot uses AI. Check for mistakes.

return hasPermission;
}

QString getSDCardPath()
Expand Down
Loading