/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.display; import com.android.internal.util.DumpUtils; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.ContentObserver; import android.hardware.display.WifiDisplay; import android.hardware.display.WifiDisplaySessionInfo; import android.hardware.display.WifiDisplayStatus; import android.media.RemoteDisplay; import android.net.NetworkInfo; import android.net.Uri; import android.net.wifi.WpsInfo; import android.net.wifi.p2p.WifiP2pConfig; import android.net.wifi.p2p.WifiP2pDevice; import android.net.wifi.p2p.WifiP2pDeviceList; import android.net.wifi.p2p.WifiP2pGroup; import android.net.wifi.p2p.WifiP2pManager; import android.net.wifi.p2p.WifiP2pWfdInfo; import android.net.wifi.p2p.WifiP2pManager.ActionListener; import android.net.wifi.p2p.WifiP2pManager.Channel; import android.net.wifi.p2p.WifiP2pManager.GroupInfoListener; import android.net.wifi.p2p.WifiP2pManager.PeerListListener; import android.os.Handler; import android.provider.Settings; import android.util.Slog; import android.view.Surface; import java.io.PrintWriter; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.ArrayList; import java.util.Enumeration; import libcore.util.Objects; /** * Manages all of the various asynchronous interactions with the {@link WifiP2pManager} * on behalf of {@link WifiDisplayAdapter}. *

* This code is isolated from {@link WifiDisplayAdapter} so that we can avoid * accidentally introducing any deadlocks due to the display manager calling * outside of itself while holding its lock. It's also way easier to write this * asynchronous code if we can assume that it is single-threaded. *

* The controller must be instantiated on the handler thread. *

*/ final class WifiDisplayController implements DumpUtils.Dump { private static final String TAG = "WifiDisplayController"; private static final boolean DEBUG = false; private static final int DEFAULT_CONTROL_PORT = 7236; private static final int MAX_THROUGHPUT = 50; private static final int CONNECTION_TIMEOUT_SECONDS = 30; private static final int RTSP_TIMEOUT_SECONDS = 30; private static final int RTSP_TIMEOUT_SECONDS_CERT_MODE = 120; // We repeatedly issue calls to discover peers every so often for a few reasons. // 1. The initial request may fail and need to retried. // 2. Discovery will self-abort after any group is initiated, which may not necessarily // be what we want to have happen. // 3. Discovery will self-timeout after 2 minutes, whereas we want discovery to // be occur for as long as a client is requesting it be. // 4. We don't seem to get updated results for displays we've already found until // we ask to discover again, particularly for the isSessionAvailable() property. private static final int DISCOVER_PEERS_INTERVAL_MILLIS = 10000; private static final int CONNECT_MAX_RETRIES = 3; private static final int CONNECT_RETRY_DELAY_MILLIS = 500; private final Context mContext; private final Handler mHandler; private final Listener mListener; private final WifiP2pManager mWifiP2pManager; private final Channel mWifiP2pChannel; private boolean mWifiP2pEnabled; private boolean mWfdEnabled; private boolean mWfdEnabling; private NetworkInfo mNetworkInfo; private final ArrayList mAvailableWifiDisplayPeers = new ArrayList(); // True if Wifi display is enabled by the user. private boolean mWifiDisplayOnSetting; // True if a scan was requested independent of whether one is actually in progress. private boolean mScanRequested; // True if there is a call to discoverPeers in progress. private boolean mDiscoverPeersInProgress; // The device to which we want to connect, or null if we want to be disconnected. private WifiP2pDevice mDesiredDevice; // The device to which we are currently connecting, or null if we have already connected // or are not trying to connect. private WifiP2pDevice mConnectingDevice; // The device from which we are currently disconnecting. private WifiP2pDevice mDisconnectingDevice; // The device to which we were previously trying to connect and are now canceling. private WifiP2pDevice mCancelingDevice; // The device to which we are currently connected, which means we have an active P2P group. private WifiP2pDevice mConnectedDevice; // The group info obtained after connecting. private WifiP2pGroup mConnectedDeviceGroupInfo; // Number of connection retries remaining. private int mConnectionRetriesLeft; // The remote display that is listening on the connection. // Created after the Wifi P2P network is connected. private RemoteDisplay mRemoteDisplay; // The remote display interface. private String mRemoteDisplayInterface; // True if RTSP has connected. private boolean mRemoteDisplayConnected; // The information we have most recently told WifiDisplayAdapter about. private WifiDisplay mAdvertisedDisplay; private Surface mAdvertisedDisplaySurface; private int mAdvertisedDisplayWidth; private int mAdvertisedDisplayHeight; private int mAdvertisedDisplayFlags; // Certification private boolean mWifiDisplayCertMode; private int mWifiDisplayWpsConfig = WpsInfo.INVALID; private WifiP2pDevice mThisDevice; public WifiDisplayController(Context context, Handler handler, Listener listener) { mContext = context; mHandler = handler; mListener = listener; mWifiP2pManager = (WifiP2pManager)context.getSystemService(Context.WIFI_P2P_SERVICE); mWifiP2pChannel = mWifiP2pManager.initialize(context, handler.getLooper(), null); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION); intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION); context.registerReceiver(mWifiP2pReceiver, intentFilter, null, mHandler); ContentObserver settingsObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange, Uri uri) { updateSettings(); } }; final ContentResolver resolver = mContext.getContentResolver(); resolver.registerContentObserver(Settings.Global.getUriFor( Settings.Global.WIFI_DISPLAY_ON), false, settingsObserver); resolver.registerContentObserver(Settings.Global.getUriFor( Settings.Global.WIFI_DISPLAY_CERTIFICATION_ON), false, settingsObserver); resolver.registerContentObserver(Settings.Global.getUriFor( Settings.Global.WIFI_DISPLAY_WPS_CONFIG), false, settingsObserver); updateSettings(); } private void updateSettings() { final ContentResolver resolver = mContext.getContentResolver(); mWifiDisplayOnSetting = Settings.Global.getInt(resolver, Settings.Global.WIFI_DISPLAY_ON, 0) != 0; mWifiDisplayCertMode = Settings.Global.getInt(resolver, Settings.Global.WIFI_DISPLAY_CERTIFICATION_ON, 0) != 0; mWifiDisplayWpsConfig = WpsInfo.INVALID; if (mWifiDisplayCertMode) { mWifiDisplayWpsConfig = Settings.Global.getInt(resolver, Settings.Global.WIFI_DISPLAY_WPS_CONFIG, WpsInfo.INVALID); } updateWfdEnableState(); } @Override public void dump(PrintWriter pw, String prefix) { pw.println("mWifiDisplayOnSetting=" + mWifiDisplayOnSetting); pw.println("mWifiP2pEnabled=" + mWifiP2pEnabled); pw.println("mWfdEnabled=" + mWfdEnabled); pw.println("mWfdEnabling=" + mWfdEnabling); pw.println("mNetworkInfo=" + mNetworkInfo); pw.println("mScanRequested=" + mScanRequested); pw.println("mDiscoverPeersInProgress=" + mDiscoverPeersInProgress); pw.println("mDesiredDevice=" + describeWifiP2pDevice(mDesiredDevice)); pw.println("mConnectingDisplay=" + describeWifiP2pDevice(mConnectingDevice)); pw.println("mDisconnectingDisplay=" + describeWifiP2pDevice(mDisconnectingDevice)); pw.println("mCancelingDisplay=" + describeWifiP2pDevice(mCancelingDevice)); pw.println("mConnectedDevice=" + describeWifiP2pDevice(mConnectedDevice)); pw.println("mConnectionRetriesLeft=" + mConnectionRetriesLeft); pw.println("mRemoteDisplay=" + mRemoteDisplay); pw.println("mRemoteDisplayInterface=" + mRemoteDisplayInterface); pw.println("mRemoteDisplayConnected=" + mRemoteDisplayConnected); pw.println("mAdvertisedDisplay=" + mAdvertisedDisplay); pw.println("mAdvertisedDisplaySurface=" + mAdvertisedDisplaySurface); pw.println("mAdvertisedDisplayWidth=" + mAdvertisedDisplayWidth); pw.println("mAdvertisedDisplayHeight=" + mAdvertisedDisplayHeight); pw.println("mAdvertisedDisplayFlags=" + mAdvertisedDisplayFlags); pw.println("mAvailableWifiDisplayPeers: size=" + mAvailableWifiDisplayPeers.size()); for (WifiP2pDevice device : mAvailableWifiDisplayPeers) { pw.println(" " + describeWifiP2pDevice(device)); } } public void requestStartScan() { if (!mScanRequested) { mScanRequested = true; updateScanState(); } } public void requestStopScan() { if (mScanRequested) { mScanRequested = false; updateScanState(); } } public void requestConnect(String address) { for (WifiP2pDevice device : mAvailableWifiDisplayPeers) { if (device.deviceAddress.equals(address)) { connect(device); } } } public void requestPause() { if (mRemoteDisplay != null) { mRemoteDisplay.pause(); } } public void requestResume() { if (mRemoteDisplay != null) { mRemoteDisplay.resume(); } } public void requestDisconnect() { disconnect(); } private void updateWfdEnableState() { if (mWifiDisplayOnSetting && mWifiP2pEnabled) { // WFD should be enabled. if (!mWfdEnabled && !mWfdEnabling) { mWfdEnabling = true; WifiP2pWfdInfo wfdInfo = new WifiP2pWfdInfo(); wfdInfo.setWfdEnabled(true); wfdInfo.setDeviceType(WifiP2pWfdInfo.WFD_SOURCE); wfdInfo.setSessionAvailable(true); wfdInfo.setControlPort(DEFAULT_CONTROL_PORT); wfdInfo.setMaxThroughput(MAX_THROUGHPUT); mWifiP2pManager.setWFDInfo(mWifiP2pChannel, wfdInfo, new ActionListener() { @Override public void onSuccess() { if (DEBUG) { Slog.d(TAG, "Successfully set WFD info."); } if (mWfdEnabling) { mWfdEnabling = false; mWfdEnabled = true; reportFeatureState(); updateScanState(); } } @Override public void onFailure(int reason) { if (DEBUG) { Slog.d(TAG, "Failed to set WFD info with reason " + reason + "."); } mWfdEnabling = false; } }); } } else { // WFD should be disabled. if (mWfdEnabled || mWfdEnabling) { WifiP2pWfdInfo wfdInfo = new WifiP2pWfdInfo(); wfdInfo.setWfdEnabled(false); mWifiP2pManager.setWFDInfo(mWifiP2pChannel, wfdInfo, new ActionListener() { @Override public void onSuccess() { if (DEBUG) { Slog.d(TAG, "Successfully set WFD info."); } } @Override public void onFailure(int reason) { if (DEBUG) { Slog.d(TAG, "Failed to set WFD info with reason " + reason + "."); } } }); } mWfdEnabling = false; mWfdEnabled = false; reportFeatureState(); updateScanState(); disconnect(); } } private void reportFeatureState() { final int featureState = computeFeatureState(); mHandler.post(new Runnable() { @Override public void run() { mListener.onFeatureStateChanged(featureState); } }); } private int computeFeatureState() { if (!mWifiP2pEnabled) { return WifiDisplayStatus.FEATURE_STATE_DISABLED; } return mWifiDisplayOnSetting ? WifiDisplayStatus.FEATURE_STATE_ON : WifiDisplayStatus.FEATURE_STATE_OFF; } private void updateScanState() { if (mScanRequested && mWfdEnabled && mDesiredDevice == null) { if (!mDiscoverPeersInProgress) { Slog.i(TAG, "Starting Wifi display scan."); mDiscoverPeersInProgress = true; handleScanStarted(); tryDiscoverPeers(); } } else { if (mDiscoverPeersInProgress) { // Cancel automatic retry right away. mHandler.removeCallbacks(mDiscoverPeers); // Defer actually stopping discovery if we have a connection attempt in progress. // The wifi display connection attempt often fails if we are not in discovery // mode. So we allow discovery to continue until we give up trying to connect. if (mDesiredDevice == null || mDesiredDevice == mConnectedDevice) { Slog.i(TAG, "Stopping Wifi display scan."); mDiscoverPeersInProgress = false; stopPeerDiscovery(); handleScanFinished(); } } } } private void tryDiscoverPeers() { mWifiP2pManager.discoverPeers(mWifiP2pChannel, new ActionListener() { @Override public void onSuccess() { if (DEBUG) { Slog.d(TAG, "Discover peers succeeded. Requesting peers now."); } if (mDiscoverPeersInProgress) { requestPeers(); } } @Override public void onFailure(int reason) { if (DEBUG) { Slog.d(TAG, "Discover peers failed with reason " + reason + "."); } // Ignore the error. // We will retry automatically in a little bit. } }); // Retry discover peers periodically until stopped. mHandler.postDelayed(mDiscoverPeers, DISCOVER_PEERS_INTERVAL_MILLIS); } private void stopPeerDiscovery() { mWifiP2pManager.stopPeerDiscovery(mWifiP2pChannel, new ActionListener() { @Override public void onSuccess() { if (DEBUG) { Slog.d(TAG, "Stop peer discovery succeeded."); } } @Override public void onFailure(int reason) { if (DEBUG) { Slog.d(TAG, "Stop peer discovery failed with reason " + reason + "."); } } }); } private void requestPeers() { mWifiP2pManager.requestPeers(mWifiP2pChannel, new PeerListListener() { @Override public void onPeersAvailable(WifiP2pDeviceList peers) { if (DEBUG) { Slog.d(TAG, "Received list of peers."); } mAvailableWifiDisplayPeers.clear(); for (WifiP2pDevice device : peers.getDeviceList()) { if (DEBUG) { Slog.d(TAG, " " + describeWifiP2pDevice(device)); } if (isWifiDisplay(device)) { mAvailableWifiDisplayPeers.add(device); } } if (mDiscoverPeersInProgress) { handleScanResults(); } } }); } private void handleScanStarted() { mHandler.post(new Runnable() { @Override public void run() { mListener.onScanStarted(); } }); } private void handleScanResults() { final int count = mAvailableWifiDisplayPeers.size(); final WifiDisplay[] displays = WifiDisplay.CREATOR.newArray(count); for (int i = 0; i < count; i++) { WifiP2pDevice device = mAvailableWifiDisplayPeers.get(i); displays[i] = createWifiDisplay(device); updateDesiredDevice(device); } mHandler.post(new Runnable() { @Override public void run() { mListener.onScanResults(displays); } }); } private void handleScanFinished() { mHandler.post(new Runnable() { @Override public void run() { mListener.onScanFinished(); } }); } private void updateDesiredDevice(WifiP2pDevice device) { // Handle the case where the device to which we are connecting or connected // may have been renamed or reported different properties in the latest scan. final String address = device.deviceAddress; if (mDesiredDevice != null && mDesiredDevice.deviceAddress.equals(address)) { if (DEBUG) { Slog.d(TAG, "updateDesiredDevice: new information " + describeWifiP2pDevice(device)); } mDesiredDevice.update(device); if (mAdvertisedDisplay != null && mAdvertisedDisplay.getDeviceAddress().equals(address)) { readvertiseDisplay(createWifiDisplay(mDesiredDevice)); } } } private void connect(final WifiP2pDevice device) { if (mDesiredDevice != null && !mDesiredDevice.deviceAddress.equals(device.deviceAddress)) { if (DEBUG) { Slog.d(TAG, "connect: nothing to do, already connecting to " + describeWifiP2pDevice(device)); } return; } if (mConnectedDevice != null && !mConnectedDevice.deviceAddress.equals(device.deviceAddress) && mDesiredDevice == null) { if (DEBUG) { Slog.d(TAG, "connect: nothing to do, already connected to " + describeWifiP2pDevice(device) + " and not part way through " + "connecting to a different device."); } return; } if (!mWfdEnabled) { Slog.i(TAG, "Ignoring request to connect to Wifi display because the " +" feature is currently disabled: " + device.deviceName); return; } mDesiredDevice = device; mConnectionRetriesLeft = CONNECT_MAX_RETRIES; updateConnection(); } private void disconnect() { mDesiredDevice = null; updateConnection(); } private void retryConnection() { // Cheap hack. Make a new instance of the device object so that we // can distinguish it from the previous connection attempt. // This will cause us to tear everything down before we try again. mDesiredDevice = new WifiP2pDevice(mDesiredDevice); updateConnection(); } /** * This function is called repeatedly after each asynchronous operation * until all preconditions for the connection have been satisfied and the * connection is established (or not). */ private void updateConnection() { // Step 0. Stop scans if necessary to prevent interference while connected. // Resume scans later when no longer attempting to connect. updateScanState(); // Step 1. Before we try to connect to a new device, tell the system we // have disconnected from the old one. if (mRemoteDisplay != null && mConnectedDevice != mDesiredDevice) { Slog.i(TAG, "Stopped listening for RTSP connection on " + mRemoteDisplayInterface + " from Wifi display: " + mConnectedDevice.deviceName); mRemoteDisplay.dispose(); mRemoteDisplay = null; mRemoteDisplayInterface = null; mRemoteDisplayConnected = false; mHandler.removeCallbacks(mRtspTimeout); mWifiP2pManager.setMiracastMode(WifiP2pManager.MIRACAST_DISABLED); unadvertiseDisplay(); // continue to next step } // Step 2. Before we try to connect to a new device, disconnect from the old one. if (mDisconnectingDevice != null) { return; // wait for asynchronous callback } if (mConnectedDevice != null && mConnectedDevice != mDesiredDevice) { Slog.i(TAG, "Disconnecting from Wifi display: " + mConnectedDevice.deviceName); mDisconnectingDevice = mConnectedDevice; mConnectedDevice = null; mConnectedDeviceGroupInfo = null; unadvertiseDisplay(); final WifiP2pDevice oldDevice = mDisconnectingDevice; mWifiP2pManager.removeGroup(mWifiP2pChannel, new ActionListener() { @Override public void onSuccess() { Slog.i(TAG, "Disconnected from Wifi display: " + oldDevice.deviceName); next(); } @Override public void onFailure(int reason) { Slog.i(TAG, "Failed to disconnect from Wifi display: " + oldDevice.deviceName + ", reason=" + reason); next(); } private void next() { if (mDisconnectingDevice == oldDevice) { mDisconnectingDevice = null; updateConnection(); } } }); return; // wait for asynchronous callback } // Step 3. Before we try to connect to a new device, stop trying to connect // to the old one. if (mCancelingDevice != null) { return; // wait for asynchronous callback } if (mConnectingDevice != null && mConnectingDevice != mDesiredDevice) { Slog.i(TAG, "Canceling connection to Wifi display: " + mConnectingDevice.deviceName); mCancelingDevice = mConnectingDevice; mConnectingDevice = null; unadvertiseDisplay(); mHandler.removeCallbacks(mConnectionTimeout); final WifiP2pDevice oldDevice = mCancelingDevice; mWifiP2pManager.cancelConnect(mWifiP2pChannel, new ActionListener() { @Override public void onSuccess() { Slog.i(TAG, "Canceled connection to Wifi display: " + oldDevice.deviceName); next(); } @Override public void onFailure(int reason) { Slog.i(TAG, "Failed to cancel connection to Wifi display: " + oldDevice.deviceName + ", reason=" + reason); next(); } private void next() { if (mCancelingDevice == oldDevice) { mCancelingDevice = null; updateConnection(); } } }); return; // wait for asynchronous callback } // Step 4. If we wanted to disconnect, or we're updating after starting an // autonomous GO, then mission accomplished. if (mDesiredDevice == null) { if (mWifiDisplayCertMode) { mListener.onDisplaySessionInfo(getSessionInfo(mConnectedDeviceGroupInfo, 0)); } unadvertiseDisplay(); return; // done } // Step 5. Try to connect. if (mConnectedDevice == null && mConnectingDevice == null) { Slog.i(TAG, "Connecting to Wifi display: " + mDesiredDevice.deviceName); mConnectingDevice = mDesiredDevice; WifiP2pConfig config = new WifiP2pConfig(); WpsInfo wps = new WpsInfo(); if (mWifiDisplayWpsConfig != WpsInfo.INVALID) { wps.setup = mWifiDisplayWpsConfig; } else if (mConnectingDevice.wpsPbcSupported()) { wps.setup = WpsInfo.PBC; } else if (mConnectingDevice.wpsDisplaySupported()) { // We do keypad if peer does display wps.setup = WpsInfo.KEYPAD; } else { wps.setup = WpsInfo.DISPLAY; } config.wps = wps; config.deviceAddress = mConnectingDevice.deviceAddress; // Helps with STA & P2P concurrency config.groupOwnerIntent = WifiP2pConfig.MIN_GROUP_OWNER_INTENT; WifiDisplay display = createWifiDisplay(mConnectingDevice); advertiseDisplay(display, null, 0, 0, 0); final WifiP2pDevice newDevice = mDesiredDevice; mWifiP2pManager.connect(mWifiP2pChannel, config, new ActionListener() { @Override public void onSuccess() { // The connection may not yet be established. We still need to wait // for WIFI_P2P_CONNECTION_CHANGED_ACTION. However, we might never // get that broadcast, so we register a timeout. Slog.i(TAG, "Initiated connection to Wifi display: " + newDevice.deviceName); mHandler.postDelayed(mConnectionTimeout, CONNECTION_TIMEOUT_SECONDS * 1000); } @Override public void onFailure(int reason) { if (mConnectingDevice == newDevice) { Slog.i(TAG, "Failed to initiate connection to Wifi display: " + newDevice.deviceName + ", reason=" + reason); mConnectingDevice = null; handleConnectionFailure(false); } } }); return; // wait for asynchronous callback } // Step 6. Listen for incoming RTSP connection. if (mConnectedDevice != null && mRemoteDisplay == null) { Inet4Address addr = getInterfaceAddress(mConnectedDeviceGroupInfo); if (addr == null) { Slog.i(TAG, "Failed to get local interface address for communicating " + "with Wifi display: " + mConnectedDevice.deviceName); handleConnectionFailure(false); return; // done } mWifiP2pManager.setMiracastMode(WifiP2pManager.MIRACAST_SOURCE); final WifiP2pDevice oldDevice = mConnectedDevice; final int port = getPortNumber(mConnectedDevice); final String iface = addr.getHostAddress() + ":" + port; mRemoteDisplayInterface = iface; Slog.i(TAG, "Listening for RTSP connection on " + iface + " from Wifi display: " + mConnectedDevice.deviceName); mRemoteDisplay = RemoteDisplay.listen(iface, new RemoteDisplay.Listener() { @Override public void onDisplayConnected(Surface surface, int width, int height, int flags, int session) { if (mConnectedDevice == oldDevice && !mRemoteDisplayConnected) { Slog.i(TAG, "Opened RTSP connection with Wifi display: " + mConnectedDevice.deviceName); mRemoteDisplayConnected = true; mHandler.removeCallbacks(mRtspTimeout); if (mWifiDisplayCertMode) { mListener.onDisplaySessionInfo( getSessionInfo(mConnectedDeviceGroupInfo, session)); } final WifiDisplay display = createWifiDisplay(mConnectedDevice); advertiseDisplay(display, surface, width, height, flags); } } @Override public void onDisplayDisconnected() { if (mConnectedDevice == oldDevice) { Slog.i(TAG, "Closed RTSP connection with Wifi display: " + mConnectedDevice.deviceName); mHandler.removeCallbacks(mRtspTimeout); disconnect(); } } @Override public void onDisplayError(int error) { if (mConnectedDevice == oldDevice) { Slog.i(TAG, "Lost RTSP connection with Wifi display due to error " + error + ": " + mConnectedDevice.deviceName); mHandler.removeCallbacks(mRtspTimeout); handleConnectionFailure(false); } } }, mHandler, mContext.getOpPackageName()); // Use extended timeout value for certification, as some tests require user inputs int rtspTimeout = mWifiDisplayCertMode ? RTSP_TIMEOUT_SECONDS_CERT_MODE : RTSP_TIMEOUT_SECONDS; mHandler.postDelayed(mRtspTimeout, rtspTimeout * 1000); } } private WifiDisplaySessionInfo getSessionInfo(WifiP2pGroup info, int session) { if (info == null) { return null; } Inet4Address addr = getInterfaceAddress(info); WifiDisplaySessionInfo sessionInfo = new WifiDisplaySessionInfo( !info.getOwner().deviceAddress.equals(mThisDevice.deviceAddress), session, info.getOwner().deviceAddress + " " + info.getNetworkName(), info.getPassphrase(), (addr != null) ? addr.getHostAddress() : ""); if (DEBUG) { Slog.d(TAG, sessionInfo.toString()); } return sessionInfo; } private void handleStateChanged(boolean enabled) { mWifiP2pEnabled = enabled; updateWfdEnableState(); } private void handlePeersChanged() { // Even if wfd is disabled, it is best to get the latest set of peers to // keep in sync with the p2p framework requestPeers(); } private void handleConnectionChanged(NetworkInfo networkInfo) { mNetworkInfo = networkInfo; if (mWfdEnabled && networkInfo.isConnected()) { if (mDesiredDevice != null || mWifiDisplayCertMode) { mWifiP2pManager.requestGroupInfo(mWifiP2pChannel, new GroupInfoListener() { @Override public void onGroupInfoAvailable(WifiP2pGroup info) { if (DEBUG) { Slog.d(TAG, "Received group info: " + describeWifiP2pGroup(info)); } if (mConnectingDevice != null && !info.contains(mConnectingDevice)) { Slog.i(TAG, "Aborting connection to Wifi display because " + "the current P2P group does not contain the device " + "we expected to find: " + mConnectingDevice.deviceName + ", group info was: " + describeWifiP2pGroup(info)); handleConnectionFailure(false); return; } if (mDesiredDevice != null && !info.contains(mDesiredDevice)) { disconnect(); return; } if (mWifiDisplayCertMode) { boolean owner = info.getOwner().deviceAddress .equals(mThisDevice.deviceAddress); if (owner && info.getClientList().isEmpty()) { // this is the case when we started Autonomous GO, // and no client has connected, save group info // and updateConnection() mConnectingDevice = mDesiredDevice = null; mConnectedDeviceGroupInfo = info; updateConnection(); } else if (mConnectingDevice == null && mDesiredDevice == null) { // this is the case when we received an incoming connection // from the sink, update both mConnectingDevice and mDesiredDevice // then proceed to updateConnection() below mConnectingDevice = mDesiredDevice = owner ? info.getClientList().iterator().next() : info.getOwner(); } } if (mConnectingDevice != null && mConnectingDevice == mDesiredDevice) { Slog.i(TAG, "Connected to Wifi display: " + mConnectingDevice.deviceName); mHandler.removeCallbacks(mConnectionTimeout); mConnectedDeviceGroupInfo = info; mConnectedDevice = mConnectingDevice; mConnectingDevice = null; updateConnection(); } } }); } } else { mConnectedDeviceGroupInfo = null; // Disconnect if we lost the network while connecting or connected to a display. if (mConnectingDevice != null || mConnectedDevice != null) { disconnect(); } // After disconnection for a group, for some reason we have a tendency // to get a peer change notification with an empty list of peers. // Perform a fresh scan. if (mWfdEnabled) { requestPeers(); } } } private final Runnable mDiscoverPeers = new Runnable() { @Override public void run() { tryDiscoverPeers(); } }; private final Runnable mConnectionTimeout = new Runnable() { @Override public void run() { if (mConnectingDevice != null && mConnectingDevice == mDesiredDevice) { Slog.i(TAG, "Timed out waiting for Wifi display connection after " + CONNECTION_TIMEOUT_SECONDS + " seconds: " + mConnectingDevice.deviceName); handleConnectionFailure(true); } } }; private final Runnable mRtspTimeout = new Runnable() { @Override public void run() { if (mConnectedDevice != null && mRemoteDisplay != null && !mRemoteDisplayConnected) { Slog.i(TAG, "Timed out waiting for Wifi display RTSP connection after " + RTSP_TIMEOUT_SECONDS + " seconds: " + mConnectedDevice.deviceName); handleConnectionFailure(true); } } }; private void handleConnectionFailure(boolean timeoutOccurred) { Slog.i(TAG, "Wifi display connection failed!"); if (mDesiredDevice != null) { if (mConnectionRetriesLeft > 0) { final WifiP2pDevice oldDevice = mDesiredDevice; mHandler.postDelayed(new Runnable() { @Override public void run() { if (mDesiredDevice == oldDevice && mConnectionRetriesLeft > 0) { mConnectionRetriesLeft -= 1; Slog.i(TAG, "Retrying Wifi display connection. Retries left: " + mConnectionRetriesLeft); retryConnection(); } } }, timeoutOccurred ? 0 : CONNECT_RETRY_DELAY_MILLIS); } else { disconnect(); } } } private void advertiseDisplay(final WifiDisplay display, final Surface surface, final int width, final int height, final int flags) { if (!Objects.equal(mAdvertisedDisplay, display) || mAdvertisedDisplaySurface != surface || mAdvertisedDisplayWidth != width || mAdvertisedDisplayHeight != height || mAdvertisedDisplayFlags != flags) { final WifiDisplay oldDisplay = mAdvertisedDisplay; final Surface oldSurface = mAdvertisedDisplaySurface; mAdvertisedDisplay = display; mAdvertisedDisplaySurface = surface; mAdvertisedDisplayWidth = width; mAdvertisedDisplayHeight = height; mAdvertisedDisplayFlags = flags; mHandler.post(new Runnable() { @Override public void run() { if (oldSurface != null && surface != oldSurface) { mListener.onDisplayDisconnected(); } else if (oldDisplay != null && !oldDisplay.hasSameAddress(display)) { mListener.onDisplayConnectionFailed(); } if (display != null) { if (!display.hasSameAddress(oldDisplay)) { mListener.onDisplayConnecting(display); } else if (!display.equals(oldDisplay)) { // The address is the same but some other property such as the // name must have changed. mListener.onDisplayChanged(display); } if (surface != null && surface != oldSurface) { mListener.onDisplayConnected(display, surface, width, height, flags); } } } }); } } private void unadvertiseDisplay() { advertiseDisplay(null, null, 0, 0, 0); } private void readvertiseDisplay(WifiDisplay display) { advertiseDisplay(display, mAdvertisedDisplaySurface, mAdvertisedDisplayWidth, mAdvertisedDisplayHeight, mAdvertisedDisplayFlags); } private static Inet4Address getInterfaceAddress(WifiP2pGroup info) { NetworkInterface iface; try { iface = NetworkInterface.getByName(info.getInterface()); } catch (SocketException ex) { Slog.w(TAG, "Could not obtain address of network interface " + info.getInterface(), ex); return null; } Enumeration addrs = iface.getInetAddresses(); while (addrs.hasMoreElements()) { InetAddress addr = addrs.nextElement(); if (addr instanceof Inet4Address) { return (Inet4Address)addr; } } Slog.w(TAG, "Could not obtain address of network interface " + info.getInterface() + " because it had no IPv4 addresses."); return null; } private static int getPortNumber(WifiP2pDevice device) { if (device.deviceName.startsWith("DIRECT-") && device.deviceName.endsWith("Broadcom")) { // These dongles ignore the port we broadcast in our WFD IE. return 8554; } return DEFAULT_CONTROL_PORT; } private static boolean isWifiDisplay(WifiP2pDevice device) { return device.wfdInfo != null && device.wfdInfo.isWfdEnabled() && isPrimarySinkDeviceType(device.wfdInfo.getDeviceType()); } private static boolean isPrimarySinkDeviceType(int deviceType) { return deviceType == WifiP2pWfdInfo.PRIMARY_SINK || deviceType == WifiP2pWfdInfo.SOURCE_OR_PRIMARY_SINK; } private static String describeWifiP2pDevice(WifiP2pDevice device) { return device != null ? device.toString().replace('\n', ',') : "null"; } private static String describeWifiP2pGroup(WifiP2pGroup group) { return group != null ? group.toString().replace('\n', ',') : "null"; } private static WifiDisplay createWifiDisplay(WifiP2pDevice device) { return new WifiDisplay(device.deviceAddress, device.deviceName, null, true, device.wfdInfo.isSessionAvailable(), false); } private final BroadcastReceiver mWifiP2pReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (action.equals(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)) { // This broadcast is sticky so we'll always get the initial Wifi P2P state // on startup. boolean enabled = (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, WifiP2pManager.WIFI_P2P_STATE_DISABLED)) == WifiP2pManager.WIFI_P2P_STATE_ENABLED; if (DEBUG) { Slog.d(TAG, "Received WIFI_P2P_STATE_CHANGED_ACTION: enabled=" + enabled); } handleStateChanged(enabled); } else if (action.equals(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)) { if (DEBUG) { Slog.d(TAG, "Received WIFI_P2P_PEERS_CHANGED_ACTION."); } handlePeersChanged(); } else if (action.equals(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) { NetworkInfo networkInfo = (NetworkInfo)intent.getParcelableExtra( WifiP2pManager.EXTRA_NETWORK_INFO); if (DEBUG) { Slog.d(TAG, "Received WIFI_P2P_CONNECTION_CHANGED_ACTION: networkInfo=" + networkInfo); } handleConnectionChanged(networkInfo); } else if (action.equals(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)) { mThisDevice = (WifiP2pDevice) intent.getParcelableExtra( WifiP2pManager.EXTRA_WIFI_P2P_DEVICE); if (DEBUG) { Slog.d(TAG, "Received WIFI_P2P_THIS_DEVICE_CHANGED_ACTION: mThisDevice= " + mThisDevice); } } } }; /** * Called on the handler thread when displays are connected or disconnected. */ public interface Listener { void onFeatureStateChanged(int featureState); void onScanStarted(); void onScanResults(WifiDisplay[] availableDisplays); void onScanFinished(); void onDisplayConnecting(WifiDisplay display); void onDisplayConnectionFailed(); void onDisplayChanged(WifiDisplay display); void onDisplayConnected(WifiDisplay display, Surface surface, int width, int height, int flags); void onDisplaySessionInfo(WifiDisplaySessionInfo sessionInfo); void onDisplayDisconnected(); } }