1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settingslib.media;
18 
19 import android.annotation.SuppressLint;
20 import android.content.Context;
21 import android.media.MediaRoute2Info;
22 import android.media.MediaRouter2;
23 import android.media.MediaRouter2.RoutingController;
24 import android.media.MediaRouter2Manager;
25 import android.media.RouteDiscoveryPreference;
26 import android.media.RouteListingPreference;
27 import android.media.RoutingSessionInfo;
28 import android.media.session.MediaController;
29 import android.os.UserHandle;
30 import android.text.TextUtils;
31 
32 import androidx.annotation.NonNull;
33 import androidx.annotation.Nullable;
34 
35 import com.android.media.flags.Flags;
36 import com.android.settingslib.bluetooth.LocalBluetoothManager;
37 
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.concurrent.Executor;
43 import java.util.concurrent.Executors;
44 import java.util.concurrent.atomic.AtomicReference;
45 import java.util.function.Consumer;
46 import java.util.stream.Collectors;
47 
48 /** Implements {@link InfoMediaManager} using {@link MediaRouter2}. */
49 @SuppressLint("MissingPermission")
50 public final class RouterInfoMediaManager extends InfoMediaManager {
51 
52     private static final String TAG = "RouterInfoMediaManager";
53 
54     private final MediaRouter2 mRouter;
55     private final MediaRouter2Manager mRouterManager;
56 
57     private final Executor mExecutor = Executors.newSingleThreadExecutor();
58 
59     private final RouteCallback mRouteCallback = new RouteCallback();
60     private final TransferCallback mTransferCallback = new TransferCallback();
61     private final ControllerCallback mControllerCallback = new ControllerCallback();
62     private final Consumer<RouteListingPreference> mRouteListingPreferenceCallback =
63             (preference) -> {
64                 notifyRouteListingPreferenceUpdated(preference);
65                 refreshDevices();
66             };
67 
68     private final AtomicReference<MediaRouter2.ScanToken> mScanToken = new AtomicReference<>();
69 
70     // TODO (b/321969740): Plumb target UserHandle between UMO and RouterInfoMediaManager.
RouterInfoMediaManager( Context context, @NonNull String packageName, @NonNull UserHandle userHandle, LocalBluetoothManager localBluetoothManager, @Nullable MediaController mediaController)71     /* package */ RouterInfoMediaManager(
72             Context context,
73             @NonNull String packageName,
74             @NonNull UserHandle userHandle,
75             LocalBluetoothManager localBluetoothManager,
76             @Nullable MediaController mediaController)
77             throws PackageNotAvailableException {
78         super(context, packageName, userHandle, localBluetoothManager, mediaController);
79 
80         MediaRouter2 router = null;
81 
82         if (Flags.enableCrossUserRoutingInMediaRouter2()) {
83             try {
84                 router = MediaRouter2.getInstance(context, packageName, userHandle);
85             } catch (IllegalArgumentException ex) {
86                 // Do nothing
87             }
88         } else {
89             router = MediaRouter2.getInstance(context, packageName);
90         }
91         if (router == null) {
92             throw new PackageNotAvailableException(
93                     "Package name " + packageName + " does not exist.");
94         }
95         // We have to defer initialization because mRouter is final.
96         mRouter = router;
97 
98         mRouterManager = MediaRouter2Manager.getInstance(context);
99     }
100 
101     @Override
startScanOnRouter()102     protected void startScanOnRouter() {
103         if (Flags.enableScreenOffScanning()) {
104             MediaRouter2.ScanRequest request = new MediaRouter2.ScanRequest.Builder().build();
105             mScanToken.compareAndSet(null, mRouter.requestScan(request));
106         } else {
107             mRouter.startScan();
108         }
109     }
110 
111     @Override
registerRouter()112     protected void registerRouter() {
113         mRouter.registerRouteCallback(mExecutor, mRouteCallback, RouteDiscoveryPreference.EMPTY);
114         mRouter.registerRouteListingPreferenceUpdatedCallback(
115                 mExecutor, mRouteListingPreferenceCallback);
116         mRouter.registerTransferCallback(mExecutor, mTransferCallback);
117         mRouter.registerControllerCallback(mExecutor, mControllerCallback);
118     }
119 
120     @Override
stopScanOnRouter()121     protected void stopScanOnRouter() {
122         if (Flags.enableScreenOffScanning()) {
123             MediaRouter2.ScanToken token = mScanToken.getAndSet(null);
124             if (token != null) {
125                 mRouter.cancelScanRequest(token);
126             }
127         } else {
128             mRouter.stopScan();
129         }
130     }
131 
132     @Override
unregisterRouter()133     protected void unregisterRouter() {
134         mRouter.unregisterControllerCallback(mControllerCallback);
135         mRouter.unregisterTransferCallback(mTransferCallback);
136         mRouter.unregisterRouteListingPreferenceUpdatedCallback(mRouteListingPreferenceCallback);
137         mRouter.unregisterRouteCallback(mRouteCallback);
138     }
139 
140     @Override
transferToRoute(@onNull MediaRoute2Info route)141     protected void transferToRoute(@NonNull MediaRoute2Info route) {
142         mRouter.transferTo(route);
143     }
144 
145     @Override
selectRoute(@onNull MediaRoute2Info route, @NonNull RoutingSessionInfo info)146     protected void selectRoute(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info) {
147         RoutingController controller = getControllerForSession(info);
148         if (controller != null) {
149             controller.selectRoute(route);
150         }
151     }
152 
153     @Override
deselectRoute(@onNull MediaRoute2Info route, @NonNull RoutingSessionInfo info)154     protected void deselectRoute(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info) {
155         RoutingController controller = getControllerForSession(info);
156         if (controller != null) {
157             controller.deselectRoute(route);
158         }
159     }
160 
161     @Override
releaseSession(@onNull RoutingSessionInfo sessionInfo)162     protected void releaseSession(@NonNull RoutingSessionInfo sessionInfo) {
163         RoutingController controller = getControllerForSession(sessionInfo);
164         if (controller != null) {
165             controller.release();
166         }
167     }
168 
169     @NonNull
170     @Override
getSelectableRoutes(@onNull RoutingSessionInfo info)171     protected List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo info) {
172         RoutingController controller = getControllerForSession(info);
173         if (controller == null) {
174             return Collections.emptyList();
175         }
176 
177         // Filter out selected routes.
178         List<String> selectedRouteIds = controller.getRoutingSessionInfo().getSelectedRoutes();
179         return controller.getSelectableRoutes().stream()
180                 .filter(route -> !selectedRouteIds.contains(route.getId()))
181                 .collect(Collectors.toList());
182     }
183 
184     @NonNull
185     @Override
getDeselectableRoutes(@onNull RoutingSessionInfo info)186     protected List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo info) {
187         RoutingController controller = getControllerForSession(info);
188         if (controller == null) {
189             return Collections.emptyList();
190         }
191 
192         return controller.getDeselectableRoutes();
193     }
194 
195     @NonNull
196     @Override
getSelectedRoutes(@onNull RoutingSessionInfo info)197     protected List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo info) {
198         RoutingController controller = getControllerForSession(info);
199         if (controller == null) {
200             return Collections.emptyList();
201         }
202         return controller.getSelectedRoutes();
203     }
204 
205     @Override
setSessionVolume(@onNull RoutingSessionInfo info, int volume)206     protected void setSessionVolume(@NonNull RoutingSessionInfo info, int volume) {
207         // TODO: b/291277292 - Implement MediaRouter2-based solution. Keeping MR2Manager call as
208         //      MR2 filters information by package name.
209         mRouterManager.setSessionVolume(info, volume);
210     }
211 
212     @Override
setRouteVolume(@onNull MediaRoute2Info route, int volume)213     protected void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
214         mRouter.setRouteVolume(route, volume);
215     }
216 
217     @Nullable
218     @Override
getRouteListingPreference()219     protected RouteListingPreference getRouteListingPreference() {
220         return mRouter.getRouteListingPreference();
221     }
222 
223     @NonNull
224     @Override
getRemoteSessions()225     protected List<RoutingSessionInfo> getRemoteSessions() {
226         // TODO: b/291277292 - Implement MediaRouter2-based solution. Keeping MR2Manager call as
227         //      MR2 filters information by package name.
228         return mRouterManager.getRemoteSessions();
229     }
230 
231     @NonNull
232     @Override
getRoutingSessionsForPackage()233     protected List<RoutingSessionInfo> getRoutingSessionsForPackage() {
234         return mRouter.getControllers().stream()
235                 .map(RoutingController::getRoutingSessionInfo)
236                 .collect(Collectors.toList());
237     }
238 
239     @Nullable
240     @Override
getRoutingSessionById(@onNull String sessionId)241     protected RoutingSessionInfo getRoutingSessionById(@NonNull String sessionId) {
242         // TODO: b/291277292 - Implement MediaRouter2-based solution. Keeping MR2Manager calls as
243         //      MR2 filters information by package name.
244 
245         for (RoutingSessionInfo sessionInfo : getRemoteSessions()) {
246             if (TextUtils.equals(sessionInfo.getId(), sessionId)) {
247                 return sessionInfo;
248             }
249         }
250 
251         RoutingSessionInfo systemSession = mRouterManager.getSystemRoutingSession(null);
252         return TextUtils.equals(systemSession.getId(), sessionId) ? systemSession : null;
253     }
254 
255     @NonNull
256     @Override
getAvailableRoutesFromRouter()257     protected List<MediaRoute2Info> getAvailableRoutesFromRouter() {
258         return mRouter.getRoutes();
259     }
260 
261     @NonNull
262     @Override
getTransferableRoutes(@onNull String packageName)263     protected List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName) {
264         List<RoutingController> controllers = mRouter.getControllers();
265         RoutingController activeController = controllers.get(controllers.size() - 1);
266         HashMap<String, MediaRoute2Info> transferableRoutes = new HashMap<>();
267 
268         activeController
269                 .getTransferableRoutes()
270                 .forEach(route -> transferableRoutes.put(route.getId(), route));
271 
272         if (activeController.getRoutingSessionInfo().isSystemSession()) {
273             mRouter.getRoutes().stream()
274                     .filter(route -> !route.isSystemRoute())
275                     .forEach(route -> transferableRoutes.put(route.getId(), route));
276         } else {
277             mRouter.getRoutes().stream()
278                     .filter(route -> route.isSystemRoute())
279                     .forEach(route -> transferableRoutes.put(route.getId(), route));
280         }
281 
282         return new ArrayList<>(transferableRoutes.values());
283     }
284 
285     @Nullable
getControllerForSession(@onNull RoutingSessionInfo sessionInfo)286     private RoutingController getControllerForSession(@NonNull RoutingSessionInfo sessionInfo) {
287         return mRouter.getController(sessionInfo.getId());
288     }
289 
290     private final class RouteCallback extends MediaRouter2.RouteCallback {
291         @Override
onRoutesUpdated(@onNull List<MediaRoute2Info> routes)292         public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {
293             refreshDevices();
294         }
295 
296         @Override
onPreferredFeaturesChanged(@onNull List<String> preferredFeatures)297         public void onPreferredFeaturesChanged(@NonNull List<String> preferredFeatures) {
298             refreshDevices();
299         }
300     }
301 
302     private final class TransferCallback extends MediaRouter2.TransferCallback {
303         @Override
onTransfer( @onNull RoutingController oldController, @NonNull RoutingController newController)304         public void onTransfer(
305                 @NonNull RoutingController oldController,
306                 @NonNull RoutingController newController) {
307             rebuildDeviceList();
308             notifyCurrentConnectedDeviceChanged();
309         }
310 
311         @Override
onTransferFailure(@onNull MediaRoute2Info requestedRoute)312         public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {
313             // Do nothing.
314         }
315 
316         @Override
onStop(@onNull RoutingController controller)317         public void onStop(@NonNull RoutingController controller) {
318             refreshDevices();
319         }
320 
321         @Override
onRequestFailed(int reason)322         public void onRequestFailed(int reason) {
323             dispatchOnRequestFailed(reason);
324         }
325     }
326 
327     private final class ControllerCallback extends MediaRouter2.ControllerCallback {
328         @Override
onControllerUpdated(@onNull RoutingController controller)329         public void onControllerUpdated(@NonNull RoutingController controller) {
330             refreshDevices();
331         }
332     }
333 }
334