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