blob: ba673f0058a90f43e8fd4cb0f3805da0caa91ef5 [file] [log] [blame]
/*
* Copyright (C) 2021 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 android.sensorprivacy.cts
import android.app.KeyguardManager
import android.app.AppOpsManager
import android.content.Intent
import android.content.pm.PackageManager
import android.hardware.SensorPrivacyManager
import android.hardware.SensorPrivacyManager.OnSensorPrivacyChangedListener
import android.os.PowerManager
import android.platform.test.annotations.AppModeFull
import android.hardware.SensorPrivacyManager.Sensors.CAMERA
import android.hardware.SensorPrivacyManager.Sensors.MICROPHONE
import android.hardware.SensorPrivacyManager.Sources.OTHER
import android.hardware.camera2.CameraManager
import android.support.test.uiautomator.By
import android.view.KeyEvent
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.compatibility.common.util.SystemUtil.callWithShellPermissionIdentity
import com.android.compatibility.common.util.SystemUtil.eventually
import com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow
import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
import com.android.compatibility.common.util.UiAutomatorUtils
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assume
import org.junit.Before
import org.junit.Test
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
abstract class SensorPrivacyBaseTest(
val sensor: Int,
vararg val extras: String
) {
companion object {
const val MIC_CAM_ACTIVITY_ACTION =
"android.sensorprivacy.cts.usemiccamera.action.USE_MIC_CAM"
const val FINISH_MIC_CAM_ACTIVITY_ACTION =
"android.sensorprivacy.cts.usemiccamera.action.FINISH_USE_MIC_CAM"
const val USE_MIC_EXTRA =
"android.sensorprivacy.cts.usemiccamera.extra.USE_MICROPHONE"
const val USE_CAM_EXTRA =
"android.sensorprivacy.cts.usemiccamera.extra.USE_CAMERA"
const val DELAYED_ACTIVITY_EXTRA =
"android.sensorprivacy.cts.usemiccamera.extra.DELAYED_ACTIVITY"
const val DELAYED_ACTIVITY_NEW_TASK_EXTRA =
"android.sensorprivacy.cts.usemiccamera.extra.DELAYED_ACTIVITY_NEW_TASK"
const val RETRY_CAM_EXTRA =
"android.sensorprivacy.cts.usemiccamera.extra.RETRY_CAM_EXTRA"
const val PKG_NAME = "android.sensorprivacy.cts.usemiccamera"
const val RECORDING_FILE_NAME = "${PKG_NAME}_record.mp4"
const val ACTIVITY_TITLE_SNIP = "CtsUseMic"
const val SENSOR_USE_TIME_MS = 5L
}
protected val instrumentation = InstrumentationRegistry.getInstrumentation()!!
protected val uiAutomation = instrumentation.uiAutomation!!
protected val uiDevice = UiDevice.getInstance(instrumentation)!!
protected val context = instrumentation.targetContext!!
protected val spm = context.getSystemService(SensorPrivacyManager::class.java)!!
protected val packageManager = context.packageManager!!
protected val op = getOpForSensor(sensor)
var oldState: Boolean = false
@Before
fun init() {
oldState = isSensorPrivacyEnabled()
setSensor(false)
Assume.assumeTrue(spm.supportsSensorToggle(sensor))
uiDevice.wakeUp()
runShellCommandOrThrow("wm dismiss-keyguard")
uiDevice.waitForIdle()
}
@After
fun tearDown() {
finishTestApp()
Thread.sleep(3000)
setSensor(oldState)
}
@Test
fun testSetSensor() {
setSensor(true)
assertTrue(isSensorPrivacyEnabled())
setSensor(false)
assertFalse(isSensorPrivacyEnabled())
}
@Test
fun testDialog() {
testDialog(delayedActivity = false, delayedActivityNewTask = false)
}
@Test
fun testDialog_remainsOnTop() {
testDialog(delayedActivity = true, delayedActivityNewTask = false)
}
@Test
fun testDialog_remainsOnTop_newTask() {
testDialog(delayedActivity = true, delayedActivityNewTask = true)
}
fun testDialog(delayedActivity: Boolean = false, delayedActivityNewTask: Boolean = false) {
checkCameraPresentIfNeeded()
try {
setSensor(true)
val intent = Intent(MIC_CAM_ACTIVITY_ACTION)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL)
for (extra in extras) {
intent.putExtra(extra, true)
}
intent.putExtra(DELAYED_ACTIVITY_EXTRA, delayedActivity)
intent.putExtra(DELAYED_ACTIVITY_NEW_TASK_EXTRA, delayedActivityNewTask)
context.startActivity(intent)
if (delayedActivity || delayedActivityNewTask) {
Thread.sleep(3000)
}
unblockSensorWithDialogAndAssert()
} finally {
runShellCommandOrThrow("am broadcast" +
" --user ${context.userId}" +
" -a $FINISH_MIC_CAM_ACTIVITY_ACTION" +
" -f ${Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS}")
}
}
@Test
fun testListener() {
val executor = Executors.newSingleThreadExecutor()
setSensor(false)
val latchEnabled = CountDownLatch(1)
var listener =
OnSensorPrivacyChangedListener { _, enabled: Boolean ->
if (enabled) {
latchEnabled.countDown()
}
}
runWithShellPermissionIdentity {
spm.addSensorPrivacyListener(sensor, executor, listener)
}
setSensor(true)
latchEnabled.await(100, TimeUnit.MILLISECONDS)
runWithShellPermissionIdentity {
spm.removeSensorPrivacyListener(sensor, listener)
}
val latchDisabled = CountDownLatch(1)
listener = OnSensorPrivacyChangedListener { _, enabled: Boolean ->
if (!enabled) {
latchDisabled.countDown()
}
}
runWithShellPermissionIdentity {
spm.addSensorPrivacyListener(sensor, executor, listener)
}
setSensor(false)
latchEnabled.await(100, TimeUnit.MILLISECONDS)
runWithShellPermissionIdentity {
spm.removeSensorPrivacyListener(sensor, listener)
}
}
@Test
@AppModeFull(reason = "Instant apps can't manage keyguard")
fun testCantChangeWhenLocked() {
Assume.assumeTrue(packageManager
.hasSystemFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN))
setSensor(false)
assertFalse(isSensorPrivacyEnabled())
runWhileLocked {
setSensor(true)
assertFalse("State was changed while device is locked",
isSensorPrivacyEnabled())
}
setSensor(true)
assertTrue(isSensorPrivacyEnabled())
runWhileLocked {
setSensor(false)
assertTrue("State was changed while device is locked",
isSensorPrivacyEnabled())
}
}
fun unblockSensorWithDialogAndAssert() {
UiAutomatorUtils.waitFindObject(By.text(
Pattern.compile("Unblock", Pattern.CASE_INSENSITIVE))).click()
eventually {
assertFalse(isSensorPrivacyEnabled())
}
}
@Test
@AppModeFull(reason = "Uses secondary app, instant apps have no visibility")
fun testOpNotRunningWhileSensorPrivacyEnabled() {
checkCameraPresentIfNeeded()
setSensor(false)
val before = System.currentTimeMillis()
startTestApp()
eventually {
assertOpRunning(true)
}
Thread.sleep(SENSOR_USE_TIME_MS)
setSensor(true)
eventually {
val after = System.currentTimeMillis()
assertOpRunning(false)
assertLastAccessTimeAndDuration(before, after)
}
}
@Test
@AppModeFull(reason = "Uses secondary app, instant apps have no visibility")
fun testOpStartsRunningAfterStartedWithSensoryPrivacyEnabled() {
checkCameraPresentIfNeeded()
setSensor(true)
// Retry camera connection because external cameras are disconnected
// if sensor privacy is enabled (b/182204067)
startTestApp(true)
UiAutomatorUtils.waitFindObject(By.text(
Pattern.compile("Cancel", Pattern.CASE_INSENSITIVE))).click()
assertOpRunning(false)
setSensor(false)
eventually {
assertOpRunning(true)
}
}
@Test
@AppModeFull(reason = "Uses secondary app, instant apps have no visibility")
fun testOpGetsRecordedAfterStartedWithSensorPrivacyEnabled() {
checkCameraPresentIfNeeded()
setSensor(true)
// Retry camera connection because external cameras are disconnected
// if sensor privacy is enabled (b/182204067)
startTestApp(true)
UiAutomatorUtils.waitFindObject(By.text(
Pattern.compile("Cancel", Pattern.CASE_INSENSITIVE))).click()
val before = System.currentTimeMillis()
setSensor(false)
eventually {
assertOpRunning(true)
}
setSensor(true)
eventually {
val after = System.currentTimeMillis()
assertLastAccessTimeAndDuration(before, after)
}
}
@Test
@AppModeFull(reason = "Uses secondary app, instant apps have no visibility")
fun testOpLastAccessUpdatesAfterToggleSensorPrivacy() {
checkCameraPresentIfNeeded()
setSensor(false)
val before = System.currentTimeMillis()
startTestApp()
eventually {
assertOpRunning(true)
}
Thread.sleep(SENSOR_USE_TIME_MS)
setSensor(true)
eventually {
val after = System.currentTimeMillis()
assertOpRunning(false)
assertLastAccessTimeAndDuration(before, after)
}
val before2 = System.currentTimeMillis()
setSensor(false)
eventually {
assertOpRunning(true)
}
Thread.sleep(SENSOR_USE_TIME_MS)
setSensor(true)
eventually {
val after = System.currentTimeMillis()
assertOpRunning(false)
assertLastAccessTimeAndDuration(before2, after)
}
}
@Test
@AppModeFull(reason = "Uses secondary app, instant apps have no visibility")
fun testOpFinishedWhileToggleOn() {
checkCameraPresentIfNeeded()
setSensor(false)
startTestApp()
eventually {
assertOpRunning(true)
}
setSensor(true)
Thread.sleep(5000)
eventually {
assertOpRunning(false)
}
finishTestApp()
Thread.sleep(1000)
setSensor(false)
Thread.sleep(1000)
assertOpRunning(false)
}
private fun checkCameraPresentIfNeeded() {
if (sensor == CAMERA) {
val cameraManager: CameraManager = context.getSystemService(CameraManager::class.java)!!
Assume.assumeTrue("No camera available", cameraManager.cameraIdList.isNotEmpty())
}
}
private fun startTestApp() {
startTestApp(false)
}
private fun startTestApp(retryCameraOnError: Boolean) {
val intent = Intent(MIC_CAM_ACTIVITY_ACTION)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL)
for (extra in extras) {
intent.putExtra(extra, true)
}
intent.putExtra(RETRY_CAM_EXTRA, retryCameraOnError)
context.startActivity(intent)
// Wait for app to open
UiAutomatorUtils.waitFindObject(By.textContains(ACTIVITY_TITLE_SNIP))
}
private fun finishTestApp() {
// instant apps can't broadcast to other instant apps; use the shell
runShellCommandOrThrow("am broadcast" +
" --user ${context.userId}" +
" -a $FINISH_MIC_CAM_ACTIVITY_ACTION" +
" -f ${Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS}")
}
protected fun setSensor(enable: Boolean) {
runWithShellPermissionIdentity {
spm.setSensorPrivacy(OTHER, sensor, enable)
}
}
private fun isSensorPrivacyEnabled(): Boolean {
return callWithShellPermissionIdentity {
spm.isSensorPrivacyEnabled(sensor)
}
}
private fun getOpForSensor(sensor: Int): String? {
return when (sensor) {
CAMERA -> AppOpsManager.OPSTR_CAMERA
MICROPHONE -> AppOpsManager.OPSTR_RECORD_AUDIO
else -> null
}
}
private fun getOpForPackage(): AppOpsManager.PackageOps {
return callWithShellPermissionIdentity {
val uid = try {
packageManager.getPackageUid(PKG_NAME, 0)
} catch (e: PackageManager.NameNotFoundException) {
// fail test
assertNull(e)
-1
}
val appOpsManager: AppOpsManager =
context.getSystemService(AppOpsManager::class.java)!!
val pkgOps = appOpsManager.getOpsForPackage(uid, PKG_NAME, op)
assertFalse("expected non empty app op list", pkgOps.isEmpty())
pkgOps[0]
}
}
private fun assertOpRunning(isRunning: Boolean) {
val pkgOp = getOpForPackage()
for (op in pkgOp.ops) {
for ((_, attrOp) in op.attributedOpEntries) {
assertEquals("Unexpected op running state", isRunning, attrOp.isRunning)
}
}
}
private fun assertLastAccessTimeAndDuration(before: Long, after: Long) {
val pkgOp = getOpForPackage()
for (op in pkgOp.ops) {
for ((_, attrOp) in op.attributedOpEntries) {
val lastAccess = attrOp.getLastAccessTime(AppOpsManager.OP_FLAGS_ALL_TRUSTED)
val lastDuration = attrOp.getLastDuration(AppOpsManager.OP_FLAGS_ALL_TRUSTED)
assertTrue("lastAccess was $lastAccess, not between $before and $after",
lastAccess in before..after)
assertTrue("lastAccess had duration $lastDuration, greater than ${after - before}",
lastDuration <= (after - before))
}
}
}
fun runWhileLocked(r: () -> Unit) {
val km = context.getSystemService(KeyguardManager::class.java)!!
val pm = context.getSystemService(PowerManager::class.java)!!
val password = byteArrayOf(1, 2, 3, 4)
try {
runWithShellPermissionIdentity {
km!!.setLock(KeyguardManager.PIN, password, KeyguardManager.PIN, null)
}
eventually {
uiDevice.pressKeyCode(KeyEvent.KEYCODE_SLEEP)
assertFalse("Device never slept.", pm.isInteractive)
}
eventually {
uiDevice.pressKeyCode(KeyEvent.KEYCODE_WAKEUP)
assertTrue("Device never woke up.", pm.isInteractive)
}
eventually {
assertTrue("Device isn't locked", km.isDeviceLocked)
}
r.invoke()
} finally {
runWithShellPermissionIdentity {
km!!.setLock(KeyguardManager.PIN, null, KeyguardManager.PIN, password)
}
// Recycle the screen power in case the keyguard is stuck open
eventually {
uiDevice.pressKeyCode(KeyEvent.KEYCODE_SLEEP)
assertFalse("Device never slept.", pm.isInteractive)
}
eventually {
uiDevice.pressKeyCode(KeyEvent.KEYCODE_WAKEUP)
assertTrue("Device never woke up.", pm.isInteractive)
}
eventually {
assertFalse("Device isn't unlocked", km.isDeviceLocked)
}
}
}
}