1 /*
2  * Copyright (C) 2014 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.systemui.statusbar.policy;
18 
19 import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
20 
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageManager;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.media.MediaRouter;
26 import android.media.MediaRouter.RouteInfo;
27 import android.media.projection.MediaProjectionInfo;
28 import android.media.projection.MediaProjectionManager;
29 import android.os.Handler;
30 import android.text.TextUtils;
31 import android.util.ArrayMap;
32 import android.util.Log;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.VisibleForTesting;
36 
37 import com.android.internal.annotations.GuardedBy;
38 import com.android.systemui.R;
39 import com.android.systemui.dagger.SysUISingleton;
40 import com.android.systemui.dump.DumpManager;
41 import com.android.systemui.util.Utils;
42 
43 import java.io.PrintWriter;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.Objects;
47 import java.util.UUID;
48 
49 import javax.inject.Inject;
50 
51 
52 /** Platform implementation of the cast controller. **/
53 @SysUISingleton
54 public class CastControllerImpl implements CastController {
55     private static final String TAG = "CastController";
56     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
57 
58     private final Context mContext;
59     @GuardedBy("mCallbacks")
60     private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
61     private final MediaRouter mMediaRouter;
62     private final ArrayMap<String, RouteInfo> mRoutes = new ArrayMap<>();
63     private final Object mDiscoveringLock = new Object();
64     private final MediaProjectionManager mProjectionManager;
65     private final Object mProjectionLock = new Object();
66 
67     private boolean mDiscovering;
68     private boolean mCallbackRegistered;
69     private MediaProjectionInfo mProjection;
70 
71     @Inject
CastControllerImpl(Context context, DumpManager dumpManager)72     public CastControllerImpl(Context context, DumpManager dumpManager) {
73         mContext = context;
74         mMediaRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
75         mMediaRouter.setRouterGroupId(MediaRouter.MIRRORING_GROUP_ID);
76         mProjectionManager = (MediaProjectionManager)
77                 context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
78         mProjection = mProjectionManager.getActiveProjectionInfo();
79         mProjectionManager.addCallback(mProjectionCallback, new Handler());
80         dumpManager.registerDumpable(TAG, this);
81         if (DEBUG) Log.d(TAG, "new CastController()");
82     }
83 
dump(PrintWriter pw, String[] args)84     public void dump(PrintWriter pw, String[] args) {
85         pw.println("CastController state:");
86         pw.print("  mDiscovering="); pw.println(mDiscovering);
87         pw.print("  mCallbackRegistered="); pw.println(mCallbackRegistered);
88         pw.print("  mCallbacks.size="); synchronized (mCallbacks) {pw.println(mCallbacks.size());}
89         pw.print("  mRoutes.size="); pw.println(mRoutes.size());
90         for (int i = 0; i < mRoutes.size(); i++) {
91             final RouteInfo route = mRoutes.valueAt(i);
92             pw.print("    "); pw.println(routeToString(route));
93         }
94         pw.print("  mProjection="); pw.println(mProjection);
95     }
96 
97     @Override
addCallback(@onNull Callback callback)98     public void addCallback(@NonNull Callback callback) {
99         synchronized (mCallbacks) {
100             mCallbacks.add(callback);
101         }
102         fireOnCastDevicesChanged(callback);
103         synchronized (mDiscoveringLock) {
104             handleDiscoveryChangeLocked();
105         }
106     }
107 
108     @Override
removeCallback(@onNull Callback callback)109     public void removeCallback(@NonNull Callback callback) {
110         synchronized (mCallbacks) {
111             mCallbacks.remove(callback);
112         }
113         synchronized (mDiscoveringLock) {
114             handleDiscoveryChangeLocked();
115         }
116     }
117 
118     @Override
setDiscovering(boolean request)119     public void setDiscovering(boolean request) {
120         synchronized (mDiscoveringLock) {
121             if (mDiscovering == request) return;
122             mDiscovering = request;
123             if (DEBUG) Log.d(TAG, "setDiscovering: " + request);
124             handleDiscoveryChangeLocked();
125         }
126     }
127 
handleDiscoveryChangeLocked()128     private void handleDiscoveryChangeLocked() {
129         if (mCallbackRegistered) {
130             mMediaRouter.removeCallback(mMediaCallback);
131             mCallbackRegistered = false;
132         }
133         if (mDiscovering) {
134             mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaCallback,
135                     MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
136             mCallbackRegistered = true;
137         } else {
138             boolean hasCallbacks = false;
139             synchronized (mCallbacks) {
140                 hasCallbacks = mCallbacks.isEmpty();
141             }
142             if (!hasCallbacks) {
143                 mMediaRouter.addCallback(ROUTE_TYPE_REMOTE_DISPLAY, mMediaCallback,
144                         MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
145                 mCallbackRegistered = true;
146             }
147         }
148     }
149 
150     @Override
setCurrentUserId(int currentUserId)151     public void setCurrentUserId(int currentUserId) {
152         mMediaRouter.rebindAsUser(currentUserId);
153     }
154 
155     @Override
getCastDevices()156     public List<CastDevice> getCastDevices() {
157         final ArrayList<CastDevice> devices = new ArrayList<>();
158         synchronized(mRoutes) {
159             for (RouteInfo route : mRoutes.values()) {
160                 final CastDevice device = new CastDevice();
161                 device.id = route.getTag().toString();
162                 final CharSequence name = route.getName(mContext);
163                 device.name = name != null ? name.toString() : null;
164                 final CharSequence description = route.getDescription();
165                 device.description = description != null ? description.toString() : null;
166 
167                 int statusCode = route.getStatusCode();
168                 if (statusCode == RouteInfo.STATUS_CONNECTING) {
169                     device.state = CastDevice.STATE_CONNECTING;
170                 } else if (route.isSelected() || statusCode == RouteInfo.STATUS_CONNECTED) {
171                     device.state = CastDevice.STATE_CONNECTED;
172                 } else {
173                     device.state = CastDevice.STATE_DISCONNECTED;
174                 }
175 
176                 device.tag = route;
177                 devices.add(device);
178             }
179         }
180 
181         synchronized (mProjectionLock) {
182             if (mProjection != null) {
183                 final CastDevice device = new CastDevice();
184                 device.id = mProjection.getPackageName();
185                 device.name = getAppName(mProjection.getPackageName());
186                 device.description = mContext.getString(R.string.quick_settings_casting);
187                 device.state = CastDevice.STATE_CONNECTED;
188                 device.tag = mProjection;
189                 devices.add(device);
190             }
191         }
192 
193         return devices;
194     }
195 
196     @Override
startCasting(CastDevice device)197     public void startCasting(CastDevice device) {
198         if (device == null || device.tag == null) return;
199         final RouteInfo route = (RouteInfo) device.tag;
200         if (DEBUG) Log.d(TAG, "startCasting: " + routeToString(route));
201         mMediaRouter.selectRoute(ROUTE_TYPE_REMOTE_DISPLAY, route);
202     }
203 
204     @Override
stopCasting(CastDevice device)205     public void stopCasting(CastDevice device) {
206         final boolean isProjection = device.tag instanceof MediaProjectionInfo;
207         if (DEBUG) Log.d(TAG, "stopCasting isProjection=" + isProjection);
208         if (isProjection) {
209             final MediaProjectionInfo projection = (MediaProjectionInfo) device.tag;
210             if (Objects.equals(mProjectionManager.getActiveProjectionInfo(), projection)) {
211                 mProjectionManager.stopActiveProjection();
212             } else {
213                 Log.w(TAG, "Projection is no longer active: " + projection);
214             }
215         } else {
216             mMediaRouter.getFallbackRoute().select();
217         }
218     }
219 
220     @Override
hasConnectedCastDevice()221     public boolean hasConnectedCastDevice() {
222         return getCastDevices().stream().anyMatch(
223                 castDevice -> castDevice.state == CastDevice.STATE_CONNECTED);
224     }
225 
setProjection(MediaProjectionInfo projection, boolean started)226     private void setProjection(MediaProjectionInfo projection, boolean started) {
227         boolean changed = false;
228         final MediaProjectionInfo oldProjection = mProjection;
229         synchronized (mProjectionLock) {
230             final boolean isCurrent = Objects.equals(projection, mProjection);
231             if (started && !isCurrent) {
232                 mProjection = projection;
233                 changed = true;
234             } else if (!started && isCurrent) {
235                 mProjection = null;
236                 changed = true;
237             }
238         }
239         if (changed) {
240             if (DEBUG) Log.d(TAG, "setProjection: " + oldProjection + " -> " + mProjection);
241             fireOnCastDevicesChanged();
242         }
243     }
244 
getAppName(String packageName)245     private String getAppName(String packageName) {
246         final PackageManager pm = mContext.getPackageManager();
247         if (Utils.isHeadlessRemoteDisplayProvider(pm, packageName)) {
248             return "";
249         }
250 
251         try {
252             final ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
253             if (appInfo != null) {
254                 final CharSequence label = appInfo.loadLabel(pm);
255                 if (!TextUtils.isEmpty(label)) {
256                     return label.toString();
257                 }
258             }
259             Log.w(TAG, "No label found for package: " + packageName);
260         } catch (NameNotFoundException e) {
261             Log.w(TAG, "Error getting appName for package: " + packageName, e);
262         }
263         return packageName;
264     }
265 
updateRemoteDisplays()266     private void updateRemoteDisplays() {
267         synchronized(mRoutes) {
268             mRoutes.clear();
269             final int n = mMediaRouter.getRouteCount();
270             for (int i = 0; i < n; i++) {
271                 final RouteInfo route = mMediaRouter.getRouteAt(i);
272                 if (!route.isEnabled()) continue;
273                 if (!route.matchesTypes(ROUTE_TYPE_REMOTE_DISPLAY)) continue;
274                 ensureTagExists(route);
275                 mRoutes.put(route.getTag().toString(), route);
276             }
277             final RouteInfo selected = mMediaRouter.getSelectedRoute(ROUTE_TYPE_REMOTE_DISPLAY);
278             if (selected != null && !selected.isDefault()) {
279                 ensureTagExists(selected);
280                 mRoutes.put(selected.getTag().toString(), selected);
281             }
282         }
283         fireOnCastDevicesChanged();
284     }
285 
ensureTagExists(RouteInfo route)286     private void ensureTagExists(RouteInfo route) {
287         if (route.getTag() == null) {
288             route.setTag(UUID.randomUUID().toString());
289         }
290     }
291 
292     @VisibleForTesting
fireOnCastDevicesChanged()293     void fireOnCastDevicesChanged() {
294         synchronized (mCallbacks) {
295             for (Callback callback : mCallbacks) {
296                 fireOnCastDevicesChanged(callback);
297             }
298 
299         }
300     }
301 
302 
fireOnCastDevicesChanged(Callback callback)303     private void fireOnCastDevicesChanged(Callback callback) {
304         callback.onCastDevicesChanged();
305     }
306 
routeToString(RouteInfo route)307     private static String routeToString(RouteInfo route) {
308         if (route == null) return null;
309         final StringBuilder sb = new StringBuilder().append(route.getName()).append('/')
310                 .append(route.getDescription()).append('@').append(route.getDeviceAddress())
311                 .append(",status=").append(route.getStatus());
312         if (route.isDefault()) sb.append(",default");
313         if (route.isEnabled()) sb.append(",enabled");
314         if (route.isConnecting()) sb.append(",connecting");
315         if (route.isSelected()) sb.append(",selected");
316         return sb.append(",id=").append(route.getTag()).toString();
317     }
318 
319     private final MediaRouter.SimpleCallback mMediaCallback = new MediaRouter.SimpleCallback() {
320         @Override
321         public void onRouteAdded(MediaRouter router, RouteInfo route) {
322             if (DEBUG) Log.d(TAG, "onRouteAdded: " + routeToString(route));
323             updateRemoteDisplays();
324         }
325         @Override
326         public void onRouteChanged(MediaRouter router, RouteInfo route) {
327             if (DEBUG) Log.d(TAG, "onRouteChanged: " + routeToString(route));
328             updateRemoteDisplays();
329         }
330         @Override
331         public void onRouteRemoved(MediaRouter router, RouteInfo route) {
332             if (DEBUG) Log.d(TAG, "onRouteRemoved: " + routeToString(route));
333             updateRemoteDisplays();
334         }
335         @Override
336         public void onRouteSelected(MediaRouter router, int type, RouteInfo route) {
337             if (DEBUG) Log.d(TAG, "onRouteSelected(" + type + "): " + routeToString(route));
338             updateRemoteDisplays();
339         }
340         @Override
341         public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) {
342             if (DEBUG) Log.d(TAG, "onRouteUnselected(" + type + "): " + routeToString(route));
343             updateRemoteDisplays();
344         }
345     };
346 
347     private final MediaProjectionManager.Callback mProjectionCallback
348             = new MediaProjectionManager.Callback() {
349         @Override
350         public void onStart(MediaProjectionInfo info) {
351             setProjection(info, true);
352         }
353 
354         @Override
355         public void onStop(MediaProjectionInfo info) {
356             setProjection(info, false);
357         }
358     };
359 }
360