From f1f2be655ff8e83d3d47a643ce3478e5082f89c1 Mon Sep 17 00:00:00 2001 From: Akshita Tiwary Date: Sun, 25 Jan 2026 21:10:21 +0530 Subject: [PATCH 1/3] refactor: extract permissions list for different launch environments --- .../src/main/java/com/ichi2/anki/InitialActivity.kt | 6 +++--- AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt index 3cc29da927fd..3ef9168a423c 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt @@ -209,12 +209,12 @@ enum class PermissionSet( val permissions: List, val permissionsFragment: Class?, ) : Parcelable { - LEGACY_ACCESS(Permissions.legacyStorageAccessPermissions, PermissionsUntil29Fragment::class.java), + LEGACY_ACCESS(Permissions.legacyStorageAccessStartupPermissions, PermissionsUntil29Fragment::class.java), @RequiresApi(Build.VERSION_CODES.R) - EXTERNAL_MANAGER(listOf(Permissions.MANAGE_EXTERNAL_STORAGE), PermissionsStartingAt30Fragment::class.java), + EXTERNAL_MANAGER(Permissions.externalManagerStorageAccessStartupPermissions, PermissionsStartingAt30Fragment::class.java), - APP_PRIVATE(emptyList(), null), + APP_PRIVATE(Permissions.appPrivateStartupPermissions, null), /** Optional. */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt b/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt index b6d6c166114f..6840e6a87580 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt @@ -169,12 +169,20 @@ object Permissions { @RequiresApi(Build.VERSION_CODES.TIRAMISU) val tiramisuAudioPermission = Manifest.permission.READ_MEDIA_AUDIO - val legacyStorageAccessPermissions = + val legacyStorageAccessStartupPermissions = listOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, ) + @RequiresApi(Build.VERSION_CODES.R) + val externalManagerStorageAccessStartupPermissions = + listOf( + Manifest.permission.MANAGE_EXTERNAL_STORAGE, + ) + + val appPrivateStartupPermissions = listOf() + const val RECORD_AUDIO_PERMISSION = Manifest.permission.RECORD_AUDIO fun canRecordAudio(context: Context): Boolean = hasPermission(context, RECORD_AUDIO_PERMISSION) From 39d80d0f39caa72d11e1a6a17ef920e7ecba919e Mon Sep 17 00:00:00 2001 From: Akshita Tiwary Date: Sun, 25 Jan 2026 21:12:49 +0530 Subject: [PATCH 2/3] feat: make internet permission mandatory across different app launch modes --- .../java/com/ichi2/anki/InitialActivity.kt | 3 +- .../java/com/ichi2/anki/settings/Prefs.kt | 2 + .../permissions/InternetPermissionFragment.kt | 33 ++++++++++++++++ .../permissions/PermissionsFragment.kt | 39 +++++++++++++++++++ .../PermissionsStartingAt30Fragment.kt | 2 + .../permissions/PermissionsUntil29Fragment.kt | 1 + .../main/java/com/ichi2/utils/Permissions.kt | 7 +++- .../fragment_permissions_starting_at_30.xml | 9 +++++ .../layout/fragment_permissions_until_29.xml | 8 ++++ .../layout/internet_permission_fragment.xml | 19 +++++++++ AnkiDroid/src/main/res/values/01-core.xml | 5 +++ AnkiDroid/src/main/res/values/constants.xml | 1 + AnkiDroid/src/main/res/values/preferences.xml | 2 + 13 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/InternetPermissionFragment.kt create mode 100644 AnkiDroid/src/main/res/layout/internet_permission_fragment.xml diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt index 3ef9168a423c..63ee6dff456e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/InitialActivity.kt @@ -30,6 +30,7 @@ import com.ichi2.anki.exception.StorageAccessException import com.ichi2.anki.servicelayer.PreferenceUpgradeService import com.ichi2.anki.servicelayer.PreferenceUpgradeService.setPreferencesUpToDate import com.ichi2.anki.servicelayer.ScopedStorageService.isLegacyStorage +import com.ichi2.anki.ui.windows.permissions.InternetPermissionFragment import com.ichi2.anki.ui.windows.permissions.NotificationsPermissionFragment import com.ichi2.anki.ui.windows.permissions.PermissionsFragment import com.ichi2.anki.ui.windows.permissions.PermissionsStartingAt30Fragment @@ -214,7 +215,7 @@ enum class PermissionSet( @RequiresApi(Build.VERSION_CODES.R) EXTERNAL_MANAGER(Permissions.externalManagerStorageAccessStartupPermissions, PermissionsStartingAt30Fragment::class.java), - APP_PRIVATE(Permissions.appPrivateStartupPermissions, null), + APP_PRIVATE(Permissions.appPrivateStartupPermissions, InternetPermissionFragment::class.java), /** Optional. */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt index 35927b4d6ec7..fbb4c5137cc4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt @@ -329,6 +329,8 @@ open class PrefsRepository( */ var recordAudioPermissionRequested by booleanPref(R.string.record_audio_permission_requested_key, false) + var internetPermissionRequested by booleanPref(R.string.internet_permission_requested_key, false) + // **************************************** Reviewer **************************************** // val ignoreDisplayCutout by booleanPref(R.string.ignore_display_cutout_key, false) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/InternetPermissionFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/InternetPermissionFragment.kt new file mode 100644 index 000000000000..9ffd78ac2fbd --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/InternetPermissionFragment.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Akshita Tiwary + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.ui.windows.permissions + +import android.os.Bundle +import android.view.View +import androidx.core.view.isVisible +import com.ichi2.anki.R +import com.ichi2.utils.Permissions + +class InternetPermissionFragment : PermissionsFragment(R.layout.internet_permission_fragment) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + view + .findViewById(R.id.internet_permission) + .initializeInternetPermissionItem() + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsFragment.kt index bfe67658a57b..f327f73cebe6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsFragment.kt @@ -15,19 +15,26 @@ */ package com.ichi2.anki.ui.windows.permissions +import android.Manifest import android.content.Intent import android.net.Uri import android.os.Build import android.provider.Settings import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.LayoutRes import androidx.annotation.RequiresApi import androidx.core.os.bundleOf import androidx.core.view.allViews +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import com.ichi2.anki.R +import com.ichi2.anki.settings.Prefs +import com.ichi2.anki.showThemedToast +import com.ichi2.utils.Permissions import com.ichi2.utils.Permissions.openAppSettingsScreen +import com.ichi2.utils.Permissions.requestPermissionThroughDialogOrSettings import com.ichi2.utils.Permissions.showToastAndOpenAppSettingsScreen import timber.log.Timber @@ -48,6 +55,18 @@ abstract class PermissionsFragment( protected fun hasAllPermissions() = permissionsItems.all { it.areGranted } + private val internetLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + // TODO: can it be internetPermissionItem.updateSwitchCheckedStatus() ? + permissionsItems.forEach { it.updateSwitchCheckedStatus() } + Timber.d("Internet permission granted") + } else { + Timber.d("Internet permission denied") + showToastAndOpenAppSettingsScreen(R.string.startup_no_internet_permission) + } + } + override fun onResume() { super.onResume() permissionsItems.forEach { it.updateSwitchCheckedStatus() } @@ -85,6 +104,26 @@ abstract class PermissionsFragment( } } + protected fun PermissionsItem.initializeInternetPermissionItem() { + if (Permissions.hasPermission(requireContext(), Manifest.permission.INTERNET)) { + // If internet permission is already granted (which is the case for most of devices), hide the permission item. + this.isVisible = false + } else { + // On devices such as Xiaomi, which allow user to deny internet permissions, show internet permission item. + setOnPermissionsRequested { areAlreadyGranted -> + if (!areAlreadyGranted) { + Timber.d("Requesting for internet permission") + requestPermissionThroughDialogOrSettings( + requireActivity(), + Manifest.permission.INTERNET, + Prefs::internetPermissionRequested, + internetLauncher, + ) + } + } + } + } + /** * If these permissions are already granted, open the OS settings to allow the user to disable them, as * it is impossible to programmatically revoke a permission. If the permissions have not been granted, diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsStartingAt30Fragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsStartingAt30Fragment.kt index 6d5d1ec8a179..6b0d24b91017 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsStartingAt30Fragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsStartingAt30Fragment.kt @@ -55,5 +55,7 @@ class PermissionsStartingAt30Fragment : PermissionsFragment(R.layout.fragment_pe .apply { allFilesPermission .requestExternalStorageOnClick(accessAllFilesLauncher) + internetPermission + .initializeInternetPermissionItem() }.root } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsUntil29Fragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsUntil29Fragment.kt index 22953d792e26..fb9e7935e5ea 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsUntil29Fragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsUntil29Fragment.kt @@ -54,6 +54,7 @@ class PermissionsUntil29Fragment : PermissionsFragment(R.layout.fragment_permiss ) = FragmentPermissionsUntil29Binding .inflate(inflater, container, false) .apply { + internetPermission.initializeInternetPermissionItem() storagePermission.setOnPermissionsRequested { areAlreadyGranted -> if (areAlreadyGranted) return@setOnPermissionsRequested if (userCanGrantWriteExternalStorage()) { diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt b/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt index 6840e6a87580..fee6c4291777 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt @@ -173,15 +173,20 @@ object Permissions { listOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.INTERNET, ) @RequiresApi(Build.VERSION_CODES.R) val externalManagerStorageAccessStartupPermissions = listOf( Manifest.permission.MANAGE_EXTERNAL_STORAGE, + Manifest.permission.INTERNET, ) - val appPrivateStartupPermissions = listOf() + val appPrivateStartupPermissions = + listOf( + Manifest.permission.INTERNET, + ) const val RECORD_AUDIO_PERMISSION = Manifest.permission.RECORD_AUDIO diff --git a/AnkiDroid/src/main/res/layout/fragment_permissions_starting_at_30.xml b/AnkiDroid/src/main/res/layout/fragment_permissions_starting_at_30.xml index 19657b6d5da7..db8e7c56367e 100644 --- a/AnkiDroid/src/main/res/layout/fragment_permissions_starting_at_30.xml +++ b/AnkiDroid/src/main/res/layout/fragment_permissions_starting_at_30.xml @@ -17,4 +17,13 @@ app:permissionIcon="@drawable/ic_save_white" app:permission="@string/manage_external_storage_permission" /> + + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/layout/fragment_permissions_until_29.xml b/AnkiDroid/src/main/res/layout/fragment_permissions_until_29.xml index 5eb4849465a4..16799adb7934 100644 --- a/AnkiDroid/src/main/res/layout/fragment_permissions_until_29.xml +++ b/AnkiDroid/src/main/res/layout/fragment_permissions_until_29.xml @@ -17,4 +17,12 @@ app:permissionIcon="@drawable/ic_save_white" app:permissions="@array/legacy_storage_permissions" /> + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/layout/internet_permission_fragment.xml b/AnkiDroid/src/main/res/layout/internet_permission_fragment.xml new file mode 100644 index 000000000000..cc1caacc4f71 --- /dev/null +++ b/AnkiDroid/src/main/res/layout/internet_permission_fragment.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/AnkiDroid/src/main/res/values/01-core.xml b/AnkiDroid/src/main/res/values/01-core.xml index 3ddeba5ebf5d..3c21d6857569 100644 --- a/AnkiDroid/src/main/res/values/01-core.xml +++ b/AnkiDroid/src/main/res/values/01-core.xml @@ -214,6 +214,11 @@ Instant card + + + Internet + Required for the app to work + Please grant AnkiDroid the ‘Internet’ permission to continue Add profile diff --git a/AnkiDroid/src/main/res/values/constants.xml b/AnkiDroid/src/main/res/values/constants.xml index ef1734f0e14d..29009fbcad18 100644 --- a/AnkiDroid/src/main/res/values/constants.xml +++ b/AnkiDroid/src/main/res/values/constants.xml @@ -343,6 +343,7 @@ android.permission.MANAGE_EXTERNAL_STORAGE + android.permission.INTERNET android.permission.READ_EXTERNAL_STORAGE android.permission.WRITE_EXTERNAL_STORAGE diff --git a/AnkiDroid/src/main/res/values/preferences.xml b/AnkiDroid/src/main/res/values/preferences.xml index c09559bd79a3..a69fc19ee0f8 100644 --- a/AnkiDroid/src/main/res/values/preferences.xml +++ b/AnkiDroid/src/main/res/values/preferences.xml @@ -263,4 +263,6 @@ reviewerFrameStyle reviewerToolbarPosition + + internetPermissionRequested \ No newline at end of file From 577ca5e1b2f2ded7bb44b96d167eca35166bd607 Mon Sep 17 00:00:00 2001 From: Akshita Tiwary Date: Sun, 25 Jan 2026 21:14:41 +0530 Subject: [PATCH 3/3] test: when INTERNET is denied, PermissionsActivity is shown Co-authored-by: David Allison <62114487+david-allison@users.noreply.github.com> --- .../permissions/InternetPermissionFragment.kt | 19 +++++---- .../permissions/PermissionsFragment.kt | 39 ++++++++++--------- .../main/java/com/ichi2/utils/Permissions.kt | 5 +++ .../fragment_permissions_starting_at_30.xml | 2 +- .../layout/fragment_permissions_until_29.xml | 2 +- .../layout/internet_permission_fragment.xml | 2 +- AnkiDroid/src/main/res/values/01-core.xml | 6 +-- .../com/ichi2/anki/DeckPickerOnDiskTest.kt | 1 + .../java/com/ichi2/anki/DeckPickerTest.kt | 28 +++++++++++++ .../com/ichi2/anki/InitialActivityTest.kt | 2 + .../java/com/ichi2/anki/RobolectricTest.kt | 4 ++ .../com/ichi2/testutils/PermissionUtils.kt | 32 +++++++++++++++ .../java/com/ichi2/utils/PermissionsTest.kt | 2 +- 13 files changed, 112 insertions(+), 32 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/InternetPermissionFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/InternetPermissionFragment.kt index 9ffd78ac2fbd..aef38546fee5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/InternetPermissionFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/InternetPermissionFragment.kt @@ -16,18 +16,23 @@ package com.ichi2.anki.ui.windows.permissions import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import androidx.core.view.isVisible import com.ichi2.anki.R +import com.ichi2.anki.databinding.AboutLayoutBinding +import com.ichi2.anki.databinding.InternetPermissionFragmentBinding import com.ichi2.utils.Permissions +import dev.androidbroadcast.vbpd.viewBinding class InternetPermissionFragment : PermissionsFragment(R.layout.internet_permission_fragment) { - override fun onViewCreated( - view: View, + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle?, - ) { - view - .findViewById(R.id.internet_permission) - .initializeInternetPermissionItem() - } + ) = InternetPermissionFragmentBinding + .inflate(inflater, container, false) + .apply { internetPermission.initializeInternetPermissionItem() } + .root } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsFragment.kt index f327f73cebe6..c55dc4932b65 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/permissions/PermissionsFragment.kt @@ -30,8 +30,8 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import com.ichi2.anki.R +import com.ichi2.anki.common.annotations.NeedsTest import com.ichi2.anki.settings.Prefs -import com.ichi2.anki.showThemedToast import com.ichi2.utils.Permissions import com.ichi2.utils.Permissions.openAppSettingsScreen import com.ichi2.utils.Permissions.requestPermissionThroughDialogOrSettings @@ -58,12 +58,13 @@ abstract class PermissionsFragment( private val internetLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { - // TODO: can it be internetPermissionItem.updateSwitchCheckedStatus() ? - permissionsItems.forEach { it.updateSwitchCheckedStatus() } - Timber.d("Internet permission granted") + // No need to explicitly do anything, onResume handles updating the UI + Timber.i("Internet permission granted") } else { - Timber.d("Internet permission denied") - showToastAndOpenAppSettingsScreen(R.string.startup_no_internet_permission) + Timber.i("Internet permission denied") + showToastAndOpenAppSettingsScreen( + getString(R.string.permission_required_message, getString(R.string.internet_access_title)), + ) } } @@ -104,22 +105,24 @@ abstract class PermissionsFragment( } } + @NeedsTest("Shows the permission item when INTERNET permission is denied") + @NeedsTest("Hides the permission item when INTERNET permission is already granted") protected fun PermissionsItem.initializeInternetPermissionItem() { if (Permissions.hasPermission(requireContext(), Manifest.permission.INTERNET)) { // If internet permission is already granted (which is the case for most of devices), hide the permission item. this.isVisible = false - } else { - // On devices such as Xiaomi, which allow user to deny internet permissions, show internet permission item. - setOnPermissionsRequested { areAlreadyGranted -> - if (!areAlreadyGranted) { - Timber.d("Requesting for internet permission") - requestPermissionThroughDialogOrSettings( - requireActivity(), - Manifest.permission.INTERNET, - Prefs::internetPermissionRequested, - internetLauncher, - ) - } + return + } + // On devices such as Xiaomi, which allow user to deny internet permissions, show internet permission item. + setOnPermissionsRequested { areAlreadyGranted -> + if (!areAlreadyGranted) { + Timber.d("Requesting for internet permission") + requestPermissionThroughDialogOrSettings( + requireActivity(), + Manifest.permission.INTERNET, + Prefs::internetPermissionRequested, + internetLauncher, + ) } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt b/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt index fee6c4291777..862071c57ff4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/Permissions.kt @@ -348,4 +348,9 @@ object Permissions { showThemedToast(requireContext(), message, false) openAppSettingsScreen() } + + fun Fragment.showToastAndOpenAppSettingsScreen(message: String) { + showThemedToast(requireContext(), message, false) + openAppSettingsScreen() + } } diff --git a/AnkiDroid/src/main/res/layout/fragment_permissions_starting_at_30.xml b/AnkiDroid/src/main/res/layout/fragment_permissions_starting_at_30.xml index db8e7c56367e..f310b8e67d0c 100644 --- a/AnkiDroid/src/main/res/layout/fragment_permissions_starting_at_30.xml +++ b/AnkiDroid/src/main/res/layout/fragment_permissions_starting_at_30.xml @@ -23,7 +23,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:permissionTitle="@string/internet_access_title" - app:permissionSummary="@string/internet_access_summary" + app:permissionSummary="@string/permission_access_summary" app:permission="@string/manage_internet_permission" /> \ No newline at end of file diff --git a/AnkiDroid/src/main/res/layout/fragment_permissions_until_29.xml b/AnkiDroid/src/main/res/layout/fragment_permissions_until_29.xml index 16799adb7934..4951a639cf67 100644 --- a/AnkiDroid/src/main/res/layout/fragment_permissions_until_29.xml +++ b/AnkiDroid/src/main/res/layout/fragment_permissions_until_29.xml @@ -22,7 +22,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:permissionTitle="@string/internet_access_title" - app:permissionSummary="@string/internet_access_summary" + app:permissionSummary="@string/permission_access_summary" app:permission="@string/manage_internet_permission" /> \ No newline at end of file diff --git a/AnkiDroid/src/main/res/layout/internet_permission_fragment.xml b/AnkiDroid/src/main/res/layout/internet_permission_fragment.xml index cc1caacc4f71..33e3ff0d66cb 100644 --- a/AnkiDroid/src/main/res/layout/internet_permission_fragment.xml +++ b/AnkiDroid/src/main/res/layout/internet_permission_fragment.xml @@ -13,7 +13,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:permissionTitle="@string/internet_access_title" - app:permissionSummary="@string/internet_access_summary" + app:permissionSummary="@string/permission_access_summary" app:permission="@string/manage_internet_permission" /> diff --git a/AnkiDroid/src/main/res/values/01-core.xml b/AnkiDroid/src/main/res/values/01-core.xml index 3c21d6857569..311d53f4d666 100644 --- a/AnkiDroid/src/main/res/values/01-core.xml +++ b/AnkiDroid/src/main/res/values/01-core.xml @@ -216,9 +216,9 @@ Instant card - Internet - Required for the app to work - Please grant AnkiDroid the ‘Internet’ permission to continue + Internet + Required for the app to work + Please grant AnkiDroid the ‘%s’ permission to continue Add profile diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerOnDiskTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerOnDiskTest.kt index 69fd8b861b7d..09680c4acb94 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerOnDiskTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerOnDiskTest.kt @@ -27,6 +27,7 @@ import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.testutils.DbUtils import com.ichi2.testutils.common.Flaky import com.ichi2.testutils.common.OS +import com.ichi2.testutils.grantPermissions import com.ichi2.utils.ResourceLoader import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt index 26913e2cc94e..493ec548725e 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTest.kt @@ -1,14 +1,17 @@ // noinspection MissingCopyrightHeader #8659 package com.ichi2.anki +import android.Manifest.permission.INTERNET import android.annotation.SuppressLint import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.view.Menu import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.core.content.IntentCompat import androidx.core.content.edit import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.view.children @@ -29,6 +32,8 @@ import com.ichi2.anki.libanki.DeckId import com.ichi2.anki.observability.ChangeManager import com.ichi2.anki.preferences.sharedPrefs import com.ichi2.anki.settings.Prefs +import com.ichi2.anki.ui.windows.permissions.PermissionsActivity +import com.ichi2.anki.ui.windows.permissions.PermissionsActivity.Companion.PERMISSIONS_SET_EXTRA import com.ichi2.anki.utils.Destination import com.ichi2.anki.utils.ext.defaultConfig import com.ichi2.anki.utils.ext.dismissAllDialogFragments @@ -38,8 +43,10 @@ import com.ichi2.testutils.common.Flaky import com.ichi2.testutils.common.OS import com.ichi2.testutils.ext.addBasicNoteWithOp import com.ichi2.testutils.ext.menu +import com.ichi2.testutils.grantPermissions import com.ichi2.testutils.grantWritePermissions import com.ichi2.testutils.revokeWritePermissions +import com.ichi2.testutils.withDeniedPermissions import com.ichi2.testutils.withWritePermissions import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.containsInAnyOrder @@ -64,6 +71,7 @@ import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.Robolectric import org.robolectric.RuntimeEnvironment import org.robolectric.Shadows +import org.robolectric.Shadows.shadowOf import org.robolectric.shadows.ShadowDialog import org.robolectric.shadows.ShadowLooper import timber.log.Timber @@ -674,6 +682,26 @@ class DeckPickerTest : RobolectricTest() { disableNullCollection() } + @Test + fun `when INTERNET is denied, PermissionsActivity is shown`() = + runTest { + withDeniedPermissions(INTERNET) { + deckPicker { + val intent = assertNotNull(shadowOf(this@deckPicker).nextStartedActivity) + + assertThat( + intent.component?.shortClassName, + equalTo(PermissionsActivity::class.java.name), + ) + + val extra = IntentCompat.getParcelableExtra(intent, PERMISSIONS_SET_EXTRA, PermissionSet::class.java) + + assertNotNull(extra) + assertThat(extra.permissions, equalTo(listOf(INTERNET))) + } + } + } + enum class CollectionType( val assetFile: String, private val deckName: String, diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/InitialActivityTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/InitialActivityTest.kt index e7196c07ec73..cd2d8cac3673 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/InitialActivityTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/InitialActivityTest.kt @@ -126,6 +126,7 @@ class InitialActivityTest : RobolectricTest() { arrayOf( android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + android.Manifest.permission.INTERNET, ) // force a safe startup before Q @@ -161,6 +162,7 @@ class InitialActivityTest : RobolectricTest() { arrayOf( android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + android.Manifest.permission.INTERNET, ) selectAnkiDroidFolder( diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/RobolectricTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/RobolectricTest.kt index 00f2ded1e91c..d77c597b1fc7 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/RobolectricTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/RobolectricTest.kt @@ -59,6 +59,7 @@ import com.ichi2.testutils.ProductionCollectionManager import com.ichi2.testutils.common.FailOnUnhandledExceptionRule import com.ichi2.testutils.common.IgnoreFlakyTestsInCIRule import com.ichi2.testutils.filter +import com.ichi2.testutils.grantPermissions import com.ichi2.utils.InMemorySQLiteOpenHelperFactory import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -173,6 +174,9 @@ open class RobolectricTest : // BUG: We do not reset the MetaDB MetaDB.closeDB() + + // https://github.com/ankidroid/Anki-Android/pull/19004#discussion_r2739833965 + grantPermissions(Manifest.permission.INTERNET) } protected open fun useLegacyHelper(): Boolean = false diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/PermissionUtils.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/PermissionUtils.kt index bef88ce36af3..eca417f80db0 100644 --- a/AnkiDroid/src/test/java/com/ichi2/testutils/PermissionUtils.kt +++ b/AnkiDroid/src/test/java/com/ichi2/testutils/PermissionUtils.kt @@ -22,6 +22,8 @@ import android.app.Application import android.content.Context import androidx.test.core.app.ApplicationProvider import org.robolectric.Shadows +import org.robolectric.Shadows.shadowOf +import timber.log.Timber /** Executes [runnable] without [WRITE_EXTERNAL_STORAGE] and [READ_EXTERNAL_STORAGE] */ fun withNoWritePermission(runnable: (() -> Unit)) { @@ -55,3 +57,33 @@ fun revokeWritePermissions() { val app = Shadows.shadowOf(ApplicationProvider.getApplicationContext() as Application) app.denyPermissions(WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE) } + +fun grantPermissions(vararg permissions: String) { + Timber.i("granting permissions: %s", permissions.contentToString()) + shadowOf(ApplicationProvider.getApplicationContext()).grantPermissions(*permissions) +} + +fun denyPermissions(vararg permissions: String) { + Timber.i("denying permissions: %s", permissions.contentToString()) + shadowOf(ApplicationProvider.getApplicationContext()).denyPermissions(*permissions) +} + +fun withGrantedPermissions( + vararg permissions: String, + block: () -> Unit, +) = try { + grantPermissions(*permissions) + block() +} finally { + denyPermissions(*permissions) +} + +suspend fun withDeniedPermissions( + vararg permissions: String, + block: suspend () -> Unit, +) = try { + denyPermissions(*permissions) + block() +} finally { + grantPermissions(*permissions) +} diff --git a/AnkiDroid/src/test/java/com/ichi2/utils/PermissionsTest.kt b/AnkiDroid/src/test/java/com/ichi2/utils/PermissionsTest.kt index 79665c54db09..2da167c8ecc0 100644 --- a/AnkiDroid/src/test/java/com/ichi2/utils/PermissionsTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/utils/PermissionsTest.kt @@ -161,7 +161,7 @@ class PermissionsTest { private fun verifyPermissionWasRequested() { assertThat("requested flag should always be true after requesting", Prefs.notificationsPermissionRequested, equalTo(true)) verify(exactly = 1) { permissionRequestLauncher.launch(DUMMY_PERMISSION_STRING) } - verify(exactly = 0) { fragment.showToastAndOpenAppSettingsScreen(any()) } + verify(exactly = 0) { fragment.showToastAndOpenAppSettingsScreen(any()) } } private fun verifyOSSettingsWasOpened() {