diff --git a/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt b/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
index cfa8404da59..6572fdc6d1f 100644
--- a/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
+++ b/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt
@@ -33,6 +33,7 @@ import mozilla.components.feature.downloads.manager.onDownloadStopped
import mozilla.components.feature.downloads.ui.DownloaderApp
import mozilla.components.feature.downloads.ui.DownloadAppChooserDialog
import mozilla.components.lib.state.ext.flowScoped
+import mozilla.components.support.base.dialog.DeniedPermissionDialogFragment
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.feature.OnNeedToRequestPermissions
import mozilla.components.support.base.feature.PermissionsFeature
@@ -213,6 +214,7 @@ class DownloadsFeature(
} else {
closeDownloadResponse(tab.id)
useCases.consumeDownload(tab.id, download.id)
+ showPermissionDeniedDialog()
}
}
}
@@ -394,6 +396,16 @@ class DownloadsFeature(
val positiveButtonTextColor: Int? = null,
val positiveButtonRadius: Float? = null
)
+
+ @VisibleForTesting
+ internal fun showPermissionDeniedDialog() {
+ fragmentManager?.let {
+ val dialog = DeniedPermissionDialogFragment.newInstance(
+ R.string.mozac_feature_downloads_write_external_storage_permissions_needed_message
+ )
+ dialog.showNow(fragmentManager, DeniedPermissionDialogFragment.FRAGMENT_TAG)
+ }
+ }
}
@VisibleForTesting
diff --git a/components/feature/downloads/src/main/res/values/strings.xml b/components/feature/downloads/src/main/res/values/strings.xml
index f3488716515..7d99c7852c5 100644
--- a/components/feature/downloads/src/main/res/values/strings.xml
+++ b/components/feature/downloads/src/main/res/values/strings.xml
@@ -45,4 +45,6 @@
Complete action using
-->
Unable to open %1$s
+
+ Storage access needed to download files. Go to Android settings, tap permissions, and tap allow.
diff --git a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
index 0a99c8216a2..7c54570b70c 100644
--- a/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
+++ b/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt
@@ -403,6 +403,7 @@ class DownloadsFeatureTest {
verify(downloadManager, never()).download(any(), anyString())
verify(feature).closeDownloadResponse("test-tab")
+ verify(feature).showPermissionDeniedDialog()
}
@Test
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AlertDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AlertDialogFragmentTest.kt
index 59733452fbf..95b3fedbd13 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AlertDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AlertDialogFragmentTest.kt
@@ -14,7 +14,7 @@ import mozilla.components.feature.prompts.PromptFeature
import mozilla.components.feature.prompts.R
import mozilla.components.feature.prompts.R.id
import mozilla.components.support.test.mock
-import mozilla.ext.appCompatContext
+import mozilla.components.support.test.ext.appCompatContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragmentTest.kt
index 2e8ad4ff3e6..6de941a1bbf 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/AuthenticationDialogFragmentTest.kt
@@ -11,7 +11,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.feature.prompts.R.id
import mozilla.components.support.test.mock
-import mozilla.ext.appCompatContext
+import mozilla.components.support.test.ext.appCompatContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt
index d6fb40ca434..c0e1dbbfdaa 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ChoiceDialogFragmentTest.kt
@@ -29,8 +29,8 @@ import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.
import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.MULTIPLE_CHOICE_DIALOG_TYPE
import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.SINGLE_CHOICE_DIALOG_TYPE
import mozilla.components.feature.prompts.dialog.ChoiceDialogFragment.Companion.newInstance
+import mozilla.components.support.test.ext.appCompatContext
import mozilla.components.support.test.robolectric.testContext
-import mozilla.ext.appCompatContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragmentTest.kt
index 616b4dcfcfe..b281dd4429a 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ColorPickerDialogFragmentTest.kt
@@ -12,8 +12,8 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.feature.prompts.R
import mozilla.components.support.test.mock
+import mozilla.components.support.test.ext.appCompatContext
import mozilla.components.support.test.robolectric.testContext
-import mozilla.ext.appCompatContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragmentTest.kt
index 72d86e7d1fe..9bbc35907e4 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/ConfirmDialogFragmentTest.kt
@@ -8,7 +8,7 @@ import android.content.DialogInterface
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.test.ext.junit.runners.AndroidJUnit4
-import mozilla.ext.appCompatContext
+import mozilla.components.support.test.ext.appCompatContext
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Before
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragmentTest.kt
index ba9b0659900..ed65eb747f2 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/MultiButtonDialogFragmentTest.kt
@@ -14,7 +14,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.feature.prompts.R
import mozilla.components.feature.prompts.R.id
import mozilla.components.support.test.mock
-import mozilla.ext.appCompatContext
+import mozilla.components.support.test.ext.appCompatContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragmentTest.kt
index bf0550bc6b3..d28c2f807b4 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/SaveLoginDialogFragmentTest.kt
@@ -14,8 +14,8 @@ import junit.framework.TestCase
import mozilla.components.concept.storage.Login
import mozilla.components.feature.prompts.R
import mozilla.components.support.test.any
+import mozilla.components.support.test.ext.appCompatContext
import mozilla.components.support.test.mock
-import mozilla.ext.appCompatContext
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragmentTest.kt
index 6d8f5763346..10cdcefeab8 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TextPromptDialogFragmentTest.kt
@@ -12,7 +12,7 @@ import androidx.core.view.isVisible
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.feature.prompts.R.id
import mozilla.components.support.test.mock
-import mozilla.ext.appCompatContext
+import mozilla.components.support.test.ext.appCompatContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt
index a474226e5ba..dd73e63a041 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt
@@ -24,9 +24,9 @@ import mozilla.components.feature.prompts.ext.year
import mozilla.components.support.ktx.kotlin.toDate
import mozilla.components.support.test.any
import mozilla.components.support.test.eq
+import mozilla.components.support.test.ext.appCompatContext
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
-import mozilla.ext.appCompatContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginSelectBarTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginSelectBarTest.kt
index 643023fda19..68fc5b1bb80 100644
--- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginSelectBarTest.kt
+++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/login/LoginSelectBarTest.kt
@@ -12,9 +12,9 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.ext.junit.runners.AndroidJUnit4
import mozilla.components.concept.storage.Login
import mozilla.components.feature.prompts.R
+import mozilla.components.support.test.ext.appCompatContext
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
-import mozilla.ext.appCompatContext
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
diff --git a/components/support/base/src/main/java/mozilla/components/support/base/dialog/DeniedPermissionDialogFragment.kt b/components/support/base/src/main/java/mozilla/components/support/base/dialog/DeniedPermissionDialogFragment.kt
new file mode 100644
index 00000000000..6d735bff900
--- /dev/null
+++ b/components/support/base/src/main/java/mozilla/components/support/base/dialog/DeniedPermissionDialogFragment.kt
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.base.dialog
+
+import android.app.Dialog
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.provider.Settings
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.app.AlertDialog
+import androidx.fragment.app.DialogFragment
+import mozilla.components.support.base.R
+
+internal const val KEY_MESSAGE = "KEY_MESSAGE"
+
+/**
+ * An dialog to display when Android permission ise denied and
+ * you want give users a way activate it on the app settings.
+ * The dialog will have two buttons one "Go to settings" and another for "Dismissing".
+ */
+class DeniedPermissionDialogFragment : DialogFragment() {
+ internal val message: Int by lazy { safeArguments.getInt(KEY_MESSAGE) }
+ val safeArguments get() = requireNotNull(arguments)
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val builder = AlertDialog.Builder(requireContext())
+ .setMessage(message)
+ .setCancelable(true)
+ .setNegativeButton(R.string.mozac_support_base_permissions_needed_negative_button) { _, _ ->
+ dismiss()
+ }
+ .setPositiveButton(R.string.mozac_support_base_permissions_needed_positive_button) { _, _ ->
+ openSettingsPage()
+ }
+ return builder.create()
+ }
+
+ @VisibleForTesting
+ internal fun openSettingsPage() {
+ dismiss()
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ val uri = Uri.fromParts("package", requireContext().packageName, null)
+ intent.data = uri
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ requireContext().startActivity(intent)
+ }
+
+ companion object {
+ /**
+ * A builder method for creating a [DeniedPermissionDialogFragment]
+ * @param message the message of the dialog.
+ **/
+ fun newInstance(
+ @StringRes message: Int
+ ): DeniedPermissionDialogFragment {
+
+ val fragment = DeniedPermissionDialogFragment()
+ val arguments = fragment.arguments ?: Bundle()
+
+ with(arguments) {
+ putInt(KEY_MESSAGE, message)
+ }
+
+ fragment.arguments = arguments
+ return fragment
+ }
+
+ const val FRAGMENT_TAG = "DENIED_DOWNLOAD_PERMISSION_PROMPT_DIALOG"
+ }
+}
diff --git a/components/support/base/src/main/res/values/strings.xml b/components/support/base/src/main/res/values/strings.xml
new file mode 100644
index 00000000000..128c86862cd
--- /dev/null
+++ b/components/support/base/src/main/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ Go to settings
+
+ Dismiss
+
diff --git a/components/support/base/src/test/java/mozilla/components/support/base/dialog/DeniedPermissionDialogFragmentTest.kt b/components/support/base/src/test/java/mozilla/components/support/base/dialog/DeniedPermissionDialogFragmentTest.kt
new file mode 100644
index 00000000000..61c569a4b71
--- /dev/null
+++ b/components/support/base/src/test/java/mozilla/components/support/base/dialog/DeniedPermissionDialogFragmentTest.kt
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package mozilla.components.support.base.dialog
+
+import android.content.DialogInterface.BUTTON_POSITIVE
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.support.base.R
+import mozilla.components.support.test.ext.appCompatContext
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class DeniedPermissionDialogFragmentTest {
+
+ @Test
+ fun `WHEN showing the dialog THEN it has the provided message`() {
+ val messageId = R.string.mozac_support_base_permissions_needed_negative_button
+ val fragment = spy(
+ DeniedPermissionDialogFragment.newInstance(messageId)
+ )
+
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+
+ dialog.show()
+
+ val messageTextView = dialog.findViewById(android.R.id.message)
+
+ assertEquals(fragment.message, messageId)
+ assertEquals(messageTextView.text.toString(), testContext.getString(messageId))
+ }
+
+ @Test
+ fun `WHEN clicking the positive button THEN the settings page will show`() {
+ val messageId = R.string.mozac_support_base_permissions_needed_negative_button
+
+ val fragment = spy(
+ DeniedPermissionDialogFragment.newInstance(messageId)
+ )
+
+ doNothing().`when`(fragment).dismiss()
+ doReturn(appCompatContext).`when`(fragment).requireContext()
+
+ val dialog = fragment.onCreateDialog(null)
+ dialog.show()
+
+ val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE)
+ positiveButton.performClick()
+
+ verify(fragment).openSettingsPage()
+ }
+}
diff --git a/components/feature/prompts/src/test/java/mozilla/ext/context.kt b/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt
similarity index 80%
rename from components/feature/prompts/src/test/java/mozilla/ext/context.kt
rename to components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt
index 64575ce0954..94e070eda71 100644
--- a/components/feature/prompts/src/test/java/mozilla/ext/context.kt
+++ b/components/support/test/src/main/java/mozilla/components/support/test/ext/Context.kt
@@ -2,9 +2,10 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-package mozilla.ext
+package mozilla.components.support.test.ext
import android.content.Context
+import androidx.annotation.VisibleForTesting
import androidx.appcompat.R
import androidx.appcompat.view.ContextThemeWrapper
import mozilla.components.support.test.robolectric.testContext
@@ -14,5 +15,5 @@ import mozilla.components.support.test.robolectric.testContext
*
* Useful for views that uses theme attributes, for example.
*/
-internal val appCompatContext: Context
+@VisibleForTesting val appCompatContext: Context
get() = ContextThemeWrapper(testContext, R.style.Theme_AppCompat)
diff --git a/docs/changelog.md b/docs/changelog.md
index 002d2da9671..f76826aa1a7 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -18,6 +18,7 @@ permalink: /changelog/
* **feature-downloads**:
* 🚒 Bug fixed [issue #9757](/~https://github.com/mozilla-mobile/android-components/issues/9757) - Remove downloads notification when private tabs are closed.
* 🚒 Bug fixed [issue #9789](/~https://github.com/mozilla-mobile/android-components/issues/9789) - Canceled first PDF download prevents following attempts from downloading.
+ * 🚒 Bug fixed [issue #9823](/~https://github.com/mozilla-mobile/android-components/issues/9823) - Downloads prompts do not show again when a user denies system permission twice.
* **concept-engine**,**browser-engine-gecko**, **browser-engine-gecko-beta**, **browser-engine-gecko-nightly**, **browser-engine-system**
* ⚠️ **This is a breaking change**: `EngineSession`.`enableTrackingProtection()` and `EngineSession`.`disableTrackingProtection()` have been removed, please use `EngineSession`.`updateTrackingProtection()` instead , for more details see [issue #9787](/~https://github.com/mozilla-mobile/android-components/issues/9787).