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