1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.permissionmultiuser.cts 18 19 import android.app.Instrumentation 20 import android.app.PendingIntent 21 import android.app.PendingIntent.FLAG_MUTABLE 22 import android.app.PendingIntent.FLAG_UPDATE_CURRENT 23 import android.app.UiAutomation 24 import android.content.BroadcastReceiver 25 import android.content.Context 26 import android.content.Context.RECEIVER_EXPORTED 27 import android.content.Intent 28 import android.content.Intent.ACTION_REVIEW_APP_DATA_SHARING_UPDATES 29 import android.content.IntentFilter 30 import android.content.pm.PackageInstaller 31 import android.content.pm.PackageInstaller.EXTRA_STATUS 32 import android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE 33 import android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID 34 import android.content.pm.PackageInstaller.STATUS_SUCCESS 35 import android.content.pm.PackageInstaller.SessionParams 36 import android.content.pm.PackageManager 37 import android.content.pm.PackageManager.FEATURE_AUTOMOTIVE 38 import android.content.pm.PackageManager.FEATURE_LEANBACK 39 import android.os.Build 40 import android.os.PersistableBundle 41 import android.os.SystemClock 42 import android.os.UserHandle 43 import android.provider.DeviceConfig 44 import android.safetylabel.SafetyLabelConstants.SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED 45 import android.support.test.uiautomator.By 46 import android.support.test.uiautomator.BySelector 47 import android.support.test.uiautomator.StaleObjectException 48 import android.support.test.uiautomator.UiDevice 49 import android.support.test.uiautomator.UiObject2 50 import android.util.Log 51 import androidx.test.filters.SdkSuppress 52 import androidx.test.platform.app.InstrumentationRegistry 53 import com.android.bedstead.enterprise.annotations.RequireRunOnWorkProfile 54 import com.android.bedstead.harrier.BedsteadJUnit4 55 import com.android.bedstead.harrier.DeviceState 56 import com.android.bedstead.permissions.annotations.EnsureHasPermission 57 import com.android.bedstead.harrier.annotations.EnsureSecureSettingSet 58 import com.android.bedstead.harrier.annotations.RequireDoesNotHaveFeature 59 import com.android.bedstead.harrier.annotations.RequireNotWatch 60 import com.android.bedstead.harrier.annotations.RequireSdkVersion 61 import com.android.bedstead.multiuser.additionalUser 62 import com.android.bedstead.multiuser.annotations.RequireRunOnAdditionalUser 63 import com.android.bedstead.permissions.CommonPermissions.INTERACT_ACROSS_USERS 64 import com.android.compatibility.common.util.ApiTest 65 import com.android.compatibility.common.util.DeviceConfigStateChangerRule 66 import com.android.compatibility.common.util.SystemUtil.runShellCommand 67 import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity 68 import com.android.compatibility.common.util.SystemUtil.waitForBroadcasts 69 import com.android.compatibility.common.util.UiAutomatorUtils 70 import com.google.common.truth.Truth.assertThat 71 import java.io.File 72 import java.util.concurrent.LinkedBlockingQueue 73 import java.util.concurrent.TimeUnit 74 import org.junit.After 75 import org.junit.Assert 76 import org.junit.Before 77 import org.junit.ClassRule 78 import org.junit.Rule 79 import org.junit.Test 80 import org.junit.runner.RunWith 81 82 /** 83 * Tests the UI that displays information about apps' updates to their data sharing policies when 84 * device has multiple users. 85 */ 86 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake") 87 @RequireSdkVersion(min = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 88 @RequireDoesNotHaveFeature(FEATURE_AUTOMOTIVE) 89 @RequireDoesNotHaveFeature(FEATURE_LEANBACK) 90 @RequireNotWatch(reason = "Data sharing update page is unavailable on watch") 91 @RunWith(BedsteadJUnit4::class) 92 @EnsureSecureSettingSet(key = "user_setup_complete", value = "1") 93 class AppDataSharingUpdatesTest { 94 95 @get:Rule 96 val deviceConfigSafetyLabelChangeNotificationsEnabled = 97 DeviceConfigStateChangerRule( 98 context, 99 DeviceConfig.NAMESPACE_PRIVACY, 100 SAFETY_LABEL_CHANGE_NOTIFICATIONS_ENABLED, 101 true.toString() 102 ) 103 104 @get:Rule 105 val deviceConfigDataSharingUpdatesPeriod = 106 DeviceConfigStateChangerRule( 107 context, 108 DeviceConfig.NAMESPACE_PRIVACY, 109 PROPERTY_DATA_SHARING_UPDATE_PERIOD_MILLIS, 110 "600000" 111 ) 112 113 /** 114 * This rule serves to limit the max number of safety labels that can be persisted, so that 115 * repeated tests don't overwhelm the disk storage on the device. 116 */ 117 @get:Rule 118 val deviceConfigMaxSafetyLabelsPersistedPerApp = 119 DeviceConfigStateChangerRule( 120 context, 121 DeviceConfig.NAMESPACE_PRIVACY, 122 PROPERTY_MAX_SAFETY_LABELS_PERSISTED_PER_APP, 123 "2" 124 ) 125 126 @Before 127 fun registerInstallSessionResultReceiver() { 128 context.registerReceiver( 129 installSessionResultReceiver, 130 IntentFilter(INSTALL_ACTION_CALLBACK), 131 RECEIVER_EXPORTED 132 ) 133 } 134 135 @After 136 fun unregisterInstallSessionResultReceiver() { 137 try { 138 context.unregisterReceiver(installSessionResultReceiver) 139 } catch (ignored: IllegalArgumentException) {} 140 } 141 142 @Test 143 @EnsureHasPermission(INTERACT_ACROSS_USERS) 144 @RequireRunOnWorkProfile 145 @ApiTest(apis = ["android.content.Intent#ACTION_REVIEW_APP_DATA_SHARING_UPDATES"]) 146 fun openDataSharingUpdatesPage_workProfile_whenAppHasUpdateAndLocationGranted_showUpdates() { 147 installPackageViaSession(LOCATION_PACKAGE_APK_PATH, createAppMetadataWithNoSharing()) 148 waitForBroadcasts() 149 installPackageViaSession( 150 LOCATION_PACKAGE_APK_PATH, 151 createAppMetadataWithLocationSharingNoAds() 152 ) 153 waitForBroadcasts() 154 grantLocationPermission(LOCATION_PACKAGE_NAME) 155 156 startAppDataSharingUpdatesActivityForUser(deviceState.initialUser().userHandle()) 157 158 try { 159 assertUpdatesPresent() 160 findView(By.textContains(LOCATION_PACKAGE_NAME_SUBSTRING), true) 161 } finally { 162 pressBack() 163 uninstallPackage(LOCATION_PACKAGE_NAME) 164 } 165 } 166 167 @Test 168 @EnsureHasPermission(INTERACT_ACROSS_USERS) 169 @RequireRunOnAdditionalUser 170 @ApiTest(apis = ["android.content.Intent#ACTION_REVIEW_APP_DATA_SHARING_UPDATES"]) 171 fun openDataSharingUpdatesPage_additionalUser_whenAppHasUpdateAndLocationGranted_showUpdates() { 172 installPackageViaSession(LOCATION_PACKAGE_APK_PATH, createAppMetadataWithNoSharing()) 173 waitForBroadcasts() 174 installPackageViaSession( 175 LOCATION_PACKAGE_APK_PATH, 176 createAppMetadataWithLocationSharingNoAds() 177 ) 178 waitForBroadcasts() 179 grantLocationPermission(LOCATION_PACKAGE_NAME) 180 181 startAppDataSharingUpdatesActivityForUser(deviceState.additionalUser().userHandle()) 182 183 try { 184 assertUpdatesPresent() 185 findView(By.textContains(LOCATION_PACKAGE_NAME_SUBSTRING), true) 186 } finally { 187 pressBack() 188 uninstallPackage(LOCATION_PACKAGE_NAME) 189 } 190 191 deviceState.initialUser().switchTo() 192 193 startAppDataSharingUpdatesActivityForUser(deviceState.initialUser().userHandle()) 194 195 try { 196 // Verify that state does not leak across users. 197 assertNoUpdatesPresent() 198 findView(By.textContains(LOCATION_PACKAGE_NAME_SUBSTRING), false) 199 } finally { 200 pressBack() 201 } 202 } 203 204 /** Companion object for [AppDataSharingUpdatesTest]. */ 205 companion object { 206 @JvmField @ClassRule @Rule val deviceState: DeviceState = DeviceState() 207 208 @JvmStatic 209 private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() 210 private val context: Context = instrumentation.context 211 @JvmStatic private val uiAutomation: UiAutomation = instrumentation.uiAutomation 212 @JvmStatic private val uiDevice: UiDevice = UiDevice.getInstance(instrumentation) 213 @JvmStatic private val packageManager: PackageManager = context.packageManager 214 @JvmStatic private val packageInstaller = packageManager.packageInstaller 215 private data class SessionResult(val status: Int?) 216 private val TAG = AppDataSharingUpdatesTest::class.simpleName 217 218 private const val APK_DIRECTORY = "/data/local/tmp/cts-permissionmultiuser" 219 private const val LOCATION_PACKAGE_NAME = "android.permissionmultiuser.cts.requestlocation" 220 private const val LOCATION_PACKAGE_APK_PATH = "CtsRequestLocationApp.apk" 221 private const val INSTALL_ACTION_CALLBACK = "AppDataSharingUpdatesTest.install_callback" 222 private const val PACKAGE_INSTALLER_TIMEOUT = 60000L 223 private const val IDLE_TIMEOUT_MILLIS: Long = 1000 224 private const val TIMEOUT_MILLIS: Long = 20000 225 226 private const val KEY_VERSION = "version" 227 private const val KEY_SAFETY_LABELS = "safety_labels" 228 private const val KEY_DATA_SHARED = "data_shared" 229 private const val KEY_DATA_LABELS = "data_labels" 230 private const val KEY_PURPOSES = "purposes" 231 private const val INITIAL_SAFETY_LABELS_VERSION = 1L 232 private const val INITIAL_TOP_LEVEL_VERSION = 1L 233 private const val LOCATION_CATEGORY = "location" 234 private const val APPROX_LOCATION = "approx_location" 235 private const val PURPOSE_FRAUD_PREVENTION_SECURITY = 4 236 237 private const val DATA_SHARING_UPDATES = "Data sharing updates for location" 238 private const val DATA_SHARING_UPDATES_SUBTITLE = 239 "These apps have changed the way they may share your location data. They may not" + 240 " have shared it before, or may now share it for advertising or marketing" + 241 " purposes." 242 private const val DATA_SHARING_NO_UPDATES_MESSAGE = "No updates at this time" 243 private const val UPDATES_IN_LAST_30_DAYS = "Updated within 30 days" 244 private const val DATA_SHARING_UPDATES_FOOTER_MESSAGE = 245 "The developers of these apps provided info about their data sharing practices" + 246 " to an app store. They may update it over time.\n\nData sharing" + 247 " practices may vary based on your app version, use, region, and age." 248 private const val LOCATION_PACKAGE_NAME_SUBSTRING = "android.permissionmultiuser" 249 private const val PROPERTY_DATA_SHARING_UPDATE_PERIOD_MILLIS = 250 "data_sharing_update_period_millis" 251 private const val PROPERTY_MAX_SAFETY_LABELS_PERSISTED_PER_APP = 252 "max_safety_labels_persisted_per_app" 253 254 private var installSessionResult = LinkedBlockingQueue<SessionResult>() 255 256 private val installSessionResultReceiver = 257 object : BroadcastReceiver() { 258 override fun onReceive(context: Context, intent: Intent) { 259 val status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID) 260 val msg = intent.getStringExtra(EXTRA_STATUS_MESSAGE) 261 Log.d(TAG, "status: $status, msg: $msg") 262 263 installSessionResult.offer(SessionResult(status)) 264 } 265 } 266 267 /** Installs an app with the provided [appMetadata] */ 268 private fun installPackageViaSession( 269 apkName: String, 270 appMetadata: PersistableBundle? = null, 271 packageSource: Int? = null 272 ) { 273 val session = createPackageInstallerSession(packageSource) 274 runWithShellPermissionIdentity { 275 writePackageInstallerSession(session, apkName) 276 if (appMetadata != null) { 277 setAppMetadata(session, appMetadata) 278 } 279 commitPackageInstallerSession(session) 280 281 // No need to click installer UI here due to running in shell permission identity 282 // and not needing user interaction to complete install. 283 // Install should have succeeded. 284 val result = getInstallSessionResult() 285 assertThat(result.status).isEqualTo(STATUS_SUCCESS) 286 } 287 } 288 289 private fun createPackageInstallerSession( 290 packageSource: Int? = null 291 ): PackageInstaller.Session { 292 val sessionParam = SessionParams(SessionParams.MODE_FULL_INSTALL) 293 if (packageSource != null) { 294 sessionParam.setPackageSource(packageSource) 295 } 296 297 val sessionId = packageInstaller.createSession(sessionParam) 298 return packageInstaller.openSession(sessionId) 299 } 300 301 private fun writePackageInstallerSession( 302 session: PackageInstaller.Session, 303 apkName: String 304 ) { 305 val apkFile = File(APK_DIRECTORY, apkName) 306 apkFile.inputStream().use { fileOnDisk -> 307 session 308 .openWrite(/* name= */ apkName, /* offsetBytes= */ 0, /* lengthBytes= */ -1) 309 .use { sessionFile -> fileOnDisk.copyTo(sessionFile) } 310 } 311 } 312 313 private fun commitPackageInstallerSession(session: PackageInstaller.Session) { 314 // PendingIntent that triggers a INSTALL_ACTION_CALLBACK broadcast that gets received by 315 // installSessionResultReceiver when install actions occur with this session 316 val installActionPendingIntent = 317 PendingIntent.getBroadcast( 318 context, 319 0, 320 Intent(INSTALL_ACTION_CALLBACK).setPackage(context.packageName), 321 FLAG_UPDATE_CURRENT or FLAG_MUTABLE 322 ) 323 session.commit(installActionPendingIntent.intentSender) 324 } 325 326 private fun setAppMetadata(session: PackageInstaller.Session, data: PersistableBundle) { 327 try { 328 session.setAppMetadata(data) 329 } catch (e: Exception) { 330 session.abandon() 331 throw e 332 } 333 } 334 335 private fun getInstallSessionResult( 336 timeout: Long = PACKAGE_INSTALLER_TIMEOUT 337 ): SessionResult { 338 return installSessionResult.poll(timeout, TimeUnit.MILLISECONDS) 339 ?: SessionResult(null /* status */) 340 } 341 342 private fun uninstallPackage(packageName: String) { 343 runShellCommand("pm uninstall $packageName").trim() 344 } 345 346 private fun pressBack() { 347 uiDevice.pressBack() 348 uiAutomation.waitForIdle(IDLE_TIMEOUT_MILLIS, TIMEOUT_MILLIS) 349 } 350 351 /** Returns an App Metadata [PersistableBundle] representation where no data is shared. */ 352 private fun createAppMetadataWithNoSharing(): PersistableBundle { 353 return createMetadataWithDataShared(PersistableBundle()) 354 } 355 356 /** 357 * Returns an App Metadata [PersistableBundle] representation where location data is shared, 358 * but not for advertising purpose. 359 */ 360 private fun createAppMetadataWithLocationSharingNoAds(): PersistableBundle { 361 val locationBundle = 362 PersistableBundle().apply { 363 putPersistableBundle( 364 APPROX_LOCATION, 365 PersistableBundle().apply { 366 putIntArray( 367 KEY_PURPOSES, 368 listOf(PURPOSE_FRAUD_PREVENTION_SECURITY).toIntArray() 369 ) 370 } 371 ) 372 } 373 374 val dataSharedBundle = 375 PersistableBundle().apply { 376 putPersistableBundle(LOCATION_CATEGORY, locationBundle) 377 } 378 379 return createMetadataWithDataShared(dataSharedBundle) 380 } 381 382 /** 383 * Returns an App Metadata [PersistableBundle] representation where with the provided data 384 * shared. 385 */ 386 private fun createMetadataWithDataShared( 387 dataSharedBundle: PersistableBundle 388 ): PersistableBundle { 389 val dataLabelBundle = 390 PersistableBundle().apply { 391 putPersistableBundle(KEY_DATA_SHARED, dataSharedBundle) 392 } 393 394 val safetyLabelBundle = 395 PersistableBundle().apply { 396 putLong(KEY_VERSION, INITIAL_SAFETY_LABELS_VERSION) 397 putPersistableBundle(KEY_DATA_LABELS, dataLabelBundle) 398 } 399 400 return PersistableBundle().apply { 401 putLong(KEY_VERSION, INITIAL_TOP_LEVEL_VERSION) 402 putPersistableBundle(KEY_SAFETY_LABELS, safetyLabelBundle) 403 } 404 } 405 406 /** 407 * Starts activity with intent [ACTION_REVIEW_APP_DATA_SHARING_UPDATES] for the provided 408 * user. 409 */ 410 fun startAppDataSharingUpdatesActivityForUser(userHandle: UserHandle) { 411 runWithShellPermissionIdentity { 412 context.startActivityAsUser( 413 Intent(ACTION_REVIEW_APP_DATA_SHARING_UPDATES).apply { 414 addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 415 }, 416 userHandle 417 ) 418 } 419 } 420 421 private fun assertUpdatesPresent() { 422 findView(By.descContains(DATA_SHARING_UPDATES), true) 423 findView(By.textContains(DATA_SHARING_UPDATES_SUBTITLE), true) 424 findView(By.textContains(UPDATES_IN_LAST_30_DAYS), true) 425 findView(By.textContains(DATA_SHARING_UPDATES_FOOTER_MESSAGE), true) 426 } 427 428 private fun assertNoUpdatesPresent() { 429 findView(By.descContains(DATA_SHARING_UPDATES), true) 430 findView(By.textContains(DATA_SHARING_NO_UPDATES_MESSAGE), true) 431 findView(By.textContains(LOCATION_PACKAGE_NAME_SUBSTRING), false) 432 } 433 434 private fun grantLocationPermission(packageName: String) { 435 uiAutomation.grantRuntimePermission( 436 packageName, 437 android.Manifest.permission.ACCESS_FINE_LOCATION 438 ) 439 } 440 441 protected fun waitFindObject( 442 selector: BySelector, 443 timeoutMillis: Long = 20_000L 444 ): UiObject2 { 445 uiAutomation.waitForIdle(IDLE_TIMEOUT_MILLIS, TIMEOUT_MILLIS) 446 val startTime = SystemClock.elapsedRealtime() 447 return try { 448 UiAutomatorUtils.waitFindObject(selector, timeoutMillis) 449 } catch (e: StaleObjectException) { 450 val remainingTime = timeoutMillis - (SystemClock.elapsedRealtime() - startTime) 451 if (remainingTime <= 0) { 452 throw e 453 } 454 UiAutomatorUtils.waitFindObject(selector, remainingTime) 455 } 456 } 457 458 private fun findView(selector: BySelector, expected: Boolean) { 459 val timeoutMillis = 460 if (expected) { 461 20000L 462 } else { 463 1000L 464 } 465 466 val exception = 467 try { 468 uiAutomation.waitForIdle(IDLE_TIMEOUT_MILLIS, TIMEOUT_MILLIS) 469 val startTime = SystemClock.elapsedRealtime() 470 try { 471 UiAutomatorUtils.waitFindObject(selector, timeoutMillis) 472 } catch (e: StaleObjectException) { 473 val remainingTime = 474 timeoutMillis - (SystemClock.elapsedRealtime() - startTime) 475 if (remainingTime <= 0) { 476 throw e 477 } 478 UiAutomatorUtils.waitFindObject(selector, remainingTime) 479 } 480 null 481 } catch (e: Exception) { 482 e 483 } 484 val actual = exception == null 485 val message = 486 if (expected) { 487 "Expected view $selector not found" 488 } else { 489 "Unexpected view found: $selector" 490 } 491 Assert.assertTrue(message, actual == expected) 492 } 493 } 494 } 495