Skip to content
This repository was archived by the owner on Nov 1, 2022. It is now read-only.

Commit e0481d5

Browse files
czluciusAmejia481
authored andcommitted
Close #9830 Do no require write storage permission to downloading files on devices with Android 10 and above
1 parent fe68b0b commit e0481d5

5 files changed

Lines changed: 86 additions & 11 deletions

File tree

components/feature/downloads/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
package="mozilla.components.feature.downloads">
99

1010
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
11-
tools:ignore="ScopedStorage" />
11+
tools:ignore="ScopedStorage"
12+
android:maxSdkVersion="28"/>
1213
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
1314
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
1415

components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/AndroidDownloadManager.kt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ import android.content.BroadcastReceiver
1313
import android.content.Context
1414
import android.content.Intent
1515
import android.content.IntentFilter
16+
import android.os.Build
17+
import android.os.Build.VERSION.SDK_INT
1618
import android.util.LongSparseArray
17-
import androidx.annotation.RequiresPermission
19+
import androidx.annotation.VisibleForTesting
1820
import androidx.core.content.getSystemService
1921
import androidx.core.net.toUri
2022
import androidx.core.util.set
@@ -46,15 +48,23 @@ class AndroidDownloadManager(
4648
private val downloadRequests = LongSparseArray<SystemRequest>()
4749
private var isSubscribedReceiver = false
4850

49-
override val permissions = arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)
51+
// Do not require WRITE_EXTERNAL_STORAGE permission on API 29 and above (using scoped storage)
52+
override val permissions
53+
get() = if (getSDKVersion() >= Build.VERSION_CODES.Q) {
54+
arrayOf(INTERNET)
55+
} else {
56+
arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)
57+
}
58+
59+
@VisibleForTesting
60+
internal fun getSDKVersion() = SDK_INT
5061

5162
/**
5263
* Schedules a download through the [AndroidDownloadManager].
5364
* @param download metadata related to the download.
5465
* @param cookie any additional cookie to add as part of the download request.
5566
* @return the id reference of the scheduled download.
5667
*/
57-
@RequiresPermission(allOf = [INTERNET, WRITE_EXTERNAL_STORAGE])
5868
override fun download(download: DownloadState, cookie: String): String? {
5969
val androidDownloadManager: SystemDownloadManager = applicationContext.getSystemService()!!
6070

@@ -106,8 +116,9 @@ class AndroidDownloadManager(
106116
override fun onReceive(context: Context, intent: Intent) {
107117
val downloadID = intent.getStringExtra(EXTRA_DOWNLOAD_ID) ?: ""
108118
val download = store.state.downloads[downloadID]
109-
val downloadStatus = intent.getSerializableExtra(AbstractFetchDownloadService.EXTRA_DOWNLOAD_STATUS)
110-
as Status
119+
val downloadStatus =
120+
intent.getSerializableExtra(AbstractFetchDownloadService.EXTRA_DOWNLOAD_STATUS)
121+
as Status
111122

112123
if (download != null) {
113124
onDownloadStopped(download, downloadID, downloadStatus)

components/feature/downloads/src/main/java/mozilla/components/feature/downloads/manager/FetchDownloadManager.kt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ package mozilla.components.feature.downloads.manager
77
import android.Manifest.permission.FOREGROUND_SERVICE
88
import android.Manifest.permission.INTERNET
99
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
10+
import android.annotation.SuppressLint
1011
import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
1112
import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
1213
import android.content.BroadcastReceiver
1314
import android.content.Context
1415
import android.content.Intent
1516
import android.content.IntentFilter
17+
import android.os.Build
1618
import android.os.Build.VERSION.SDK_INT
1719
import android.os.Build.VERSION_CODES.P
20+
import androidx.annotation.VisibleForTesting
1821
import androidx.localbroadcastmanager.content.LocalBroadcastManager
1922
import mozilla.components.browser.state.action.DownloadAction
2023
import mozilla.components.browser.state.state.content.DownloadState
@@ -41,11 +44,19 @@ class FetchDownloadManager<T : AbstractFetchDownloadService>(
4144

4245
private var isSubscribedReceiver = false
4346

44-
override val permissions = if (SDK_INT >= P) {
45-
arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE, FOREGROUND_SERVICE)
46-
} else {
47-
arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)
48-
}
47+
// Do not require WRITE_EXTERNAL_STORAGE permission on API 29 and above (using scoped storage)
48+
override val permissions
49+
@SuppressLint("InlinedApi")
50+
get() = if (getSDKVersion() >= Build.VERSION_CODES.Q) {
51+
arrayOf(INTERNET, FOREGROUND_SERVICE)
52+
} else if (getSDKVersion() >= P) {
53+
arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE, FOREGROUND_SERVICE)
54+
} else {
55+
arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE)
56+
}
57+
58+
@VisibleForTesting
59+
internal fun getSDKVersion() = SDK_INT
4960

5061
/**
5162
* Schedules a download through the [AbstractFetchDownloadService].

components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/AndroidDownloadManagerTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
99
import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
1010
import android.app.DownloadManager.Request
1111
import android.content.Intent
12+
import android.os.Build
1213
import androidx.test.ext.junit.runners.AndroidJUnit4
1314
import mozilla.components.browser.state.state.content.DownloadState
1415
import mozilla.components.browser.state.store.BrowserStore
@@ -24,6 +25,8 @@ import org.junit.Before
2425
import org.junit.Test
2526
import org.junit.runner.RunWith
2627
import org.mockito.ArgumentMatchers.anyString
28+
import org.mockito.Mockito.doReturn
29+
import org.mockito.Mockito.spy
2730
import org.mockito.Mockito.verify
2831
import org.mockito.Mockito.verifyNoInteractions
2932

@@ -94,6 +97,28 @@ class AndroidDownloadManagerTest {
9497
assertNull(id)
9598
}
9699

100+
@Test
101+
fun `GIVEN a device that supports scoped storage THEN permissions must not included file access`() {
102+
val downloadManager = spy(AndroidDownloadManager(testContext, store))
103+
104+
doReturn(Build.VERSION_CODES.Q).`when`(downloadManager).getSDKVersion()
105+
println(downloadManager.permissions.joinToString { it })
106+
assertTrue(WRITE_EXTERNAL_STORAGE !in downloadManager.permissions)
107+
}
108+
109+
@Test
110+
fun `GIVEN a device does not supports scoped storage THEN permissions must be included file access`() {
111+
val downloadManager = spy(AndroidDownloadManager(testContext, store))
112+
113+
doReturn(Build.VERSION_CODES.P).`when`(downloadManager).getSDKVersion()
114+
115+
assertTrue(WRITE_EXTERNAL_STORAGE in downloadManager.permissions)
116+
117+
doReturn(Build.VERSION_CODES.O_MR1).`when`(downloadManager).getSDKVersion()
118+
119+
assertTrue(WRITE_EXTERNAL_STORAGE in downloadManager.permissions)
120+
}
121+
97122
@Test
98123
fun `sendBroadcast with valid downloadID must call onDownloadStopped after download`() {
99124
var downloadCompleted = false

components/feature/downloads/src/test/java/mozilla/components/feature/downloads/manager/FetchDownloadManagerTest.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
1111
import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
1212
import android.content.Context
1313
import android.content.Intent
14+
import android.os.Build
1415
import androidx.localbroadcastmanager.content.LocalBroadcastManager
1516
import androidx.test.ext.junit.runners.AndroidJUnit4
1617
import mozilla.components.browser.state.state.content.DownloadState
@@ -30,7 +31,9 @@ import org.junit.Assert.assertTrue
3031
import org.junit.Before
3132
import org.junit.Test
3233
import org.junit.runner.RunWith
34+
import org.mockito.Mockito.doReturn
3335
import org.mockito.Mockito.never
36+
import org.mockito.Mockito.spy
3437
import org.mockito.Mockito.verify
3538

3639
@RunWith(AndroidJUnit4::class)
@@ -101,6 +104,30 @@ class FetchDownloadManagerTest {
101104
assertTrue(downloadStopped)
102105
}
103106

107+
@Test
108+
fun `GIVEN a device that supports scoped storage THEN permissions must not included file access`() {
109+
downloadManager =
110+
spy(FetchDownloadManager(mock(), store, MockDownloadService::class, broadcastManager))
111+
112+
doReturn(Build.VERSION_CODES.Q).`when`(downloadManager).getSDKVersion()
113+
114+
assertTrue(WRITE_EXTERNAL_STORAGE !in downloadManager.permissions)
115+
}
116+
117+
@Test
118+
fun `GIVEN a device does not supports scoped storage THEN permissions must be included file access`() {
119+
downloadManager =
120+
spy(FetchDownloadManager(mock(), store, MockDownloadService::class, broadcastManager))
121+
122+
doReturn(Build.VERSION_CODES.P).`when`(downloadManager).getSDKVersion()
123+
124+
assertTrue(WRITE_EXTERNAL_STORAGE in downloadManager.permissions)
125+
126+
doReturn(Build.VERSION_CODES.O_MR1).`when`(downloadManager).getSDKVersion()
127+
128+
assertTrue(WRITE_EXTERNAL_STORAGE in downloadManager.permissions)
129+
}
130+
104131
@Test
105132
fun `try again should not crash when download does not exist`() {
106133
val context: Context = mock()

0 commit comments

Comments
 (0)