Skip to content

Commit 3323eaa

Browse files
Merge pull request #15368 from nextcloud/backport/15240/stable-3.32
[stable-3.32] Check Storage Permission Before Upload
2 parents c1ec085 + ef58bfa commit 3323eaa

File tree

8 files changed

+200
-0
lines changed

8 files changed

+200
-0
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@
269269
android:exported="false"
270270
tools:replace="android:exported" />
271271

272+
<receiver
273+
android:name=".operations.upload.UploadFileBroadcastReceiver"
274+
android:exported="false" />
272275
<receiver
273276
android:name="com.nextcloud.client.jobs.MediaFoldersDetectionWork$NotificationReceiver"
274277
android:exported="false" />

app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ class UploadNotificationManager(private val context: Context, viewThemeUtils: Vi
7878
credentialIntent: PendingIntent?,
7979
errorMessage: String
8080
) {
81+
if (uploadFileOperation.isMissingPermissionThrown) {
82+
return
83+
}
84+
8185
val textId = getFailedResultTitleId(resultCode)
8286

8387
notificationBuilder.run {

app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
import com.owncloud.android.operations.e2e.E2EClientData;
5959
import com.owncloud.android.operations.e2e.E2EData;
6060
import com.owncloud.android.operations.e2e.E2EFiles;
61+
import com.owncloud.android.operations.upload.UploadFileException;
62+
import com.owncloud.android.operations.upload.UploadFileOperationExtensionsKt;
6163
import com.owncloud.android.utils.EncryptionUtils;
6264
import com.owncloud.android.utils.EncryptionUtilsV2;
6365
import com.owncloud.android.utils.FileStorageUtils;
@@ -118,6 +120,7 @@ public class UploadFileOperation extends SyncOperation {
118120
public static final int CREATED_BY_USER = 0;
119121
public static final int CREATED_AS_INSTANT_PICTURE = 1;
120122
public static final int CREATED_AS_INSTANT_VIDEO = 2;
123+
public static final int MISSING_FILE_PERMISSION_NOTIFICATION_ID = 2501;
121124

122125
/**
123126
* OCFile which is to be uploaded.
@@ -166,6 +169,7 @@ public class UploadFileOperation extends SyncOperation {
166169

167170
private boolean encryptedAncestor;
168171
private OCFile duplicatedEncryptedFile;
172+
private AtomicBoolean missingPermissionThrown = new AtomicBoolean(false);
169173

170174
public static OCFile obtainNewOCFileToUpload(String remotePath, String localPath, String mimeType) {
171175
OCFile newFile = new OCFile(remotePath);
@@ -403,9 +407,31 @@ public Context getContext() {
403407
return mContext;
404408
}
405409

410+
public boolean isMissingPermissionThrown() {
411+
return missingPermissionThrown.get();
412+
}
413+
406414
@Override
407415
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
408416
protected RemoteOperationResult run(OwnCloudClient client) {
417+
if (TextUtils.isEmpty(getStoragePath())) {
418+
Log_OC.e(TAG, "Upload cancelled for " + getStoragePath() + ": file path is null or empty.");
419+
return new RemoteOperationResult<>(new UploadFileException.EmptyOrNullFilePath());
420+
}
421+
422+
final var localFile = new File(getStoragePath());
423+
if (!localFile.exists()) {
424+
Log_OC.e(TAG, "Upload cancelled for " + getStoragePath() + ": local file not exists.");
425+
return new RemoteOperationResult<>(ResultCode.LOCAL_FILE_NOT_FOUND);
426+
}
427+
428+
if (!localFile.canRead()) {
429+
Log_OC.e(TAG, "Upload cancelled for " + getStoragePath() + ": file is not readable or inaccessible.");
430+
UploadFileOperationExtensionsKt.showStoragePermissionNotification(this);
431+
missingPermissionThrown.set(true);
432+
return new RemoteOperationResult<>(new UploadFileException.MissingPermission());
433+
}
434+
409435
mCancellationRequested.set(false);
410436
mUploadStarted.set(true);
411437

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.operations.upload
8+
9+
import android.app.NotificationManager
10+
import android.content.BroadcastReceiver
11+
import android.content.Context
12+
import android.content.Intent
13+
import android.net.Uri
14+
import android.os.Build
15+
import android.provider.Settings
16+
import androidx.annotation.RequiresApi
17+
import androidx.core.content.IntentCompat
18+
import androidx.core.net.toUri
19+
import com.owncloud.android.operations.UploadFileOperation
20+
21+
class UploadFileBroadcastReceiver : BroadcastReceiver() {
22+
companion object {
23+
const val ACTION_TYPE = "UploadFileBroadcastReceiver.ACTION_TYPE"
24+
}
25+
26+
override fun onReceive(context: Context, intent: Intent) {
27+
val actionType =
28+
IntentCompat.getSerializableExtra(intent, ACTION_TYPE, UploadFileBroadcastReceiverActions::class.java)
29+
?: return
30+
31+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
32+
notificationManager.cancel(UploadFileOperation.MISSING_FILE_PERMISSION_NOTIFICATION_ID)
33+
34+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
35+
actionType == UploadFileBroadcastReceiverActions.ALLOW_ALL_FILES
36+
) {
37+
redirectToAllFilesAccess(context)
38+
} else {
39+
redirectToAppInfo(context)
40+
}
41+
}
42+
43+
@RequiresApi(Build.VERSION_CODES.R)
44+
private fun redirectToAllFilesAccess(context: Context) {
45+
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
46+
data = "package:${context.packageName}".toUri()
47+
flags = Intent.FLAG_ACTIVITY_NEW_TASK
48+
}.run {
49+
context.startActivity(this)
50+
}
51+
}
52+
53+
private fun redirectToAppInfo(context: Context) {
54+
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
55+
data = Uri.fromParts("package", context.packageName, null)
56+
flags = Intent.FLAG_ACTIVITY_NEW_TASK
57+
}.run {
58+
context.startActivity(this)
59+
}
60+
}
61+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.owncloud.android.operations.upload
9+
10+
enum class UploadFileBroadcastReceiverActions : java.io.Serializable {
11+
ALLOW_ALL_FILES,
12+
APP_PERMISSIONS
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.owncloud.android.operations.upload
9+
10+
sealed class UploadFileException(message: String) : Exception(message) {
11+
class EmptyOrNullFilePath : UploadFileException("Empty or null file path")
12+
class MissingPermission : UploadFileException("Missing storage permission")
13+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Alper Ozturk <[email protected]>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.operations.upload
8+
9+
import android.app.NotificationManager
10+
import android.app.PendingIntent
11+
import android.content.Context
12+
import android.content.Intent
13+
import androidx.core.app.NotificationCompat
14+
import androidx.core.content.ContextCompat
15+
import com.owncloud.android.R
16+
import com.owncloud.android.operations.UploadFileOperation
17+
import com.owncloud.android.operations.UploadFileOperation.MISSING_FILE_PERMISSION_NOTIFICATION_ID
18+
import com.owncloud.android.ui.notifications.NotificationUtils
19+
20+
fun UploadFileOperation.showStoragePermissionNotification() {
21+
val notificationManager = ContextCompat.getSystemService(context, NotificationManager::class.java)
22+
?: return
23+
val alreadyShown = notificationManager.activeNotifications.any {
24+
it.id == MISSING_FILE_PERMISSION_NOTIFICATION_ID
25+
}
26+
if (alreadyShown) {
27+
return
28+
}
29+
30+
val allowAllFileAccessAction = getAllowAllFileAccessAction(context)
31+
val appPermissionsAction = getAppPermissionsAction(context)
32+
33+
val notificationBuilder =
34+
NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD)
35+
.setSmallIcon(android.R.drawable.stat_sys_warning)
36+
.setContentTitle(context.getString(R.string.upload_missing_storage_permission_title))
37+
.setContentText(context.getString(R.string.upload_missing_storage_permission_description))
38+
.setPriority(NotificationCompat.PRIORITY_HIGH)
39+
.addAction(allowAllFileAccessAction)
40+
.addAction(appPermissionsAction)
41+
.setAutoCancel(true)
42+
43+
notificationManager.notify(MISSING_FILE_PERMISSION_NOTIFICATION_ID, notificationBuilder.build())
44+
}
45+
46+
private fun getActionPendingIntent(context: Context, actionType: UploadFileBroadcastReceiverActions): PendingIntent {
47+
val intent = Intent(context, UploadFileBroadcastReceiver::class.java).apply {
48+
action = "com.owncloud.android.ACTION_UPLOAD_FILE_PERMISSION"
49+
putExtra(UploadFileBroadcastReceiver.ACTION_TYPE, actionType)
50+
}
51+
52+
return PendingIntent.getBroadcast(
53+
context,
54+
actionType.ordinal,
55+
intent,
56+
PendingIntent.FLAG_IMMUTABLE
57+
)
58+
}
59+
60+
private fun getAllowAllFileAccessAction(context: Context): NotificationCompat.Action {
61+
val pendingIntent = getActionPendingIntent(context, UploadFileBroadcastReceiverActions.ALLOW_ALL_FILES)
62+
return NotificationCompat.Action(
63+
null,
64+
context.getString(R.string.upload_missing_storage_permission_allow_file_access),
65+
pendingIntent
66+
)
67+
}
68+
69+
private fun getAppPermissionsAction(context: Context): NotificationCompat.Action {
70+
val pendingIntent = getActionPendingIntent(context, UploadFileBroadcastReceiverActions.APP_PERMISSIONS)
71+
return NotificationCompat.Action(
72+
null,
73+
context.getString(R.string.upload_missing_storage_permission_app_permissions),
74+
pendingIntent
75+
)
76+
}

app/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,4 +1357,8 @@
13571357
<string name="server_not_reachable_content">The device is likely not connected to the internet</string>
13581358
<string name="file_details_sharing_fragment_custom_permission_not_selected">Please select custom permission</string>
13591359
<string name="link_not_followed_due_to_security_settings">Link not followed due to security settings.</string>
1360+
<string name="upload_missing_storage_permission_title">Upload Stopped – Storage Permission Required</string>
1361+
<string name="upload_missing_storage_permission_description">Your files cannot be uploaded without access to local storage. Tap to grant permission.</string>
1362+
<string name="upload_missing_storage_permission_allow_file_access">Allow all file access</string>
1363+
<string name="upload_missing_storage_permission_app_permissions">App permissions</string>
13601364
</resources>

0 commit comments

Comments
 (0)