Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

permissions flow test #215

Open
wants to merge 64 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
66f00a5
initial WIP for creating a scalable flow for permissions screen
Kimblebee Apr 4, 2024
675048d
wip something something permission
Kimblebee Apr 9, 2024
a6cd4cd
WIP: debugging and cleeanup
Kimblebee Apr 9, 2024
72e4800
more debugging and cleanup
Kimblebee Apr 10, 2024
091c202
refactoring... added audio permission screen
Kimblebee Apr 24, 2024
c423ef6
spotless
Kimblebee Apr 24, 2024
dd997ed
Merge branch 'main' into kim/permissions_flow
Kimblebee Apr 24, 2024
7617ad7
Address PR Comments
Kimblebee Apr 26, 2024
080a965
address comments
Kimblebee Apr 26, 2024
5604b0a
added bug number
Kimblebee Apr 26, 2024
1ed3c8c
spotless apply
Kimblebee Apr 26, 2024
e192d74
add "record audio" to APP_REQUIRED_PERMISSIONS
Kimblebee Apr 26, 2024
47c3bfc
extract amplitude
Kimblebee Apr 29, 2024
d855865
WIP: Very basic display of audio on preview
Kimblebee Apr 30, 2024
1a1ae4c
Merge branch 'main' into kim/permissions_flow
Kimblebee May 1, 2024
f9caf81
address PR comments
Kimblebee May 1, 2024
146bb91
Merge branch 'main' into kim/audio_visualizer
Kimblebee May 1, 2024
20080fb
styling vumeter
Kimblebee May 1, 2024
79d88d9
add mute icon for zero audio input
Kimblebee May 2, 2024
c84deb9
add permissions module
Kimblebee May 3, 2024
cbc30ab
migrate permissions file from app to permissions module
Kimblebee May 3, 2024
21d58ca
WIP use routes and dependency injection for permissions screen
Kimblebee May 3, 2024
e4047f7
change where amplitude gets converted to float
Kimblebee May 6, 2024
ff42e24
spotless apply
Kimblebee May 6, 2024
22c4221
Merge branch 'main' into kim/audio_visualizer
Kimblebee May 6, 2024
5bd904c
use assisted injection
Kimblebee May 6, 2024
2cba02f
WIP auto navigate to permissions screen when camera permission is rev…
Kimblebee May 6, 2024
4e881ce
spotless apply
Kimblebee May 6, 2024
b0e1f7d
Merge branch 'main' into kim/audio_visualizer
Kimblebee May 7, 2024
0945b16
change circle size instead of canvas size for audio animation
Kimblebee May 7, 2024
5e73a86
spotless apply
Kimblebee May 7, 2024
c4af2ef
prevent optional permission page from showing when granted
Kimblebee May 7, 2024
c079974
clear out navigation stack when transitioning into and out of permiss…
Kimblebee May 7, 2024
e39c34f
Merge branch 'main' into kim/audio_visualizer
Kimblebee May 7, 2024
e66f318
Merge branch 'main' into kim/permissions_flow
Kimblebee May 7, 2024
537cbdf
remove duplicate dependency
Kimblebee May 8, 2024
d6c60f1
address PR comments
Kimblebee May 9, 2024
a7e5d3b
change displayed text when required camera permission is explicitly d…
Kimblebee May 9, 2024
885800d
remove the funny files
Kimblebee May 9, 2024
ef04da0
fix audio permission denial bug
Kimblebee May 9, 2024
de7dadc
Merge branch 'kim/audio_visualizer' into kim/permissions_testing
Kimblebee May 13, 2024
9892b21
wip testing permissions screen
Kimblebee May 22, 2024
753f5b1
Merge branch 'main' into kim/tests/permissions
Kimblebee May 30, 2024
414cf91
fix individually crashing tests
Kimblebee May 31, 2024
4e35f66
orchestrate tests
Kimblebee May 31, 2024
89b72cd
Merge branch 'main' into kim/tests/permissions
Kimblebee Jun 3, 2024
4b5be9e
spotless
Kimblebee Jun 3, 2024
a7f7551
address PR Comments
Kimblebee Jun 6, 2024
9db101e
Merge branch 'main' into kim/tests/permissions
Kimblebee Jun 6, 2024
e0ad689
Merge branch 'main' into kim/tests/permissions
Kimblebee Jun 7, 2024
9feff80
address pr comments
Kimblebee Jun 12, 2024
1428ebd
Merge branch 'main' into kim/tests/permissions
Kimblebee Jun 12, 2024
c1ab85a
spotless
Kimblebee Jun 12, 2024
91f2569
Merge branch 'main' into kim/tests/permissions
Kimblebee Jun 13, 2024
65fd874
Merge branch 'main' into kim/tests/permissions
Kimblebee Jun 17, 2024
ef3c5a7
Merge branch 'main' into kim/tests/permissions
Kimblebee Jun 24, 2024
edf721a
spotless
Kimblebee Jun 24, 2024
fd50518
Merge branch 'main' into kim/tests/permissions
Kimblebee Jun 28, 2024
b563a70
Merge branch 'main' into kim/tests/permissions
Kimblebee Jun 29, 2024
d30e415
rename const
Kimblebee Jun 29, 2024
3bf07b3
spotless
Kimblebee Jul 1, 2024
b69176f
Merge branch 'main' into kim/tests/permissions
Kimblebee Jul 31, 2024
8805a4f
Merge branch 'main' into kim/tests/permissions
Kimblebee Aug 20, 2024
7114bb4
Merge branch 'main' into kim/tests/permissions
Kimblebee Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ android {
versionCode = 1
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}


buildTypes {
getByName("debug") {
signingConfig = signingConfigs.getByName("debug")
Expand Down Expand Up @@ -83,8 +85,11 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}

@Suppress("UnstableApiUsage")
testOptions {
execution = "ANDROIDX_TEST_ORCHESTRATOR"

managedDevices {
localDevices {
create("pixel2Api28") {
Expand Down Expand Up @@ -138,6 +143,8 @@ dependencies {
androidTestImplementation(libs.androidx.rules)
androidTestImplementation(libs.androidx.uiautomator)
androidTestImplementation(libs.truth)
androidTestUtil(libs.androidx.orchestrator)


implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
Expand Down Expand Up @@ -166,7 +173,6 @@ dependencies {

// benchmark
implementation(libs.androidx.profileinstaller)

}

// Allow references to generated code
Expand Down
198 changes: 198 additions & 0 deletions app/src/androidTest/java/com/google/jetpackcamera/PermissionsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.jetpackcamera

import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.google.jetpackcamera.feature.preview.ui.CAPTURE_BUTTON
import com.google.jetpackcamera.permissions.ui.CAMERA_PERMISSION_BUTTON
import com.google.jetpackcamera.permissions.ui.RECORD_AUDIO_PERMISSION_BUTTON
import com.google.jetpackcamera.permissions.ui.REQUEST_PERMISSION_BUTTON
import com.google.jetpackcamera.utils.APP_REQUIRED_PERMISSIONS
import com.google.jetpackcamera.utils.APP_START_TIMEOUT_MILLIS
import com.google.jetpackcamera.utils.IndividualTestGrantPermissionRule
import com.google.jetpackcamera.utils.askEveryTimeDialog
import com.google.jetpackcamera.utils.denyPermissionDialog
import com.google.jetpackcamera.utils.grantPermissionDialog
import com.google.jetpackcamera.utils.onNodeWithText
import com.google.jetpackcamera.utils.runScenarioTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

const val CAMERA_PERMISSION = "android.permission.CAMERA"

@RunWith(AndroidJUnit4::class)
class PermissionsTest {
@get:Rule
val composeTestRule = createEmptyComposeRule()

@get:Rule
val allPermissionsRule = IndividualTestGrantPermissionRule(
permissions = APP_REQUIRED_PERMISSIONS.toTypedArray(),
targetTestNames = arrayOf(
"allPermissions_alreadyGranted_screenNotShown"
)
)

@get:Rule
val cameraPermissionRule = IndividualTestGrantPermissionRule(
permissions = arrayOf(CAMERA_PERMISSION),
targetTestNames = arrayOf(
"recordAudioPermission_granted_closesPage",
"recordAudioPermission_denied_closesPage"
)
)

private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val uiDevice = UiDevice.getInstance(instrumentation)

@Test
fun allPermissions_alreadyGranted_screenNotShown() {
runScenarioTest<MainActivity> {
composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).isDisplayed()
}
}
}

@Test
fun cameraPermission_granted_closesPage() = runScenarioTest<MainActivity> {
// Wait for the camera permission screen to be displayed
composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).isDisplayed()
}

// Click button to request permission
composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON)
.assertExists()
.performClick()

uiDevice.waitForIdle()
// grant permission
grantPermissionDialog(uiDevice)

// Assert we're no longer on camera permission screen
composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).assertDoesNotExist()
}

@SdkSuppress(minSdkVersion = 30)
@Test
fun cameraPermission_askEveryTime_closesPage() {
uiDevice.waitForIdle()
runScenarioTest<MainActivity> {
// Wait for the camera permission screen to be displayed
composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).isDisplayed()
}

// Click button to request permission
composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON)
.assertExists()
.performClick()

// set permission to ask every time
askEveryTimeDialog(uiDevice)

// Assert we're no longer on camera permission screen
composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).assertDoesNotExist()
}
}

@Test
fun cameraPermission_declined_staysOnScreen() {
// required permissions should persist on screen
// Wait for the permission screen to be displayed
runScenarioTest<MainActivity> {
composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).isDisplayed()
}

// Click button to request permission
composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON)
.assertExists()
.performClick()

// deny permission
denyPermissionDialog(uiDevice)

uiDevice.waitForIdle()

// Assert we're still on camera permission screen
composeTestRule.onNodeWithTag(CAMERA_PERMISSION_BUTTON).isDisplayed()

// request permissions button should now say to navigate to settings
composeTestRule.onNodeWithText(
com.google.jetpackcamera.permissions
.R.string.navigate_to_settings
).assertExists()
}
}

@Test
fun recordAudioPermission_granted_closesPage() {
// optional permissions should close the screen after declining
runScenarioTest<MainActivity> {
composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(RECORD_AUDIO_PERMISSION_BUTTON).isDisplayed()
}

// Click button to request permission
composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON)
.assertExists()
.performClick()

// deny permission
grantPermissionDialog(uiDevice)
uiDevice.waitForIdle()

// Assert we're now on preview screen
composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().isDisplayed()
}
}
}

@Test
fun recordAudioPermission_denied_closesPage() {
// optional permissions should close the screen after declining
runScenarioTest<MainActivity> {
composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(RECORD_AUDIO_PERMISSION_BUTTON).isDisplayed()
}

// Click button to request permission
composeTestRule.onNodeWithTag(REQUEST_PERMISSION_BUTTON)
.assertExists()
.performClick()

// deny permission
denyPermissionDialog(uiDevice)
uiDevice.waitForIdle()

// Assert we're now on preview screen
composeTestRule.waitUntil(timeoutMillis = APP_START_TIMEOUT_MILLIS) {
composeTestRule.onNodeWithTag(CAPTURE_BUTTON).assertExists().isDisplayed()
}
}
}
}
117 changes: 117 additions & 0 deletions app/src/androidTest/java/com/google/jetpackcamera/utils/UiTestUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import android.app.Activity
import android.app.Instrumentation
import android.content.ComponentName
import android.content.Intent
import android.os.Build
import android.util.Log
import android.net.Uri
import android.provider.MediaStore
import androidx.compose.ui.semantics.SemanticsProperties
Expand All @@ -27,17 +29,27 @@ import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.core.app.ActivityScenario
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
import com.google.jetpackcamera.MainActivity
import com.google.jetpackcamera.feature.preview.R
import com.google.jetpackcamera.feature.preview.quicksettings.ui.QUICK_SETTINGS_FLIP_CAMERA_BUTTON
import com.google.jetpackcamera.settings.model.LensFacing
import java.io.File
import java.net.URLConnection
import org.junit.Assert.fail
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

const val APP_START_TIMEOUT_MILLIS = 10_000L
const val IMAGE_CAPTURE_TIMEOUT_MILLIS = 5_000L
const val VIDEO_CAPTURE_TIMEOUT_MILLIS = 5_000L
const val VIDEO_DURATION_MILLIS = 2_000L
private const val TAG = "UiTestUtil"

inline fun <reified T : Activity> runScenarioTest(
crossinline block: ActivityScenario<T>.() -> Unit
Expand Down Expand Up @@ -147,3 +159,108 @@ fun getIntent(uri: Uri, action: String): Intent {
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
return intent
}

/**
* Rule that you to specify test methods that will have permissions granted prior to starting
*
* @param permissions the permissions to be granted
* @param targetTestNames the names of the tests that this rule will apply to
*/
class IndividualTestGrantPermissionRule(
private val permissions: Array<String>,
private val targetTestNames: Array<String>
) :
TestRule {
private lateinit var wrappedRule: GrantPermissionRule

override fun apply(base: Statement, description: Description): Statement {
for (targetName in targetTestNames) {
if (description.methodName == targetName) {
wrappedRule = GrantPermissionRule.grant(*permissions)
return wrappedRule.apply(base, description)
}
}
// If no match, return the base statement without granting permissions
return base
}
}

// functions for interacting with system permission dialog
fun askEveryTimeDialog(uiDevice: UiDevice) {
if (Build.VERSION.SDK_INT >= 30) {
Log.d(TAG, "Searching for Allow Once Button...")

val askPermission = findObjectById(
uiDevice = uiDevice,
resId = "com.android.permissioncontroller:id/permission_allow_one_time_button"
)

Log.d(TAG, "Clicking Allow Once Button")

askPermission!!.click()
}
}

/**
* Clicks ALLOW option on an open permission dialog
*/
fun grantPermissionDialog(uiDevice: UiDevice) {
if (Build.VERSION.SDK_INT >= 23) {
Log.d(TAG, "Searching for Allow Button...")

val allowPermission = findObjectById(
uiDevice = uiDevice,
resId = when {
Build.VERSION.SDK_INT <= 29 ->
"com.android.packageinstaller:id/permission_allow_button"
else ->
"com.android.permissioncontroller:id/permission_allow_foreground_only_button"
}
)
Log.d(TAG, "Clicking Allow Button")

allowPermission!!.click()
}
}

/**
* Clicks the DENY option on an open permission dialog
*/
fun denyPermissionDialog(uiDevice: UiDevice) {
if (Build.VERSION.SDK_INT >= 23) {
Log.d(TAG, "Searching for Deny Button...")
val denyPermission = findObjectById(
uiDevice = uiDevice,

resId = when {
Build.VERSION.SDK_INT <= 29 ->
"com.android.packageinstaller:id/permission_deny_button"
else -> "com.android.permissioncontroller:id/permission_deny_button"
}
)
Log.d(TAG, "Clicking Deny Button")

denyPermission!!.click()
}
}

/**
* Finds a system button by its resource ID.
* fails if not found
*/
private fun findObjectById(
uiDevice: UiDevice,
resId: String,
timeout: Long = 10000,
shouldFailIfNotFound: Boolean = true
): UiObject2? {
val selector = By.res(resId)
return if (!uiDevice.wait(Until.hasObject(selector), timeout)) {
if (shouldFailIfNotFound) {
fail("Could not find object with RESOURCE ID: $resId")
}
null
} else {
uiDevice.findObject(selector)
}
}
Loading
Loading