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).