Introduce Powered Off Finding API

Bug: 307898240
Test: atest CtsNearbyFastPairTestCases
Test: atest NearbyIntegrationPrivilegedTests
Test: atest NearbyIntegrationUntrustedTests
Change-Id: I57b37ea796c0791d72fec931f731a0143e816dac
This commit is contained in:
Yahav Nussbaum 2024-01-29 12:23:28 +00:00
commit c712a571dc
8 changed files with 366 additions and 2 deletions

View file

@ -34,6 +34,7 @@ java_defaults {
name: "enable-framework-connectivity-t-targets",
enabled: true,
}
// The above defaults can be used to disable framework-connectivity t
// targets while minimizing merge conflicts in the build rules.
@ -116,6 +117,7 @@ java_library {
"framework-bluetooth",
"framework-wifi",
"framework-connectivity-pre-jarjar",
"framework-location.stubs.module_lib",
],
visibility: ["//packages/modules/Connectivity:__subpackages__"],
}
@ -138,6 +140,7 @@ java_sdk_library {
"sdk_module-lib_current_framework-connectivity",
],
libs: [
"framework-location.stubs.module_lib",
"sdk_module-lib_current_framework-connectivity",
],
permitted_packages: [

View file

@ -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<android.nearby.OffloadCapability>);
method @FlaggedApi("com.android.nearby.flags.powered_off_finding") @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public void setPoweredOffFindingEphemeralIds(@NonNull java.util.List<byte[]>);
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 {

View file

@ -49,6 +49,7 @@ java_library {
"androidx.annotation_annotation",
"framework-annotations-lib",
"framework-bluetooth",
"framework-location.stubs.module_lib",
],
static_libs: [
"modules-utils-preconditions",

View file

@ -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)}.
*
* <p>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.
*
* <p>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.
*
* <p>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<byte[]> eids) {
Objects.requireNonNull(eids);
if (!isPoweredOffFindingSupported()) {
throw new UnsupportedOperationException(
"Powered off finding is not supported on this device");
}
List<PoweredOffFindingEphemeralId> 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.
*
* <p>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.
*
* <p>{@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();
}
}

View file

@ -33,6 +33,7 @@ android_test {
"framework-bluetooth.stubs.module_lib",
"framework-configinfrastructure",
"framework-connectivity-t.impl",
"framework-location.stubs.module_lib",
],
srcs: ["src/**/*.java"],
test_suites: [

View file

@ -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<OffloadCapability> {
@Override
public void accept(OffloadCapability aBoolean) {

View file

@ -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)
}
}

View file

@ -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)
}