Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@
-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Runtime permissions introduced in Android 13 (API level 33) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
tools:ignore="PhotoAndVideoPolicy,SelectedPhotoAccess" />
tools:ignore="PhotoAndVideoPolicy" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"
tools:ignore="PhotoAndVideoPolicy,SelectedPhotoAccess" /> <!-- Needed for Android 14 (API level 34) -->
tools:ignore="PhotoAndVideoPolicy" /> <!-- Needed for Android 14 (API level 34) -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!--
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import javax.inject.Inject;

Expand Down Expand Up @@ -117,6 +118,7 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
private SearchView mSearchView;
private UploadFilesLayoutBinding binding;
private boolean isWithinEncryptedFolder = false;
private String[] mChosenFilePaths;

public LocalFileListFragment getFileListFragment() {
return mFileListFragment;
Expand Down Expand Up @@ -523,13 +525,7 @@ public void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String.

preferences.setUploaderBehaviour(FileUploadWorker.LOCAL_BEHAVIOUR_DELETE);
} else {
final var chosenFiles = mFileListFragment.getCheckedFilePaths();
if (chosenFiles.length > FileUploadHelper.MAX_FILE_COUNT) {
FileUploadHelper.Companion.instance().showFileUploadLimitMessage(this);
return;
}

data.putExtra(EXTRA_CHOSEN_FILES, chosenFiles);
data.putExtra(EXTRA_CHOSEN_FILES, filesToUpload);
data.putExtra(LOCAL_BASE_PATH, mCurrentDir.getAbsolutePath());

// set result code
Expand Down Expand Up @@ -688,17 +684,27 @@ public void onClick(View v) {
finish();
}
} else {
final var chosenFiles = mFileListFragment.getCheckedFilePaths();
if (chosenFiles.length > FileUploadHelper.MAX_FILE_COUNT) {
FileUploadHelper.Companion.instance().showFileUploadLimitMessage(this);
return;
}
boolean isPositionZero = (binding.uploadFilesSpinnerBehaviour.getSelectedItemPosition() == 0);
new CheckAvailableSpaceTask(this, chosenFiles).execute(isPositionZero);
collectChosenFiles(chosenFiles -> {
boolean isPositionZero = (binding.uploadFilesSpinnerBehaviour.getSelectedItemPosition() == 0);
new CheckAvailableSpaceTask(this, chosenFiles).execute(isPositionZero);
});
}
}
}

private void collectChosenFiles(Consumer<String[]> onCollected) {
mFileListFragment.collectCheckedFilePaths(chosenFiles -> {
if (chosenFiles.length > FileUploadHelper.MAX_FILE_COUNT) {
FileUploadHelper.Companion.instance().showFileUploadLimitMessage(this);
return Unit.INSTANCE;
}

mChosenFilePaths = chosenFiles;
onCollected.accept(chosenFiles);
return Unit.INSTANCE;
});
}

private void showSubFolderWarningDialog() {
final var dialog = ConfirmationDialogFragment.newInstance(
R.string.auto_upload_sub_folder_warning,
Expand Down Expand Up @@ -735,17 +741,12 @@ public void onCancel(@Nullable String callerTag) {
@Override
public void onConfirmation(String callerTag) {
Log_OC.d(TAG, "Positive button in dialog was clicked; dialog tag is " + callerTag);
final var chosenFiles = mFileListFragment.getCheckedFilePaths();
if (chosenFiles.length > FileUploadHelper.MAX_FILE_COUNT) {
FileUploadHelper.Companion.instance().showFileUploadLimitMessage(this);
return;
}

if (QUERY_TO_MOVE_DIALOG_TAG.equals(callerTag)) {
if (QUERY_TO_MOVE_DIALOG_TAG.equals(callerTag) && mChosenFilePaths != null) {
// return the list of selected files to the caller activity (success),
// signaling that they should be moved to the ownCloud folder, instead of copied
Intent data = new Intent();
data.putExtra(EXTRA_CHOSEN_FILES, chosenFiles);
data.putExtra(EXTRA_CHOSEN_FILES, mChosenFilePaths);
data.putExtra(LOCAL_BASE_PATH, mCurrentDir.getAbsolutePath());
setResult(RESULT_OK_AND_MOVE, data);
finish();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,6 @@ class LocalFileListAdapter(
val filesCount: Int
get() = visibleEntries.size

val checkedFilesPath: Array<String>
get() {
val result = FileHelper.listFilesRecursive(checkedFiles)
Log_OC.d(TAG, "Returning ${result.size} selected files")
return result.toTypedArray()
}

fun isCheckedFile(file: File): Boolean = checkedFiles.contains(file)

fun onItemCheckboxClicked(file: File) {
Expand Down Expand Up @@ -142,10 +135,10 @@ class LocalFileListAdapter(
return if (index < 0) RecyclerView.NO_POSITION else index + headerOffset
}

private fun shouldShowHeader(): Boolean = !PermissionUtil.checkStoragePermission(activity)
private var showsPermissionBanner = !PermissionUtil.checkStoragePermission(activity)

private val headerOffset: Int
get() = if (shouldShowHeader()) HEADER_ITEM_COUNT else 0
get() = if (showsPermissionBanner) HEADER_ITEM_COUNT else 0

override fun getItemCount(): Int = visibleEntries.size + FOOTER_ITEM_COUNT + headerOffset

Expand Down Expand Up @@ -379,6 +372,25 @@ class LocalFileListAdapter(
showFirstPage(newFiles)
}

@SuppressLint("NotifyDataSetChanged")
fun refreshPermissionBanner() {
showsPermissionBanner = !PermissionUtil.checkStoragePermission(activity)
notifyDataSetChanged()
}

fun collectCheckedFilePaths(onCompleted: (Array<String>) -> Unit) {
val selection = checkedFiles.toList()

backgroundScope.launch {
val paths = FileHelper.listFilesRecursive(selection).toTypedArray()
Log_OC.d(TAG, "Collected ${paths.size} selected files")

withContext(Dispatchers.Main) {
onCompleted(paths)
}
}
}

fun cleanup() {
backgroundScope.cancel()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

package com.owncloud.android.ui.fragment.localfilelist

import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.os.Environment
Expand Down Expand Up @@ -191,8 +190,7 @@ class LocalFileListFragment :
//endregion

//region File selection
val checkedFilePaths: Array<String>
get() = adapter.checkedFilesPath
fun collectCheckedFilePaths(onCompleted: (Array<String>) -> Unit) = adapter.collectCheckedFilePaths(onCompleted)

val checkedFilesCount: Int
get() = adapter.checkedFilesCount()
Expand Down Expand Up @@ -266,9 +264,8 @@ class LocalFileListFragment :
adapter.setFiles(newFiles)
}

@SuppressLint("NotifyDataSetChanged")
fun setupStoragePermissionWarningBanner() {
adapter.notifyDataSetChanged()
adapter.refreshPermissionBanner()
}
//endregion

Expand Down
22 changes: 12 additions & 10 deletions app/src/main/java/com/owncloud/android/utils/PermissionUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -175,25 +175,27 @@ object PermissionUtil {

// region Storage permission checks

/**
* Checks if the application has storage/media access permissions.
*
* - Android 11+ (API 30+): Checks for MANAGE_EXTERNAL_STORAGE (all files system access)
* - Android 13+ (API 33+): Checks for granular media permissions (READ_MEDIA_IMAGES, READ_MEDIA_VIDEO)
* - Android 14+ (API 34+): Also checks for limited/partial media access (READ_MEDIA_VISUAL_USER_SELECTED)
* - Below Android 11: Uses legacy WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE permission
*/
@JvmStatic
fun checkStoragePermission(context: Context): Boolean = checkAllFilesAccess() || checkMediaAccess(context)
fun checkStoragePermission(context: Context): Boolean =
checkAllFilesAccess() || checkMediaAccess(context) || checkPartialMediaAccess(context)

@JvmStatic
fun checkAllFilesAccess(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()

fun checkMediaAccess(context: Context): Boolean = checkPermissions(context, getRequiredStoragePermissions())
fun checkMediaAccess(context: Context): Boolean = checkPermissions(context, getFullMediaAccessPermissions())

@JvmStatic
fun checkPartialMediaAccess(context: Context): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
checkSelfPermission(context, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)

private fun getRequiredStoragePermissions() = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> getApiLevel34StoragePermissions()
else -> getFullMediaAccessPermissions()
}

private fun getFullMediaAccessPermissions() = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getApiLevel33StoragePermissions()
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> getApiLevel29StoragePermissions()
else -> getLegacyStoragePermissions()
Expand Down
Loading