content: /*
   * 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 android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.hardware.display.DisplayManager;
  import android.hardware.display.WifiDisplay;
  import android.hardware.display.WifiDisplaySessionInfo;
  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.util.Slog;
  import android.view.Display;
  import android.view.DisplayAddress;
  import android.view.Surface;
  import android.view.SurfaceControl;
  
  import com.android.internal.util.DumpUtils;
  import com.android.internal.util.IndentingPrintWriter;
  
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Objects;
  
  /**
   * Connects to Wifi displays that implement the Miracast protocol.
   * <p>
   * 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.
   * </p><p>
   * This class is responsible for connecting to Wifi displays and mediating
   * the interactions between Media Server, Surface Flinger and the Display Manager Service.
   * </p><p>
   * Display adapters are guarded by the {@link DisplayManagerService.SyncRoot} lock.
   * </p>
   */
  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 String ACTION_DISCONNECT = "android.server.display.wfd.DISCONNECT";
  
      // Unique id prefix for wifi displays
      private static final String DISPLAY_NAME_PREFIX = "wifi:";
  
      private final WifiDisplayHandler mHandler;
      private final PersistentDataStore mPersistentDataStore;
      private final boolean mSupportsProtectedBuffers;
  
      private WifiDisplayController mDisplayController;
      private WifiDisplayDevice mDisplayDevice;
  
      private WifiDisplayStatus mCurrentStatus;
      private int mFeatureState;
      private int mScanState;
      private int mActiveDisplayState;
      private WifiDisplay mActiveDisplay;
      private WifiDisplay[] mDisplays = WifiDisplay.EMPTY_ARRAY;
      private WifiDisplay[] mAvailableDisplays = WifiDisplay.EMPTY_ARRAY;
      private WifiDisplay[] mRememberedDisplays = WifiDisplay.EMPTY_ARRAY;
      private WifiDisplaySessionInfo mSessionInfo;
  
      private boolean mPendingStatusChangeBroadcast;
  
      // 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);
      }
  
      @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("mDisplays=" + Arrays.toString(mDisplays));
          pw.println("mAvailableDisplays=" + Arrays.toString(mAvailableDisplays));
          pw.println("mRememberedDisplays=" + Arrays.toString(mRememberedDisplays));
          pw.println("mPendingStatusChangeBroadcast=" + mPendingStatusChangeBroadcast);
          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 requestStartScanLocked() {
          if (DEBUG) {
              Slog.d(TAG, "requestStartScanLocked");
          }
  
          getHandler().post(new Runnable() {
              @Override
              public void run() {
                  if (mDisplayController != null) {
                      mDisplayController.requestStartScan();
                  }
              }
          });
      }
  
      public void requestStopScanLocked() {
          if (DEBUG) {
              Slog.d(TAG, "requestStopScanLocked");
          }
  
          getHandler().post(new Runnable() {
              @Override
              public void run() {
                  if (mDisplayController != null) {
                      mDisplayController.requestStopScan();
                  }
              }
          });
      }
  
      public void requestConnectLocked(final String address) {
          if (DEBUG) {
              Slog.d(TAG, "requestConnectLocked: address=" + address);
          }
  
          getHandler().post(new Runnable() {
              @Override
              public void run() {
                  if (mDisplayController != null) {
                      mDisplayController.requestConnect(address);
                  }
              }
          });
      }
  
      public void requestPauseLocked() {
          if (DEBUG) {
              Slog.d(TAG, "requestPauseLocked");
          }
  
          getHandler().post(new Runnable() {
              @Override
              public void run() {
                  if (mDisplayController != null) {
                      mDisplayController.requestPause();
                  }
              }
          });
        }
  
      public void requestResumeLocked() {
          if (DEBUG) {
              Slog.d(TAG, "requestResumeLocked");
          }
  
          getHandler().post(new Runnable() {
              @Override
              public void run() {
                  if (mDisplayController != null) {
                      mDisplayController.requestResume();
                  }
              }
          });
      }
  
      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.equals(display.getDeviceAlias(), alias)) {
              display = new WifiDisplay(address, display.getDeviceName(), alias,
                      false, false, false);
              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, mDisplays, mSessionInfo);
          }
  
          if (DEBUG) {
              Slog.d(TAG, "getWifiDisplayStatusLocked: result=" + mCurrentStatus);
          }
          return mCurrentStatus;
      }
  
      private void updateDisplaysLocked() {
          List<WifiDisplay> displays = new ArrayList<WifiDisplay>(
                  mAvailableDisplays.length + mRememberedDisplays.length);
          boolean[] remembered = new boolean[mAvailableDisplays.length];
          for (WifiDisplay d : mRememberedDisplays) {
              boolean available = false;
              for (int i = 0; i < mAvailableDisplays.length; i++) {
                  if (d.equals(mAvailableDisplays[i])) {
                      remembered[i] = available = true;
                      break;
                  }
              }
              if (!available) {
                  displays.add(new WifiDisplay(d.getDeviceAddress(), d.getDeviceName(),
                          d.getDeviceAlias(), false, false, true));
              }
          }
          for (int i = 0; i < mAvailableDisplays.length; i++) {
              WifiDisplay d = mAvailableDisplays[i];
              displays.add(new WifiDisplay(d.getDeviceAddress(), d.getDeviceName(),
                      d.getDeviceAlias(), true, d.canConnect(), remembered[i]));
          }
          mDisplays = displays.toArray(WifiDisplay.EMPTY_ARRAY);
      }
  
      private void updateRememberedDisplaysLocked() {
          mRememberedDisplays = mPersistentDataStore.getRememberedWifiDisplays();
          mActiveDisplay = mPersistentDataStore.applyWifiDisplayAlias(mActiveDisplay);
          mAvailableDisplays = mPersistentDataStore.applyWifiDisplayAliases(mAvailableDisplays);
          updateDisplaysLocked();
      }
  
      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 = DisplayDeviceInfo.FLAG_PRESENTATION;
          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);
      }
  
      private void removeDisplayDeviceLocked() {
          if (mDisplayDevice != null) {
              mDisplayDevice.destroyLocked();
              sendDisplayDeviceEventLocked(mDisplayDevice, DISPLAY_DEVICE_EVENT_REMOVED);
              mDisplayDevice = null;
          }
      }
  
      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);
          }
      }
  
      // 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);
      }
  
      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 onScanResults(WifiDisplay[] availableDisplays) {
              synchronized (getSyncRoot()) {
                  availableDisplays = mPersistentDataStore.applyWifiDisplayAliases(
                          availableDisplays);
  
                  boolean changed = !Arrays.equals(mAvailableDisplays, availableDisplays);
  
                  // Check whether any of the available displays changed canConnect status.
                  for (int i = 0; !changed && i<availableDisplays.length; i++) {
                      changed = availableDisplays[i].canConnect()
                              != mAvailableDisplays[i].canConnect();
                  }
  
                  if (changed) {
                      mAvailableDisplays = availableDisplays;
                      fixRememberedDisplayNamesFromAvailableDisplaysLocked();
                      updateDisplaysLocked();
                      scheduleStatusChangedBroadcastLocked();
                  }
              }
          }
  
          @Override
          public void onScanFinished() {
              synchronized (getSyncRoot()) {
                  if (mScanState != WifiDisplayStatus.SCAN_STATE_NOT_SCANNING) {
                      mScanState = WifiDisplayStatus.SCAN_STATE_NOT_SCANNING;
                      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 onDisplaySessionInfo(WifiDisplaySessionInfo sessionInfo) {
              synchronized (getSyncRoot()) {
                  mSessionInfo = sessionInfo;
                  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 DisplayAddress mAddress;
          private final Display.Mode mMode;
  
          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, DISPLAY_NAME_PREFIX + address);
              mName = name;
              mWidth = width;
              mHeight = height;
              mRefreshRate = refreshRate;
              mFlags = flags;
              mAddress = DisplayAddress.fromMacAddress(address);
              mSurface = surface;
              mMode = createMode(width, height, refreshRate);
          }
  
          @Override
          public boolean hasStableUniqueId() {
              return true;
          }
  
          public void destroyLocked() {
              if (mSurface != null) {
                  mSurface.release();
                  mSurface = null;
              }
              SurfaceControl.destroyDisplay(getDisplayTokenLocked());
          }
  
          public void setNameLocked(String name) {
              mName = name;
              mInfo = null;
          }
  
          @Override
          public void performTraversalLocked(SurfaceControl.Transaction t) {
              if (mSurface != null) {
                  setSurfaceLocked(t, mSurface);
              }
          }
  
          @Override
          public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
              if (mInfo == null) {
                  mInfo = new DisplayDeviceInfo();
                  mInfo.name = mName;
                  mInfo.uniqueId = getUniqueId();
                  mInfo.width = mWidth;
                  mInfo.height = mHeight;
                  mInfo.modeId = mMode.getModeId();
                  mInfo.defaultModeId = mMode.getModeId();
                  mInfo.supportedModes = new Display.Mode[] { mMode };
                  mInfo.presentationDeadlineNanos = 1000000000L / (int) mRefreshRate; // 1 frame
                  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;
              }
          }
      }
  }
  