1 /* 2 * Copyright (C) 2021 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.permission.cts; 18 19 import static android.Manifest.permission.ACCESS_FINE_LOCATION; 20 import static android.app.AppOpsManager.OPSTR_FINE_LOCATION; 21 22 import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 23 24 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow; 25 26 import static com.google.common.truth.Truth.assertThat; 27 28 import static org.junit.Assert.assertTrue; 29 import static org.junit.Assume.assumeTrue; 30 31 import android.app.AppOpsManager; 32 import android.app.AsyncNotedAppOp; 33 import android.app.SyncNotedAppOp; 34 import android.bluetooth.BluetoothAdapter; 35 import android.bluetooth.BluetoothManager; 36 import android.bluetooth.cts.BTAdapterUtils; 37 import android.bluetooth.le.BluetoothLeScanner; 38 import android.bluetooth.le.ScanCallback; 39 import android.bluetooth.le.ScanResult; 40 import android.content.AttributionSource; 41 import android.content.Context; 42 import android.content.ContextParams; 43 import android.content.pm.PackageManager; 44 import android.os.Process; 45 import android.os.SystemClock; 46 import android.platform.test.annotations.AppModeFull; 47 import android.util.ArraySet; 48 import android.util.Base64; 49 import android.util.Log; 50 51 import androidx.test.InstrumentationRegistry; 52 53 import com.android.compatibility.common.util.SystemUtil; 54 55 import org.junit.After; 56 import org.junit.Before; 57 import org.junit.Test; 58 59 import java.util.HashSet; 60 import java.util.List; 61 import java.util.Set; 62 63 /** 64 * Tests behaviour when performing bluetooth scans with renounced location permission. 65 */ 66 public class NearbyDevicesRenouncePermissionTest { 67 68 private static final String TAG = "NearbyDevicesRenouncePermissionTest"; 69 private static final String OPSTR_BLUETOOTH_SCAN = "android:bluetooth_scan"; 70 71 private AppOpsManager mAppOpsManager; 72 private int mLocationNoteCount; 73 private int mScanNoteCount; 74 private Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); 75 private BluetoothAdapter mBluetoothAdapter; 76 private boolean mBluetoothAdapterWasEnabled; 77 78 private enum Result { 79 UNKNOWN, EXCEPTION, EMPTY, FILTERED, FULL 80 } 81 82 private enum Scenario { 83 DEFAULT, RENOUNCE, RENOUNCE_MIDDLE, RENOUNCE_END 84 } 85 86 @Before enableBluetooth()87 public void enableBluetooth() { 88 assumeTrue(supportsBluetooth()); 89 mBluetoothAdapter = mContext.getSystemService(BluetoothManager.class).getAdapter(); 90 mBluetoothAdapterWasEnabled = mBluetoothAdapter.isEnabled(); 91 assertTrue(BTAdapterUtils.enableAdapter(mBluetoothAdapter, mContext)); 92 enableTestMode(); 93 } 94 95 @After disableBluetooth()96 public void disableBluetooth() { 97 assumeTrue(supportsBluetooth()); 98 disableTestMode(); 99 if (!mBluetoothAdapterWasEnabled) { 100 assertTrue(BTAdapterUtils.disableAdapter(mBluetoothAdapter, mContext)); 101 } 102 } 103 104 @Before setUp()105 public void setUp() { 106 mAppOpsManager = getApplicationContext().getSystemService(AppOpsManager.class); 107 mAppOpsManager.setOnOpNotedCallback(getApplicationContext().getMainExecutor(), 108 new AppOpsManager.OnOpNotedCallback() { 109 @Override 110 public void onNoted(SyncNotedAppOp op) { 111 switch (op.getOp()) { 112 case OPSTR_FINE_LOCATION: 113 mLocationNoteCount++; 114 break; 115 case OPSTR_BLUETOOTH_SCAN: 116 mScanNoteCount++; 117 break; 118 default: 119 } 120 } 121 122 @Override 123 public void onSelfNoted(SyncNotedAppOp op) { 124 } 125 126 @Override 127 public void onAsyncNoted(AsyncNotedAppOp asyncOp) { 128 switch (asyncOp.getOp()) { 129 case OPSTR_FINE_LOCATION: 130 mLocationNoteCount++; 131 break; 132 case OPSTR_BLUETOOTH_SCAN: 133 mScanNoteCount++; 134 break; 135 default: 136 } 137 } 138 }); 139 } 140 141 @After tearDown()142 public void tearDown() { 143 mAppOpsManager.setOnOpNotedCallback(null, null); 144 } 145 clearNoteCounts()146 private void clearNoteCounts() { 147 mLocationNoteCount = 0; 148 mScanNoteCount = 0; 149 } 150 151 @AppModeFull 152 @Test scanWithoutRenouncingNotesBluetoothAndLocation()153 public void scanWithoutRenouncingNotesBluetoothAndLocation() throws Exception { 154 clearNoteCounts(); 155 assertThat(performScan(Scenario.DEFAULT)).isEqualTo(Result.FULL); 156 SystemUtil.eventually(() -> { 157 assertThat(mLocationNoteCount).isGreaterThan(0); 158 assertThat(mScanNoteCount).isGreaterThan(0); 159 }); 160 } 161 162 @AppModeFull 163 @Test scanRenouncingLocationNotesBluetoothButNotLocation()164 public void scanRenouncingLocationNotesBluetoothButNotLocation() throws Exception { 165 clearNoteCounts(); 166 assertThat(performScan(Scenario.RENOUNCE)).isEqualTo(Result.FILTERED); 167 SystemUtil.eventually(() -> { 168 assertThat(mLocationNoteCount).isEqualTo(0); 169 assertThat(mScanNoteCount).isGreaterThan(0); 170 }); 171 } 172 173 @AppModeFull 174 @Test scanRenouncingInMiddleOfChainNotesBluetoothButNotLocation()175 public void scanRenouncingInMiddleOfChainNotesBluetoothButNotLocation() throws Exception { 176 clearNoteCounts(); 177 assertThat(performScan(Scenario.RENOUNCE_MIDDLE)).isEqualTo(Result.FILTERED); 178 SystemUtil.eventually(() -> { 179 assertThat(mLocationNoteCount).isEqualTo(0); 180 assertThat(mScanNoteCount).isGreaterThan(0); 181 }); 182 } 183 184 @AppModeFull 185 @Test scanRenouncingAtEndOfChainNotesBluetoothButNotLocation()186 public void scanRenouncingAtEndOfChainNotesBluetoothButNotLocation() throws Exception { 187 clearNoteCounts(); 188 assertThat(performScan(Scenario.RENOUNCE_END)).isEqualTo(Result.FILTERED); 189 SystemUtil.eventually(() -> { 190 assertThat(mLocationNoteCount).isEqualTo(0); 191 assertThat(mScanNoteCount).isGreaterThan(0); 192 }); 193 } 194 performScan(Scenario scenario)195 private Result performScan(Scenario scenario) { 196 try { 197 Context context = createContext(scenario, getApplicationContext()); 198 199 final BluetoothManager bm = context.getSystemService(BluetoothManager.class); 200 final BluetoothLeScanner scanner = bm.getAdapter().getBluetoothLeScanner(); 201 202 final HashSet<String> observed = new HashSet<>(); 203 204 ScanCallback callback = new ScanCallback() { 205 public void onScanResult(int callbackType, ScanResult result) { 206 Log.v(TAG, String.valueOf(result)); 207 observed.add(Base64.encodeToString(result.getScanRecord().getBytes(), 0)); 208 } 209 210 public void onBatchScanResults(List<ScanResult> results) { 211 for (ScanResult result : results) { 212 onScanResult(0, result); 213 } 214 } 215 }; 216 scanner.startScan(callback); 217 218 // Wait a few seconds to figure out what we actually observed 219 SystemClock.sleep(3000); 220 scanner.stopScan(callback); 221 switch (observed.size()) { 222 case 0: return Result.EMPTY; 223 case 1: return Result.FILTERED; 224 case 5: return Result.FULL; 225 default: return Result.UNKNOWN; 226 } 227 } catch (Throwable t) { 228 Log.v(TAG, "Failed to scan", t); 229 return Result.EXCEPTION; 230 } 231 } 232 createContext(Scenario scenario, Context context)233 private Context createContext(Scenario scenario, Context context) throws Exception { 234 if (scenario == Scenario.DEFAULT) { 235 return context; 236 } 237 238 Set<String> renouncedPermissions = new ArraySet<>(); 239 renouncedPermissions.add(ACCESS_FINE_LOCATION); 240 241 switch (scenario) { 242 case RENOUNCE: 243 return SystemUtil.callWithShellPermissionIdentity(() -> 244 context.createContext( 245 new ContextParams.Builder() 246 .setRenouncedPermissions(renouncedPermissions) 247 .setAttributionTag(context.getAttributionTag()) 248 .build()) 249 ); 250 case RENOUNCE_MIDDLE: 251 AttributionSource nextAttrib = new AttributionSource( 252 Process.SHELL_UID, "com.android.shell", null, (Set<String>) null, null); 253 return SystemUtil.callWithShellPermissionIdentity(() -> 254 context.createContext( 255 new ContextParams.Builder() 256 .setRenouncedPermissions(renouncedPermissions) 257 .setAttributionTag(context.getAttributionTag()) 258 .setNextAttributionSource(nextAttrib) 259 .build()) 260 ); 261 case RENOUNCE_END: 262 nextAttrib = new AttributionSource( 263 Process.SHELL_UID, "com.android.shell", null, renouncedPermissions, null); 264 return SystemUtil.callWithShellPermissionIdentity(() -> 265 context.createContext( 266 new ContextParams.Builder() 267 .setAttributionTag(context.getAttributionTag()) 268 .setNextAttributionSource(nextAttrib) 269 .build()) 270 ); 271 default: 272 throw new IllegalStateException(); 273 } 274 } 275 276 supportsBluetooth()277 private boolean supportsBluetooth() { 278 return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH); 279 } 280 enableTestMode()281 private void enableTestMode() { 282 runShellCommandOrThrow("dumpsys activity service" 283 + " com.android.bluetooth/.btservice.AdapterService set-test-mode enabled"); 284 } 285 disableTestMode()286 private void disableTestMode() { 287 runShellCommandOrThrow("dumpsys activity service" 288 + " com.android.bluetooth/.btservice.AdapterService set-test-mode disabled"); 289 } 290 291 } 292