diff --git a/framework-t/Android.bp b/framework-t/Android.bp index f485a4472b..9203a3ed27 100644 --- a/framework-t/Android.bp +++ b/framework-t/Android.bp @@ -118,6 +118,7 @@ java_library { "framework-bluetooth", "framework-wifi", "framework-connectivity-pre-jarjar", + "framework-location.stubs.module_lib", ], visibility: ["//packages/modules/Connectivity:__subpackages__"], } @@ -140,6 +141,7 @@ java_sdk_library { "sdk_module-lib_current_framework-connectivity", ], libs: [ + "framework-location.stubs.module_lib", "sdk_module-lib_current_framework-connectivity", ], permitted_packages: [ diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt index 8251f85623..1f1953cf53 100644 --- a/framework-t/api/system-current.txt +++ b/framework-t/api/system-current.txt @@ -59,11 +59,17 @@ package android.nearby { } public class NearbyManager { + method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getPoweredOffFindingMode(); method public void queryOffloadCapability(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer); + method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingEphemeralIds(@NonNull java.util.List); + method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingMode(int); method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void startBroadcast(@NonNull android.nearby.BroadcastRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.BroadcastCallback); method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int startScan(@NonNull android.nearby.ScanRequest, @NonNull java.util.concurrent.Executor, @NonNull android.nearby.ScanCallback); method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopBroadcast(@NonNull android.nearby.BroadcastCallback); method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void stopScan(@NonNull android.nearby.ScanCallback); + field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1; // 0x1 + field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2; // 0x2 + field @FlaggedApi("com.android.nearby.flags.powered_off_finding") public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0; // 0x0 } public final class OffloadCapability implements android.os.Parcelable { diff --git a/nearby/framework/Android.bp b/nearby/framework/Android.bp index 0fd9a89372..4be102c207 100644 --- a/nearby/framework/Android.bp +++ b/nearby/framework/Android.bp @@ -50,6 +50,7 @@ java_library { "androidx.annotation_annotation", "framework-annotations-lib", "framework-bluetooth", + "framework-location.stubs.module_lib", ], static_libs: [ "modules-utils-preconditions", diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl index 7af271ecf2..21ae0ac6cc 100644 --- a/nearby/framework/java/android/nearby/INearbyManager.aidl +++ b/nearby/framework/java/android/nearby/INearbyManager.aidl @@ -20,6 +20,7 @@ import android.nearby.IBroadcastListener; import android.nearby.IScanListener; import android.nearby.BroadcastRequestParcelable; import android.nearby.ScanRequest; +import android.nearby.PoweredOffFindingEphemeralId; import android.nearby.aidl.IOffloadCallback; /** @@ -40,4 +41,10 @@ interface INearbyManager { void stopBroadcast(in IBroadcastListener callback, String packageName, @nullable String attributionTag); void queryOffloadCapability(in IOffloadCallback callback) ; -} \ No newline at end of file + + void setPoweredOffFindingEphemeralIds(in List eids); + + void setPoweredOffModeEnabled(boolean enabled); + + boolean getPoweredOffModeEnabled(); +} diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java index 00f1c38923..cae653d889 100644 --- a/nearby/framework/java/android/nearby/NearbyManager.java +++ b/nearby/framework/java/android/nearby/NearbyManager.java @@ -18,6 +18,7 @@ package android.nearby; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -25,9 +26,12 @@ import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.bluetooth.BluetoothManager; import android.content.Context; +import android.location.LocationManager; import android.nearby.aidl.IOffloadCallback; import android.os.RemoteException; +import android.os.SystemProperties; import android.provider.Settings; import android.util.Log; @@ -37,6 +41,7 @@ import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; +import java.util.List; import java.util.Objects; import java.util.WeakHashMap; import java.util.concurrent.Executor; @@ -75,8 +80,51 @@ public class NearbyManager { int ERROR = 2; } + /** + * Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not + * supported the device. + */ + @FlaggedApi("com.android.nearby.flags.powered_off_finding") + public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0; + + /** + * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link + * #setPoweredOffFindingMode(int)} when powered off finding is supported but disabled. The + * device will not start to advertise when powered off. + */ + @FlaggedApi("com.android.nearby.flags.powered_off_finding") + public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1; + + /** + * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link + * #setPoweredOffFindingMode(int)} when powered off finding is enabled. The device will start to + * advertise when powered off. + */ + @FlaggedApi("com.android.nearby.flags.powered_off_finding") + public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2; + + /** + * Powered off finding modes. + * + * @hide + */ + @IntDef( + prefix = {"POWERED_OFF_FINDING_MODE"}, + value = { + POWERED_OFF_FINDING_MODE_UNSUPPORTED, + POWERED_OFF_FINDING_MODE_DISABLED, + POWERED_OFF_FINDING_MODE_ENABLED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PoweredOffFindingMode {} + private static final String TAG = "NearbyManager"; + private static final int POWERED_OFF_FINDING_EID_LENGTH = 20; + + private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY = + "ro.bluetooth.finder.supported"; + /** * TODO(b/286137024): Remove this when CTS R5 is rolled out. * Whether allows Fast Pair to scan. @@ -456,4 +504,124 @@ public class NearbyManager { "successfully %s Fast Pair scan", enable ? "enables" : "disables")); } + /** + * Sets the precomputed EIDs for advertising when the phone is powered off. The Bluetooth + * controller will store these EIDs in its memory, and will start advertising them in Find My + * Device network EID frames when powered off, only if the powered off finding mode was + * previously enabled by calling {@link #setPoweredOffFindingMode(int)}. + * + *

The EIDs are cryptographic ephemeral identifiers that change periodically, based on the + * Android clock at the time of the shutdown. They are used as the public part of asymmetric key + * pairs. Members of the Find My Device network can use them to encrypt the location of where + * they sight the advertising device. Only someone in possession of the private key (the device + * owner or someone that the device owner shared the key with) can decrypt this encrypted + * location. + * + *

Android will typically call this method during the shutdown process. Even after the + * method was called, it is still possible to call {#link setPoweredOffFindingMode() to disable + * the advertisement, for example to temporarily disable it for a single shutdown. + * + *

If called more than once, the EIDs of the most recent call overrides the EIDs from any + * previous call. + * + * @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes + */ + @FlaggedApi("com.android.nearby.flags.powered_off_finding") + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public void setPoweredOffFindingEphemeralIds(@NonNull List eids) { + Objects.requireNonNull(eids); + if (!isPoweredOffFindingSupported()) { + throw new UnsupportedOperationException( + "Powered off finding is not supported on this device"); + } + List ephemeralIdList = eids.stream().map( + eid -> { + Preconditions.checkArgument(eid.length == POWERED_OFF_FINDING_EID_LENGTH); + PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId(); + ephemeralId.bytes = eid; + return ephemeralId; + }).toList(); + try { + mService.setPoweredOffFindingEphemeralIds(ephemeralIdList); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + } + + /** + * Turns the powered off finding on or off. Power off finding will operate only if this method + * was called at least once since boot, and the value of the argument {@code + * poweredOffFindinMode} was {@link #POWERED_OFF_FINDING_MODE_ENABLED} the last time the method + * was called. + * + *

When an Android device with the powered off finding feature is turned off (either as part + * of a normal shutdown or due to dead battery), its Bluetooth chip starts to advertise Find My + * Device network EID frames with the EID payload that were provided by the last call to {@link + * #setPoweredOffFindingEphemeralIds(List)}. These EIDs can be sighted by other Android devices + * in BLE range that are part of the Find My Device network. The Android sighters use the EID to + * encrypt the location of the Android device and upload it to the server, in a way that only + * the owner of the advertising device, or people that the owner shared their encryption key + * with, can decrypt the location. + * + * @param poweredOffFindingMode {@link #POWERED_OFF_FINDING_MODE_ENABLED} or {@link + * #POWERED_OFF_FINDING_MODE_DISABLED} + * + * @throws IllegalStateException if called with {@link #POWERED_OFF_FINDING_MODE_ENABLED} when + * Bluetooth or location services are disabled + */ + @FlaggedApi("com.android.nearby.flags.powered_off_finding") + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) { + Preconditions.checkArgument( + poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED + || poweredOffFindingMode == POWERED_OFF_FINDING_MODE_DISABLED, + "invalid poweredOffFindingMode"); + if (!isPoweredOffFindingSupported()) { + throw new UnsupportedOperationException( + "Powered off finding is not supported on this device"); + } + if (poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED) { + Preconditions.checkState(areLocationAndBluetoothEnabled(), + "Location services and Bluetooth must be on"); + } + try { + mService.setPoweredOffModeEnabled( + poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the state of the powered off finding feature. + * + *

{@link #POWERED_OFF_FINDING_MODE_UNSUPPORTED} if the feature is not supported by the + * device, {@link #POWERED_OFF_FINDING_MODE_DISABLED} if this was the last value set by {@link + * #setPoweredOffFindingMode(int)} or if no value was set since boot, {@link + * #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link + * #setPoweredOffFindingMode(int)} + */ + @FlaggedApi("com.android.nearby.flags.powered_off_finding") + @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) + public @PoweredOffFindingMode int getPoweredOffFindingMode() { + if (!isPoweredOffFindingSupported()) { + return POWERED_OFF_FINDING_MODE_UNSUPPORTED; + } + try { + return mService.getPoweredOffModeEnabled() + ? POWERED_OFF_FINDING_MODE_ENABLED : POWERED_OFF_FINDING_MODE_DISABLED; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private boolean isPoweredOffFindingSupported() { + return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY)); + } + + private boolean areLocationAndBluetoothEnabled() { + return mContext.getSystemService(BluetoothManager.class).getAdapter().isEnabled() + && mContext.getSystemService(LocationManager.class).isLocationEnabled(); + } } diff --git a/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl new file mode 100644 index 0000000000..9f4bfef6df --- /dev/null +++ b/nearby/framework/java/android/nearby/PoweredOffFindingEphemeralId.aidl @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024, 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.nearby; + +/** + * Find My Device network ephemeral ID for powered off finding. + * + * @hide + */ +parcelable PoweredOffFindingEphemeralId { + byte[20] bytes; +} diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java index 3c183ec22c..1575f0776a 100644 --- a/nearby/service/java/com/android/server/nearby/NearbyService.java +++ b/nearby/service/java/com/android/server/nearby/NearbyService.java @@ -35,12 +35,14 @@ import android.nearby.IBroadcastListener; import android.nearby.INearbyManager; import android.nearby.IScanListener; import android.nearby.NearbyManager; +import android.nearby.PoweredOffFindingEphemeralId; import android.nearby.ScanRequest; import android.nearby.aidl.IOffloadCallback; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.server.nearby.injector.Injector; +import com.android.server.nearby.managers.BluetoothFinderManager; import com.android.server.nearby.managers.BroadcastProviderManager; import com.android.server.nearby.managers.DiscoveryManager; import com.android.server.nearby.managers.DiscoveryProviderManager; @@ -50,6 +52,8 @@ import com.android.server.nearby.util.identity.CallerIdentity; import com.android.server.nearby.util.permissions.BroadcastPermissions; import com.android.server.nearby.util.permissions.DiscoveryPermissions; +import java.util.List; + /** Service implementing nearby functionality. */ public class NearbyService extends INearbyManager.Stub { public static final String TAG = "NearbyService"; @@ -79,6 +83,7 @@ public class NearbyService extends INearbyManager.Stub { }; private final DiscoveryManager mDiscoveryProviderManager; private final BroadcastProviderManager mBroadcastProviderManager; + private final BluetoothFinderManager mBluetoothFinderManager; public NearbyService(Context context) { mContext = context; @@ -90,6 +95,7 @@ public class NearbyService extends INearbyManager.Stub { mNearbyConfiguration.refactorDiscoveryManager() ? new DiscoveryProviderManager(context, mInjector) : new DiscoveryProviderManagerLegacy(context, mInjector); + mBluetoothFinderManager = new BluetoothFinderManager(); } @VisibleForTesting @@ -148,6 +154,30 @@ public class NearbyService extends INearbyManager.Stub { mDiscoveryProviderManager.queryOffloadCapability(callback); } + @Override + public void setPoweredOffFindingEphemeralIds(List eids) { + // Permissions check + enforceBluetoothPrivilegedPermission(mContext); + + mBluetoothFinderManager.sendEids(eids); + } + + @Override + public void setPoweredOffModeEnabled(boolean enabled) { + // Permissions check + enforceBluetoothPrivilegedPermission(mContext); + + mBluetoothFinderManager.setPoweredOffFinderMode(enabled); + } + + @Override + public boolean getPoweredOffModeEnabled() { + // Permissions check + enforceBluetoothPrivilegedPermission(mContext); + + return mBluetoothFinderManager.getPoweredOffFinderMode(); + } + /** * Called by the service initializer. * diff --git a/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java new file mode 100644 index 0000000000..63ff516ce5 --- /dev/null +++ b/nearby/service/java/com/android/server/nearby/managers/BluetoothFinderManager.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 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 com.android.server.nearby.managers; + +import android.nearby.PoweredOffFindingEphemeralId; + +import java.util.List; + +/** Connects to {@link IBluetoothFinder} HAL and invokes its API. */ +// A placeholder implementation until the HAL API can be used. +public class BluetoothFinderManager { + + private boolean mPoweredOffFindingModeEnabled = false; + + /** An empty implementation of the corresponding HAL API call. */ + public void sendEids(List eids) {} + + /** A placeholder implementation of the corresponding HAL API call. */ + public void setPoweredOffFinderMode(boolean enable) { + mPoweredOffFindingModeEnabled = enable; + } + + /** A placeholder implementation of the corresponding HAL API call. */ + public boolean getPoweredOffFinderMode() { + return mPoweredOffFindingModeEnabled; + } +} diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp index aa2806d1b7..800930342c 100644 --- a/nearby/tests/cts/fastpair/Android.bp +++ b/nearby/tests/cts/fastpair/Android.bp @@ -34,6 +34,7 @@ android_test { "framework-bluetooth.stubs.module_lib", "framework-configinfrastructure", "framework-connectivity-t.impl", + "framework-location.stubs.module_lib", ], srcs: ["src/**/*.java"], test_suites: [ diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java index bc9691da33..832ac038bf 100644 --- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java +++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java @@ -25,12 +25,14 @@ import static android.nearby.ScanCallback.ERROR_UNSUPPORTED; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeTrue; import android.app.UiAutomation; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.bluetooth.cts.BTAdapterUtils; import android.content.Context; +import android.location.LocationManager; import android.nearby.BroadcastCallback; import android.nearby.BroadcastRequest; import android.nearby.NearbyDevice; @@ -42,6 +44,8 @@ import android.nearby.PrivateCredential; import android.nearby.ScanCallback; import android.nearby.ScanRequest; import android.os.Build; +import android.os.Process; +import android.os.UserHandle; import android.provider.DeviceConfig; import androidx.annotation.NonNull; @@ -50,6 +54,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; +import com.android.compatibility.common.util.SystemUtil; import com.android.modules.utils.build.SdkLevel; import org.junit.Before; @@ -57,6 +62,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.Collections; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -189,6 +195,92 @@ public class NearbyManagerTest { mScanCallback.onError(ERROR_UNSUPPORTED); } + @Test + public void testsetPoweredOffFindingEphemeralIds() { + // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used. + assumeTrue(SdkLevel.isAtLeastV()); + // Only test supporting devices. + if (mNearbyManager.getPoweredOffFindingMode() + == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return; + + mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20], new byte[20])); + } + + @Test + public void testsetPoweredOffFindingEphemeralIds_noPrivilegedPermission() { + // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used. + assumeTrue(SdkLevel.isAtLeastV()); + // Only test supporting devices. + if (mNearbyManager.getPoweredOffFindingMode() + == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return; + + mUiAutomation.dropShellPermissionIdentity(); + + assertThrows(SecurityException.class, + () -> mNearbyManager.setPoweredOffFindingEphemeralIds(List.of(new byte[20]))); + } + + + @Test + public void testSetAndGetPoweredOffFindingMode_enabled() { + // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used. + assumeTrue(SdkLevel.isAtLeastV()); + // Only test supporting devices. + if (mNearbyManager.getPoweredOffFindingMode() + == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return; + + enableLocation(); + // enableLocation() has dropped shell permission identity. + mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_PRIVILEGED); + + mNearbyManager.setPoweredOffFindingMode( + NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED); + assertThat(mNearbyManager.getPoweredOffFindingMode()) + .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED); + } + + @Test + public void testSetAndGetPoweredOffFindingMode_disabled() { + // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used. + assumeTrue(SdkLevel.isAtLeastV()); + // Only test supporting devices. + if (mNearbyManager.getPoweredOffFindingMode() + == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return; + + mNearbyManager.setPoweredOffFindingMode( + NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED); + assertThat(mNearbyManager.getPoweredOffFindingMode()) + .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED); + } + + @Test + public void testSetPoweredOffFindingMode_noPrivilegedPermission() { + // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used. + assumeTrue(SdkLevel.isAtLeastV()); + // Only test supporting devices. + if (mNearbyManager.getPoweredOffFindingMode() + == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return; + + enableLocation(); + mUiAutomation.dropShellPermissionIdentity(); + + assertThrows(SecurityException.class, () -> mNearbyManager + .setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED)); + } + + @Test + public void testGetPoweredOffFindingMode_noPrivilegedPermission() { + // Replace with minSdkVersion when Build.VERSION_CODES.VANILLA_ICE_CREAM can be used. + assumeTrue(SdkLevel.isAtLeastV()); + // Only test supporting devices. + if (mNearbyManager.getPoweredOffFindingMode() + == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return; + + mUiAutomation.dropShellPermissionIdentity(); + + assertThrows(SecurityException.class, () -> mNearbyManager.getPoweredOffFindingMode()); + } + private void enableBluetooth() { BluetoothManager manager = mContext.getSystemService(BluetoothManager.class); BluetoothAdapter bluetoothAdapter = manager.getAdapter(); @@ -197,6 +289,13 @@ public class NearbyManagerTest { } } + private void enableLocation() { + LocationManager locationManager = mContext.getSystemService(LocationManager.class); + UserHandle user = Process.myUserHandle(); + SystemUtil.runWithShellPermissionIdentity( + mUiAutomation, () -> locationManager.setLocationEnabledForUser(true, user)); + } + private static class OffloadCallback implements Consumer { @Override public void accept(OffloadCapability aBoolean) { diff --git a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt index 506b4e2e9f..b949720c3d 100644 --- a/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt +++ b/nearby/tests/integration/privileged/src/android/nearby/integration/privileged/NearbyManagerTest.kt @@ -29,6 +29,7 @@ import android.nearby.ScanRequest import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -96,4 +97,49 @@ class NearbyManagerTest { ) nearbyManager.stopBroadcast(broadcastCallback) } + + /** Verify privileged app can set powered off finding ephemeral IDs without exception. */ + @Test + fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromPrivilegedApp_succeed() { + val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager + // Only test supporting devices. + if (nearbyManager.getPoweredOffFindingMode() + == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return + + val eid = ByteArray(20) + + nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid)) + } + + /** + * Verifies that [NearbyManager.setPoweredOffFindingEphemeralIds] checkes the ephemeral ID + * length. + */ + @Test + fun testNearbyManagerSetPoweredOffFindingEphemeralIds_wrongSize_throwsException() { + val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager + // Only test supporting devices. + if (nearbyManager.getPoweredOffFindingMode() + == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return + + assertThrows(IllegalArgumentException::class.java) { + nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(21))) + } + assertThrows(IllegalArgumentException::class.java) { + nearbyManager.setPoweredOffFindingEphemeralIds(listOf(ByteArray(19))) + } + } + + /** Verify privileged app can set and get powered off finding mode without exception. */ + @Test + fun testNearbyManagerSetGetPoweredOffMode_fromPrivilegedApp_succeed() { + val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager + // Only test supporting devices. + if (nearbyManager.getPoweredOffFindingMode() + == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) return + + nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED) + assertThat(nearbyManager.getPoweredOffFindingMode()) + .isEqualTo(NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED) + } } diff --git a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt index 7bf9f63dfa..015d022494 100644 --- a/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt +++ b/nearby/tests/integration/untrusted/src/android/nearby/integration/untrusted/NearbyManagerTest.kt @@ -30,12 +30,12 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.uiautomator.LogcatWaitMixin import com.google.common.truth.Truth.assertThat +import java.time.Duration +import java.util.Calendar import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import java.time.Duration -import java.util.Calendar @RunWith(AndroidJUnit4::class) class NearbyManagerTest { @@ -151,6 +151,46 @@ class NearbyManagerTest { ).isTrue() } + /** + * Verify untrusted app can't set powered off finding ephemeral IDs because it needs + * BLUETOOTH_PRIVILEGED permission which is not for use by third-party applications. + */ + @Test + fun testNearbyManagerSetPoweredOffFindingEphemeralIds_fromUnTrustedApp_throwsException() { + val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager + val eid = ByteArray(20) + + assertThrows(SecurityException::class.java) { + nearbyManager.setPoweredOffFindingEphemeralIds(listOf(eid)) + } + } + + /** + * Verify untrusted app can't set powered off finding mode because it needs BLUETOOTH_PRIVILEGED + * permission which is not for use by third-party applications. + */ + @Test + fun testNearbyManagerSetPoweredOffFindingMode_fromUnTrustedApp_throwsException() { + val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager + + assertThrows(SecurityException::class.java) { + nearbyManager.setPoweredOffFindingMode(NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED) + } + } + + /** + * Verify untrusted app can't get powered off finding mode because it needs BLUETOOTH_PRIVILEGED + * permission which is not for use by third-party applications. + */ + @Test + fun testNearbyManagerGetPoweredOffFindingMode_fromUnTrustedApp_throwsException() { + val nearbyManager = appContext.getSystemService(Context.NEARBY_SERVICE) as NearbyManager + + assertThrows(SecurityException::class.java) { + nearbyManager.getPoweredOffFindingMode() + } + } + companion object { private val WAIT_INVALID_OPERATIONS_LOGS_TIMEOUT = Duration.ofSeconds(5) }