Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1719,9 +1719,20 @@ private void handleContinuousPingUpdate(final String beaconId, final BleSoundTri
}

private void stopContinuousPing() {
if (this.continuousPingDisposable != null && !this.continuousPingDisposable.isDisposed()) {
final boolean wasRinging = this.continuousPingDisposable != null
&& !this.continuousPingDisposable.isDisposed();
if (wasRinging) {
this.continuousPingDisposable.dispose();
}
// The last trigger may still be sounding (a DULT device plays until told otherwise or
// its own timeout), so tell it to stop rather than only not asking again
final BeaconData ringing = this.continuousPingBeaconId == null ? null
: this.beacons.get(this.continuousPingBeaconId);
if (wasRinging && ringing != null) {
final var async = AppDependencies.accessorySoundTrigger()
.stopSound(this.getApplicationContext(), ringing.getInfo().getOwnedBeaconAccessoryJson())
.subscribe(() -> Log.d(TAG, "Stop sent (or nothing to stop)"));
}
this.continuousPingDisposable = null;

if (this.continuousPingBeaconId != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,13 @@ public interface AccessorySoundTrigger {
* end the loop. Dispose the subscription to stop.
*/
Observable<BleSoundTriggerUpdate> playSoundContinuously(Context context, String accessoryJson);

/**
* Asks a sounding accessory to stop, where its protocol has a stop command (DULT's
* Sound_Stop). AirTags stop by themselves and have none, so the default does nothing.
* Never errors; best effort.
*/
default io.reactivex.rxjava3.core.Completable stopSound(Context context, String accessoryJson) {
return io.reactivex.rxjava3.core.Completable.complete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,19 @@ public Observable<BleSoundTriggerUpdate> playSound(final Context context, final
}).subscribeOn(Schedulers.io());
}

@Override
public io.reactivex.rxjava3.core.Completable stopSound(final Context context, final String accessoryJson) {
return io.reactivex.rxjava3.core.Completable.defer(() -> {
if (!this.permissionCheck.granted(context)) return io.reactivex.rxjava3.core.Completable.complete();
final Map<String, Integer> candidates = this.macResolver.currentMacAddresses(accessoryJson);
if (candidates == null || candidates.isEmpty()) return io.reactivex.rxjava3.core.Completable.complete();
return this.scanner.findNearby(context, candidates.keySet(), SCAN_TIMEOUT_MS)
.flatMapCompletable(device -> device instanceof android.bluetooth.BluetoothDevice
? BleGattSoundTrigger.stopDult(context, (android.bluetooth.BluetoothDevice) device)
: io.reactivex.rxjava3.core.Completable.complete());
}).onErrorComplete().subscribeOn(Schedulers.io());
}

/**
* {@link GattTrigger#trigger}, retried up to {@code attemptsLeft} times as long as each
* failure is {@link BleSoundTriggerStatus#FAILED} - see {@link #GATT_ATTEMPTS}. Only the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,66 @@ private static void writeDescriptorCompat(
gatt.writeDescriptor(descriptor);
}
}

/** DULT Sound_Stop, little-endian like the start opcode. */
private static final byte[] DULT_STOP_OPCODE = {0x01, 0x03};

/**
* Connects and writes DULT's Sound_Stop, if the device has the DULT service; completes
* either way (and on any failure) within about ten seconds.
*/
@SuppressLint("MissingPermission")
public static io.reactivex.rxjava3.core.Completable stopDult(
final Context context, final BluetoothDevice device) {
return io.reactivex.rxjava3.core.Completable.create(emitter -> {
final BluetoothGatt[] gattRef = new BluetoothGatt[1];
final BluetoothGattCallback callback = new BluetoothGattCallback() {
private BluetoothGattCharacteristic dult;

@Override
public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
gatt.close();
emitter.onComplete();
}
}

@Override
public void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
this.dult = characteristicOf(gatt, DULT_SERVICE, DULT_CHARACTERISTIC);
if (this.dult == null) {
gatt.disconnect();
return;
}
gatt.setCharacteristicNotification(this.dult, true);
final BluetoothGattDescriptor descriptor = this.dult.getDescriptor(CCCD);
if (descriptor == null) {
writeCharacteristicCompat(gatt, this.dult, DULT_STOP_OPCODE);
} else {
writeDescriptorCompat(gatt, descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
}
}

@Override
public void onDescriptorWrite(final BluetoothGatt gatt, final BluetoothGattDescriptor d, final int status) {
if (this.dult != null) writeCharacteristicCompat(gatt, this.dult, DULT_STOP_OPCODE);
}

@Override
public void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic c, final int status) {
Log.i(TAG, "DULT stop written to " + device.getAddress() + " (status=" + status + ")");
gatt.disconnect();
}
};
gattRef[0] = device.connectGatt(context, false, callback);
emitter.setCancellable(() -> {
if (gattRef[0] != null) {
gattRef[0].disconnect();
gattRef[0].close();
}
});
}).timeout(10, java.util.concurrent.TimeUnit.SECONDS).onErrorComplete();
}
}