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
17package com.android.systemui.statusbar.policy;
18
19import android.content.Context;
20import android.content.pm.ApplicationInfo;
21import android.content.pm.PackageManager;
22import android.content.pm.PackageManager.NameNotFoundException;
23import android.media.MediaRouter;
24import android.media.MediaRouter.RouteInfo;
25import android.media.projection.MediaProjectionInfo;
26import android.media.projection.MediaProjectionManager;
27import android.os.Handler;
28import android.text.TextUtils;
29import android.util.ArrayMap;
30import android.util.ArraySet;
31import android.util.Log;
32
33import com.android.systemui.R;
34
35import java.io.FileDescriptor;
36import java.io.PrintWriter;
37import java.util.ArrayList;
38import java.util.Objects;
39import java.util.Set;
40import java.util.UUID;
41
42import static android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
43
44/** Platform implementation of the cast controller. **/
45public 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