Loading android/app/src/com/android/bluetooth/audio_util/MediaBrowserWrapper.java 0 → 100644 +389 −0 Original line number Original line Diff line number Diff line /* * Copyright 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.bluetooth.audio_util; import android.content.ComponentName; import android.content.Context; import android.media.browse.MediaBrowser.MediaItem; import android.os.Handler; import android.os.Looper; import android.util.Log; import com.android.bluetooth.R; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; /** * Handles API calls to a MediaBrowser. * * <p>{@link MediaBrowser} APIs work with callbacks only and need a connection beforehand. * * <p>This class handles the connection then will call the appropriate API and trigger the given * callback when it gets the answer from MediaBrowser. */ class MediaBrowserWrapper { private static final String TAG = MediaBrowserWrapper.class.getSimpleName(); /** * Some devices will continuously request each item in a folder one at a time. * * <p>This timeout is here to remove the connection between this class and the {@link * MediaBrowser} after a certain time without requests from the remote device to browse the * player. If the next request happens soon after, the bound will still exist. * * <p>Note: Previous implementation was keeping a local list of fetched items, this worked at * the cost of not having the items actualized if fetching again the same folder. */ private static final Duration BROWSER_DISCONNECT_TIMEOUT = Duration.ofSeconds(5); private enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, } public interface RequestCallback { void run(); } public interface GetPlayerRootCallback { void run(String rootId); } public interface GetFolderItemsCallback { void run(String parentId, List<ListItem> items); } private final MediaBrowser mWrappedBrowser; private final Context mContext; private final Looper mLooper; private final String mPackageName; private final Handler mRunHandler; private ConnectionState mBrowserConnectionState = ConnectionState.DISCONNECTED; private final ArrayList<RequestCallback> mRequestsList = new ArrayList<>(); // GetFolderItems also works with a callback, so we need to store all requests made before we // got the results and prevent new subscriptions. private final HashMap<String, ArrayList<GetFolderItemsCallback>> mSubscribedIds = new HashMap<>(); private final Runnable mDisconnectRunnable = () -> _disconnect(); public MediaBrowserWrapper( Context context, Looper looper, String packageName, String className) { mContext = context; mPackageName = packageName; mLooper = looper; mRunHandler = new Handler(mLooper); mWrappedBrowser = MediaBrowserFactory.make( context, new ComponentName(packageName, className), new MediaConnectionCallback(), null); } /** Returns the package name of the {@link MediaBrowser}. */ public final String getPackageName() { Log.v(TAG, "getPackageName: " + mPackageName); return mPackageName; } /** Retrieves the root path of the {@link MediaBrowser}. */ public void getRootId(GetPlayerRootCallback callback) { Log.v(TAG, "getRootId: " + mPackageName); browseRequest( () -> { if (mBrowserConnectionState != ConnectionState.CONNECTED) { Log.e(TAG, "getRootId: cb triggered but MediaBrowser is not connected."); callback.run(""); return; } String rootId = mWrappedBrowser.getRoot(); Log.v(TAG, "getRootId for " + mPackageName + ": " + rootId); callback.run(rootId); setDisconnectDelay(); }); } /** Plays the specified {@code mediaId}. */ public void playItem(String mediaId) { Log.v(TAG, "playItem for " + mPackageName + ": " + mediaId); browseRequest( () -> { if (mBrowserConnectionState != ConnectionState.CONNECTED) { Log.e(TAG, "playItem: cb triggered but MediaBrowser is not connected."); return; } setDisconnectDelay(); // Retrieve the MediaController linked with this MediaBrowser. // Note that the MediaBrowser should be connected for this. MediaController controller = MediaControllerFactory.make( mContext, mWrappedBrowser.getSessionToken()); // Retrieve TransportControls from this MediaController and play mediaId MediaController.TransportControls ctrl = controller.getTransportControls(); Log.v(TAG, "playItem for " + mPackageName + ": " + mediaId + " playing."); ctrl.playFromMediaId(mediaId, null); }); } /** * Retrieves the content of a specific {@link MediaBrowser} path. * * @param mediaId the path to retrieve content of * @param callback to be called when the content list is retrieved */ public void getFolderItems(String mediaId, GetFolderItemsCallback callback) { Log.v(TAG, "getFolderItems for " + mPackageName + " and " + mediaId); browseRequest( () -> { if (mBrowserConnectionState != ConnectionState.CONNECTED) { Log.e( TAG, "getFolderItems: cb triggered but MediaBrowser is not connected."); callback.run(mediaId, Collections.emptyList()); return; } setDisconnectDelay(); if (mSubscribedIds.containsKey(mediaId)) { Log.v( TAG, "getFolderItems for " + mPackageName + " and " + mediaId + ": adding callback, already subscribed."); ArrayList<GetFolderItemsCallback> newList = (ArrayList) mSubscribedIds.get(mediaId); newList.add(callback); mSubscribedIds.put(mediaId, newList); return; } Log.v( TAG, "getFolderItems for " + mPackageName + " and " + mediaId + ": adding callback and subscribing."); mSubscribedIds.put(mediaId, new ArrayList<>(Arrays.asList(callback))); mWrappedBrowser.subscribe( mediaId, new BrowserSubscriptionCallback(mLooper, mediaId)); }); } /** * Requests information from {@link MediaBrowser}. * * <p>If the {@link MediaBrowser} this instance wraps around is already connected, calls the * callback directly. * * <p>If it is connecting, adds the callback to the {@code mRequestsList}, to be called once the * connection is done. * * <p>If the connection isn't started, starts it and adds the callback to the {@code * mRequestsList} */ private void browseRequest(RequestCallback callback) { mRunHandler.post( () -> { switch (mBrowserConnectionState) { case CONNECTED: callback.run(); break; case DISCONNECTED: connect(); mRequestsList.add(callback); break; case CONNECTING: mRequestsList.add(callback); break; } }); } /** Connects to the {@link MediaBrowser} this instance wraps around. */ private void connect() { if (mBrowserConnectionState != ConnectionState.DISCONNECTED) { Log.e( TAG, "Trying to bind to a player that is not disconnected: " + mBrowserConnectionState); return; } mBrowserConnectionState = ConnectionState.CONNECTING; Log.v(TAG, "connect: " + mPackageName + " connecting"); mWrappedBrowser.connect(); } /** Disconnects from the {@link MediaBrowser} */ public void disconnect() { mRunHandler.post(() -> _disconnect()); } private void _disconnect() { mRunHandler.removeCallbacks(mDisconnectRunnable); if (mBrowserConnectionState == ConnectionState.DISCONNECTED) { Log.e( TAG, "disconnect: Trying to disconnect a player that is not connected: " + mBrowserConnectionState); return; } mBrowserConnectionState = ConnectionState.DISCONNECTED; Log.v(TAG, "disconnect: " + mPackageName + " disconnected"); mWrappedBrowser.disconnect(); } /** Sets the delay before the disconnection from the {@link MediaBrowser} happens. */ private void setDisconnectDelay() { mRunHandler.removeCallbacks(mDisconnectRunnable); mRunHandler.postDelayed(mDisconnectRunnable, BROWSER_DISCONNECT_TIMEOUT.toMillis()); } /** Callback for {@link MediaBrowser} connection. */ private class MediaConnectionCallback extends MediaBrowser.ConnectionCallback { @Override public void onConnected() { mRunHandler.post( () -> { mBrowserConnectionState = ConnectionState.CONNECTED; Log.v(TAG, "MediaConnectionCallback: " + mPackageName + " onConnected"); runCallbacks(); }); } @Override public void onConnectionFailed() { mRunHandler.post( () -> { Log.e( TAG, "MediaConnectionCallback: " + mPackageName + " onConnectionFailed"); mBrowserConnectionState = ConnectionState.DISCONNECTED; runCallbacks(); }); } @Override public void onConnectionSuspended() { mRunHandler.post( () -> { Log.e( TAG, "MediaConnectionCallback: " + mPackageName + " onConnectionSuspended"); runCallbacks(); mWrappedBrowser.disconnect(); }); } /** * Executes all the callbacks stored during the connection process * * <p>This has to run on constructor's Looper. */ private void runCallbacks() { for (RequestCallback callback : mRequestsList) { callback.run(); } mRequestsList.clear(); } } private class BrowserSubscriptionCallback extends MediaBrowser.SubscriptionCallback { private final Runnable mTimeoutRunnable; private boolean mCallbacksExecuted = false; public BrowserSubscriptionCallback(Looper looper, String mediaId) { mTimeoutRunnable = () -> { executeCallbacks(mediaId, new ArrayList<>()); }; mRunHandler.postDelayed(mTimeoutRunnable, BROWSER_DISCONNECT_TIMEOUT.toMillis()); } private void executeCallbacks(String parentId, ArrayList<ListItem> browsableContent) { if (mCallbacksExecuted) { return; } mCallbacksExecuted = true; mRunHandler.removeCallbacks(mTimeoutRunnable); for (GetFolderItemsCallback callback : mSubscribedIds.get(parentId)) { Log.v( TAG, "getFolderItems for " + mPackageName + " and " + parentId + ": callback called with " + browsableContent.size() + " items."); callback.run(parentId, browsableContent); } mSubscribedIds.remove(parentId); mWrappedBrowser.unsubscribe(parentId); } @Override public void onChildrenLoaded(String parentId, List<MediaItem> children) { ArrayList<ListItem> browsableContent = new ArrayList<>(); for (MediaItem item : children) { if (item.isBrowsable()) { String title = item.getDescription().getTitle().toString(); if (title.isEmpty()) { title = mContext.getString(R.string.not_provided); } Folder f = new Folder(item.getMediaId(), false, title); browsableContent.add(new ListItem(f)); } else { Metadata data = Util.toMetadata(mContext, item); if (Util.isEmptyData(data)) { continue; } browsableContent.add(new ListItem(data)); } } mRunHandler.post(() -> executeCallbacks(parentId, browsableContent)); } @Override public void onError(String parentId) { mRunHandler.post(() -> executeCallbacks(parentId, new ArrayList<>())); } @Override public Handler getTimeoutHandler() { return mRunHandler; } } } android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java +382 −212 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
android/app/src/com/android/bluetooth/audio_util/MediaBrowserWrapper.java 0 → 100644 +389 −0 Original line number Original line Diff line number Diff line /* * Copyright 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.bluetooth.audio_util; import android.content.ComponentName; import android.content.Context; import android.media.browse.MediaBrowser.MediaItem; import android.os.Handler; import android.os.Looper; import android.util.Log; import com.android.bluetooth.R; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; /** * Handles API calls to a MediaBrowser. * * <p>{@link MediaBrowser} APIs work with callbacks only and need a connection beforehand. * * <p>This class handles the connection then will call the appropriate API and trigger the given * callback when it gets the answer from MediaBrowser. */ class MediaBrowserWrapper { private static final String TAG = MediaBrowserWrapper.class.getSimpleName(); /** * Some devices will continuously request each item in a folder one at a time. * * <p>This timeout is here to remove the connection between this class and the {@link * MediaBrowser} after a certain time without requests from the remote device to browse the * player. If the next request happens soon after, the bound will still exist. * * <p>Note: Previous implementation was keeping a local list of fetched items, this worked at * the cost of not having the items actualized if fetching again the same folder. */ private static final Duration BROWSER_DISCONNECT_TIMEOUT = Duration.ofSeconds(5); private enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, } public interface RequestCallback { void run(); } public interface GetPlayerRootCallback { void run(String rootId); } public interface GetFolderItemsCallback { void run(String parentId, List<ListItem> items); } private final MediaBrowser mWrappedBrowser; private final Context mContext; private final Looper mLooper; private final String mPackageName; private final Handler mRunHandler; private ConnectionState mBrowserConnectionState = ConnectionState.DISCONNECTED; private final ArrayList<RequestCallback> mRequestsList = new ArrayList<>(); // GetFolderItems also works with a callback, so we need to store all requests made before we // got the results and prevent new subscriptions. private final HashMap<String, ArrayList<GetFolderItemsCallback>> mSubscribedIds = new HashMap<>(); private final Runnable mDisconnectRunnable = () -> _disconnect(); public MediaBrowserWrapper( Context context, Looper looper, String packageName, String className) { mContext = context; mPackageName = packageName; mLooper = looper; mRunHandler = new Handler(mLooper); mWrappedBrowser = MediaBrowserFactory.make( context, new ComponentName(packageName, className), new MediaConnectionCallback(), null); } /** Returns the package name of the {@link MediaBrowser}. */ public final String getPackageName() { Log.v(TAG, "getPackageName: " + mPackageName); return mPackageName; } /** Retrieves the root path of the {@link MediaBrowser}. */ public void getRootId(GetPlayerRootCallback callback) { Log.v(TAG, "getRootId: " + mPackageName); browseRequest( () -> { if (mBrowserConnectionState != ConnectionState.CONNECTED) { Log.e(TAG, "getRootId: cb triggered but MediaBrowser is not connected."); callback.run(""); return; } String rootId = mWrappedBrowser.getRoot(); Log.v(TAG, "getRootId for " + mPackageName + ": " + rootId); callback.run(rootId); setDisconnectDelay(); }); } /** Plays the specified {@code mediaId}. */ public void playItem(String mediaId) { Log.v(TAG, "playItem for " + mPackageName + ": " + mediaId); browseRequest( () -> { if (mBrowserConnectionState != ConnectionState.CONNECTED) { Log.e(TAG, "playItem: cb triggered but MediaBrowser is not connected."); return; } setDisconnectDelay(); // Retrieve the MediaController linked with this MediaBrowser. // Note that the MediaBrowser should be connected for this. MediaController controller = MediaControllerFactory.make( mContext, mWrappedBrowser.getSessionToken()); // Retrieve TransportControls from this MediaController and play mediaId MediaController.TransportControls ctrl = controller.getTransportControls(); Log.v(TAG, "playItem for " + mPackageName + ": " + mediaId + " playing."); ctrl.playFromMediaId(mediaId, null); }); } /** * Retrieves the content of a specific {@link MediaBrowser} path. * * @param mediaId the path to retrieve content of * @param callback to be called when the content list is retrieved */ public void getFolderItems(String mediaId, GetFolderItemsCallback callback) { Log.v(TAG, "getFolderItems for " + mPackageName + " and " + mediaId); browseRequest( () -> { if (mBrowserConnectionState != ConnectionState.CONNECTED) { Log.e( TAG, "getFolderItems: cb triggered but MediaBrowser is not connected."); callback.run(mediaId, Collections.emptyList()); return; } setDisconnectDelay(); if (mSubscribedIds.containsKey(mediaId)) { Log.v( TAG, "getFolderItems for " + mPackageName + " and " + mediaId + ": adding callback, already subscribed."); ArrayList<GetFolderItemsCallback> newList = (ArrayList) mSubscribedIds.get(mediaId); newList.add(callback); mSubscribedIds.put(mediaId, newList); return; } Log.v( TAG, "getFolderItems for " + mPackageName + " and " + mediaId + ": adding callback and subscribing."); mSubscribedIds.put(mediaId, new ArrayList<>(Arrays.asList(callback))); mWrappedBrowser.subscribe( mediaId, new BrowserSubscriptionCallback(mLooper, mediaId)); }); } /** * Requests information from {@link MediaBrowser}. * * <p>If the {@link MediaBrowser} this instance wraps around is already connected, calls the * callback directly. * * <p>If it is connecting, adds the callback to the {@code mRequestsList}, to be called once the * connection is done. * * <p>If the connection isn't started, starts it and adds the callback to the {@code * mRequestsList} */ private void browseRequest(RequestCallback callback) { mRunHandler.post( () -> { switch (mBrowserConnectionState) { case CONNECTED: callback.run(); break; case DISCONNECTED: connect(); mRequestsList.add(callback); break; case CONNECTING: mRequestsList.add(callback); break; } }); } /** Connects to the {@link MediaBrowser} this instance wraps around. */ private void connect() { if (mBrowserConnectionState != ConnectionState.DISCONNECTED) { Log.e( TAG, "Trying to bind to a player that is not disconnected: " + mBrowserConnectionState); return; } mBrowserConnectionState = ConnectionState.CONNECTING; Log.v(TAG, "connect: " + mPackageName + " connecting"); mWrappedBrowser.connect(); } /** Disconnects from the {@link MediaBrowser} */ public void disconnect() { mRunHandler.post(() -> _disconnect()); } private void _disconnect() { mRunHandler.removeCallbacks(mDisconnectRunnable); if (mBrowserConnectionState == ConnectionState.DISCONNECTED) { Log.e( TAG, "disconnect: Trying to disconnect a player that is not connected: " + mBrowserConnectionState); return; } mBrowserConnectionState = ConnectionState.DISCONNECTED; Log.v(TAG, "disconnect: " + mPackageName + " disconnected"); mWrappedBrowser.disconnect(); } /** Sets the delay before the disconnection from the {@link MediaBrowser} happens. */ private void setDisconnectDelay() { mRunHandler.removeCallbacks(mDisconnectRunnable); mRunHandler.postDelayed(mDisconnectRunnable, BROWSER_DISCONNECT_TIMEOUT.toMillis()); } /** Callback for {@link MediaBrowser} connection. */ private class MediaConnectionCallback extends MediaBrowser.ConnectionCallback { @Override public void onConnected() { mRunHandler.post( () -> { mBrowserConnectionState = ConnectionState.CONNECTED; Log.v(TAG, "MediaConnectionCallback: " + mPackageName + " onConnected"); runCallbacks(); }); } @Override public void onConnectionFailed() { mRunHandler.post( () -> { Log.e( TAG, "MediaConnectionCallback: " + mPackageName + " onConnectionFailed"); mBrowserConnectionState = ConnectionState.DISCONNECTED; runCallbacks(); }); } @Override public void onConnectionSuspended() { mRunHandler.post( () -> { Log.e( TAG, "MediaConnectionCallback: " + mPackageName + " onConnectionSuspended"); runCallbacks(); mWrappedBrowser.disconnect(); }); } /** * Executes all the callbacks stored during the connection process * * <p>This has to run on constructor's Looper. */ private void runCallbacks() { for (RequestCallback callback : mRequestsList) { callback.run(); } mRequestsList.clear(); } } private class BrowserSubscriptionCallback extends MediaBrowser.SubscriptionCallback { private final Runnable mTimeoutRunnable; private boolean mCallbacksExecuted = false; public BrowserSubscriptionCallback(Looper looper, String mediaId) { mTimeoutRunnable = () -> { executeCallbacks(mediaId, new ArrayList<>()); }; mRunHandler.postDelayed(mTimeoutRunnable, BROWSER_DISCONNECT_TIMEOUT.toMillis()); } private void executeCallbacks(String parentId, ArrayList<ListItem> browsableContent) { if (mCallbacksExecuted) { return; } mCallbacksExecuted = true; mRunHandler.removeCallbacks(mTimeoutRunnable); for (GetFolderItemsCallback callback : mSubscribedIds.get(parentId)) { Log.v( TAG, "getFolderItems for " + mPackageName + " and " + parentId + ": callback called with " + browsableContent.size() + " items."); callback.run(parentId, browsableContent); } mSubscribedIds.remove(parentId); mWrappedBrowser.unsubscribe(parentId); } @Override public void onChildrenLoaded(String parentId, List<MediaItem> children) { ArrayList<ListItem> browsableContent = new ArrayList<>(); for (MediaItem item : children) { if (item.isBrowsable()) { String title = item.getDescription().getTitle().toString(); if (title.isEmpty()) { title = mContext.getString(R.string.not_provided); } Folder f = new Folder(item.getMediaId(), false, title); browsableContent.add(new ListItem(f)); } else { Metadata data = Util.toMetadata(mContext, item); if (Util.isEmptyData(data)) { continue; } browsableContent.add(new ListItem(data)); } } mRunHandler.post(() -> executeCallbacks(parentId, browsableContent)); } @Override public void onError(String parentId) { mRunHandler.post(() -> executeCallbacks(parentId, new ArrayList<>())); } @Override public Handler getTimeoutHandler() { return mRunHandler; } } }
android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java +382 −212 File changed.Preview size limit exceeded, changes collapsed. Show changes