ConfigCameraPermissionTest.kt (6198B)
1 package net.taler.merchantpos.config 2 3 import android.Manifest 4 import android.app.Application 5 import android.content.Context 6 import android.content.pm.PackageManager 7 import android.content.res.Configuration 8 import android.os.Bundle 9 import android.os.Looper 10 import android.provider.Settings 11 import android.widget.FrameLayout 12 13 import androidx.fragment.app.FragmentActivity 14 import androidx.lifecycle.ViewModelProvider 15 import androidx.test.core.app.ApplicationProvider 16 17 import net.taler.merchantpos.R 18 19 import org.junit.After 20 import org.junit.Assert.assertEquals 21 import org.junit.Assert.assertNotNull 22 import org.junit.Assert.assertNotSame 23 import org.junit.Assert.assertNull 24 import org.junit.Assert.assertSame 25 import org.junit.Before 26 import org.junit.Test 27 import org.junit.runner.RunWith 28 import org.robolectric.Robolectric 29 import org.robolectric.RobolectricTestRunner 30 import org.robolectric.Shadows.shadowOf 31 import org.robolectric.android.controller.ActivityController 32 import org.robolectric.annotation.Config 33 import org.robolectric.shadows.ShadowChoreographer 34 import org.robolectric.shadows.ShadowToast 35 36 @RunWith(RobolectricTestRunner::class) 37 @Config(sdk = [28], application = Application::class) 38 class ConfigCameraPermissionTest 39 { 40 private lateinit var controller: ActivityController<CameraTestActivity> 41 private val fragment: ConfigFragment 42 get() = controller.get().supportFragmentManager.findFragmentByTag("config") as ConfigFragment 43 44 @Before 45 fun setUp() 46 { 47 val app = ApplicationProvider.getApplicationContext<Application>() 48 shadowOf(app).grantPermissions("${app.packageName}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION") 49 shadowOf(app).denyPermissions(Manifest.permission.CAMERA) 50 app 51 .getSharedPreferences("config.cameraPermission", Context.MODE_PRIVATE) 52 .edit() 53 .clear() 54 .commit() 55 ShadowChoreographer.setPaused(true) 56 controller = Robolectric.buildActivity(CameraTestActivity::class.java).setup() 57 ViewModelProvider(controller.get())[ConfigDraftViewModel::class.java].apply { 58 initialized = true 59 mode = ConfigMode.Qr 60 merchantUrl = "example.invalid" 61 } 62 controller 63 .get() 64 .supportFragmentManager 65 .beginTransaction() 66 .add(android.R.id.content, ConfigFragment(), "config") 67 .commitNow() 68 idle() 69 } 70 71 @After 72 fun tearDown() 73 { 74 controller.pause().stop().destroy() 75 idle() 76 } 77 78 @Test 79 fun `entering and resuming QR setup do not automatically request permission or toast`() 80 { 81 repeat(3) { 82 controller.pause().resume() 83 idle() 84 } 85 assertNull(shadowOf(controller.get()).lastRequestedPermission) 86 assertEquals(0, ShadowToast.shownToastCount()) 87 } 88 89 @Test 90 fun `only confirmation launches permission and repeated taps cannot launch duplicates`() 91 { 92 confirm() 93 val request = shadowOf(controller.get()).lastRequestedPermission 94 assertNotNull(request) 95 assertEquals(listOf(Manifest.permission.CAMERA), request.requestedPermissions.toList()) 96 97 confirm() 98 assertSame(request, shadowOf(controller.get()).lastRequestedPermission) 99 } 100 101 @Test 102 fun `two denials stop requesting and offer app settings without toasts`() 103 { 104 confirm() 105 deny(canAskAgain = true) 106 val first = shadowOf(controller.get()).lastRequestedPermission 107 controller.pause().resume() 108 idle() 109 assertSame(first, shadowOf(controller.get()).lastRequestedPermission) 110 111 confirm() 112 val second = shadowOf(controller.get()).lastRequestedPermission 113 assertNotSame(first, second) 114 deny(canAskAgain = false) 115 116 repeat(3) { 117 controller.pause().resume() 118 idle() 119 } 120 assertSame(second, shadowOf(controller.get()).lastRequestedPermission) 121 assertEquals(0, ShadowToast.shownToastCount()) 122 123 confirm() 124 val settings = shadowOf(controller.get()).nextStartedActivity 125 assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, settings.action) 126 assertEquals("package:${controller.get().packageName}", settings.dataString) 127 assertSame(second, shadowOf(controller.get()).lastRequestedPermission) 128 } 129 130 @Test 131 fun `blocked permission still opens settings after rotation`() 132 { 133 confirm() 134 deny(canAskAgain = false) 135 controller.configurationChange( 136 Configuration(controller.get().resources.configuration).apply { 137 orientation = Configuration.ORIENTATION_LANDSCAPE 138 }, 139 ) 140 idle() 141 142 confirm() 143 assertEquals( 144 Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 145 shadowOf(controller.get()).nextStartedActivity.action 146 ) 147 assertNull(shadowOf(controller.get()).lastRequestedPermission) 148 assertEquals(0, ShadowToast.shownToastCount()) 149 } 150 151 private fun confirm() 152 { 153 // Exercise the same action wired to the Compose dialog button without a camera backend. 154 ConfigFragment::class.java 155 .getDeclaredMethod("confirmCameraPermission") 156 .apply { 157 isAccessible = true 158 }.invoke(fragment) 159 idle() 160 } 161 162 private fun deny(canAskAgain: Boolean) 163 { 164 shadowOf(controller.get().packageManager) 165 .setShouldShowRequestPermissionRationale(Manifest.permission.CAMERA, canAskAgain) 166 val request = shadowOf(controller.get()).lastRequestedPermission 167 controller.get().onRequestPermissionsResult( 168 request.requestCode, 169 request.requestedPermissions, 170 intArrayOf(PackageManager.PERMISSION_DENIED), 171 ) 172 idle() 173 } 174 175 private fun idle() = shadowOf(Looper.getMainLooper()).idle() 176 177 class CameraTestActivity : FragmentActivity() 178 { 179 override fun onCreate(savedInstanceState: Bundle?) 180 { 181 setTheme(R.style.AppTheme) 182 super.onCreate(savedInstanceState) 183 setContentView(FrameLayout(this)) 184 } 185 } 186 }