Browse Source

Merge pull request #9044 from nextcloud/target-sdk-30

Target SDK 30
Álvaro Brey 3 years ago
parent
commit
a994ebc3f4
47 changed files with 549 additions and 252 deletions
  1. 4 4
      build.gradle
  2. 2 4
      src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.java
  3. 2 5
      src/androidTest/java/com/nextcloud/client/AuthenticatorActivityIT.java
  4. 2 4
      src/androidTest/java/com/nextcloud/client/CommunityActivityIT.java
  5. 8 8
      src/androidTest/java/com/nextcloud/client/EndToEndRandomIT.java
  6. 2 4
      src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.java
  7. 2 5
      src/androidTest/java/com/nextcloud/client/FileDisplayActivityScreenshotIT.java
  8. 2 4
      src/androidTest/java/com/nextcloud/client/FirstRunActivityIT.java
  9. 34 0
      src/androidTest/java/com/nextcloud/client/GrantStoragePermissionRule.kt
  10. 2 4
      src/androidTest/java/com/nextcloud/client/SettingsActivityIT.java
  11. 2 4
      src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.java
  12. 2 5
      src/androidTest/java/com/nextcloud/client/UploadListActivityActivityIT.java
  13. 13 9
      src/androidTest/java/com/owncloud/android/AbstractIT.java
  14. 2 2
      src/androidTest/java/com/owncloud/android/DownloadIT.java
  15. 3 5
      src/androidTest/java/com/owncloud/android/ScreenshotsIT.java
  16. 17 17
      src/androidTest/java/com/owncloud/android/UploadIT.java
  17. 96 0
      src/androidTest/java/com/owncloud/android/datamodel/ContentResolverHelperTest.kt
  18. 3 3
      src/androidTest/java/com/owncloud/android/ui/LoginIT.java
  19. 3 4
      src/androidTest/java/com/owncloud/android/ui/activity/DrawerActivityIT.java
  20. 4 3
      src/androidTest/java/com/owncloud/android/ui/activity/FileDisplayActivityTest.java
  21. 17 9
      src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentIT.kt
  22. 2 3
      src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt
  23. 3 5
      src/androidTest/java/com/owncloud/android/ui/preview/PreviewTextFileFragmentTest.java
  24. 3 2
      src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt
  25. 10 3
      src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt
  26. 5 1
      src/debug/AndroidManifest.xml
  27. 5 1
      src/main/AndroidManifest.xml
  28. 2 2
      src/main/java/com/nextcloud/client/errorhandling/ShowErrorActivity.kt
  29. 2 2
      src/main/java/com/nextcloud/client/etm/EtmActivity.kt
  30. 1 1
      src/main/java/com/nextcloud/client/files/downloader/FileTransferService.kt
  31. 2 0
      src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt
  32. 1 2
      src/main/java/com/nextcloud/client/jobs/NotificationWork.kt
  33. 2 2
      src/main/java/com/nextcloud/client/media/PlayerService.kt
  34. 1 2
      src/main/java/com/owncloud/android/MainApp.java
  35. 113 0
      src/main/java/com/owncloud/android/datamodel/ContentResolverHelper.kt
  36. 35 33
      src/main/java/com/owncloud/android/datamodel/MediaProvider.java
  37. 5 3
      src/main/java/com/owncloud/android/operations/DownloadFileOperation.java
  38. 5 2
      src/main/java/com/owncloud/android/operations/RenameFileOperation.java
  39. 3 2
      src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java
  40. 1 1
      src/main/java/com/owncloud/android/services/OperationsService.java
  41. 5 6
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  42. 1 1
      src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java
  43. 1 1
      src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java
  44. 1 1
      src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java
  45. 2 0
      src/main/java/com/owncloud/android/utils/FileStorageUtils.java
  46. 0 73
      src/main/java/com/owncloud/android/utils/PermissionUtil.java
  47. 116 0
      src/main/java/com/owncloud/android/utils/PermissionUtil.kt

+ 4 - 4
build.gradle

@@ -66,7 +66,7 @@ ext {
     prismVersion = "2.0.0"
     androidLibraryVersion = "master-SNAPSHOT"
     mockitoVersion = "3.12.4"
-    mockkVersion = "1.10.4"
+    mockkVersion = "1.12.0"
     powermockVersion = "2.0.9"
     byteBuddyVersion = "1.11.22"
     espressoVersion = "3.4.0"
@@ -117,11 +117,11 @@ android {
             'InvalidPeriodicWorkRequestInterval' // crashes due to a bug in lint itself
     }
 
-    compileSdkVersion 29
+    compileSdkVersion 30
 
     defaultConfig {
         minSdkVersion 23
-        targetSdkVersion 29
+        targetSdkVersion 30
 
         // arguments to be passed to functional tests
         testInstrumentationRunner "com.nextcloud.client.ScreenshotTestRunner"
@@ -374,7 +374,7 @@ dependencies {
     }
     androidTestImplementation "net.bytebuddy:byte-buddy:$byteBuddyVersion"
     androidTestImplementation "net.bytebuddy:byte-buddy-android:$byteBuddyVersion"
-    androidTestImplementation "io.mockk:mockk-android:1.10.4"
+    androidTestImplementation "io.mockk:mockk-android:$mockkVersion"
     androidTestImplementation 'androidx.arch.core:core-testing:2.0.1'
     androidTestImplementation "com.facebook.testing.screenshot:core:0.13.0"
 

+ 2 - 4
src/androidTest/java/com/nextcloud/client/ActivitiesActivityIT.java

@@ -22,7 +22,6 @@
 
 package com.nextcloud.client;
 
-import android.Manifest;
 import android.view.View;
 
 import com.owncloud.android.AbstractIT;
@@ -32,10 +31,10 @@ import com.owncloud.android.utils.ScreenshotTest;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.espresso.contrib.DrawerActions;
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
@@ -47,8 +46,7 @@ public class ActivitiesActivityIT extends AbstractIT {
                                                                                           false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     @ScreenshotTest

+ 2 - 5
src/androidTest/java/com/nextcloud/client/AuthenticatorActivityIT.java

@@ -20,8 +20,6 @@
 
 package com.nextcloud.client;
 
-import android.Manifest;
-
 import com.owncloud.android.AbstractIT;
 import com.owncloud.android.R;
 import com.owncloud.android.authentication.AuthenticatorActivity;
@@ -29,9 +27,9 @@ import com.owncloud.android.utils.ScreenshotTest;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.action.ViewActions.typeText;
@@ -45,8 +43,7 @@ public class AuthenticatorActivityIT extends AbstractIT {
                                                                                              false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     @ScreenshotTest

+ 2 - 4
src/androidTest/java/com/nextcloud/client/CommunityActivityIT.java

@@ -22,7 +22,6 @@
 
 package com.nextcloud.client;
 
-import android.Manifest;
 import android.app.Activity;
 
 import com.owncloud.android.AbstractIT;
@@ -31,9 +30,9 @@ import com.owncloud.android.utils.ScreenshotTest;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 
 public class CommunityActivityIT extends AbstractIT {
@@ -42,8 +41,7 @@ public class CommunityActivityIT extends AbstractIT {
                                                                                          false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     @ScreenshotTest

+ 8 - 8
src/androidTest/java/com/nextcloud/client/EndToEndRandomIT.java

@@ -378,13 +378,13 @@ public class EndToEndRandomIT extends AbstractOnServerIT {
     public void testUploadWithCopy() throws Exception {
         init();
 
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          currentFolder.getRemotePath() + "nonEmpty.txt",
                                          account.name);
 
         uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_COPY);
 
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
+        File originalFile = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt");
         OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
                                                                                       "nonEmpty.txt");
 
@@ -396,13 +396,13 @@ public class EndToEndRandomIT extends AbstractOnServerIT {
     public void testUploadWithMove() throws Exception {
         init();
 
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          currentFolder.getRemotePath() + "nonEmpty.txt",
                                          account.name);
 
         uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_MOVE);
 
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
+        File originalFile = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt");
         OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
                                                                                       "nonEmpty.txt");
 
@@ -414,13 +414,13 @@ public class EndToEndRandomIT extends AbstractOnServerIT {
     public void testUploadWithForget() throws Exception {
         init();
 
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          currentFolder.getRemotePath() + "nonEmpty.txt",
                                          account.name);
 
         uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_FORGET);
 
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
+        File originalFile = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt");
         OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
                                                                                       "nonEmpty.txt");
 
@@ -432,13 +432,13 @@ public class EndToEndRandomIT extends AbstractOnServerIT {
     public void testUploadWithDelete() throws Exception {
         init();
 
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          currentFolder.getRemotePath() + "nonEmpty.txt",
                                          account.name);
 
         uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_DELETE);
 
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
+        File originalFile = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt");
         OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(currentFolder.getRemotePath() +
                                                                                       "nonEmpty.txt");
 

+ 2 - 4
src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.java

@@ -22,7 +22,6 @@
 
 package com.nextcloud.client;
 
-import android.Manifest;
 import android.app.Activity;
 
 import com.owncloud.android.AbstractOnServerIT;
@@ -42,12 +41,12 @@ import org.greenrobot.eventbus.EventBus;
 import org.junit.Assert;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.espresso.Espresso;
 import androidx.test.espresso.contrib.DrawerActions;
 import androidx.test.espresso.contrib.NavigationViewActions;
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.action.ViewActions.click;
@@ -62,8 +61,7 @@ public class FileDisplayActivityIT extends AbstractOnServerIT {
                                                                                            false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     // @ScreenshotTest // todo run without real server

+ 2 - 5
src/androidTest/java/com/nextcloud/client/FileDisplayActivityScreenshotIT.java

@@ -22,8 +22,6 @@
 
 package com.nextcloud.client;
 
-import android.Manifest;
-
 import com.owncloud.android.AbstractIT;
 import com.owncloud.android.R;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
@@ -32,11 +30,11 @@ import com.owncloud.android.utils.ScreenshotTest;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.espresso.contrib.DrawerActions;
 import androidx.test.espresso.contrib.NavigationViewActions;
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.matcher.ViewMatchers.withId;
@@ -48,8 +46,7 @@ public class FileDisplayActivityScreenshotIT extends AbstractIT {
                                                                                            false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     @ScreenshotTest

+ 2 - 4
src/androidTest/java/com/nextcloud/client/FirstRunActivityIT.java

@@ -22,7 +22,6 @@
 
 package com.nextcloud.client;
 
-import android.Manifest;
 import android.app.Activity;
 
 import com.nextcloud.client.onboarding.FirstRunActivity;
@@ -31,9 +30,9 @@ import com.owncloud.android.utils.ScreenshotTest;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 
 public class FirstRunActivityIT extends AbstractIT {
@@ -42,8 +41,7 @@ public class FirstRunActivityIT extends AbstractIT {
                                                                                         false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     @ScreenshotTest

+ 34 - 0
src/androidTest/java/com/nextcloud/client/GrantStoragePermissionRule.kt

@@ -0,0 +1,34 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Álvaro Brey Vilas
+ * Copyright (C) 2021 Álvaro Brey Vilas
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client
+
+import androidx.test.rule.GrantPermissionRule
+import com.owncloud.android.utils.PermissionUtil
+import org.junit.rules.TestRule
+
+class GrantStoragePermissionRule private constructor() {
+
+    companion object {
+        @JvmStatic
+        fun grant(): TestRule = GrantPermissionRule.grant(PermissionUtil.getExternalStoragePermission())
+    }
+}

+ 2 - 4
src/androidTest/java/com/nextcloud/client/SettingsActivityIT.java

@@ -22,7 +22,6 @@
 
 package com.nextcloud.client;
 
-import android.Manifest;
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Looper;
@@ -36,9 +35,9 @@ import com.owncloud.android.utils.ScreenshotTest;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 import static org.junit.Assert.assertTrue;
 
@@ -49,8 +48,7 @@ public class SettingsActivityIT extends AbstractIT {
                                                                                         false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     @ScreenshotTest

+ 2 - 4
src/androidTest/java/com/nextcloud/client/SyncedFoldersActivityIT.java

@@ -22,7 +22,6 @@
 
 package com.nextcloud.client;
 
-import android.Manifest;
 import android.app.Activity;
 import android.content.Intent;
 
@@ -35,11 +34,11 @@ import com.owncloud.android.utils.ScreenshotTest;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import java.util.Objects;
 
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
@@ -50,8 +49,7 @@ public class SyncedFoldersActivityIT extends AbstractIT {
                                                                                              false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     @ScreenshotTest

+ 2 - 5
src/androidTest/java/com/nextcloud/client/UploadListActivityActivityIT.java

@@ -22,17 +22,15 @@
 
 package com.nextcloud.client;
 
-import android.Manifest;
-
 import com.owncloud.android.AbstractIT;
 import com.owncloud.android.ui.activity.UploadListActivity;
 import com.owncloud.android.utils.ScreenshotTest;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 
 public class UploadListActivityActivityIT extends AbstractIT {
@@ -41,8 +39,7 @@ public class UploadListActivityActivityIT extends AbstractIT {
                                                                                           false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     @ScreenshotTest

+ 13 - 9
src/androidTest/java/com/owncloud/android/AbstractIT.java

@@ -1,6 +1,5 @@
 package com.owncloud.android;
 
-import android.Manifest;
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AuthenticatorException;
@@ -8,12 +7,14 @@ import android.accounts.OperationCanceledException;
 import android.app.Activity;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
+import android.os.Build;
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.view.View;
 
 import com.facebook.testing.screenshot.Screenshot;
 import com.facebook.testing.screenshot.internal.TestNameDetector;
+import com.nextcloud.client.GrantStoragePermissionRule;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.account.UserAccountManagerImpl;
@@ -45,6 +46,7 @@ import org.apache.commons.io.FileUtils;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Rule;
+import org.junit.rules.TestRule;
 
 import java.io.File;
 import java.io.FileWriter;
@@ -58,7 +60,6 @@ import androidx.fragment.app.DialogFragment;
 import androidx.test.espresso.contrib.DrawerActions;
 import androidx.test.espresso.intent.rule.IntentsTestRule;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.GrantPermissionRule;
 import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
 import androidx.test.runner.lifecycle.Stage;
 
@@ -75,8 +76,7 @@ import static org.junit.Assert.assertTrue;
 
 public abstract class AbstractIT {
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     protected static OwnCloudClient client;
     protected static Account account;
@@ -205,7 +205,7 @@ public abstract class AbstractIT {
     }
 
     protected static void createDummyFiles() throws IOException {
-        File tempPath = new File(FileStorageUtils.getTemporalPath(account.name));
+        File tempPath = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext));
         if (!tempPath.exists()) {
             assertTrue(tempPath.mkdirs());
         }
@@ -218,7 +218,7 @@ public abstract class AbstractIT {
     }
 
     protected static File getDummyFile(String name) throws IOException {
-        File file = new File(FileStorageUtils.getTemporalPath(account.name) + File.separator + name);
+        File file = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + File.separator + name);
 
         if (file.exists()) {
             return file;
@@ -243,7 +243,7 @@ public abstract class AbstractIT {
     }
 
     public static File createFile(String name, int iteration) throws IOException {
-        File file = new File(FileStorageUtils.getTemporalPath(account.name) + File.separator + name);
+        File file = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + File.separator + name);
         if (!file.getParentFile().exists()) {
             assertTrue(file.getParentFile().mkdirs());
         }
@@ -387,11 +387,15 @@ public abstract class AbstractIT {
     }
 
     protected void screenshot(View view, String prefix) {
-        Screenshot.snap(view).setName(createName(prefix)).record();
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            Screenshot.snap(view).setName(createName(prefix)).record();
+        }
     }
 
     protected void screenshot(Activity sut) {
-        Screenshot.snapActivity(sut).setName(createName()).record();
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            Screenshot.snapActivity(sut).setName(createName()).record();
+        }
     }
 
     protected void screenshot(DialogFragment dialogFragment, String prefix) {

+ 2 - 2
src/androidTest/java/com/owncloud/android/DownloadIT.java

@@ -73,13 +73,13 @@ public class DownloadIT extends AbstractOnServerIT {
 
     @Test
     public void verifyDownload() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          FOLDER + "nonEmpty.txt",
                                          account.name);
 
         uploadOCUpload(ocUpload);
 
-        OCUpload ocUpload2 = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload2 = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                           FOLDER + "nonEmpty2.txt",
                                           account.name);
 

+ 3 - 5
src/androidTest/java/com/owncloud/android/ScreenshotsIT.java

@@ -1,7 +1,6 @@
 package com.owncloud.android;
 
-import android.Manifest;
-
+import com.nextcloud.client.GrantStoragePermissionRule;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.operations.CreateFolderOperation;
 import com.owncloud.android.operations.common.SyncOperation;
@@ -14,6 +13,7 @@ import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
@@ -23,7 +23,6 @@ import androidx.test.espresso.contrib.DrawerActions;
 import androidx.test.espresso.contrib.RecyclerViewActions;
 import androidx.test.espresso.matcher.PreferenceMatchers;
 import androidx.test.filters.LargeTest;
-import androidx.test.rule.GrantPermissionRule;
 import tools.fastlane.screengrab.Screengrab;
 import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy;
 import tools.fastlane.screengrab.locale.LocaleTestRule;
@@ -44,8 +43,7 @@ public class ScreenshotsIT extends AbstractOnServerIT {
     public static final LocaleTestRule localeTestRule = new LocaleTestRule();
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @BeforeClass
     public static void beforeScreenshot() {

+ 17 - 17
src/androidTest/java/com/owncloud/android/UploadIT.java

@@ -122,7 +122,7 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testEmptyUpload() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/empty.txt",
                                          FOLDER + "empty.txt",
                                          account.name);
 
@@ -131,7 +131,7 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testNonEmptyUpload() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          FOLDER + "nonEmpty.txt",
                                          account.name);
 
@@ -140,13 +140,13 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testUploadWithCopy() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          FOLDER + "nonEmpty.txt",
                                          account.name);
 
         uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_COPY);
 
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
+        File originalFile = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt");
         OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt");
 
         assertTrue(originalFile.exists());
@@ -156,13 +156,13 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testUploadWithMove() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          FOLDER + "nonEmpty.txt",
                                          account.name);
 
         uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_MOVE);
 
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
+        File originalFile = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt");
         OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt");
 
         assertFalse(originalFile.exists());
@@ -172,13 +172,13 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testUploadWithForget() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          FOLDER + "nonEmpty.txt",
                                          account.name);
 
         uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_FORGET);
 
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
+        File originalFile = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt");
         OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt");
 
         assertTrue(originalFile.exists());
@@ -188,13 +188,13 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testUploadWithDelete() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt",
                                          FOLDER + "nonEmpty.txt",
                                          account.name);
 
         uploadOCUpload(ocUpload, FileUploader.LOCAL_BEHAVIOUR_DELETE);
 
-        File originalFile = new File(FileStorageUtils.getTemporalPath(account.name) + "/nonEmpty.txt");
+        File originalFile = new File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/nonEmpty.txt");
         OCFile uploadedFile = fileDataStorageManager.getFileByDecryptedRemotePath(FOLDER + "nonEmpty.txt");
 
         assertFalse(originalFile.exists());
@@ -204,7 +204,7 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testChunkedUpload() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/chunkedFile.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/chunkedFile.txt",
                                          FOLDER + "chunkedFile.txt", account.name);
 
         uploadOCUpload(ocUpload);
@@ -212,7 +212,7 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testUploadInNonExistingFolder() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/empty.txt",
                                          FOLDER + "2/3/4/1.txt", account.name);
 
         uploadOCUpload(ocUpload);
@@ -220,7 +220,7 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testUploadOnChargingOnlyButNotCharging() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/empty.txt",
                                          FOLDER + "notCharging.txt", account.name);
         ocUpload.setWhileChargingOnly(true);
 
@@ -268,7 +268,7 @@ public class UploadIT extends AbstractOnServerIT {
             }
         };
 
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/empty.txt",
                                          FOLDER + "charging.txt", account.name);
         ocUpload.setWhileChargingOnly(true);
 
@@ -308,7 +308,7 @@ public class UploadIT extends AbstractOnServerIT {
                 return new Connectivity(true, false, false, true);
             }
         };
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/empty.txt",
                                          FOLDER + "noWifi.txt", account.name);
         ocUpload.setUseWifiOnly(true);
 
@@ -338,7 +338,7 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testUploadOnWifiOnlyAndWifi() {
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/empty.txt",
                                          FOLDER + "wifi.txt", account.name);
         ocUpload.setWhileChargingOnly(true);
 
@@ -387,7 +387,7 @@ public class UploadIT extends AbstractOnServerIT {
                 return new Connectivity(true, true, true, true);
             }
         };
-        OCUpload ocUpload = new OCUpload(FileStorageUtils.getTemporalPath(account.name) + "/empty.txt",
+        OCUpload ocUpload = new OCUpload(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + "/empty.txt",
                                          FOLDER + "noWifi.txt",
                                          account.name);
         ocUpload.setUseWifiOnly(true);

+ 96 - 0
src/androidTest/java/com/owncloud/android/datamodel/ContentResolverHelperTest.kt

@@ -0,0 +1,96 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Álvaro Brey Vilas
+ * Copyright (C) 2021 Álvaro Brey Vilas
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.datamodel
+
+import android.content.ContentResolver
+import android.net.Uri
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import com.nhaarman.mockitokotlin2.argThat
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.verify
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidJUnit4::class)
+class ContentResolverHelperTest {
+
+    companion object {
+        private val URI = Uri.parse("http://foo.bar")
+        private val PROJECTION = arrayOf("Foo")
+        private const val SELECTION = "selection"
+        private const val SORT_COLUMN = "sortColumn"
+        private const val SORT_DIRECTION = ContentResolverHelper.SORT_DIRECTION_ASCENDING
+        private const val SORT_DIRECTION_INT = ContentResolver.QUERY_SORT_DIRECTION_ASCENDING
+        private const val LIMIT = 10
+    }
+
+    @Mock
+    lateinit var resolver: ContentResolver
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun contentResolver_onAndroid26_usesNewAPI() {
+
+        ContentResolverHelper
+            .queryResolver(resolver, URI, PROJECTION, SELECTION, null, SORT_COLUMN, SORT_DIRECTION, LIMIT)
+
+        verify(resolver).query(
+            eq(URI),
+            eq(PROJECTION),
+            argThat { bundle ->
+                bundle.getString(ContentResolver.QUERY_ARG_SQL_SELECTION) == SELECTION &&
+                    bundle.getInt(ContentResolver.QUERY_ARG_LIMIT) == LIMIT &&
+                    bundle.getStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS)!!
+                        .contentEquals(arrayOf(SORT_COLUMN)) &&
+                    bundle.getInt(ContentResolver.QUERY_ARG_SORT_DIRECTION) == SORT_DIRECTION_INT
+            },
+            null
+        )
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.N_MR1)
+    fun contentResolver_onAndroidBelow26_usesOldAPI() {
+
+        ContentResolverHelper
+            .queryResolver(resolver, URI, PROJECTION, SELECTION, null, SORT_COLUMN, SORT_DIRECTION, LIMIT)
+
+        verify(resolver).query(
+            eq(URI),
+            eq(PROJECTION),
+            eq(SELECTION),
+            eq(null),
+            eq("$SORT_COLUMN $SORT_DIRECTION LIMIT $LIMIT"),
+            eq(null)
+        )
+    }
+}

+ 3 - 3
src/androidTest/java/com/owncloud/android/ui/LoginIT.java

@@ -21,11 +21,11 @@
 
 package com.owncloud.android.ui;
 
-import android.Manifest;
 import android.accounts.Account;
 import android.content.Context;
 import android.os.Bundle;
 
+import com.nextcloud.client.GrantStoragePermissionRule;
 import com.nextcloud.client.RetryTestRule;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.account.UserAccountManagerImpl;
@@ -37,13 +37,13 @@ import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.core.app.ActivityScenario;
 import androidx.test.espresso.web.webdriver.DriverAtoms;
 import androidx.test.espresso.web.webdriver.Locator;
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.GrantPermissionRule;
 
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.action.ViewActions.click;
@@ -59,7 +59,7 @@ import static org.junit.Assert.assertEquals;
 @LargeTest
 public class LoginIT extends AbstractIT {
     @Rule
-    public GrantPermissionRule permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
     @Rule
     public RetryTestRule retryTestRule = new RetryTestRule();
 

+ 3 - 4
src/androidTest/java/com/owncloud/android/ui/activity/DrawerActivityIT.java

@@ -22,12 +22,12 @@
 
 package com.owncloud.android.ui.activity;
 
-import android.Manifest;
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.net.Uri;
 import android.os.Bundle;
 
+import com.nextcloud.client.GrantStoragePermissionRule;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.account.UserAccountManagerImpl;
@@ -39,9 +39,9 @@ import com.owncloud.android.lib.common.accounts.AccountUtils;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 import static androidx.test.espresso.Espresso.onView;
 import static androidx.test.espresso.action.ViewActions.click;
@@ -56,8 +56,7 @@ public class DrawerActivityIT extends AbstractIT {
                                                                                            false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
     private static Account account1;
     private static User user1;
     private static Account account2;

+ 4 - 3
src/androidTest/java/com/owncloud/android/ui/activity/FileDisplayActivityTest.java

@@ -2,23 +2,24 @@ package com.owncloud.android.ui.activity;
 
 import android.app.Activity;
 
+import com.nextcloud.client.GrantStoragePermissionRule;
 import com.nextcloud.client.onboarding.WhatsNewActivity;
 import com.owncloud.android.AbstractIT;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import androidx.test.core.app.ActivityScenario;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.GrantPermissionRule;
 import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
 
-import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
 import static androidx.test.runner.lifecycle.Stage.RESUMED;
 
 public class FileDisplayActivityTest extends AbstractIT {
 
-    @Rule public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(WRITE_EXTERNAL_STORAGE);
+    @Rule
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     public void testSetupToolbar() {

+ 17 - 9
src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentIT.kt

@@ -22,9 +22,10 @@
  */
 package com.owncloud.android.ui.fragment
 
-import android.Manifest
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
 import androidx.test.espresso.intent.rule.IntentsTestRule
-import androidx.test.rule.GrantPermissionRule
+import com.nextcloud.client.GrantStoragePermissionRule
 import com.nextcloud.client.device.BatteryStatus
 import com.nextcloud.client.device.PowerManagementService
 import com.nextcloud.client.network.Connectivity
@@ -36,6 +37,7 @@ import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TestRule
 import java.io.File
 
 class OCFileListFragmentIT : AbstractOnServerIT() {
@@ -48,7 +50,7 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
     val activityRule = IntentsTestRule(FileDisplayActivity::class.java, true, false)
 
     @get:Rule
-    val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+    val permissionRule: TestRule = GrantStoragePermissionRule.grant()
 
     private val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
         override fun isInternetWalled(): Boolean {
@@ -71,12 +73,18 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
             get() = BatteryStatus()
     }
 
+    private fun openFile(name: String): File {
+        val ctx: Context = ApplicationProvider.getApplicationContext()
+        val externalFilesDir = ctx.getExternalFilesDir(null)
+        return File(externalFilesDir, name)
+    }
+
     @Test
     @SuppressWarnings("MagicNumber")
     fun testEnoughSpaceWithoutLocalFile() {
         val sut = OCFileListFragment()
         val ocFile = OCFile("/test.txt")
-        val file = File("/sdcard/test.txt")
+        val file = openFile("test.txt")
         file.createNewFile()
 
         ocFile.storagePath = file.absolutePath
@@ -99,7 +107,7 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
     fun testEnoughSpaceWithLocalFile() {
         val sut = OCFileListFragment()
         val ocFile = OCFile("/test.txt")
-        val file = File("/sdcard/test.txt")
+        val file = openFile("test.txt")
         file.writeText("123123")
 
         ocFile.storagePath = file.absolutePath
@@ -122,8 +130,8 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
     fun testEnoughSpaceWithoutLocalFolder() {
         val sut = OCFileListFragment()
         val ocFile = OCFile("/test/")
-        val file = File("/sdcard/test/")
-        File("/sdcard/test/1.txt").writeText("123123")
+        val file = openFile("test")
+        File(file, "1.txt").writeText("123123")
 
         ocFile.storagePath = file.absolutePath
 
@@ -145,9 +153,9 @@ class OCFileListFragmentIT : AbstractOnServerIT() {
     fun testEnoughSpaceWithLocalFolder() {
         val sut = OCFileListFragment()
         val ocFile = OCFile("/test/")
-        val folder = File("/sdcard/test/")
+        val folder = openFile("test")
         folder.mkdirs()
-        val file = File("/sdcard/test/1.txt")
+        val file = File(folder, "1.txt")
         file.createNewFile()
         file.writeText("123123")
 

+ 2 - 3
src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt

@@ -22,9 +22,8 @@
  */
 package com.owncloud.android.ui.fragment
 
-import android.Manifest
 import androidx.test.espresso.intent.rule.IntentsTestRule
-import androidx.test.rule.GrantPermissionRule
+import com.nextcloud.client.GrantStoragePermissionRule
 import com.nextcloud.client.TestActivity
 import com.owncloud.android.AbstractIT
 import com.owncloud.android.datamodel.OCFile
@@ -41,7 +40,7 @@ class OCFileListFragmentStaticServerIT : AbstractIT() {
     val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
 
     @get:Rule
-    val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+    val permissionRule = GrantStoragePermissionRule.grant()
 
     @Test
     @ScreenshotTest

+ 3 - 5
src/androidTest/java/com/owncloud/android/ui/preview/PreviewTextFileFragmentTest.java

@@ -22,8 +22,7 @@
 
 package com.owncloud.android.ui.preview;
 
-import android.Manifest;
-
+import com.nextcloud.client.GrantStoragePermissionRule;
 import com.owncloud.android.AbstractIT;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.ui.activity.FileDisplayActivity;
@@ -31,12 +30,12 @@ import com.owncloud.android.utils.MimeTypeUtil;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 
 import java.io.File;
 import java.io.IOException;
 
 import androidx.test.espresso.intent.rule.IntentsTestRule;
-import androidx.test.rule.GrantPermissionRule;
 
 public class PreviewTextFileFragmentTest extends AbstractIT {
     @Rule public IntentsTestRule<FileDisplayActivity> activityRule = new IntentsTestRule<>(FileDisplayActivity.class,
@@ -44,8 +43,7 @@ public class PreviewTextFileFragmentTest extends AbstractIT {
                                                                                            false);
 
     @Rule
-    public final GrantPermissionRule permissionRule = GrantPermissionRule.grant(
-        Manifest.permission.WRITE_EXTERNAL_STORAGE);
+    public final TestRule permissionRule = GrantStoragePermissionRule.grant();
 
     @Test
     // @ScreenshotTest // todo run without real server

+ 3 - 2
src/androidTest/java/com/owncloud/android/utils/FileUtilTest.kt

@@ -43,7 +43,8 @@ class FileUtilTest : AbstractIT() {
 
     @Test
     fun assertSlashInput() {
-        val tempPath = File(FileStorageUtils.getTemporalPath(account.name) + File.pathSeparator + "folder")
+        val tempPath =
+            File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + File.pathSeparator + "folder")
         if (!tempPath.exists()) {
             Assert.assertTrue(tempPath.mkdirs())
         }
@@ -58,7 +59,7 @@ class FileUtilTest : AbstractIT() {
 
     @Test
     fun assertFolderInput() {
-        val tempPath = File(FileStorageUtils.getTemporalPath(account.name))
+        val tempPath = File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext))
         if (!tempPath.exists()) {
             Assert.assertTrue(tempPath.mkdirs())
         }

+ 10 - 3
src/androidTest/java/com/owncloud/android/utils/SyncedFolderUtilsTest.kt

@@ -195,7 +195,10 @@ class SyncedFolderUtilsTest : AbstractIT() {
         getDummyFile(THUMBNAILS_FOLDER + File.separatorChar + IMAGE_JPEG)
         getDummyFile(THUMBNAILS_FOLDER + File.separatorChar + IMAGE_BITMAP)
         val folder = SyncedFolder(
-            FileStorageUtils.getTemporalPath(account.name) + File.separatorChar + THUMBNAILS_FOLDER,
+            FileStorageUtils.getInternalTemporalPath(
+                account.name,
+                targetContext
+            ) + File.separatorChar + THUMBNAILS_FOLDER,
             "",
             true,
             false,
@@ -230,7 +233,11 @@ class SyncedFolderUtilsTest : AbstractIT() {
         @BeforeClass
         @JvmStatic
         fun setUp() {
-            val tempPath = File(FileStorageUtils.getTemporalPath(account.name) + File.separatorChar + THUMBNAILS_FOLDER)
+            val tempPath =
+                File(
+                    FileStorageUtils.getInternalTemporalPath(account.name, targetContext) + File.separatorChar +
+                        THUMBNAILS_FOLDER
+                )
             if (!tempPath.exists()) {
                 tempPath.mkdirs()
             }
@@ -253,7 +260,7 @@ class SyncedFolderUtilsTest : AbstractIT() {
         @AfterClass
         @JvmStatic
         fun tearDown() {
-            FileUtils.deleteDirectory(File(FileStorageUtils.getTemporalPath(account.name)))
+            FileUtils.deleteDirectory(File(FileStorageUtils.getInternalTemporalPath(account.name, targetContext)))
         }
     }
 }

+ 5 - 1
src/debug/AndroidManifest.xml

@@ -5,7 +5,11 @@
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
 
     <!-- Allows for storing and retrieving screenshots -->
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission
+        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="29"
+        tools:ignore="ScopedStorage" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 
     <!-- Allows changing locales -->

+ 5 - 1
src/main/AndroidManifest.xml

@@ -28,7 +28,11 @@
 
     <!-- WRITE_EXTERNAL_STORAGE may be enabled or disabled by the user after installation in
         API >= 23; the app needs to handle this -->
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission
+        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="29"
+        tools:ignore="ScopedStorage" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.VIBRATE" />
 

+ 2 - 2
src/main/java/com/nextcloud/client/errorhandling/ShowErrorActivity.kt

@@ -78,8 +78,8 @@ class ShowErrorActivity : AppCompatActivity() {
         return super.onCreateOptionsMenu(menu)
     }
 
-    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
-        return when (item?.itemId) {
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return when (item.itemId) {
             R.id.error_share -> { onClickedShare(); true }
             else -> super.onOptionsItemSelected(item)
         }

+ 2 - 2
src/main/java/com/nextcloud/client/etm/EtmActivity.kt

@@ -59,8 +59,8 @@ class EtmActivity : ToolbarActivity(), Injectable {
         )
     }
 
-    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
-        return when (item?.itemId) {
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return when (item.itemId) {
             android.R.id.home -> {
                 if (!vm.onBackPressed()) {
                     finish()

+ 1 - 1
src/main/java/com/nextcloud/client/files/downloader/FileTransferService.kt

@@ -113,7 +113,7 @@ class FileTransferService : Service() {
             )
         }
 
-        val request = intent.getParcelableExtra(EXTRA_REQUEST) as Request
+        val request: Request = intent.getParcelableExtra(EXTRA_REQUEST)!!
         val transferManager = getTransferManager(request.user)
         transferManager.enqueue(request)
 

+ 2 - 0
src/main/java/com/nextcloud/client/jobs/AccountRemovalWork.kt

@@ -195,8 +195,10 @@ class AccountRemovalWork(
 
     private fun removeFiles(user: User, storageManager: FileDataStorageManager) {
         val tempDir = File(FileStorageUtils.getTemporalPath(user.accountName))
+        val internalTempDir = File(FileStorageUtils.getInternalTemporalPath(user.accountName, context))
         val saveDir = File(FileStorageUtils.getSavePath(user.accountName))
         FileStorageUtils.deleteRecursively(tempDir, storageManager)
+        FileStorageUtils.deleteRecursively(internalTempDir, storageManager)
         FileStorageUtils.deleteRecursively(saveDir, storageManager)
     }
 

+ 1 - 2
src/main/java/com/nextcloud/client/jobs/NotificationWork.kt

@@ -297,8 +297,7 @@ class NotificationWork constructor(
                                     .getClientFor(user.toOwnCloudAccount(), context)
                                 val actionType = intent.getStringExtra(KEY_NOTIFICATION_ACTION_TYPE)
                                 val actionLink = intent.getStringExtra(KEY_NOTIFICATION_ACTION_LINK)
-                                val success: Boolean
-                                success = if (!TextUtils.isEmpty(actionType) && !TextUtils.isEmpty(actionLink)) {
+                                val success: Boolean = if (!actionType.isNullOrEmpty() && !actionLink.isNullOrEmpty()) {
                                     val resultCode = executeAction(actionType, actionLink, client)
                                     resultCode == HttpStatus.SC_OK || resultCode == HttpStatus.SC_ACCEPTED
                                 } else {

+ 2 - 2
src/main/java/com/nextcloud/client/media/PlayerService.kt

@@ -137,8 +137,8 @@ class PlayerService : Service() {
     }
 
     private fun onActionPlay(intent: Intent) {
-        val user: User = intent.getParcelableExtra(EXTRA_USER) as User
-        val file: OCFile = intent.getParcelableExtra(EXTRA_FILE) as OCFile
+        val user: User = intent.getParcelableExtra(EXTRA_USER)!!
+        val file: OCFile = intent.getParcelableExtra(EXTRA_FILE)!!
         val startPos = intent.getLongExtra(EXTRA_START_POSITION_MS, 0)
         val autoPlay = intent.getBooleanExtra(EXTRA_AUTO_PLAY, true)
         val item = PlaylistItem(file = file, startPositionMs = startPos, autoPlay = autoPlay, user = user)

+ 1 - 2
src/main/java/com/owncloud/android/MainApp.java

@@ -439,8 +439,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
         updateAutoUploadEntries(clock);
 
         if (getAppContext() != null) {
-            if (PermissionUtil.checkSelfPermission(getAppContext(),
-                                                   Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+            if (PermissionUtil.checkExternalStoragePermission(getAppContext())) {
                 splitOutAutoUploadEntries(clock);
             } else {
                 preferences.setAutoUploadSplitEntriesEnabled(true);

+ 113 - 0
src/main/java/com/owncloud/android/datamodel/ContentResolverHelper.kt

@@ -0,0 +1,113 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Álvaro Brey Vilas
+ * Copyright (C) 2021 Álvaro Brey Vilas
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.datamodel
+
+import android.content.ContentResolver
+import android.database.Cursor
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.CancellationSignal
+import androidx.annotation.RequiresApi
+
+object ContentResolverHelper {
+    const val SORT_DIRECTION_ASCENDING = "ASC"
+    const val SORT_DIRECTION_DESCENDING = "DESC"
+
+    @JvmStatic
+    @JvmOverloads
+    @Suppress("LongParameterList")
+    /**
+     * Queries the content resolver with the given params using the correct API level-dependant syntax.
+     * This is needed in order to use LIMIT or OFFSET from android 11.
+     */
+    fun queryResolver(
+        contentResolver: ContentResolver,
+        uri: Uri,
+        projection: Array<String>,
+        selection: String? = null,
+        cancellationSignal: CancellationSignal? = null,
+        sortColumn: String? = null,
+        sortDirection: String? = null,
+        limit: Int? = null
+    ): Cursor? {
+        require(!(sortColumn != null && sortDirection == null)) {
+            "Sort direction is mandatory if sort column is provided"
+        }
+        require(
+            listOf(null, SORT_DIRECTION_ASCENDING, SORT_DIRECTION_DESCENDING).contains(sortDirection)
+        ) {
+            "Invalid sort direction"
+        }
+        return when {
+            Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
+                val queryArgs = getQueryArgsBundle(selection, sortColumn, sortDirection, limit)
+                contentResolver.query(uri, projection, queryArgs, cancellationSignal)
+            }
+            else -> {
+                val sortOrder = getSortOrderString(sortColumn, sortDirection, limit)
+                contentResolver.query(
+                    uri,
+                    projection,
+                    selection,
+                    null,
+                    sortOrder,
+                    cancellationSignal
+                )
+            }
+        }
+    }
+
+    private fun getSortOrderString(sortColumn: String?, sortDirection: String?, limit: Int?): String {
+        val sortOrderBuilder = StringBuilder()
+        if (sortColumn != null) {
+            sortOrderBuilder.append("$sortColumn $sortDirection")
+        }
+        if (limit != null) {
+            if (sortOrderBuilder.isNotEmpty()) {
+                sortOrderBuilder.append(" ")
+            }
+            sortOrderBuilder.append("LIMIT $limit")
+        }
+        return sortOrderBuilder.toString()
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    private fun
+    getQueryArgsBundle(selection: String?, sortColumn: String?, sortDirection: String?, limit: Int?): Bundle {
+        return Bundle().apply {
+            if (selection != null) {
+                putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection)
+            }
+            if (sortColumn != null) {
+                putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(sortColumn))
+                val direction = when (sortDirection) {
+                    SORT_DIRECTION_ASCENDING -> ContentResolver.QUERY_SORT_DIRECTION_ASCENDING
+                    else -> ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
+                }
+                putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, direction)
+            }
+            if (limit != null) {
+                putInt(ContentResolver.QUERY_ARG_LIMIT, limit)
+            }
+        }
+    }
+}

+ 35 - 33
src/main/java/com/owncloud/android/datamodel/MediaProvider.java

@@ -21,7 +21,6 @@
 
 package com.owncloud.android.datamodel;
 
-import android.Manifest;
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.database.Cursor;
@@ -54,11 +53,12 @@ public final class MediaProvider {
     private static final String[] FILE_PROJECTION = new String[]{MediaStore.MediaColumns.DATA};
     private static final String IMAGES_FILE_SELECTION = MediaStore.Images.Media.BUCKET_ID + "=";
     private static final String[] IMAGES_FOLDER_PROJECTION = {MediaStore.Images.Media.BUCKET_ID,
-            MediaStore.Images.Media.BUCKET_DISPLAY_NAME};
-    private static final String IMAGES_FOLDER_SORT_ORDER = MediaStore.Images.Media.BUCKET_DISPLAY_NAME + " ASC";
+        MediaStore.Images.Media.BUCKET_DISPLAY_NAME};
+    private static final String IMAGES_FOLDER_SORT_COLUMN = MediaStore.Images.Media.BUCKET_DISPLAY_NAME;
+    private static final String IMAGES_SORT_DIRECTION = ContentResolverHelper.SORT_DIRECTION_ASCENDING;
 
     private static final String[] VIDEOS_FOLDER_PROJECTION = {MediaStore.Video.Media.BUCKET_ID,
-            MediaStore.Video.Media.BUCKET_DISPLAY_NAME};
+        MediaStore.Video.Media.BUCKET_DISPLAY_NAME};
 
     private MediaProvider() {
         // utility class -> private constructor
@@ -78,17 +78,17 @@ public final class MediaProvider {
 
         // query media/image folders
         Cursor cursorFolders = null;
-        if ((activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
-                Manifest.permission.WRITE_EXTERNAL_STORAGE)) || getWithoutActivity) {
-            cursorFolders = contentResolver.query(IMAGES_MEDIA_URI, IMAGES_FOLDER_PROJECTION, null, null,
-                    IMAGES_FOLDER_SORT_ORDER);
+        if (activity != null && PermissionUtil.checkExternalStoragePermission(activity.getApplicationContext())
+            || getWithoutActivity) {
+            cursorFolders = ContentResolverHelper.queryResolver(contentResolver, IMAGES_MEDIA_URI,
+                                                                IMAGES_FOLDER_PROJECTION, null, null,
+                                                                IMAGES_FOLDER_SORT_COLUMN, IMAGES_SORT_DIRECTION, null);
         }
 
         List<MediaFolder> mediaFolders = new ArrayList<>();
         String dataPath = MainApp.getStoragePath() + File.separator + MainApp.getDataFolder();
 
         if (cursorFolders != null) {
-            String fileSortOrder = MediaStore.Images.Media.DATE_TAKEN + " DESC LIMIT " + itemLimit;
             Cursor cursorImages;
 
             Map<String, String> uniqueFolders = new HashMap<>();
@@ -111,13 +111,14 @@ public final class MediaProvider {
                 mediaFolder.filePaths = new ArrayList<>();
 
                 // query images
-                cursorImages = contentResolver.query(
-                    IMAGES_MEDIA_URI,
-                    FILE_PROJECTION,
-                    IMAGES_FILE_SELECTION + folder.getKey(),
-                    null,
-                    fileSortOrder
-                );
+                cursorImages = ContentResolverHelper.queryResolver(contentResolver,
+                                                                   IMAGES_MEDIA_URI,
+                                                                   FILE_PROJECTION,
+                                                                   IMAGES_FILE_SELECTION + folder.getKey(),
+                                                                   null,
+                                                                   MediaStore.Images.Media.DATE_TAKEN,
+                                                                   ContentResolverHelper.SORT_DIRECTION_DESCENDING,
+                                                                   itemLimit);
                 Log.d(TAG, "Reading images for " + mediaFolder.folderName);
 
                 if (cursorImages != null) {
@@ -170,22 +171,21 @@ public final class MediaProvider {
 
     private static void checkPermissions(@Nullable Activity activity) {
         if (activity != null &&
-                !PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
-                        Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+            !PermissionUtil.checkExternalStoragePermission(activity.getApplicationContext())) {
             // Check if we should show an explanation
-            if (PermissionUtil.shouldShowRequestPermissionRationale(activity,
-                    Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+            if (PermissionUtil
+                .shouldShowRequestPermissionRationale(activity, PermissionUtil.getExternalStoragePermission())) {
                 // Show explanation to the user and then request permission
                 Snackbar snackbar = Snackbar.make(activity.findViewById(R.id.ListLayout),
-                        R.string.permission_storage_access, Snackbar.LENGTH_INDEFINITE)
-                        .setAction(R.string.common_ok, v -> PermissionUtil.requestWriteExternalStoreagePermission(activity));
+                                                  R.string.permission_storage_access, Snackbar.LENGTH_INDEFINITE)
+                    .setAction(R.string.common_ok, v -> PermissionUtil.requestExternalStoragePermission(activity));
 
                 ThemeSnackbarUtils.colorSnackbar(activity.getApplicationContext(), snackbar);
 
                 snackbar.show();
             } else {
                 // No explanation needed, request the permission.
-                PermissionUtil.requestWriteExternalStoreagePermission(activity);
+                PermissionUtil.requestExternalStoragePermission(activity);
             }
         }
     }
@@ -197,17 +197,16 @@ public final class MediaProvider {
 
         // query media/image folders
         Cursor cursorFolders = null;
-        if ((activity != null && PermissionUtil.checkSelfPermission(activity.getApplicationContext(),
-                Manifest.permission.WRITE_EXTERNAL_STORAGE)) || getWithoutActivity) {
+        if ((activity != null && PermissionUtil.checkExternalStoragePermission(activity.getApplicationContext()))
+            || getWithoutActivity) {
             cursorFolders = contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, VIDEOS_FOLDER_PROJECTION,
-                    null, null, null);
+                                                  null, null, null);
         }
 
         List<MediaFolder> mediaFolders = new ArrayList<>();
         String dataPath = MainApp.getStoragePath() + File.separator + MainApp.getDataFolder();
 
         if (cursorFolders != null) {
-            String fileSortOrder = MediaStore.Video.Media.DATE_TAKEN + " DESC LIMIT " + itemLimit;
             Cursor cursorVideos;
 
             Map<String, String> uniqueFolders = new HashMap<>();
@@ -229,12 +228,15 @@ public final class MediaProvider {
                 mediaFolder.filePaths = new ArrayList<>();
 
                 // query videos
-                cursorVideos = contentResolver.query(
-                    MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
-                    FILE_PROJECTION,
-                    MediaStore.Video.Media.BUCKET_ID + "=" + folder.getKey(),
-                    null,
-                    fileSortOrder);
+                cursorVideos = ContentResolverHelper.queryResolver(contentResolver,
+                                                                   MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                                                                   FILE_PROJECTION,
+                                                                   MediaStore.Video.Media.BUCKET_ID + "=" + folder.getKey(),
+                                                                   null,
+                                                                   MediaStore.Video.Media.DATE_TAKEN,
+                                                                   ContentResolverHelper.SORT_DIRECTION_DESCENDING,
+                                                                   itemLimit);
+
                 Log.d(TAG, "Reading videos for " + mediaFolder.folderName);
 
                 if (cursorVideos != null) {

+ 5 - 3
src/main/java/com/owncloud/android/operations/DownloadFileOperation.java

@@ -23,6 +23,7 @@ package com.owncloud.android.operations;
 
 import android.accounts.Account;
 import android.content.Context;
+import android.os.FileUtils;
 import android.text.TextUtils;
 import android.webkit.MimeTypeMap;
 
@@ -104,11 +105,11 @@ public class DownloadFileOperation extends RemoteOperation {
     }
 
     public String getTmpPath() {
-        return FileStorageUtils.getTemporalPath(account.name) + file.getRemotePath();
+        return FileStorageUtils.getInternalTemporalPath(account.name, context) + file.getRemotePath();
     }
 
     public String getTmpFolder() {
-        return FileStorageUtils.getTemporalPath(account.name);
+        return FileStorageUtils.getInternalTemporalPath(account.name, context);
     }
 
     public String getRemotePath() {
@@ -203,7 +204,8 @@ public class DownloadFileOperation extends RemoteOperation {
                     return new RemoteOperationResult(e);
                 }
             }
-            moved = tmpFile.renameTo(newFile);
+
+            moved = FileStorageUtils.moveFile(tmpFile, newFile);
             newFile.setLastModified(file.getModificationTimestamp());
             if (!moved) {
                 result = new RemoteOperationResult(RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED);

+ 5 - 2
src/main/java/com/owncloud/android/operations/RenameFileOperation.java

@@ -21,6 +21,7 @@
 
 package com.owncloud.android.operations;
 
+import android.content.Context;
 import android.text.TextUtils;
 
 import com.owncloud.android.datamodel.FileDataStorageManager;
@@ -48,6 +49,7 @@ public class RenameFileOperation extends SyncOperation {
     private OCFile file;
     private String remotePath;
     private String newName;
+    private Context context;
 
     /**
      * Constructor
@@ -55,11 +57,12 @@ public class RenameFileOperation extends SyncOperation {
      * @param remotePath RemotePath of the OCFile instance describing the remote file or folder to rename
      * @param newName    New name to set as the name of file.
      */
-    public RenameFileOperation(String remotePath, String newName, FileDataStorageManager storageManager) {
+    public RenameFileOperation(String remotePath, String newName, FileDataStorageManager storageManager, Context context) {
         super(storageManager);
 
         this.remotePath = remotePath;
         this.newName = newName;
+        this.context = context;
     }
 
     /**
@@ -168,7 +171,7 @@ public class RenameFileOperation extends SyncOperation {
             return false;
         }
         // create a test file
-        String tmpFolderName = FileStorageUtils.getTemporalPath("");
+        String tmpFolderName = FileStorageUtils.getInternalTemporalPath("", context);
         File testFile = new File(tmpFolderName + newName);
         File tmpFolder = testFile.getParentFile();
         if (! tmpFolder.mkdirs()) {

+ 3 - 2
src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java

@@ -343,7 +343,8 @@ public class DocumentsStorageProvider extends DocumentsProvider {
 
         RemoteOperationResult result = new RenameFileOperation(document.getRemotePath(),
                                                                displayName,
-                                                               document.getStorageManager())
+                                                               document.getStorageManager(),
+                                                               getNonNullContext())
             .execute(document.getClient());
 
         if (!result.isSuccess()) {
@@ -508,7 +509,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
         User user = targetFolder.getUser();
 
         // create dummy file
-        File tempDir = new File(FileStorageUtils.getTemporalPath(user.getAccountName()));
+        File tempDir = new File(FileStorageUtils.getInternalTemporalPath(user.getAccountName(), getNonNullContext()));
 
         if (!tempDir.exists() && !tempDir.mkdirs()) {
             throw new FileNotFoundException("Temp folder could not be created: " + tempDir.getAbsolutePath());

+ 1 - 1
src/main/java/com/owncloud/android/services/OperationsService.java

@@ -658,7 +658,7 @@ public class OperationsService extends Service {
                     case ACTION_RENAME:
                         remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH);
                         String newName = operationIntent.getStringExtra(EXTRA_NEWNAME);
-                        operation = new RenameFileOperation(remotePath, newName, fileDataStorageManager);
+                        operation = new RenameFileOperation(remotePath, newName, fileDataStorageManager, getApplicationContext());
                         break;
 
                     case ACTION_REMOVE:

+ 5 - 6
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -25,7 +25,6 @@
 
 package com.owncloud.android.ui.activity;
 
-import android.Manifest;
 import android.accounts.AuthenticatorException;
 import android.app.Activity;
 import android.content.BroadcastReceiver;
@@ -320,20 +319,20 @@ public class FileDisplayActivity extends FileActivity
         super.onPostCreate(savedInstanceState);
 
 
-        if (!PermissionUtil.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+        if (!PermissionUtil.checkExternalStoragePermission(this)) {
             // Check if we should show an explanation
             if (PermissionUtil.shouldShowRequestPermissionRationale(this,
-                                                                    Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+                                                                    PermissionUtil.getExternalStoragePermission())) {
                 // Show explanation to the user and then request permission
                 Snackbar snackbar = Snackbar.make(binding.rootLayout,
                                                   R.string.permission_storage_access,
                                                   Snackbar.LENGTH_INDEFINITE)
-                    .setAction(R.string.common_ok, v -> PermissionUtil.requestWriteExternalStoreagePermission(this));
+                    .setAction(R.string.common_ok, v -> PermissionUtil.requestExternalStoragePermission(this));
                 ThemeSnackbarUtils.colorSnackbar(this, snackbar);
                 snackbar.show();
             } else {
                 // No explanation needed, request the permission.
-                PermissionUtil.requestWriteExternalStoreagePermission(this);
+                PermissionUtil.requestExternalStoragePermission(this);
             }
         }
 
@@ -403,7 +402,7 @@ public class FileDisplayActivity extends FileActivity
     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                            @NonNull int[] grantResults) {
         switch (requestCode) {
-            case PermissionUtil.PERMISSIONS_WRITE_EXTERNAL_STORAGE: {
+            case PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE: {
                 // If request is cancelled, result arrays are empty.
                 if (grantResults.length > 0
                     && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

+ 1 - 1
src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java

@@ -225,7 +225,7 @@ public class RichDocumentsEditorWebView extends EditorWebView {
             return;
         }
 
-        File targetFile = new File(FileStorageUtils.getTemporalPath(account.getName()) + "/print.pdf");
+        File targetFile = new File(FileStorageUtils.getInternalTemporalPath(account.getName(), getBaseContext()) + "/print.pdf");
 
         new PrintAsyncTask(targetFile, url.toString(), new WeakReference<>(this)).execute();
     }

+ 1 - 1
src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java

@@ -764,7 +764,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
     public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
                                            @NonNull int[] grantResults) {
         switch (requestCode) {
-            case PermissionUtil.PERMISSIONS_WRITE_EXTERNAL_STORAGE: {
+            case PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE: {
                 // If request is cancelled, result arrays are empty.
                 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                     // permission was granted

+ 1 - 1
src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java

@@ -162,7 +162,7 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
                     }
                 }
 
-                fullTempPath = FileStorageUtils.getTemporalPath(user.getAccountName()) + currentRemotePath;
+                fullTempPath = FileStorageUtils.getInternalTemporalPath(user.getAccountName(), mAppContext) + currentRemotePath;
                 inputStream = leakedContentResolver.openInputStream(currentUri);
                 File cacheFile = new File(fullTempPath);
                 File tempDir = cacheFile.getParentFile();

+ 2 - 0
src/main/java/com/owncloud/android/utils/FileStorageUtils.java

@@ -96,7 +96,9 @@ public final class FileStorageUtils {
 
     /**
      * Get absolute path to tmp folder inside datafolder in sd-card for given accountName.
+     * @deprecated use {@link #getInternalTemporalPath(String, Context)}, as this one is broken on sdk >= 30
      */
+    @Deprecated
     public static String getTemporalPath(String accountName) {
         return MainApp.getStoragePath()
                 + File.separator

+ 0 - 73
src/main/java/com/owncloud/android/utils/PermissionUtil.java

@@ -1,73 +0,0 @@
-package com.owncloud.android.utils;
-
-import android.Manifest;
-import android.app.Activity;
-import android.content.Context;
-
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-
-/**
- * Created by scherzia on 29.12.2015.
- */
-public final class PermissionUtil {
-    public static final int PERMISSIONS_WRITE_EXTERNAL_STORAGE = 1;
-    public static final int PERMISSIONS_READ_CONTACTS_AUTOMATIC = 2;
-    public static final int PERMISSIONS_WRITE_CONTACTS = 4;
-    public static final int PERMISSIONS_CAMERA = 5;
-    public static final int PERMISSIONS_READ_CALENDAR_AUTOMATIC = 6;
-    public static final int PERMISSIONS_WRITE_CALENDAR = 7;
-
-    private PermissionUtil() {
-        // utility class -> private constructor
-    }
-
-    /**
-     * Wrapper method for ContextCompat.checkSelfPermission().
-     * Determine whether <em>the app</em> has been granted a particular permission.
-     *
-     * @param permission The name of the permission being checked.
-     * @return <code>true</code> if app has the permission, or <code>false</code> if not.
-     */
-    public static boolean checkSelfPermission(Context context, String permission) {
-        return ContextCompat.checkSelfPermission(context, permission)
-                == android.content.pm.PackageManager.PERMISSION_GRANTED;
-    }
-
-    /**
-     * Wrapper method for ActivityCompat.shouldShowRequestPermissionRationale().
-     * Gets whether you should show UI with rationale for requesting a permission.
-     * You should do this only if you do not have the permission and the context in
-     * which the permission is requested does not clearly communicate to the user
-     * what would be the benefit from granting this permission.
-     *
-     * @param activity   The target activity.
-     * @param permission A permission to be requested.
-     * @return Whether to show permission rationale UI.
-     */
-    public static boolean shouldShowRequestPermissionRationale(Activity activity, String permission) {
-        return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
-    }
-
-    /**
-     * request the write permission for external storage.
-     *
-     * @param activity The target activity.
-     */
-    public static void requestWriteExternalStoreagePermission(Activity activity) {
-        ActivityCompat.requestPermissions(activity,
-                new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
-                PERMISSIONS_WRITE_EXTERNAL_STORAGE);
-    }
-
-    /**
-     * request camera permission.
-     *
-     * @param activity The target activity.
-     */
-    public static void requestCameraPermission(Activity activity) {
-        ActivityCompat.requestPermissions(activity,
-                                          new String[]{Manifest.permission.CAMERA},
-                                          PERMISSIONS_CAMERA);
-    }
-}

+ 116 - 0
src/main/java/com/owncloud/android/utils/PermissionUtil.kt

@@ -0,0 +1,116 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Álvaro Brey Vilas
+ * Copyright (C) 2021 Álvaro Brey Vilas
+ * Copyright (C) 2021 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.utils
+
+import android.Manifest
+import android.app.Activity
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+
+/**
+ * Created by scherzia on 29.12.2015.
+ */
+object PermissionUtil {
+    const val PERMISSIONS_EXTERNAL_STORAGE = 1
+    const val PERMISSIONS_READ_CONTACTS_AUTOMATIC = 2
+    const val PERMISSIONS_WRITE_CONTACTS = 4
+    const val PERMISSIONS_CAMERA = 5
+    const val PERMISSIONS_READ_CALENDAR_AUTOMATIC = 6
+    const val PERMISSIONS_WRITE_CALENDAR = 7
+
+    /**
+     * Wrapper method for ContextCompat.checkSelfPermission().
+     * Determine whether *the app* has been granted a particular permission.
+     *
+     * @param permission The name of the permission being checked.
+     * @return `true` if app has the permission, or `false` if not.
+     */
+    @JvmStatic
+    fun checkSelfPermission(context: Context, permission: String): Boolean =
+        ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
+
+    /**
+     * Wrapper method for ActivityCompat.shouldShowRequestPermissionRationale().
+     * Gets whether you should show UI with rationale for requesting a permission.
+     * You should do this only if you do not have the permission and the context in
+     * which the permission is requested does not clearly communicate to the user
+     * what would be the benefit from granting this permission.
+     *
+     * @param activity   The target activity.
+     * @param permission A permission to be requested.
+     * @return Whether to show permission rationale UI.
+     */
+    @JvmStatic
+    fun shouldShowRequestPermissionRationale(activity: Activity, permission: String): Boolean =
+        ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
+
+    /**
+     * For SDK < 30, we can do whatever we want using WRITE_EXTERNAL_STORAGE.
+     * For SDK above 30, scoped storage is in effect, and WRITE_EXTERNAL_STORAGE is useless. However, we do still need
+     * READ_EXTERNAL_STORAGE to read and upload files from folders that we don't manage and are not public access.
+     *
+     * @return The relevant external storage permission, depending on SDK
+     */
+    @JvmStatic
+    fun getExternalStoragePermission(): String = when {
+        Build.VERSION.SDK_INT < Build.VERSION_CODES.R -> Manifest.permission.WRITE_EXTERNAL_STORAGE
+        else -> Manifest.permission.READ_EXTERNAL_STORAGE
+    }
+
+    /**
+     * Determine whether *the app* has been granted external storage permissions depending on SDK.
+     *
+     * @return `true` if app has the permission, or `false` if not.
+     */
+    @JvmStatic
+    fun checkExternalStoragePermission(context: Context): Boolean =
+        ContextCompat.checkSelfPermission(context, getExternalStoragePermission()) == PackageManager.PERMISSION_GRANTED
+
+    /**
+     * Request relevant external storage permission depending on SDK.
+     *
+     * @param activity The target activity.
+     */
+    @JvmStatic
+    fun requestExternalStoragePermission(activity: Activity) {
+        ActivityCompat.requestPermissions(
+            activity, arrayOf(getExternalStoragePermission()),
+            PERMISSIONS_EXTERNAL_STORAGE
+        )
+    }
+
+    /**
+     * request camera permission.
+     *
+     * @param activity The target activity.
+     */
+    @JvmStatic
+    fun requestCameraPermission(activity: Activity) {
+        ActivityCompat.requestPermissions(
+            activity, arrayOf(Manifest.permission.CAMERA),
+            PERMISSIONS_CAMERA
+        )
+    }
+}