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.Context;
  import android.database.ContentObserver;
  import android.graphics.SurfaceTexture;
  import android.os.Handler;
  import android.os.IBinder;
  import android.provider.Settings;
  import android.util.DisplayMetrics;
  import android.util.Slog;
  import android.view.Display;
  import android.view.Gravity;
  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.regex.Matcher;
  import java.util.regex.Pattern;
  
  /**
   * A display adapter that uses overlay windows to simulate secondary displays
   * for development purposes.  Use Development Settings to enable one or more
   * overlay displays.
   * <p>
   * This object has two different handlers (which may be the same) which must not
   * get confused.  The main handler is used to posting messages to the display manager
   * service as usual.  The UI handler is only used by the {@link OverlayDisplayWindow}.
   * </p><p>
   * Display adapters are guarded by the {@link DisplayManagerService.SyncRoot} lock.
   * </p><p>
   * This adapter is configured via the
   * {@link android.provider.Settings.Global#OVERLAY_DISPLAY_DEVICES} setting. This setting should be
   * formatted as follows:
   * <pre>
   * [mode1]|[mode2]|...,[flag1],[flag2],...
   * </pre>
   * with each mode specified as:
   * <pre>
   * [width]x[height]/[densityDpi]
   * </pre>
   * Supported flags:
   * <ul>
   * <li><pre>secure</pre>: creates a secure display</li>
   * </ul>
   * </p>
   */
  final class OverlayDisplayAdapter extends DisplayAdapter {
      static final String TAG = "OverlayDisplayAdapter";
      static final boolean DEBUG = false;
  
      private static final int MIN_WIDTH = 100;
      private static final int MIN_HEIGHT = 100;
      private static final int MAX_WIDTH = 4096;
      private static final int MAX_HEIGHT = 4096;
  
      private static final Pattern DISPLAY_PATTERN =
              Pattern.compile("([^,]+)(,[a-z]+)*");
      private static final Pattern MODE_PATTERN =
              Pattern.compile("(\\d+)x(\\d+)/(\\d+)");
  
      // Unique id prefix for overlay displays.
      private static final String UNIQUE_ID_PREFIX = "overlay:";
  
      private final Handler mUiHandler;
      private final ArrayList<OverlayDisplayHandle> mOverlays =
              new ArrayList<OverlayDisplayHandle>();
      private String mCurrentOverlaySetting = "";
  
      // Called with SyncRoot lock held.
      public OverlayDisplayAdapter(DisplayManagerService.SyncRoot syncRoot,
              Context context, Handler handler, Listener listener, Handler uiHandler) {
          super(syncRoot, context, handler, listener, TAG);
          mUiHandler = uiHandler;
      }
  
      @Override
      public void dumpLocked(PrintWriter pw) {
          super.dumpLocked(pw);
  
          pw.println("mCurrentOverlaySetting=" + mCurrentOverlaySetting);
          pw.println("mOverlays: size=" + mOverlays.size());
          for (OverlayDisplayHandle overlay : mOverlays) {
              overlay.dumpLocked(pw);
          }
      }
  
      @Override
      public void registerLocked() {
          super.registerLocked();
  
          getHandler().post(new Runnable() {
              @Override
              public void run() {
                  getContext().getContentResolver().registerContentObserver(
                          Settings.Global.getUriFor(Settings.Global.OVERLAY_DISPLAY_DEVICES),
                          true, new ContentObserver(getHandler()) {
                              @Override
                              public void onChange(boolean selfChange) {
                                  updateOverlayDisplayDevices();
                              }
                          });
  
                  updateOverlayDisplayDevices();
              }
          });
      }
  
      private void updateOverlayDisplayDevices() {
          synchronized (getSyncRoot()) {
              updateOverlayDisplayDevicesLocked();
          }
      }
  
      private void updateOverlayDisplayDevicesLocked() {
          String value = Settings.Global.getString(getContext().getContentResolver(),
                  Settings.Global.OVERLAY_DISPLAY_DEVICES);
          if (value == null) {
              value = "";
          }
  
          if (value.equals(mCurrentOverlaySetting)) {
              return;
          }
          mCurrentOverlaySetting = value;
  
          if (!mOverlays.isEmpty()) {
              Slog.i(TAG, "Dismissing all overlay display devices.");
              for (OverlayDisplayHandle overlay : mOverlays) {
                  overlay.dismissLocked();
              }
              mOverlays.clear();
          }
  
          int count = 0;
          for (String part : value.split(";")) {
              Matcher displayMatcher = DISPLAY_PATTERN.matcher(part);
              if (displayMatcher.matches()) {
                  if (count >= 4) {
                      Slog.w(TAG, "Too many overlay display devices specified: " + value);
                      break;
                  }
                  String modeString = displayMatcher.group(1);
                  String flagString = displayMatcher.group(2);
                  ArrayList<OverlayMode> modes = new ArrayList<>();
                  for (String mode : modeString.split("\\|")) {
                      Matcher modeMatcher = MODE_PATTERN.matcher(mode);
                      if (modeMatcher.matches()) {
                          try {
                              int width = Integer.parseInt(modeMatcher.group(1), 10);
                              int height = Integer.parseInt(modeMatcher.group(2), 10);
                              int densityDpi = Integer.parseInt(modeMatcher.group(3), 10);
                              if (width >= MIN_WIDTH && width <= MAX_WIDTH
                                      && height >= MIN_HEIGHT && height <= MAX_HEIGHT
                                      && densityDpi >= DisplayMetrics.DENSITY_LOW
                                      && densityDpi <= DisplayMetrics.DENSITY_XXXHIGH) {
                                  modes.add(new OverlayMode(width, height, densityDpi));
                                  continue;
                              } else {
                                  Slog.w(TAG, "Ignoring out-of-range overlay display mode: " + mode);
                              }
                          } catch (NumberFormatException ex) {
                          }
                      } else if (mode.isEmpty()) {
                          continue;
                      }
                  }
                  if (!modes.isEmpty()) {
                      int number = ++count;
                      String name = getContext().getResources().getString(
                              com.android.internal.R.string.display_manager_overlay_display_name,
                              number);
                      int gravity = chooseOverlayGravity(number);
                      boolean secure = flagString != null && flagString.contains(",secure");
  
                      Slog.i(TAG, "Showing overlay display device #" + number
                              + ": name=" + name + ", modes=" + Arrays.toString(modes.toArray()));
  
                      mOverlays.add(new OverlayDisplayHandle(name, modes, gravity, secure, number));
                      continue;
                  }
              }
              Slog.w(TAG, "Malformed overlay display devices setting: " + value);
          }
      }
  
      private static int chooseOverlayGravity(int overlayNumber) {
          switch (overlayNumber) {
              case 1:
                  return Gravity.TOP | Gravity.LEFT;
              case 2:
                  return Gravity.BOTTOM | Gravity.RIGHT;
              case 3:
                  return Gravity.TOP | Gravity.RIGHT;
              case 4:
              default:
                  return Gravity.BOTTOM | Gravity.LEFT;
          }
      }
  
      private abstract class OverlayDisplayDevice extends DisplayDevice {
          private final String mName;
          private final float mRefreshRate;
          private final long mDisplayPresentationDeadlineNanos;
          private final boolean mSecure;
          private final List<OverlayMode> mRawModes;
          private final Display.Mode[] mModes;
          private final int mDefaultMode;
  
          private int mState;
          private SurfaceTexture mSurfaceTexture;
          private Surface mSurface;
          private DisplayDeviceInfo mInfo;
          private int mActiveMode;
  
          public OverlayDisplayDevice(IBinder displayToken, String name,
                  List<OverlayMode> modes, int activeMode, int defaultMode,
                  float refreshRate, long presentationDeadlineNanos,
                  boolean secure, int state,
                  SurfaceTexture surfaceTexture, int number) {
              super(OverlayDisplayAdapter.this, displayToken, UNIQUE_ID_PREFIX + number);
              mName = name;
              mRefreshRate = refreshRate;
              mDisplayPresentationDeadlineNanos = presentationDeadlineNanos;
              mSecure = secure;
              mState = state;
              mSurfaceTexture = surfaceTexture;
              mRawModes = modes;
              mModes = new Display.Mode[modes.size()];
              for (int i = 0; i < modes.size(); i++) {
                  OverlayMode mode = modes.get(i);
                  mModes[i] = createMode(mode.mWidth, mode.mHeight, refreshRate);
              }
              mActiveMode = activeMode;
              mDefaultMode = defaultMode;
          }
  
          public void destroyLocked() {
              mSurfaceTexture = null;
              if (mSurface != null) {
                  mSurface.release();
                  mSurface = null;
              }
              SurfaceControl.destroyDisplay(getDisplayTokenLocked());
          }
  
          @Override
          public boolean hasStableUniqueId() {
              return false;
          }
  
          @Override
          public void performTraversalLocked(SurfaceControl.Transaction t) {
              if (mSurfaceTexture != null) {
                  if (mSurface == null) {
                      mSurface = new Surface(mSurfaceTexture);
                  }
                  setSurfaceLocked(t, mSurface);
              }
          }
  
          public void setStateLocked(int state) {
              mState = state;
              mInfo = null;
          }
  
          @Override
          public DisplayDeviceInfo getDisplayDeviceInfoLocked() {
              if (mInfo == null) {
                  Display.Mode mode = mModes[mActiveMode];
                  OverlayMode rawMode = mRawModes.get(mActiveMode);
                  mInfo = new DisplayDeviceInfo();
                  mInfo.name = mName;
                  mInfo.uniqueId = getUniqueId();
                  mInfo.width = mode.getPhysicalWidth();
                  mInfo.height = mode.getPhysicalHeight();
                  mInfo.modeId = mode.getModeId();
                  mInfo.defaultModeId = mModes[0].getModeId();
                  mInfo.supportedModes = mModes;
                  mInfo.densityDpi = rawMode.mDensityDpi;
                  mInfo.xDpi = rawMode.mDensityDpi;
                  mInfo.yDpi = rawMode.mDensityDpi;
                  mInfo.presentationDeadlineNanos = mDisplayPresentationDeadlineNanos +
                          1000000000L / (int) mRefreshRate;   // display's deadline + 1 frame
                  mInfo.flags = DisplayDeviceInfo.FLAG_PRESENTATION;
                  if (mSecure) {
                      mInfo.flags |= DisplayDeviceInfo.FLAG_SECURE;
                  }
                  mInfo.type = Display.TYPE_OVERLAY;
                  mInfo.touch = DisplayDeviceInfo.TOUCH_VIRTUAL;
                  mInfo.state = mState;
              }
              return mInfo;
          }
  
          @Override
          public void setAllowedDisplayModesLocked(int[] modes) {
              final int id;
              if (modes.length > 0) {
                  // The allowed modes should be ordered by preference, so just use the first mode
                  // here.
                  id = modes[0];
              } else {
                  // If we don't have any allowed modes, just use the default mode.
                  id = 0;
              }
              int index = -1;
              if (id == 0) {
                  // Use the default.
                  index = 0;
              } else {
                  for (int i = 0; i < mModes.length; i++) {
                      if (mModes[i].getModeId() == id) {
                          index = i;
                          break;
                      }
                  }
              }
              if (index == -1) {
                  Slog.w(TAG, "Unable to locate mode " + id + ", reverting to default.");
                  index = mDefaultMode;
              }
              if (mActiveMode == index) {
                  return;
              }
              mActiveMode = index;
              mInfo = null;
              sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED);
              onModeChangedLocked(index);
          }
  
          /**
           * Called when the device switched to a new mode.
           *
           * @param index index of the mode in the list of modes
           */
          public abstract void onModeChangedLocked(int index);
      }
  
      /**
       * Functions as a handle for overlay display devices which are created and
       * destroyed asynchronously.
       *
       * Guarded by the {@link DisplayManagerService.SyncRoot} lock.
       */
      private final class OverlayDisplayHandle implements OverlayDisplayWindow.Listener {
          private static final int DEFAULT_MODE_INDEX = 0;
  
          private final String mName;
          private final List<OverlayMode> mModes;
          private final int mGravity;
          private final boolean mSecure;
          private final int mNumber;
  
          private OverlayDisplayWindow mWindow;
          private OverlayDisplayDevice mDevice;
          private int mActiveMode;
  
          public OverlayDisplayHandle(String name, List<OverlayMode> modes, int gravity,
                  boolean secure, int number) {
              mName = name;
              mModes = modes;
              mGravity = gravity;
              mSecure = secure;
              mNumber = number;
  
              mActiveMode = 0;
  
              showLocked();
          }
  
          private void showLocked() {
              mUiHandler.post(mShowRunnable);
          }
  
          public void dismissLocked() {
              mUiHandler.removeCallbacks(mShowRunnable);
              mUiHandler.post(mDismissRunnable);
          }
  
          private void onActiveModeChangedLocked(int index) {
              mUiHandler.removeCallbacks(mResizeRunnable);
              mActiveMode = index;
              if (mWindow != null) {
                  mUiHandler.post(mResizeRunnable);
              }
          }
  
          // Called on the UI thread.
          @Override
          public void onWindowCreated(SurfaceTexture surfaceTexture, float refreshRate,
                  long presentationDeadlineNanos, int state) {
              synchronized (getSyncRoot()) {
                  IBinder displayToken = SurfaceControl.createDisplay(mName, mSecure);
                  mDevice = new OverlayDisplayDevice(displayToken, mName, mModes, mActiveMode,
                          DEFAULT_MODE_INDEX, refreshRate, presentationDeadlineNanos,
                          mSecure, state, surfaceTexture, mNumber) {
                      @Override
                      public void onModeChangedLocked(int index) {
                          onActiveModeChangedLocked(index);
                      }
                  };
  
                  sendDisplayDeviceEventLocked(mDevice, DISPLAY_DEVICE_EVENT_ADDED);
              }
          }
  
          // Called on the UI thread.
          @Override
          public void onWindowDestroyed() {
              synchronized (getSyncRoot()) {
                  if (mDevice != null) {
                      mDevice.destroyLocked();
                      sendDisplayDeviceEventLocked(mDevice, DISPLAY_DEVICE_EVENT_REMOVED);
                  }
              }
          }
  
          // Called on the UI thread.
          @Override
          public void onStateChanged(int state) {
              synchronized (getSyncRoot()) {
                  if (mDevice != null) {
                      mDevice.setStateLocked(state);
                      sendDisplayDeviceEventLocked(mDevice, DISPLAY_DEVICE_EVENT_CHANGED);
                  }
              }
          }
  
          public void dumpLocked(PrintWriter pw) {
              pw.println("  " + mName + ":");
              pw.println("    mModes=" + Arrays.toString(mModes.toArray()));
              pw.println("    mActiveMode=" + mActiveMode);
              pw.println("    mGravity=" + mGravity);
              pw.println("    mSecure=" + mSecure);
              pw.println("    mNumber=" + mNumber);
  
              // Try to dump the window state.
              if (mWindow != null) {
                  final IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "    ");
                  ipw.increaseIndent();
                  DumpUtils.dumpAsync(mUiHandler, mWindow, ipw, "", 200);
              }
          }
  
          // Runs on the UI thread.
          private final Runnable mShowRunnable = new Runnable() {
              @Override
              public void run() {
                  OverlayMode mode = mModes.get(mActiveMode);
                  OverlayDisplayWindow window = new OverlayDisplayWindow(getContext(),
                          mName, mode.mWidth, mode.mHeight, mode.mDensityDpi, mGravity, mSecure,
                          OverlayDisplayHandle.this);
                  window.show();
  
                  synchronized (getSyncRoot()) {
                      mWindow = window;
                  }
              }
          };
  
          // Runs on the UI thread.
          private final Runnable mDismissRunnable = new Runnable() {
              @Override
              public void run() {
                  OverlayDisplayWindow window;
                  synchronized (getSyncRoot()) {
                      window = mWindow;
                      mWindow = null;
                  }
  
                  if (window != null) {
                      window.dismiss();
                  }
              }
          };
  
          // Runs on the UI thread.
          private final Runnable mResizeRunnable = new Runnable() {
              @Override
              public void run() {
                  OverlayMode mode;
                  OverlayDisplayWindow window;
                  synchronized (getSyncRoot()) {
                      if (mWindow == null) {
                          return;
                      }
                      mode = mModes.get(mActiveMode);
                      window = mWindow;
                  }
                  window.resize(mode.mWidth, mode.mHeight, mode.mDensityDpi);
              }
          };
      }
  
      /**
       * A display mode for an overlay display.
       */
      private static final class OverlayMode {
          final int mWidth;
          final int mHeight;
          final int mDensityDpi;
  
          OverlayMode(int width, int height, int densityDpi) {
              mWidth = width;
              mHeight = height;
              mDensityDpi = densityDpi;
          }
  
          @Override
          public String toString() {
              return new StringBuilder("{")
                      .append("width=").append(mWidth)
                      .append(", height=").append(mHeight)
                      .append(", densityDpi=").append(mDensityDpi)
                      .append("}")
                      .toString();
          }
      }
  }
  