1 /* <lambda>null2 * Copyright (C) 2025 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.bluetooth.opp 18 19 import android.Manifest 20 import android.app.KeyguardManager 21 import android.bluetooth.BluetoothManager 22 import android.bluetooth.Host 23 import android.bluetooth.PandoraDevice 24 import android.bluetooth.test_utils.EnableBluetoothRule 25 import android.content.ClipData 26 import android.content.Intent 27 import android.content.Intent.EXTRA_STREAM 28 import android.net.Uri 29 import android.os.Build 30 import android.platform.test.annotations.RequiresFlagsEnabled 31 import android.platform.test.flag.junit.DeviceFlagsValueProvider 32 import androidx.test.filters.SdkSuppress 33 import androidx.test.platform.app.InstrumentationRegistry 34 import androidx.test.uiautomator.By 35 import androidx.test.uiautomator.UiDevice 36 import androidx.test.uiautomator.Until 37 import com.android.bluetooth.flags.Flags 38 import com.android.compatibility.common.util.AdoptShellPermissionsRule 39 import com.google.common.truth.Truth.assertThat 40 import com.google.testing.junit.testparameterinjector.TestParameter 41 import com.google.testing.junit.testparameterinjector.TestParameterInjector 42 import kotlin.time.Duration 43 import kotlin.time.Duration.Companion.milliseconds 44 import kotlinx.coroutines.ExperimentalCoroutinesApi 45 import org.junit.After 46 import org.junit.Before 47 import org.junit.Rule 48 import org.junit.Test 49 import org.junit.runner.RunWith 50 51 /** 52 * Test cases for sending a file via OPP using a share intent. Uses the Bumble helper app as a 53 * separate package to launch [Intent.ACTION_SEND] or [Intent.ACTION_SEND_MULTIPLE] intents with 54 * content URIs that it may (not) have permissions to access. 55 */ 56 @RunWith(TestParameterInjector::class) 57 @ExperimentalCoroutinesApi 58 class OppSendFileTest { 59 val mInstrumentation = InstrumentationRegistry.getInstrumentation() 60 val mContext = mInstrumentation.targetContext 61 val mDevice 62 get() = UiDevice.getInstance(mInstrumentation) 63 64 val mAdapter 65 get() = mContext.getSystemService(BluetoothManager::class.java).adapter 66 67 @get:Rule(order = 1) // Cleans up shell permissions, must be run before shell permissions rule 68 val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() 69 70 @get:Rule(order = 2) 71 val mPermissionRule = 72 AdoptShellPermissionsRule( 73 mInstrumentation.uiAutomation, 74 Manifest.permission.BLUETOOTH_PRIVILEGED, 75 Manifest.permission.CREATE_USERS, 76 Manifest.permission.INTERACT_ACROSS_USERS, 77 ) 78 79 @get:Rule(order = 3) val mBumble = PandoraDevice() 80 81 @get:Rule(order = 4) 82 val mEnableBluetoothRule = 83 EnableBluetoothRule(/* enableTestMode= */ false, /* toggleBluetooth= */ true) 84 85 val mRemoteDevice 86 get() = mBumble.remoteDevice 87 88 lateinit var mHost: Host 89 90 @Before 91 fun setUp() { 92 mHost = Host(mContext) 93 navigateToUnlockedHomeScreen() 94 mAdapter.bondedDevices.forEach(mHost::removeBondAndVerify) 95 mHost.createBondAndVerify(mRemoteDevice) 96 } 97 98 private fun navigateToUnlockedHomeScreen() { 99 val keyguardManager: KeyguardManager = 100 mContext.getSystemService(KeyguardManager::class.java) 101 if (keyguardManager.isKeyguardLocked) { 102 dismissKeyguard() 103 } 104 mDevice.pressHome() 105 } 106 107 private fun dismissKeyguard() { 108 val keyguardManager: KeyguardManager = 109 mContext.getSystemService(KeyguardManager::class.java) 110 retryUntil(condition = { !keyguardManager.isKeyguardLocked }) { 111 mDevice.executeShellCommand("wm dismiss-keyguard") 112 } 113 } 114 115 private fun retryUntil( 116 times: Int = DEFAULT_MAX_RETRIES, 117 delay: Duration = DEFAULT_RETRY_DELAY, 118 condition: () -> Boolean, 119 block: () -> Unit, 120 ) { 121 for (i in 1..times) { 122 block() 123 if (!condition()) { 124 Thread.sleep(delay.inWholeMilliseconds) 125 } 126 if (condition()) { 127 break 128 } 129 assertThat(i).isAtMost(times) 130 } 131 } 132 133 @After 134 fun tearDown() { 135 mRemoteDevice.removeBond() 136 mDevice.pressBack() 137 mDevice.pressHome() 138 } 139 140 @Test 141 @Throws(Exception::class) 142 @RequiresFlagsEnabled(Flags.FLAG_OPP_CHECK_CONTENT_URI_PERMISSIONS) 143 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) 144 fun sendViaBluetoothShare( 145 @TestParameter crossUser: Boolean, 146 @TestParameter hasPermission: Boolean, 147 @TestParameter hasGrant: Boolean, 148 ) = 149 contentUriTest("content://${authority(crossUser, hasPermission)}/test_image.png") { uris -> 150 val intent = createSendIntent(uris.single(), hasGrant) 151 152 startIntentAndAssertDevicePicker( 153 intent, 154 shouldSucceed = !crossUser && (hasPermission || hasGrant), 155 ) 156 } 157 158 private fun createSendIntent(uri: Uri, grantUriPermission: Boolean): Intent = 159 Intent(Intent.ACTION_MAIN) 160 .putExtra(EXTRA_STREAM, uri) 161 .setFlags(flags(grantUriPermission)) 162 .setClassName(HELPER_PACKAGE, SEND_ACTIVITY) 163 .setData(uri) 164 165 @Test 166 @Throws(Exception::class) 167 @RequiresFlagsEnabled(Flags.FLAG_OPP_CHECK_CONTENT_URI_PERMISSIONS) 168 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) 169 fun sendMultipleViaBluetoothShare( 170 @TestParameter crossUser: Boolean, 171 @TestParameter hasPermission: Boolean, 172 @TestParameter hasGrant: Boolean, 173 ) = 174 contentUriTest( 175 "content://${authority(crossUser, hasPermission)}/test_image.png", 176 "content://${authority(crossUser, hasPermission)}/test_image2.png", 177 ) { uris -> 178 val intent = createSendMultipleIntent(uris, hasGrant) 179 180 startIntentAndAssertDevicePicker( 181 intent, 182 shouldSucceed = !crossUser && (hasPermission || hasGrant), 183 ) 184 } 185 186 @Test 187 @Throws(Exception::class) 188 @RequiresFlagsEnabled(Flags.FLAG_OPP_CHECK_CONTENT_URI_PERMISSIONS) 189 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) 190 fun sendMultipleViaBluetoothShare_oneAlwaysAllowed( 191 @TestParameter crossUser: Boolean, 192 @TestParameter hasPermission: Boolean, 193 @TestParameter hasGrant: Boolean, 194 ) = 195 contentUriTest( 196 "content://${authority(crossUser, hasPermission)}/test_image.png", 197 "content://$PERMITTED_AUTHORITY/test_image2.png", 198 ) { uris -> 199 val intent = createSendMultipleIntent(uris, hasGrant) 200 201 startIntentAndAssertDevicePicker(intent, shouldSucceed = true) 202 } 203 204 private fun createSendMultipleIntent( 205 uris: ArrayList<Uri>, 206 grantUriPermission: Boolean, 207 ): Intent = 208 Intent(Intent.ACTION_MAIN) 209 .putExtra(EXTRA_STREAM, uris) 210 .setType(MIME_TYPE_IMAGE) 211 .setFlags(flags(grantUriPermission)) 212 .setClassName(HELPER_PACKAGE, SEND_MULTIPLE_ACTIVITY) 213 .apply { clipData = uris.toClipData() } 214 215 private fun ArrayList<Uri>.toClipData(): ClipData { 216 val items = map(ClipData::Item) 217 return ClipData("URIs", arrayOf(MIME_TYPE_IMAGE), items.first()).apply { 218 items.drop(1).forEach(::addItem) 219 } 220 } 221 222 private fun startIntentAndAssertDevicePicker(intent: Intent, shouldSucceed: Boolean) { 223 mContext.startActivity(intent) 224 225 val devicePickerText = 226 mDevice.wait(Until.findObject(By.text("Available devices")), TIMEOUT_MS) 227 if (shouldSucceed) { 228 assertThat(devicePickerText).isNotNull() 229 230 val name = mRemoteDevice.name 231 232 val shareToBumbleButton = 233 mDevice.wait(Until.findObject(By.textStartsWith(name)), ASSERT_TIMEOUT_MS) 234 235 assertThat(shareToBumbleButton).isNotNull() 236 } else { 237 assertThat(devicePickerText).isNull() 238 } 239 } 240 241 private fun authority(crossUser: Boolean, hasPermission: Boolean): String { 242 val hostPart = 243 if (hasPermission) { 244 PERMITTED_AUTHORITY 245 } else { 246 RESTRICTED_AUTHORITY 247 } 248 return if (crossUser) { 249 "$OTHER_USER_ID@$hostPart" 250 } else { 251 hostPart 252 } 253 } 254 255 private fun flags(grantUriPermission: Boolean) = 256 Intent.FLAG_ACTIVITY_NEW_TASK or 257 if (grantUriPermission) { 258 Intent.FLAG_GRANT_READ_URI_PERMISSION 259 } else { 260 0 261 } 262 263 /** Provides parsed content URIs to the test block. Cleans up URI grants before and after. */ 264 private fun contentUriTest(vararg uriString: String, test: (uris: ArrayList<Uri>) -> Unit) { 265 val uris = uriString.map(Uri::parse).let(::ArrayList) 266 AutoCloseable { uris.revokePermissions() } 267 .use { 268 uris.revokePermissions() 269 test(uris) 270 } 271 } 272 273 private fun List<Uri>.revokePermissions() = forEach { 274 mContext.revokeUriPermission(it, Intent.FLAG_GRANT_READ_URI_PERMISSION) 275 } 276 277 companion object { 278 private const val MIME_TYPE_IMAGE = "image/*" 279 private const val TIMEOUT_MS = 5000L 280 private const val ASSERT_TIMEOUT_MS = 10000L 281 private const val PERMITTED_AUTHORITY = "android.bluetooth.opp" 282 private const val RESTRICTED_AUTHORITY = "android.bluetooth.opp.restricted" 283 private const val HELPER_PACKAGE = "android.bluetooth.helper" 284 private const val SEND_ACTIVITY = 285 "android.bluetooth.helper.opp.SendToBluetoothShareActivity" 286 private const val SEND_MULTIPLE_ACTIVITY = 287 "android.bluetooth.helper.opp.SendMultipleToBluetoothShareActivity" 288 private const val OTHER_USER_ID = 10 289 private const val DEFAULT_MAX_RETRIES = 8 290 private val DEFAULT_RETRY_DELAY = 250.milliseconds 291 } 292 } 293