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