diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 297239a070f46..535d6a2779f9d 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -917,8 +917,10 @@ package android.app { public class StatusBarManager { method @NonNull @RequiresPermission(android.Manifest.permission.STATUS_BAR) public android.app.StatusBarManager.DisableInfo getDisableInfo(); method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public int getNavBarModeOverride(); + method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void registerNearbyMediaDevicesProvider(@NonNull android.media.NearbyMediaDevicesProvider); method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setDisabledForSetup(boolean); method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setNavBarModeOverride(int); + method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void unregisterNearbyMediaDevicesProvider(@NonNull android.media.NearbyMediaDevicesProvider); method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void updateMediaTapToTransferReceiverDisplay(int, @NonNull android.media.MediaRoute2Info, @Nullable android.graphics.drawable.Icon, @Nullable CharSequence); method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void updateMediaTapToTransferSenderDisplay(int, @NonNull android.media.MediaRoute2Info, @Nullable java.util.concurrent.Executor, @Nullable Runnable); field public static final int MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER = 0; // 0x0 @@ -6257,6 +6259,26 @@ package android.media { field public static final int SYNC_EVENT_SHARE_AUDIO_HISTORY = 100; // 0x64 } + public final class NearbyDevice implements android.os.Parcelable { + ctor public NearbyDevice(@NonNull String, int); + method public int describeContents(); + method @NonNull public String getMediaRoute2Id(); + method public int getRangeZone(); + method @NonNull public static String rangeZoneToString(int); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public static final int RANGE_CLOSE = 3; // 0x3 + field public static final int RANGE_FAR = 1; // 0x1 + field public static final int RANGE_LONG = 2; // 0x2 + field public static final int RANGE_UNKNOWN = 0; // 0x0 + field public static final int RANGE_WITHIN_REACH = 4; // 0x4 + } + + public interface NearbyMediaDevicesProvider { + method public void registerNearbyDevicesCallback(@NonNull java.util.function.Consumer>); + method public void unregisterNearbyDevicesCallback(@NonNull java.util.function.Consumer>); + } + public class PlayerProxy { method public void pause(); method public void setPan(float); diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 5f0034285f0ac..0326e11b643e9 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -28,7 +28,11 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; import android.graphics.drawable.Icon; +import android.media.INearbyMediaDevicesProvider; +import android.media.INearbyMediaDevicesUpdateCallback; import android.media.MediaRoute2Info; +import android.media.NearbyDevice; +import android.media.NearbyMediaDevicesProvider; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -46,6 +50,9 @@ import com.android.internal.statusbar.NotificationVisibility; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; @@ -503,6 +510,16 @@ public class StatusBarManager { @Retention(RetentionPolicy.SOURCE) public @interface MediaTransferReceiverState {} + /** + * A map from a provider registered in + * {@link #registerNearbyMediaDevicesProvider(NearbyMediaDevicesProvider)} to the wrapper + * around the provider that was created internally. We need the wrapper to make the provider + * binder-compatible, and we need to store a reference to the wrapper so that when the provider + * is un-registered, we un-register the saved wrapper instance. + */ + private final Map + nearbyMediaDevicesProviderMap = new HashMap<>(); + @UnsupportedAppUsage private Context mContext; private IStatusBarService mService; @@ -1033,6 +1050,67 @@ public class StatusBarManager { } } + /** + * Registers a provider that notifies callbacks about the status of nearby devices that are able + * to play media. + *

+ * If multiple providers are registered, all the providers will be used for nearby device + * information. + *

+ * @param provider the nearby device information provider to register + *

+ * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) + public void registerNearbyMediaDevicesProvider( + @NonNull NearbyMediaDevicesProvider provider + ) { + Objects.requireNonNull(provider); + if (nearbyMediaDevicesProviderMap.containsKey(provider)) { + return; + } + try { + final IStatusBarService svc = getService(); + NearbyMediaDevicesProviderWrapper providerWrapper = + new NearbyMediaDevicesProviderWrapper(provider); + nearbyMediaDevicesProviderMap.put(provider, providerWrapper); + svc.registerNearbyMediaDevicesProvider(providerWrapper); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Unregisters a provider that gives information about nearby devices that are able to play + * media. + *

+ * See {@link registerNearbyMediaDevicesProvider}. + *

+ * @param provider the nearby device information provider to unregister + *

+ * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) + public void unregisterNearbyMediaDevicesProvider( + @NonNull NearbyMediaDevicesProvider provider + ) { + Objects.requireNonNull(provider); + if (!nearbyMediaDevicesProviderMap.containsKey(provider)) { + return; + } + try { + final IStatusBarService svc = getService(); + NearbyMediaDevicesProviderWrapper providerWrapper = + nearbyMediaDevicesProviderMap.get(provider); + nearbyMediaDevicesProviderMap.remove(provider); + svc.unregisterNearbyMediaDevicesProvider(providerWrapper); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** @hide */ public static String windowStateToString(int state) { if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING"; @@ -1340,4 +1418,48 @@ public class StatusBarManager { } } } + + /** + * @hide + */ + static final class NearbyMediaDevicesProviderWrapper extends INearbyMediaDevicesProvider.Stub { + @NonNull + private final NearbyMediaDevicesProvider mProvider; + // Because we're wrapping a {@link NearbyMediaDevicesProvider} in a binder-compatible + // interface, we also need to wrap the callbacks that the provider receives. We use + // this map to keep track of the original callback and the wrapper callback so that + // unregistering the callback works correctly. + @NonNull + private final Map>> + mRegisteredCallbacks = new HashMap<>(); + + NearbyMediaDevicesProviderWrapper(@NonNull NearbyMediaDevicesProvider provider) { + mProvider = provider; + } + + @Override + public void registerNearbyDevicesCallback( + @NonNull INearbyMediaDevicesUpdateCallback callback) { + Consumer> callbackAsConsumer = nearbyDevices -> { + try { + callback.onDevicesUpdated(nearbyDevices); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + }; + + mRegisteredCallbacks.put(callback, callbackAsConsumer); + mProvider.registerNearbyDevicesCallback(callbackAsConsumer); + } + + @Override + public void unregisterNearbyDevicesCallback( + @NonNull INearbyMediaDevicesUpdateCallback callback) { + if (!mRegisteredCallbacks.containsKey(callback)) { + return; + } + mProvider.unregisterNearbyDevicesCallback(mRegisteredCallbacks.get(callback)); + mRegisteredCallbacks.remove(callback); + } + } } diff --git a/core/java/android/media/NearbyMediaDevicesProvider.java b/core/java/android/media/NearbyMediaDevicesProvider.java new file mode 100644 index 0000000000000..7cc799b66dee8 --- /dev/null +++ b/core/java/android/media/NearbyMediaDevicesProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 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.media; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.app.StatusBarManager; + +import java.util.List; +import java.util.function.Consumer; + +/** + * An interface that provides information about nearby devices that are able to play media. + *

+ * External clients can implement this interface and pass it to the system via + * {@link StatusBarManager#registerNearbyMediaDevicesProvider} to inform the system of nearby media + * devices. + *

+ * @hide + */ +@SystemApi +public interface NearbyMediaDevicesProvider { + /** + * Registers a callback that should be notified each time nearby media device(s) change. + *

+ * When a callback is newly registered, it should be immediately notified of the current nearby + * media devices. Afterwards, the list of devices passed to the callback should always contain + * the full set of nearby media devices any time you get an update. If a device is no longer + * valid (went offline, e.g.) then it should be omitted from the list in the next update. + *

+ * @param callback the callback that will consume updates to the nearby media devices. + */ + void registerNearbyDevicesCallback(@NonNull Consumer> callback); + + /** + * Unregisters a callback. @see #registerNearbyDevicesCallback. + *

+ * @param callback the callback to unregister. + */ + void unregisterNearbyDevicesCallback(@NonNull Consumer> callback); +} diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 46f54ce5ef59a..099d1fc933d22 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -24,6 +24,7 @@ import android.hardware.biometrics.IBiometricContextListener; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.fingerprint.IUdfpsHbmListener; +import android.media.INearbyMediaDevicesProvider; import android.media.MediaRoute2Info; import android.os.Bundle; import android.os.ParcelFileDescriptor; @@ -311,4 +312,10 @@ oneway interface IStatusBar in MediaRoute2Info routeInfo, in Icon appIcon, in CharSequence appName); + + /** Registers a nearby media devices provider. */ + void registerNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider); + + /** Unregisters a nearby media devices provider. */ + void unregisterNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 6c17df1295b2c..f6a1cc0fd59c2 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -24,6 +24,7 @@ import android.hardware.biometrics.IBiometricContextListener; import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.fingerprint.IUdfpsHbmListener; +import android.media.INearbyMediaDevicesProvider; import android.media.MediaRoute2Info; import android.net.Uri; import android.os.Bundle; @@ -211,4 +212,10 @@ interface IStatusBarService in MediaRoute2Info routeInfo, in Icon appIcon, in CharSequence appName); + + /** Registers a nearby media devices provider. */ + void registerNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider); + + /** Unregisters a nearby media devices provider. */ + void unregisterNearbyMediaDevicesProvider(in INearbyMediaDevicesProvider provider); } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/media/INearbyMediaDevicesProvider.aidl b/media/java/android/media/INearbyMediaDevicesProvider.aidl similarity index 59% rename from packages/SystemUI/shared/src/com/android/systemui/shared/media/INearbyMediaDevicesProvider.aidl rename to media/java/android/media/INearbyMediaDevicesProvider.aidl index 6db06f046c551..533012335009b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/media/INearbyMediaDevicesProvider.aidl +++ b/media/java/android/media/INearbyMediaDevicesProvider.aidl @@ -14,25 +14,20 @@ * limitations under the License. */ -package com.android.systemui.shared.media; +package android.media; -import com.android.systemui.shared.media.INearbyMediaDevicesUpdateCallback; -import com.android.systemui.shared.media.NearbyDevice; +import android.media.INearbyMediaDevicesUpdateCallback; +import android.media.NearbyDevice; /** - * An interface that provides information about nearby devices that are able to play media. + * A binder-compatible version of {@link android.media.NearbyMediaDevicesProvider}. See that class + * for more information. * - * External clients will implement this interface and System UI will invoke it if it's passed to - * SystemUI via {@link INearbyMediaDevicesService.registerProvider}. + * @hide */ interface INearbyMediaDevicesProvider { /** - * Returns a list of nearby devices that are able to play media. - */ - List getCurrentNearbyDevices() = 1; - - /** - * Registers a callback that will be notified each time the status of a nearby device changes. + * Registers a callback that should be notified each time nearby media device(s) change. */ oneway void registerNearbyDevicesCallback(in INearbyMediaDevicesUpdateCallback callback) = 2; diff --git a/media/java/android/media/INearbyMediaDevicesUpdateCallback.aidl b/media/java/android/media/INearbyMediaDevicesUpdateCallback.aidl new file mode 100644 index 0000000000000..24397bb9073cb --- /dev/null +++ b/media/java/android/media/INearbyMediaDevicesUpdateCallback.aidl @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 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.media; + +import android.media.NearbyDevice; + +/** + * A callback used to receive updates about the status of nearby devices that are able to play + * media. + * + * External clients may allow registration of these callbacks and external clients will be + * responsible for notifying the callbacks appropriately. + * + * @hide + */ +oneway interface INearbyMediaDevicesUpdateCallback { + /** + * Invoked by external clients when changes in nearby media device(s) are detected. + * + * When a callback is newly registered, it should be immediately notified of the current nearby + * media devices. Afterwards, the list of devices passed to the callback should always contain + * the full set of nearby media devices any time you get an update. If a device is no longer + * valid (went offline, e.g.) then it should be omitted from the list in the next update. + * + * @param nearbyDevices the list of nearby devices that have changed status due to moving closer + * or further away. + */ + void onDevicesUpdated(in List nearbyDevices); +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/media/NearbyDevice.aidl b/media/java/android/media/NearbyDevice.aidl similarity index 93% rename from packages/SystemUI/shared/src/com/android/systemui/shared/media/NearbyDevice.aidl rename to media/java/android/media/NearbyDevice.aidl index 62b50ed5f953b..d6140a79a5df4 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/media/NearbyDevice.aidl +++ b/media/java/android/media/NearbyDevice.aidl @@ -14,6 +14,6 @@ * limitations under the License. */ -package com.android.systemui.shared.media; +package android.media; parcelable NearbyDevice; diff --git a/media/java/android/media/NearbyDevice.java b/media/java/android/media/NearbyDevice.java new file mode 100644 index 0000000000000..cb85109be6d1e --- /dev/null +++ b/media/java/android/media/NearbyDevice.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2022 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.media; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A parcelable representing a nearby device that can be used for media transfer. + *

+ * This class includes: + *

    + *
  • an ID identifying the media route.
  • + *
  • a range zone specifying how far away this device is from the device with the media route. + *
  • + *
+ * @hide + */ +@SystemApi +public final class NearbyDevice implements Parcelable { + /** + * Unknown distance range. + */ + public static final int RANGE_UNKNOWN = 0; + + /** + * Distance is very far away from the peer device. + */ + public static final int RANGE_FAR = 1; + + /** + * Distance is relatively long from the peer device, typically a few meters. + */ + public static final int RANGE_LONG = 2; + + /** + * Distance is close to the peer device, typically with one or two meter. + */ + public static final int RANGE_CLOSE = 3; + + /** + * Distance is very close to the peer device, typically within one meter or less. + */ + public static final int RANGE_WITHIN_REACH = 4; + + /** + * The various range zones a device can be in, in relation to the current device. + * + * @hide + */ + @IntDef(prefix = { "RANGE_" }, value = { + RANGE_UNKNOWN, + RANGE_FAR, + RANGE_LONG, + RANGE_CLOSE, + RANGE_WITHIN_REACH + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RangeZone {} + + /** + * Gets a human-readable string of the range zone. + */ + @NonNull + public static String rangeZoneToString(@RangeZone int rangeZone) { + switch (rangeZone) { + case RANGE_UNKNOWN: + return "UNKNOWN"; + case RANGE_FAR: + return "FAR"; + case RANGE_LONG: + return "LONG"; + case RANGE_CLOSE: + return "CLOSE"; + case RANGE_WITHIN_REACH: + return "WITHIN_REACH"; + default: + return "Invalid"; + } + } + + @NonNull private final String mMediaRoute2Id; + @RangeZone private final int mRangeZone; + + public NearbyDevice(@NonNull String mediaRoute2Id, int rangeZone) { + mMediaRoute2Id = mediaRoute2Id; + mRangeZone = rangeZone; + } + + private NearbyDevice(@NonNull Parcel in) { + mMediaRoute2Id = in.readString(); + mRangeZone = in.readInt(); + } + + @NonNull + public static final Creator CREATOR = new Creator() { + @Override + public NearbyDevice createFromParcel(@NonNull Parcel in) { + return new NearbyDevice(in); + } + + @Override + public NearbyDevice[] newArray(int size) { + return new NearbyDevice[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "NearbyDevice{mediaRoute2Id=" + mMediaRoute2Id + + " rangeZone=" + rangeZoneToString(mRangeZone) + "}"; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mMediaRoute2Id); + dest.writeInt(mRangeZone); + } + + /** + * Returns the ID of the media route associated with the device. + * + * @see MediaRoute2Info#getId + */ + @NonNull + public String getMediaRoute2Id() { + return mMediaRoute2Id; + } + + /** Returns the range that the device is currently in. */ + @RangeZone + public int getRangeZone() { + return mRangeZone; + } +} diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 6b6aa71d74c0a..ad6074a93c96c 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -886,12 +886,6 @@ android:singleUser="true" android:permission="android.permission.BIND_DREAM_SERVICE" /> - - - - diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/media/INearbyMediaDevicesService.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/media/INearbyMediaDevicesService.aidl deleted file mode 100644 index 4f3e10d801f0c..0000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/media/INearbyMediaDevicesService.aidl +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2022 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.systemui.shared.media; - -import com.android.systemui.shared.media.INearbyMediaDevicesProvider; - -/** - * An interface that can be invoked to notify System UI of nearby media devices. - * - * External clients wanting to notify System UI about the status of nearby media devices should - * implement {@link INearbyMediaDevicesProvider} and then register it with system UI using this - * service. - * - * System UI will implement this interface and external clients will invoke it. - */ -interface INearbyMediaDevicesService { - /** Registers a new provider. */ - oneway void registerProvider(INearbyMediaDevicesProvider provider) = 1; -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/media/INearbyMediaDevicesUpdateCallback.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/media/INearbyMediaDevicesUpdateCallback.aidl deleted file mode 100644 index a835f52278521..0000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/media/INearbyMediaDevicesUpdateCallback.aidl +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2022 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.systemui.shared.media; - -/** - * A callback used to notify implementors of changes in the status of nearby devices that are able - * to play media. - * - * External clients may allow registration of these callbacks and external clients will be - * responsible for notifying the callbacks appropriately. System UI is only a mediator between the - * external client and these callbacks. - */ -interface INearbyMediaDevicesUpdateCallback { - /** Unknown distance range. */ - const int RANGE_UNKNOWN = 0; - /** Distance is very far away from the peer device. */ - const int RANGE_FAR = 1; - /** Distance is relatively long from the peer device, typically a few meters. */ - const int RANGE_LONG = 2; - /** Distance is close to the peer device, typically with one or two meter. */ - const int RANGE_CLOSE = 3; - /** Distance is very close to the peer device, typically within one meter or less. */ - const int RANGE_WITHIN_REACH = 4; - - /** Invoked by external clients when media device changes are detected. */ - oneway void nearbyDeviceUpdate(in String routeId, in int rangeZone) = 1; -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/media/NearbyDevice.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/media/NearbyDevice.kt deleted file mode 100644 index 9cab3ab355ddb..0000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/media/NearbyDevice.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2022 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.systemui.shared.media - -import android.os.Parcel -import android.os.Parcelable - -/** - * A parcelable representing a nearby device that can be used for media transfer. - * - * This class includes: - * - [routeId] identifying the media route - * - [rangeZone] specifying how far away the device with the media route is from this device. - */ -class NearbyDevice( - val routeId: String?, - @RangeZone val rangeZone: Int -) : Parcelable { - - private constructor(parcel: Parcel) : this( - routeId = parcel.readString() ?: null, - rangeZone = parcel.readInt() - ) - - override fun describeContents() = 0 - - override fun writeToParcel(out: Parcel, flags: Int) { - out.writeString(routeId) - out.writeInt(rangeZone) - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel) = NearbyDevice(parcel) - override fun newArray(size: Int) = arrayOfNulls(size) - } -} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/media/RangeZone.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/media/RangeZone.kt deleted file mode 100644 index b5eaff6e1cd9e..0000000000000 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/media/RangeZone.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2022 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.systemui.shared.media - -import androidx.annotation.IntDef -import kotlin.annotation.AnnotationRetention - -@IntDef( - INearbyMediaDevicesUpdateCallback.RANGE_UNKNOWN, - INearbyMediaDevicesUpdateCallback.RANGE_FAR, - INearbyMediaDevicesUpdateCallback.RANGE_LONG, - INearbyMediaDevicesUpdateCallback.RANGE_CLOSE, - INearbyMediaDevicesUpdateCallback.RANGE_WITHIN_REACH -) -@Retention(AnnotationRetention.SOURCE) -/** The various range zones a device can be in, in relation to the current device. */ -annotation class RangeZone diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 81fa99a8d22b2..701972ab7264c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.PerUser; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardSliceProvider; import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; +import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper; import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver; import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender; @@ -152,6 +153,7 @@ public interface SysUIComponent { getMediaTttChipControllerReceiver(); getMediaTttCommandLineHelper(); getMediaMuteAwaitConnectionCli(); + getNearbyMediaDevicesManager(); getUnfoldLatencyTracker().init(); getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init); getFoldStateLogger().ifPresent(FoldStateLogger::init); @@ -231,6 +233,9 @@ public interface SysUIComponent { /** */ Optional getMediaMuteAwaitConnectionCli(); + /** */ + Optional getNearbyMediaDevicesManager(); + /** * Returns {@link CoreStartable}s that should be started with the application. */ diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 1ba6e34533109..55e48a7f7501a 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -146,6 +146,7 @@ public class Flags { public static final BooleanFlag MEDIA_TAP_TO_TRANSFER = new BooleanFlag(900, true); public static final BooleanFlag MEDIA_SESSION_ACTIONS = new BooleanFlag(901, true); public static final BooleanFlag MEDIA_SESSION_LAYOUT = new BooleanFlag(902, false); + public static final BooleanFlag MEDIA_NEARBY_DEVICES = new BooleanFlag(903, true); public static final BooleanFlag MEDIA_MUTE_AWAIT = new BooleanFlag(904, true); // 1000 - dock diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt index e1467683c986c..dd35a9a813996 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaFlags.kt @@ -42,4 +42,10 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { * Check whether we support displaying information about mute await connections. */ fun areMuteAwaitConnectionsEnabled() = featureFlags.isEnabled(Flags.MEDIA_MUTE_AWAIT) + + /** + * Check whether we enable support for nearby media devices. See + * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information. + */ + fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES) } diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index 3225f73adb82a..2dff9472be688 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -16,7 +16,6 @@ package com.android.systemui.media.dagger; -import android.app.Service; import android.content.Context; import android.os.Handler; import android.view.WindowManager; @@ -30,7 +29,7 @@ import com.android.systemui.media.MediaHost; import com.android.systemui.media.MediaHostStatesManager; import com.android.systemui.media.dream.dagger.MediaComplicationComponent; import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli; -import com.android.systemui.media.nearby.NearbyMediaDevicesService; +import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper; import com.android.systemui.media.taptotransfer.MediaTttFlags; import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver; @@ -43,11 +42,9 @@ import java.util.concurrent.Executor; import javax.inject.Named; -import dagger.Binds; +import dagger.Lazy; import dagger.Module; import dagger.Provides; -import dagger.multibindings.ClassKey; -import dagger.multibindings.IntoMap; /** Dagger module for the media package. */ @Module(subcomponents = { @@ -159,9 +156,15 @@ public interface MediaModule { return Optional.of(new MediaMuteAwaitConnectionCli(commandRegistry, context)); } - /** Inject into NearbyMediaDevicesService. */ - @Binds - @IntoMap - @ClassKey(NearbyMediaDevicesService.class) - Service bindMediaNearbyDevicesService(NearbyMediaDevicesService service); + /** */ + @Provides + @SysUISingleton + static Optional providesNearbyMediaDevicesManager( + MediaFlags mediaFlags, + Lazy nearbyMediaDevicesManagerLazy) { + if (!mediaFlags.areNearbyMediaDevicesEnabled()) { + return Optional.empty(); + } + return Optional.of(nearbyMediaDevicesManagerLazy.get()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt new file mode 100644 index 0000000000000..9875ffbaf668e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesManager.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 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.systemui.media.nearby + +import android.media.INearbyMediaDevicesProvider +import android.media.INearbyMediaDevicesUpdateCallback +import com.android.systemui.dagger.SysUISingleton +import android.os.IBinder +import com.android.systemui.statusbar.CommandQueue +import javax.inject.Inject + +/** + * A service that acts as a bridge between (1) external clients that have data on nearby devices + * that are able to play media and (2) internal clients (like media Output Switcher) that need data + * on these nearby devices. + * + * TODO(b/216313420): Add logging to this class. + */ +@SysUISingleton +class NearbyMediaDevicesManager @Inject constructor( + commandQueue: CommandQueue +) { + private var providers: MutableList = mutableListOf() + private var activeCallbacks: MutableList = mutableListOf() + + private val commandQueueCallbacks = object : CommandQueue.Callbacks { + override fun registerNearbyMediaDevicesProvider(newProvider: INearbyMediaDevicesProvider) { + if (providers.contains(newProvider)) { + return + } + activeCallbacks.forEach { + newProvider.registerNearbyDevicesCallback(it) + } + providers.add(newProvider) + newProvider.asBinder().linkToDeath(deathRecipient, /* flags= */ 0) + } + + override fun unregisterNearbyMediaDevicesProvider( + newProvider: INearbyMediaDevicesProvider + ) { + providers.remove(newProvider) + } + } + + private val deathRecipient = object : IBinder.DeathRecipient { + override fun binderDied() { + // Should not be used as binderDied(IBinder who) is overridden. + } + + override fun binderDied(who: IBinder) { + binderDiedInternal(who) + } + } + + init { + commandQueue.addCallback(commandQueueCallbacks) + } + + /** + * Registers [callback] to be notified each time a device's range changes or when a new device + * comes within range. + * + * If a new provider is added, previously-registered callbacks will be registered with the + * new provider. + */ + fun registerNearbyDevicesCallback(callback: INearbyMediaDevicesUpdateCallback) { + providers.forEach { + it.registerNearbyDevicesCallback(callback) + } + activeCallbacks.add(callback) + } + + /** + * Un-registers [callback]. See [registerNearbyDevicesCallback]. + */ + fun unregisterNearbyDevicesCallback(callback: INearbyMediaDevicesUpdateCallback) { + activeCallbacks.remove(callback) + providers.forEach { + it.unregisterNearbyDevicesCallback(callback) + } + } + + private fun binderDiedInternal(who: IBinder) { + synchronized(providers) { + for (i in providers.size - 1 downTo 0) { + if (providers[i].asBinder() == who) { + providers.removeAt(i) + break + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesService.kt b/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesService.kt deleted file mode 100644 index eaf2bd96e37a9..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/nearby/NearbyMediaDevicesService.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2022 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.systemui.media.nearby - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.shared.media.INearbyMediaDevicesProvider -import com.android.systemui.shared.media.INearbyMediaDevicesService -import com.android.systemui.shared.media.INearbyMediaDevicesUpdateCallback -import com.android.systemui.shared.media.NearbyDevice -import javax.inject.Inject - -/** - * A service that acts as a bridge between (1) external clients that have data on nearby devices - * that are able to play media and (2) internal clients (like media Output Switcher) that need data - * on these nearby devices. - * - * TODO(b/216313420): Add logging to this class. - */ -@SysUISingleton -class NearbyMediaDevicesService @Inject constructor() : Service() { - - private var provider: INearbyMediaDevicesProvider? = null - - private val binder: IBinder = object : INearbyMediaDevicesService.Stub() { - override fun registerProvider(newProvider: INearbyMediaDevicesProvider) { - provider = newProvider - newProvider.asBinder().linkToDeath( - { - // We might've gotten a new provider before the old provider died, so we only - // need to clear our provider if the most recent provider died. - if (provider == newProvider) { - provider = null - } - }, - /* flags= */ 0 - ) - } - } - - override fun onBind(intent: Intent?): IBinder = binder - - /** Returns a list containing the current nearby devices. */ - fun getCurrentNearbyDevices(): List { - val currentProvider = provider ?: return emptyList() - return currentProvider.currentNearbyDevices - } - - /** - * Registers [callback] to be notified each time a device's range changes or when a new device - * comes within range. - */ - fun registerNearbyDevicesCallback(callback: INearbyMediaDevicesUpdateCallback) { - val currentProvider = provider ?: return - currentProvider.registerNearbyDevicesCallback(callback) - } - - /** - * Un-registers [callback]. See [registerNearbyDevicesCallback]. - */ - fun unregisterNearbyDevicesCallback(callback: INearbyMediaDevicesUpdateCallback) { - val currentProvider = provider ?: return - currentProvider.unregisterNearbyDevicesCallback(callback) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index b355b0551abec..645c5accb5418 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -43,6 +43,7 @@ import android.hardware.biometrics.PromptInfo; import android.hardware.display.DisplayManager; import android.hardware.fingerprint.IUdfpsHbmListener; import android.inputmethodservice.InputMethodService.BackDispositionMode; +import android.media.INearbyMediaDevicesProvider; import android.media.MediaRoute2Info; import android.os.Bundle; import android.os.Handler; @@ -160,6 +161,8 @@ public class CommandQueue extends IStatusBar.Stub implements private static final int MSG_SET_BIOMETRICS_LISTENER = 63 << MSG_SHIFT; private static final int MSG_MEDIA_TRANSFER_SENDER_STATE = 64 << MSG_SHIFT; private static final int MSG_MEDIA_TRANSFER_RECEIVER_STATE = 65 << MSG_SHIFT; + private static final int MSG_REGISTER_NEARBY_MEDIA_DEVICE_PROVIDER = 66 << MSG_SHIFT; + private static final int MSG_UNREGISTER_NEARBY_MEDIA_DEVICE_PROVIDER = 67 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -456,6 +459,18 @@ public class CommandQueue extends IStatusBar.Stub implements @NonNull MediaRoute2Info routeInfo, @Nullable Icon appIcon, @Nullable CharSequence appName) {} + + /** + * @see IStatusBar#registerNearbyMediaDevicesProvider + */ + default void registerNearbyMediaDevicesProvider( + @NonNull INearbyMediaDevicesProvider provider) {} + + /** + * @see IStatusBar#unregisterNearbyMediaDevicesProvider + */ + default void unregisterNearbyMediaDevicesProvider( + @NonNull INearbyMediaDevicesProvider provider) {} } public CommandQueue(Context context) { @@ -1221,6 +1236,18 @@ public class CommandQueue extends IStatusBar.Stub implements mHandler.obtainMessage(MSG_MEDIA_TRANSFER_RECEIVER_STATE, args).sendToTarget(); } + @Override + public void registerNearbyMediaDevicesProvider(@NonNull INearbyMediaDevicesProvider provider) { + mHandler.obtainMessage(MSG_REGISTER_NEARBY_MEDIA_DEVICE_PROVIDER, provider).sendToTarget(); + } + + @Override + public void unregisterNearbyMediaDevicesProvider( + @NonNull INearbyMediaDevicesProvider provider) { + mHandler.obtainMessage(MSG_UNREGISTER_NEARBY_MEDIA_DEVICE_PROVIDER, provider) + .sendToTarget(); + } + private final class H extends Handler { private H(Looper l) { super(l); @@ -1643,6 +1670,18 @@ public class CommandQueue extends IStatusBar.Stub implements } args.recycle(); break; + case MSG_REGISTER_NEARBY_MEDIA_DEVICE_PROVIDER: + INearbyMediaDevicesProvider provider = (INearbyMediaDevicesProvider) msg.obj; + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).registerNearbyMediaDevicesProvider(provider); + } + break; + case MSG_UNREGISTER_NEARBY_MEDIA_DEVICE_PROVIDER: + provider = (INearbyMediaDevicesProvider) msg.obj; + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).unregisterNearbyMediaDevicesProvider(provider); + } + break; } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt new file mode 100644 index 0000000000000..f87a67310ff02 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesManagerTest.kt @@ -0,0 +1,146 @@ +package com.android.systemui.media.nearby + +import android.media.INearbyMediaDevicesProvider +import android.media.INearbyMediaDevicesUpdateCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import android.media.NearbyDevice +import com.android.systemui.statusbar.CommandQueue +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +@SmallTest +class NearbyMediaDevicesManagerTest : SysuiTestCase() { + + private lateinit var manager: NearbyMediaDevicesManager + @Mock + private lateinit var commandQueue: CommandQueue + private lateinit var commandQueueCallbacks: CommandQueue.Callbacks + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + manager = NearbyMediaDevicesManager(commandQueue) + + val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) + Mockito.verify(commandQueue).addCallback(callbackCaptor.capture()) + commandQueueCallbacks = callbackCaptor.value!! + } + + @Test + fun registerNearbyDevicesCallback_noProviderRegistered_noCrash() { + // No assert, just needs no crash + manager.registerNearbyDevicesCallback(object : INearbyMediaDevicesUpdateCallback.Stub() { + override fun onDevicesUpdated(nearbyDevices: List) {} + }) + } + + @Test + fun registerNearbyDevicesCallback_providerRegistered_providerReceivesCallback() { + val provider = TestProvider() + commandQueueCallbacks.registerNearbyMediaDevicesProvider(provider) + + val callback = object : INearbyMediaDevicesUpdateCallback.Stub() { + override fun onDevicesUpdated(nearbyDevices: List) {} + } + + manager.registerNearbyDevicesCallback(callback) + + assertThat(provider.lastRegisteredCallback).isEqualTo(callback) + } + + @Test + fun registerNearbyDevicesCallback_multipleProviders_allProvidersReceiveCallback() { + val provider1 = TestProvider() + val provider2 = TestProvider() + commandQueueCallbacks.registerNearbyMediaDevicesProvider(provider1) + commandQueueCallbacks.registerNearbyMediaDevicesProvider(provider2) + + val callback = object : INearbyMediaDevicesUpdateCallback.Stub() { + override fun onDevicesUpdated(nearbyDevices: List) {} + } + + manager.registerNearbyDevicesCallback(callback) + + assertThat(provider1.lastRegisteredCallback).isEqualTo(callback) + assertThat(provider2.lastRegisteredCallback).isEqualTo(callback) + } + + @Test + fun unregisterNearbyDevicesCallback_noProviderRegistered_noCrash() { + // No assert, just needs no crash + manager.unregisterNearbyDevicesCallback(object : INearbyMediaDevicesUpdateCallback.Stub() { + override fun onDevicesUpdated(nearbyDevices: List) {} + }) + } + + @Test + fun unregisterNearbyDevicesCallback_providerRegistered_providerReceivesCallback() { + val provider = TestProvider() + commandQueueCallbacks.registerNearbyMediaDevicesProvider(provider) + + val callback = object : INearbyMediaDevicesUpdateCallback.Stub() { + override fun onDevicesUpdated(nearbyDevices: List) {} + } + + manager.unregisterNearbyDevicesCallback(callback) + + assertThat(provider.lastUnregisteredCallback).isEqualTo(callback) + } + + @Test + fun unregisterNearbyDevicesCallback_multipleProviders_allProvidersReceiveCallback() { + val provider1 = TestProvider() + val provider2 = TestProvider() + commandQueueCallbacks.registerNearbyMediaDevicesProvider(provider1) + commandQueueCallbacks.registerNearbyMediaDevicesProvider(provider2) + + val callback = object : INearbyMediaDevicesUpdateCallback.Stub() { + override fun onDevicesUpdated(nearbyDevices: List) {} + } + + manager.unregisterNearbyDevicesCallback(callback) + + assertThat(provider1.lastUnregisteredCallback).isEqualTo(callback) + assertThat(provider2.lastUnregisteredCallback).isEqualTo(callback) + } + + @Test + fun newProviderRegisteredAfterCallbacksRegistered_providerGetsPreviouslyRegisteredCallbacks() { + // Start off with an existing provider and callback + val provider1 = TestProvider() + commandQueueCallbacks.registerNearbyMediaDevicesProvider(provider1) + val callback = object : INearbyMediaDevicesUpdateCallback.Stub() { + override fun onDevicesUpdated(nearbyDevices: List) {} + } + manager.registerNearbyDevicesCallback(callback) + + // Add a new provider + val provider2 = TestProvider() + commandQueueCallbacks.registerNearbyMediaDevicesProvider(provider2) + + // Verify the new provider received the previously-registered callbacks + assertThat(provider2.lastRegisteredCallback).isEqualTo(callback) + } + + private class TestProvider : INearbyMediaDevicesProvider.Stub() { + var lastRegisteredCallback: INearbyMediaDevicesUpdateCallback? = null + var lastUnregisteredCallback: INearbyMediaDevicesUpdateCallback? = null + override fun registerNearbyDevicesCallback( + callback: INearbyMediaDevicesUpdateCallback + ) { + lastRegisteredCallback = callback + } + + override fun unregisterNearbyDevicesCallback( + callback: INearbyMediaDevicesUpdateCallback + ) { + lastUnregisteredCallback = callback + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesServiceTest.kt deleted file mode 100644 index c26108619ff4f..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/media/nearby/NearbyMediaDevicesServiceTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.android.systemui.media.nearby - -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.shared.media.INearbyMediaDevicesProvider -import com.android.systemui.shared.media.INearbyMediaDevicesService -import com.android.systemui.shared.media.INearbyMediaDevicesUpdateCallback -import com.android.systemui.shared.media.INearbyMediaDevicesUpdateCallback.RANGE_LONG -import com.android.systemui.shared.media.INearbyMediaDevicesUpdateCallback.RANGE_WITHIN_REACH -import com.android.systemui.shared.media.NearbyDevice -import com.google.common.truth.Truth.assertThat -import org.junit.Before -import org.junit.Test - -@SmallTest -class NearbyMediaDevicesServiceTest : SysuiTestCase() { - - private lateinit var service: NearbyMediaDevicesService - private lateinit var binderInterface: INearbyMediaDevicesService - - @Before - fun setUp() { - service = NearbyMediaDevicesService() - binderInterface = INearbyMediaDevicesService.Stub.asInterface(service.onBind(null)) - } - - @Test - fun getCurrentNearbyDevices_noProviderRegistered_returnsEmptyList() { - assertThat(service.getCurrentNearbyDevices()).isEmpty() - } - - @Test - fun getCurrentNearbyDevices_providerRegistered_returnsProviderInfo() { - val nearbyDevice1 = NearbyDevice("routeId1", RANGE_LONG) - val nearbyDevice2 = NearbyDevice("routeId2", RANGE_WITHIN_REACH) - val provider = object : INearbyMediaDevicesProvider.Stub() { - override fun getCurrentNearbyDevices(): List { - return listOf(nearbyDevice1, nearbyDevice2) - } - - override fun registerNearbyDevicesCallback( - callback: INearbyMediaDevicesUpdateCallback? - ) {} - override fun unregisterNearbyDevicesCallback( - callback: INearbyMediaDevicesUpdateCallback? - ) {} - } - binderInterface.registerProvider(provider) - - val returnedNearbyDevices = service.getCurrentNearbyDevices() - - assertThat(returnedNearbyDevices).isEqualTo(listOf(nearbyDevice1, nearbyDevice2)) - } - - @Test - fun registerNearbyDevicesCallback_noProviderRegistered_noCrash() { - // No assert, just needs no crash - service.registerNearbyDevicesCallback(object : INearbyMediaDevicesUpdateCallback.Stub() { - override fun nearbyDeviceUpdate(routeId: String?, rangeZone: Int) {} - }) - } - - @Test - fun registerNearbyDevicesCallback_providerRegistered_providerReceivesCallback() { - val provider = object : INearbyMediaDevicesProvider.Stub() { - var registeredCallback: INearbyMediaDevicesUpdateCallback? = null - override fun getCurrentNearbyDevices(): List = listOf() - - override fun registerNearbyDevicesCallback( - callback: INearbyMediaDevicesUpdateCallback? - ) { - registeredCallback = callback - } - - override fun unregisterNearbyDevicesCallback( - callback: INearbyMediaDevicesUpdateCallback? - ) {} - } - binderInterface.registerProvider(provider) - - val callback = object : INearbyMediaDevicesUpdateCallback.Stub() { - override fun nearbyDeviceUpdate(routeId: String?, rangeZone: Int) {} - } - - service.registerNearbyDevicesCallback(callback) - - assertThat(provider.registeredCallback).isEqualTo(callback) - } - - @Test - fun unregisterNearbyDevicesCallback_noProviderRegistered_noCrash() { - // No assert, just needs no crash - service.unregisterNearbyDevicesCallback(object : INearbyMediaDevicesUpdateCallback.Stub() { - override fun nearbyDeviceUpdate(routeId: String?, rangeZone: Int) {} - }) - } - - @Test - fun unregisterNearbyDevicesCallback_providerRegistered_providerReceivesCallback() { - val provider = object : INearbyMediaDevicesProvider.Stub() { - var unregisteredCallback: INearbyMediaDevicesUpdateCallback? = null - override fun getCurrentNearbyDevices(): List = listOf() - - override fun registerNearbyDevicesCallback( - callback: INearbyMediaDevicesUpdateCallback? - ) {} - - override fun unregisterNearbyDevicesCallback( - callback: INearbyMediaDevicesUpdateCallback? - ) { - unregisteredCallback = callback - } - } - binderInterface.registerProvider(provider) - - val callback = object : INearbyMediaDevicesUpdateCallback.Stub() { - override fun nearbyDeviceUpdate(routeId: String?, rangeZone: Int) {} - } - - service.unregisterNearbyDevicesCallback(callback) - - assertThat(provider.unregisteredCallback).isEqualTo(callback) - } -} diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 2049f3d3be245..8fee5392a08a4 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -53,6 +53,7 @@ import android.hardware.biometrics.PromptInfo; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.hardware.fingerprint.IUdfpsHbmListener; +import android.media.INearbyMediaDevicesProvider; import android.media.MediaRoute2Info; import android.net.Uri; import android.os.Binder; @@ -2045,6 +2046,56 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } } + /** + * Registers a provider that gives information about nearby devices that are able to play media. + * See {@link StatusBarmanager.registerNearbyMediaDevicesProvider}. + * + * Requires the caller to have the {@link android.Manifest.permission.MEDIA_CONTENT_CONTROL} + * permission. + * + * @param provider the nearby device information provider to register + * + * @hide + */ + @Override + public void registerNearbyMediaDevicesProvider( + @NonNull INearbyMediaDevicesProvider provider + ) { + enforceMediaContentControl(); + if (mBar != null) { + try { + mBar.registerNearbyMediaDevicesProvider(provider); + } catch (RemoteException e) { + Slog.e(TAG, "registerNearbyMediaDevicesProvider", e); + } + } + } + + /** + * Unregisters a provider that gives information about nearby devices that are able to play + * media. See {@link StatusBarmanager.unregisterNearbyMediaDevicesProvider}. + * + * Requires the caller to have the {@link android.Manifest.permission.MEDIA_CONTENT_CONTROL} + * permission. + * + * @param provider the nearby device information provider to unregister + * + * @hide + */ + @Override + public void unregisterNearbyMediaDevicesProvider( + @NonNull INearbyMediaDevicesProvider provider + ) { + enforceMediaContentControl(); + if (mBar != null) { + try { + mBar.unregisterNearbyMediaDevicesProvider(provider); + } catch (RemoteException e) { + Slog.e(TAG, "unregisterNearbyMediaDevicesProvider", e); + } + } + } + /** @hide */ public void passThroughShellCommand(String[] args, FileDescriptor fd) { enforceStatusBarOrShell();