/* * 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.R; import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Resources; import android.hardware.display.DisplayManager; import android.hardware.display.WifiDisplay; import android.hardware.display.WifiDisplayStatus; import android.media.RemoteDisplay; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.UserHandle; import android.provider.Settings; import android.util.Slog; import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; import java.io.PrintWriter; import java.util.Arrays; import libcore.util.Objects; /** * Connects to Wifi displays that implement the Miracast protocol. *
* The Wifi display protocol relies on Wifi direct for discovering and pairing * with the display. Once connected, the Media Server opens an RTSP socket and accepts * a connection from the display. After session negotiation, the Media Server * streams encoded buffers to the display. *
* This class is responsible for connecting to Wifi displays and mediating * the interactions between Media Server, Surface Flinger and the Display Manager Service. *
* Display adapters are guarded by the {@link DisplayManagerService.SyncRoot} lock. *
*/ final class WifiDisplayAdapter extends DisplayAdapter { private static final String TAG = "WifiDisplayAdapter"; private static final boolean DEBUG = false; private static final int MSG_SEND_STATUS_CHANGE_BROADCAST = 1; private static final int MSG_UPDATE_NOTIFICATION = 2; private static final String ACTION_DISCONNECT = "android.server.display.wfd.DISCONNECT"; private final WifiDisplayHandler mHandler; private final PersistentDataStore mPersistentDataStore; private final boolean mSupportsProtectedBuffers; private final NotificationManager mNotificationManager; private PendingIntent mSettingsPendingIntent; private PendingIntent mDisconnectPendingIntent; private WifiDisplayController mDisplayController; private WifiDisplayDevice mDisplayDevice; private WifiDisplayStatus mCurrentStatus; private int mFeatureState; private int mScanState; private int mActiveDisplayState; private WifiDisplay mActiveDisplay; private WifiDisplay[] mAvailableDisplays = WifiDisplay.EMPTY_ARRAY; private WifiDisplay[] mRememberedDisplays = WifiDisplay.EMPTY_ARRAY; private boolean mPendingStatusChangeBroadcast; private boolean mPendingNotificationUpdate; // Called with SyncRoot lock held. public WifiDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context, Handler handler, Listener listener, PersistentDataStore persistentDataStore) { super(syncRoot, context, handler, listener, TAG); mHandler = new WifiDisplayHandler(handler.getLooper()); mPersistentDataStore = persistentDataStore; mSupportsProtectedBuffers = context.getResources().getBoolean( com.android.internal.R.bool.config_wifiDisplaySupportsProtectedBuffers); mNotificationManager = (NotificationManager)context.getSystemService( Context.NOTIFICATION_SERVICE); } @Override public void dumpLocked(PrintWriter pw) { super.dumpLocked(pw); pw.println("mCurrentStatus=" + getWifiDisplayStatusLocked()); pw.println("mFeatureState=" + mFeatureState); pw.println("mScanState=" + mScanState); pw.println("mActiveDisplayState=" + mActiveDisplayState); pw.println("mActiveDisplay=" + mActiveDisplay); pw.println("mAvailableDisplays=" + Arrays.toString(mAvailableDisplays)); pw.println("mRememberedDisplays=" + Arrays.toString(mRememberedDisplays)); pw.println("mPendingStatusChangeBroadcast=" + mPendingStatusChangeBroadcast); pw.println("mPendingNotificationUpdate=" + mPendingNotificationUpdate); pw.println("mSupportsProtectedBuffers=" + mSupportsProtectedBuffers); // Try to dump the controller state. if (mDisplayController == null) { pw.println("mDisplayController=null"); } else { pw.println("mDisplayController:"); final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); ipw.increaseIndent(); DumpUtils.dumpAsync(getHandler(), mDisplayController, ipw, 200); } } @Override public void registerLocked() { super.registerLocked(); updateRememberedDisplaysLocked(); getHandler().post(new Runnable() { @Override public void run() { mDisplayController = new WifiDisplayController( getContext(), getHandler(), mWifiDisplayListener); getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, new IntentFilter(ACTION_DISCONNECT), null, mHandler); } }); } public void requestScanLocked() { if (DEBUG) { Slog.d(TAG, "requestScanLocked"); } getHandler().post(new Runnable() { @Override public void run() { if (mDisplayController != null) { mDisplayController.requestScan(); } } }); } public void requestConnectLocked(final String address, final boolean trusted) { if (DEBUG) { Slog.d(TAG, "requestConnectLocked: address=" + address + ", trusted=" + trusted); } if (!trusted) { synchronized (getSyncRoot()) { if (!isRememberedDisplayLocked(address)) { Slog.w(TAG, "Ignoring request by an untrusted client to connect to " + "an unknown wifi display: " + address); return; } } } getHandler().post(new Runnable() { @Override public void run() { if (mDisplayController != null) { mDisplayController.requestConnect(address); } } }); } private boolean isRememberedDisplayLocked(String address) { for (WifiDisplay display : mRememberedDisplays) { if (display.getDeviceAddress().equals(address)) { return true; } } return false; } public void requestDisconnectLocked() { if (DEBUG) { Slog.d(TAG, "requestDisconnectedLocked"); } getHandler().post(new Runnable() { @Override public void run() { if (mDisplayController != null) { mDisplayController.requestDisconnect(); } } }); } public void requestRenameLocked(String address, String alias) { if (DEBUG) { Slog.d(TAG, "requestRenameLocked: address=" + address + ", alias=" + alias); } if (alias != null) { alias = alias.trim(); if (alias.isEmpty() || alias.equals(address)) { alias = null; } } WifiDisplay display = mPersistentDataStore.getRememberedWifiDisplay(address); if (display != null && !Objects.equal(display.getDeviceAlias(), alias)) { display = new WifiDisplay(address, display.getDeviceName(), alias); if (mPersistentDataStore.rememberWifiDisplay(display)) { mPersistentDataStore.saveIfNeeded(); updateRememberedDisplaysLocked(); scheduleStatusChangedBroadcastLocked(); } } if (mActiveDisplay != null && mActiveDisplay.getDeviceAddress().equals(address)) { renameDisplayDeviceLocked(mActiveDisplay.getFriendlyDisplayName()); } } public void requestForgetLocked(String address) { if (DEBUG) { Slog.d(TAG, "requestForgetLocked: address=" + address); } if (mPersistentDataStore.forgetWifiDisplay(address)) { mPersistentDataStore.saveIfNeeded(); updateRememberedDisplaysLocked(); scheduleStatusChangedBroadcastLocked(); } if (mActiveDisplay != null && mActiveDisplay.getDeviceAddress().equals(address)) { requestDisconnectLocked(); } } public WifiDisplayStatus getWifiDisplayStatusLocked() { if (mCurrentStatus == null) { mCurrentStatus = new WifiDisplayStatus( mFeatureState, mScanState, mActiveDisplayState, mActiveDisplay, mAvailableDisplays, mRememberedDisplays); } if (DEBUG) { Slog.d(TAG, "getWifiDisplayStatusLocked: result=" + mCurrentStatus); } return mCurrentStatus; } private void updateRememberedDisplaysLocked() { mRememberedDisplays = mPersistentDataStore.getRememberedWifiDisplays(); mActiveDisplay = mPersistentDataStore.applyWifiDisplayAlias(mActiveDisplay); mAvailableDisplays = mPersistentDataStore.applyWifiDisplayAliases(mAvailableDisplays); } private void fixRememberedDisplayNamesFromAvailableDisplaysLocked() { // It may happen that a display name has changed since it was remembered. // Consult the list of available displays and update the name if needed. // We don't do anything special for the active display here. The display // controller will send a separate event when it needs to be updates. boolean changed = false; for (int i = 0; i < mRememberedDisplays.length; i++) { WifiDisplay rememberedDisplay = mRememberedDisplays[i]; WifiDisplay availableDisplay = findAvailableDisplayLocked( rememberedDisplay.getDeviceAddress()); if (availableDisplay != null && !rememberedDisplay.equals(availableDisplay)) { if (DEBUG) { Slog.d(TAG, "fixRememberedDisplayNamesFromAvailableDisplaysLocked: " + "updating remembered display to " + availableDisplay); } mRememberedDisplays[i] = availableDisplay; changed |= mPersistentDataStore.rememberWifiDisplay(availableDisplay); } } if (changed) { mPersistentDataStore.saveIfNeeded(); } } private WifiDisplay findAvailableDisplayLocked(String address) { for (WifiDisplay display : mAvailableDisplays) { if (display.getDeviceAddress().equals(address)) { return display; } } return null; } private void addDisplayDeviceLocked(WifiDisplay display, Surface surface, int width, int height, int flags) { removeDisplayDeviceLocked(); if (mPersistentDataStore.rememberWifiDisplay(display)) { mPersistentDataStore.saveIfNeeded(); updateRememberedDisplaysLocked(); scheduleStatusChangedBroadcastLocked(); } boolean secure = (flags & RemoteDisplay.DISPLAY_FLAG_SECURE) != 0; int deviceFlags = 0; if (secure) { deviceFlags |= DisplayDeviceInfo.FLAG_SECURE; if (mSupportsProtectedBuffers) { deviceFlags |= DisplayDeviceInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS; } } float refreshRate = 60.0f; // TODO: get this for real String name = display.getFriendlyDisplayName(); String address = display.getDeviceAddress(); IBinder displayToken = SurfaceControl.createDisplay(name, secure); mDisplayDevice = new WifiDisplayDevice(displayToken, name, width, height, refreshRate, deviceFlags, address, surface); sendDisplayDeviceEventLocked(mDisplayDevice, DISPLAY_DEVICE_EVENT_ADDED); scheduleUpdateNotificationLocked(); } private void removeDisplayDeviceLocked() { if (mDisplayDevice != null) { mDisplayDevice.clearSurfaceLocked(); sendDisplayDeviceEventLocked(mDisplayDevice, DISPLAY_DEVICE_EVENT_REMOVED); mDisplayDevice = null; scheduleUpdateNotificationLocked(); } } private void renameDisplayDeviceLocked(String name) { if (mDisplayDevice != null && !mDisplayDevice.getNameLocked().equals(name)) { mDisplayDevice.setNameLocked(name); sendDisplayDeviceEventLocked(mDisplayDevice, DISPLAY_DEVICE_EVENT_CHANGED); } } private void scheduleStatusChangedBroadcastLocked() { mCurrentStatus = null; if (!mPendingStatusChangeBroadcast) { mPendingStatusChangeBroadcast = true; mHandler.sendEmptyMessage(MSG_SEND_STATUS_CHANGE_BROADCAST); } } private void scheduleUpdateNotificationLocked() { if (!mPendingNotificationUpdate) { mPendingNotificationUpdate = true; mHandler.sendEmptyMessage(MSG_UPDATE_NOTIFICATION); } } // Runs on the handler. private void handleSendStatusChangeBroadcast() { final Intent intent; synchronized (getSyncRoot()) { if (!mPendingStatusChangeBroadcast) { return; } mPendingStatusChangeBroadcast = false; intent = new Intent(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); intent.putExtra(DisplayManager.EXTRA_WIFI_DISPLAY_STATUS, getWifiDisplayStatusLocked()); } // Send protected broadcast about wifi display status to registered receivers. getContext().sendBroadcastAsUser(intent, UserHandle.ALL); } // Runs on the handler. private void handleUpdateNotification() { final boolean isConnected; synchronized (getSyncRoot()) { if (!mPendingNotificationUpdate) { return; } mPendingNotificationUpdate = false; isConnected = (mDisplayDevice != null); } // Cancel the old notification if there is one. mNotificationManager.cancelAsUser(null, R.string.wifi_display_notification_title, UserHandle.ALL); if (isConnected) { Context context = getContext(); // Initialize pending intents for the notification outside of the lock because // creating a pending intent requires a call into the activity manager. if (mSettingsPendingIntent == null) { Intent settingsIntent = new Intent(Settings.ACTION_WIFI_DISPLAY_SETTINGS); settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); mSettingsPendingIntent = PendingIntent.getActivityAsUser( context, 0, settingsIntent, 0, null, UserHandle.CURRENT); } if (mDisconnectPendingIntent == null) { Intent disconnectIntent = new Intent(ACTION_DISCONNECT); mDisconnectPendingIntent = PendingIntent.getBroadcastAsUser( context, 0, disconnectIntent, 0, UserHandle.CURRENT); } // Post the notification. Resources r = context.getResources(); Notification notification = new Notification.Builder(context) .setContentTitle(r.getString( R.string.wifi_display_notification_title)) .setContentText(r.getString( R.string.wifi_display_notification_message)) .setContentIntent(mSettingsPendingIntent) .setSmallIcon(R.drawable.ic_notify_wifidisplay) .setOngoing(true) .addAction(android.R.drawable.ic_menu_close_clear_cancel, r.getString(R.string.wifi_display_notification_disconnect), mDisconnectPendingIntent) .build(); mNotificationManager.notifyAsUser(null, R.string.wifi_display_notification_title, notification, UserHandle.ALL); } } private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(ACTION_DISCONNECT)) { synchronized (getSyncRoot()) { requestDisconnectLocked(); } } } }; private final WifiDisplayController.Listener mWifiDisplayListener = new WifiDisplayController.Listener() { @Override public void onFeatureStateChanged(int featureState) { synchronized (getSyncRoot()) { if (mFeatureState != featureState) { mFeatureState = featureState; scheduleStatusChangedBroadcastLocked(); } } } @Override public void onScanStarted() { synchronized (getSyncRoot()) { if (mScanState != WifiDisplayStatus.SCAN_STATE_SCANNING) { mScanState = WifiDisplayStatus.SCAN_STATE_SCANNING; scheduleStatusChangedBroadcastLocked(); } } } @Override public void onScanFinished(WifiDisplay[] availableDisplays) { synchronized (getSyncRoot()) { availableDisplays = mPersistentDataStore.applyWifiDisplayAliases( availableDisplays); if (mScanState != WifiDisplayStatus.SCAN_STATE_NOT_SCANNING || !Arrays.equals(mAvailableDisplays, availableDisplays)) { mScanState = WifiDisplayStatus.SCAN_STATE_NOT_SCANNING; mAvailableDisplays = availableDisplays; fixRememberedDisplayNamesFromAvailableDisplaysLocked(); scheduleStatusChangedBroadcastLocked(); } } } @Override public void onDisplayConnecting(WifiDisplay display) { synchronized (getSyncRoot()) { display = mPersistentDataStore.applyWifiDisplayAlias(display); if (mActiveDisplayState != WifiDisplayStatus.DISPLAY_STATE_CONNECTING || mActiveDisplay == null || !mActiveDisplay.equals(display)) { mActiveDisplayState = WifiDisplayStatus.DISPLAY_STATE_CONNECTING; mActiveDisplay = display; scheduleStatusChangedBroadcastLocked(); } } } @Override public void onDisplayConnectionFailed() { synchronized (getSyncRoot()) { if (mActiveDisplayState != WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED || mActiveDisplay != null) { mActiveDisplayState = WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED; mActiveDisplay = null; scheduleStatusChangedBroadcastLocked(); } } } @Override public void onDisplayConnected(WifiDisplay display, Surface surface, int width, int height, int flags) { synchronized (getSyncRoot()) { display = mPersistentDataStore.applyWifiDisplayAlias(display); addDisplayDeviceLocked(display, surface, width, height, flags); if (mActiveDisplayState != WifiDisplayStatus.DISPLAY_STATE_CONNECTED || mActiveDisplay == null || !mActiveDisplay.equals(display)) { mActiveDisplayState = WifiDisplayStatus.DISPLAY_STATE_CONNECTED; mActiveDisplay = display; scheduleStatusChangedBroadcastLocked(); } } } @Override public void onDisplayChanged(WifiDisplay display) { synchronized (getSyncRoot()) { display = mPersistentDataStore.applyWifiDisplayAlias(display); if (mActiveDisplay != null && mActiveDisplay.hasSameAddress(display) && !mActiveDisplay.equals(display)) { mActiveDisplay = display; renameDisplayDeviceLocked(display.getFriendlyDisplayName()); scheduleStatusChangedBroadcastLocked(); } } } @Override public void onDisplayDisconnected() { // Stop listening. synchronized (getSyncRoot()) { removeDisplayDeviceLocked(); if (mActiveDisplayState != WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED || mActiveDisplay != null) { mActiveDisplayState = WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED; mActiveDisplay = null; scheduleStatusChangedBroadcastLocked(); } } } }; private final class WifiDisplayDevice extends DisplayDevice { private String mName; private final int mWidth; private final int mHeight; private final float mRefreshRate; private final int mFlags; private final String mAddress; private Surface mSurface; private DisplayDeviceInfo mInfo; public WifiDisplayDevice(IBinder displayToken, String name, int width, int height, float refreshRate, int flags, String address, Surface surface) { super(WifiDisplayAdapter.this, displayToken); mName = name; mWidth = width; mHeight = height; mRefreshRate = refreshRate; mFlags = flags; mAddress = address; mSurface = surface; } public void clearSurfaceLocked() { mSurface = null; sendTraversalRequestLocked(); } public void setNameLocked(String name) { mName = name; mInfo = null; } @Override public void performTraversalInTransactionLocked() { setSurfaceInTransactionLocked(mSurface); } @Override public DisplayDeviceInfo getDisplayDeviceInfoLocked() { if (mInfo == null) { mInfo = new DisplayDeviceInfo(); mInfo.name = mName; mInfo.width = mWidth; mInfo.height = mHeight; mInfo.refreshRate = mRefreshRate; mInfo.flags = mFlags; mInfo.type = Display.TYPE_WIFI; mInfo.address = mAddress; mInfo.touch = DisplayDeviceInfo.TOUCH_EXTERNAL; mInfo.setAssumedDensityForExternalDisplay(mWidth, mHeight); } return mInfo; } } private final class WifiDisplayHandler extends Handler { public WifiDisplayHandler(Looper looper) { super(looper, null, true /*async*/); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_SEND_STATUS_CHANGE_BROADCAST: handleSendStatusChangeBroadcast(); break; case MSG_UPDATE_NOTIFICATION: handleUpdateNotification(); break; } } } }