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