blob: 700ca0911dc887797a7ee0f188a0632fa5cddc7b [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.permission.cts;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static android.app.AppOpsManager.OPSTR_FINE_LOCATION;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import android.app.AppOpsManager;
import android.app.AsyncNotedAppOp;
import android.app.SyncNotedAppOp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.cts.BTAdapterUtils;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.content.AttributionSource;
import android.content.Context;
import android.content.ContextParams;
import android.content.pm.PackageManager;
import android.os.Process;
import android.os.SystemClock;
import android.platform.test.annotations.AppModeFull;
import android.util.ArraySet;
import android.util.Base64;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Tests behaviour when performing bluetooth scans with renounced location permission.
*/
public class NearbyDevicesRenouncePermissionTest {
private static final String TAG = "NearbyDevicesRenouncePermissionTest";
private static final String OPSTR_BLUETOOTH_SCAN = "android:bluetooth_scan";
private AppOpsManager mAppOpsManager;
private int mLocationNoteCount;
private int mScanNoteCount;
private Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
private BluetoothAdapter mBluetoothAdapter;
private boolean mBluetoothAdapterWasEnabled;
private enum Result {
UNKNOWN, EXCEPTION, EMPTY, FILTERED, FULL
}
private enum Scenario {
DEFAULT, RENOUNCE, RENOUNCE_MIDDLE, RENOUNCE_END
}
@Before
public void enableBluetooth() {
assumeTrue(supportsBluetooth());
mBluetoothAdapter = mContext.getSystemService(BluetoothManager.class).getAdapter();
mBluetoothAdapterWasEnabled = mBluetoothAdapter.isEnabled();
assertTrue(BTAdapterUtils.enableAdapter(mBluetoothAdapter, mContext));
enableTestMode();
}
@After
public void disableBluetooth() {
assumeTrue(supportsBluetooth());
disableTestMode();
if (!mBluetoothAdapterWasEnabled) {
assertTrue(BTAdapterUtils.disableAdapter(mBluetoothAdapter, mContext));
}
}
@Before
public void setUp() {
mAppOpsManager = getApplicationContext().getSystemService(AppOpsManager.class);
mAppOpsManager.setOnOpNotedCallback(getApplicationContext().getMainExecutor(),
new AppOpsManager.OnOpNotedCallback() {
@Override
public void onNoted(SyncNotedAppOp op) {
switch (op.getOp()) {
case OPSTR_FINE_LOCATION:
mLocationNoteCount++;
break;
case OPSTR_BLUETOOTH_SCAN:
mScanNoteCount++;
break;
default:
}
}
@Override
public void onSelfNoted(SyncNotedAppOp op) {
}
@Override
public void onAsyncNoted(AsyncNotedAppOp asyncOp) {
switch (asyncOp.getOp()) {
case OPSTR_FINE_LOCATION:
mLocationNoteCount++;
break;
case OPSTR_BLUETOOTH_SCAN:
mScanNoteCount++;
break;
default:
}
}
});
}
@After
public void tearDown() {
mAppOpsManager.setOnOpNotedCallback(null, null);
}
private void clearNoteCounts() {
mLocationNoteCount = 0;
mScanNoteCount = 0;
}
@AppModeFull
@Test
public void scanWithoutRenouncingNotesBluetoothAndLocation() throws Exception {
clearNoteCounts();
assertThat(performScan(Scenario.DEFAULT)).isEqualTo(Result.FULL);
SystemUtil.eventually(() -> {
assertThat(mLocationNoteCount).isGreaterThan(0);
assertThat(mScanNoteCount).isGreaterThan(0);
});
}
@AppModeFull
@Test
public void scanRenouncingLocationNotesBluetoothButNotLocation() throws Exception {
clearNoteCounts();
assertThat(performScan(Scenario.RENOUNCE)).isEqualTo(Result.FILTERED);
SystemUtil.eventually(() -> {
assertThat(mLocationNoteCount).isEqualTo(0);
assertThat(mScanNoteCount).isGreaterThan(0);
});
}
@AppModeFull
@Test
public void scanRenouncingInMiddleOfChainNotesBluetoothButNotLocation() throws Exception {
clearNoteCounts();
assertThat(performScan(Scenario.RENOUNCE_MIDDLE)).isEqualTo(Result.FILTERED);
SystemUtil.eventually(() -> {
assertThat(mLocationNoteCount).isEqualTo(0);
assertThat(mScanNoteCount).isGreaterThan(0);
});
}
@AppModeFull
@Test
public void scanRenouncingAtEndOfChainNotesBluetoothButNotLocation() throws Exception {
clearNoteCounts();
assertThat(performScan(Scenario.RENOUNCE_END)).isEqualTo(Result.FILTERED);
SystemUtil.eventually(() -> {
assertThat(mLocationNoteCount).isEqualTo(0);
assertThat(mScanNoteCount).isGreaterThan(0);
});
}
private Result performScan(Scenario scenario) {
try {
Context context = createContext(scenario, getApplicationContext());
final BluetoothManager bm = context.getSystemService(BluetoothManager.class);
final BluetoothLeScanner scanner = bm.getAdapter().getBluetoothLeScanner();
final HashSet<String> observed = new HashSet<>();
ScanCallback callback = new ScanCallback() {
public void onScanResult(int callbackType, ScanResult result) {
Log.v(TAG, String.valueOf(result));
observed.add(Base64.encodeToString(result.getScanRecord().getBytes(), 0));
}
public void onBatchScanResults(List<ScanResult> results) {
for (ScanResult result : results) {
onScanResult(0, result);
}
}
};
scanner.startScan(callback);
// Wait a few seconds to figure out what we actually observed
SystemClock.sleep(3000);
scanner.stopScan(callback);
switch (observed.size()) {
case 0: return Result.EMPTY;
case 1: return Result.FILTERED;
case 5: return Result.FULL;
default: return Result.UNKNOWN;
}
} catch (Throwable t) {
Log.v(TAG, "Failed to scan", t);
return Result.EXCEPTION;
}
}
private Context createContext(Scenario scenario, Context context) throws Exception {
if (scenario == Scenario.DEFAULT) {
return context;
}
Set<String> renouncedPermissions = new ArraySet<>();
renouncedPermissions.add(ACCESS_FINE_LOCATION);
switch (scenario) {
case RENOUNCE:
return SystemUtil.callWithShellPermissionIdentity(() ->
context.createContext(
new ContextParams.Builder()
.setRenouncedPermissions(renouncedPermissions)
.setAttributionTag(context.getAttributionTag())
.build())
);
case RENOUNCE_MIDDLE:
AttributionSource nextAttrib = new AttributionSource(
Process.SHELL_UID, "com.android.shell", null, (Set<String>) null, null);
return SystemUtil.callWithShellPermissionIdentity(() ->
context.createContext(
new ContextParams.Builder()
.setRenouncedPermissions(renouncedPermissions)
.setAttributionTag(context.getAttributionTag())
.setNextAttributionSource(nextAttrib)
.build())
);
case RENOUNCE_END:
nextAttrib = new AttributionSource(
Process.SHELL_UID, "com.android.shell", null, renouncedPermissions, null);
return SystemUtil.callWithShellPermissionIdentity(() ->
context.createContext(
new ContextParams.Builder()
.setAttributionTag(context.getAttributionTag())
.setNextAttributionSource(nextAttrib)
.build())
);
default:
throw new IllegalStateException();
}
}
private boolean supportsBluetooth() {
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
}
private void enableTestMode() {
runShellCommandOrThrow("dumpsys activity service"
+ " com.android.bluetooth.btservice.AdapterService set-test-mode enabled");
}
private void disableTestMode() {
runShellCommandOrThrow("dumpsys activity service"
+ " com.android.bluetooth.btservice.AdapterService set-test-mode disabled");
}
}